Skip to content

A statement after a binding in a CE block is silently parsed as application arguments #64

Description

@simontreanor

Found while writing an ordinary program with Pyfun 0.5.0.

A bare statement is not a CE item, which is fine and deliberate. But it is only
reported as such in the first position. Anywhere after a binding, the
binding's value expression silently swallows the following line as application
arguments, and the failure surfaces as a type error on the wrong line naming a
type nobody wrote.

let f n =
  result {
    let! x = Option.toResult "bad" (String.toInt n)
    let y = x * 2
    print f"y is {y}"
    return y
  }
$ pyfun check ce1.pyfun
error: type mismatch: expected int, found ('a ->{io} unit) -> 'b
 --> 4:17
  |
4 |     let y = x * 2
  |                 ^

The reported line is the one before the mistake, and ('a ->{io} unit) -> 'b
is print partially applied — there is no way to get from that message to
"a statement cannot go here".

The tell

Put the same statement first and the parser says exactly the right thing:

let f =
  result {
    print "first item, no binding before it"
    return 1
  }
error: expected `let!`, `let`, `do!`, `return`, or `yield`, found identifier `print`
 --> 3:5

So the rule is already stated correctly; it just is not reached once a binding
has been parsed.

Why it happens

The lexer suppresses layout inside brackets — src/lexer/mod.rs:67, "Nesting
depth of ()/{}; line breaks inside brackets never separate"
— and the
separator is only emitted at depth 0 (mod.rs:114). Tok::LBrace increments
that depth (mod.rs:843), so a CE's opening { turns off statement separation
for the whole block.

parse_ce_item (src/parser/mod.rs:1806) parses a binding's value with a plain
self.parse_expr() (mod.rs:1813). With no separator token to stop at, that
call runs straight through the newline and takes the next line as more arguments
to the value expression. Control never returns to parse_ce_item, so the
expected let!, let, do!, return, or yield arm at mod.rs:1853 — which has the
right message — is never reached.

Consistent with that: the same shape is fine in an ordinary function body, where
the offside rule is active and does emit a separator.

let f n =
  let y = n * 2
  print f"y is {y}"
  y
$ pyfun check ce4.pyfun
ok: no type errors

Scope

Confirmed on 0.5.0 for result { } and option { }, after both let and
let!. It looks parse-level rather than builder-specific, so seq, async and
user-defined builders are presumably all affected.

Workaround

Wrapping the effect in the builder's type works, though it reads poorly:

do! Ok (print f"x is {x}")

Otherwise: do not perform effects inside a CE block, and return what the caller
should print. That is usually the better shape anyway — it is what I ended up
with — but it should be a choice, not something arrived at through a misleading
type error.

Suggested direction

The message at mod.rs:1853 is already the one a user needs; the fix is to
reach it. Either have the CE block parse its items line-sensitively rather than
inheriting the bracket-depth suppression, or have parse_ce_item reject a value
expression that has run past a line break onto a token that starts a new item.


Also noticed (minor, docs)

Lesson 19 shows total active patterns with payload-free cases ((|Even|Odd|))
and partial ones with a single payload ((|Positive|_|)), so it is not obvious
that a total pattern can carry payloads per case. It can:

let (|Quit|Colour|Swap|Other|) ts =match ts:
  case Swap letters: f"swap {letters}"

I had to test it to find out, and it is the form that makes active patterns a
real alternative to a dispatch ADT. Worth one example in the lesson.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions