Skip to content

fix(parser): prefer the arrow reading for (args): t => body in expres… - #1

Open
seprov wants to merge 1 commit into
masterfrom
fix-refmt-breaking-module-arg-lambdas
Open

fix(parser): prefer the arrow reading for (args): t => body in expres…#1
seprov wants to merge 1 commit into
masterfrom
fix-refmt-breaking-module-arg-lambdas

Conversation

@seprov

@seprov seprov commented Jul 24, 2026

Copy link
Copy Markdown
Owner

…sion position

The lexer injects ES6_FUN when a parenthesized group is followed by => or :, and the multi-parser forks: one parser consumes the token (the arrow-function reading) and one ignores it (e.g. the expression reading). When the return annotation's type is followed by => and the arrow body is itself valid type syntax -- e.g. f((x: int): int => x), where int => x is also an arrow type -- both parsers run to completion, and the winner was whichever came first in the fork list: the expression reading, f((x: int) : (int => x)).

That preference also breaks the printer round-trip: printing hoists a function body's type constraint into return-annotation position ((module M: SIG) => (module M: SUB) prints as
(module M: SIG): (module SUB) => (module M)), and re-parsing that output under the expression preference yields an ill-typed constraint instead of the original function -- refmt could turn a compiling program into a non-compiling one.

Put the forked (arrow) parser first instead. This matches how the same text already parses in let-binding position, where the bare-constraint reading is not grammatical. Constraining an expression against an arrow type remains expressible with a parenthesized type: (e: ((a, b) => t)); the one fixture relying on the bare form (wrapping-re.t, df_locallyAbstractFuncAnnotated: 'figureMeOut) is updated to that spelling and formats back to byte-identical output.

Fixes reasonml#2925

…sion position

The lexer injects ES6_FUN when a parenthesized group is followed by => or
:, and the multi-parser forks: one parser consumes the token (the
arrow-function reading) and one ignores it (e.g. the expression reading).
When the return annotation's type is followed by => and the arrow body is
itself valid type syntax -- e.g. f((x: int): int => x), where int => x is
also an arrow *type* -- both parsers run to completion, and the winner was
whichever came first in the fork list: the expression reading,
f((x: int) : (int => x)).

That preference also breaks the printer round-trip: printing hoists a
function body's type constraint into return-annotation position
((module M: SIG) => (module M: SUB) prints as
(module M: SIG): (module SUB) => (module M)), and re-parsing that output
under the expression preference yields an ill-typed constraint instead of
the original function -- refmt could turn a compiling program into a
non-compiling one.

Put the forked (arrow) parser first instead. This matches how the same
text already parses in let-binding position, where the bare-constraint
reading is not grammatical. Constraining an expression against an arrow
type remains expressible with a parenthesized type: (e: ((a, b) => t));
the one fixture relying on the bare form (wrapping-re.t,
df_locallyAbstractFuncAnnotated: 'figureMeOut) is updated to that
spelling and formats back to byte-identical output.

Fixes reasonml#2925

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refmt rewrites valid code into code that doesn't compile

1 participant