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.
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.
The reported line is the one before the mistake, and
('a ->{io} unit) -> 'bis
printpartially 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:
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, "Nestingdepth of
()/{}; line breaks inside brackets never separate" — and theseparator is only emitted at depth 0 (
mod.rs:114).Tok::LBraceincrementsthat depth (
mod.rs:843), so a CE's opening{turns off statement separationfor the whole block.
parse_ce_item(src/parser/mod.rs:1806) parses a binding's value with a plainself.parse_expr()(mod.rs:1813). With no separator token to stop at, thatcall 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 theexpected let!, let, do!, return, or yieldarm atmod.rs:1853— which has theright 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.
Scope
Confirmed on 0.5.0 for
result { }andoption { }, after bothletandlet!. It looks parse-level rather than builder-specific, soseq,asyncanduser-defined builders are presumably all affected.
Workaround
Wrapping the effect in the builder's type works, though it reads poorly:
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:1853is already the one a user needs; the fix is toreach it. Either have the CE block parse its items line-sensitively rather than
inheriting the bracket-depth suppression, or have
parse_ce_itemreject a valueexpression 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 obviousthat a total pattern can carry payloads per case. It can:
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.