fix(printer): keep package constraints on the lambda body (#2925) - #2941
fix(printer): keep package constraints on the lambda body (#2925)#2941MavenRain wants to merge 3 commits into
Conversation
Fixes reasonml#2925. refmt moved a package constraint on a lambda body into a return annotation: Option.map((module Foo_with_x: FOO_WITH_X) => (module Foo_with_x: FOO) ); became Option.map((module Foo_with_x: FOO_WITH_X): (module FOO) => (module Foo_with_x) ); In expression position that output does not re-parse as a function. The parser reads `(module FOO)` after the colon as the start of an arrow type, so the whole argument becomes a constraint expression with the type `(module FOO) => (module Foo_with_x)`. refmt turned a program that compiles into one that does not, and a second format pass rewrote the line again. The printer now keeps a Ptyp_package constraint on the body in the two printing paths that serve expression positions: formatPexpFun (arguments, JSX) and the trailing-callback path in formatFunAppl. Let bindings keep the return-annotation form, which re-parses correctly there. Ordinary return types are not affected. New cram test covers the issue example, a non-trailing callback, an ordinary return type, and a let binding, and checks that formatting is idempotent and that the output re-parses with the same meaning. Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
| | Pexp_constraint (_, { ptyp_desc = Ptyp_package _; _ }) -> | ||
| retCb, cbArgs | ||
| | Pexp_constraint (a, t) -> |
There was a problem hiding this comment.
instead of adding the _ { ptyp_desc = Ptyp_package _; _ } branch at the top, why don't you ensure the 9766 condition is met on the opposite of Ptyp_package? This way the code remains simpler, and you have a chance to test all other cases
There was a problem hiding this comment.
Done. Both sites now keep the single Pexp_constraint arm and guard it with when not (is_package_typ ...). The package case falls through to the existing _ arm. The predicate lives next to the other small helpers at the top of the file. The full cram suite passes, and a plain constraint such as (a) => (a: int) still prints as (a): int => a, so the general branch stays covered.
Review feedback on reasonml#2941: keep the single Pexp_constraint arm at both sites and guard it with `when not (is_package_typ ...)`. The package case falls through to the existing default arm. All other constraints keep their current path. No behavior change. Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
| (* A package constraint must stay on the lambda body: as a return annotation, | ||
| `(module Foo)` re-parses as the start of an arrow type, which changes the | ||
| meaning of the printed code (see #2925). *) |
There was a problem hiding this comment.
let's remove this comment as it is a bit unnecesary
Requested in review: the helper name and the guard usage already say what it does; the rationale lives in reasonml#2925. Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
Fixes #2925.
Problem
refmt hoists a package constraint on a lambda body into a return annotation. For the program in #2925:
one
refmtpass produces:That output does not re-parse as a function. After the colon,
(module FOO)starts an arrow type, so the whole argument re-parses as a constraint expression with the type(module FOO) => (module Foo_with_x). The formatted file no longer compiles, and a second format pass rewrites the line again.No parenthesization of the return annotation avoids this:
((module FOO))re-parses the same way. The lexer inserts a speculativeES6_FUNtoken in front of(module FOO)because=>follows it, and the constraint-expression interpretation wins in the multi-parser whenever the lambda body is itself a valid type, which a package expression always is. So the return-annotation form is not a faithful printing of this AST in expression position, and the printer must not produce it there.Fix
Keep a
Ptyp_packageconstraint on the body in the two printing paths that serve expression positions:formatPexpFun(lambdas in argument position and JSX expression containers)formatFunApplThe output for the issue example becomes:
which re-parses to the same AST and is stable under repeated formatting.
Let bindings keep the return-annotation form (
let f = (module X: S): (module FOO) => ...): it re-parses correctly there, so their formatting does not change. Ordinary (non-package) return types are not affected anywhere.Testing
New cram test
test/firstClassModuleLambda.tcovers the issue example, a non-trailing callback argument, an ordinary return type, and a let binding. It checks the formatted output, checks that it re-parses with the same meaning via--parse re --print ml, and checks idempotency. The--print mloutput of the formatted issue example compiles with ocamlc. All 77 non-rtop cram tests pass.