Skip to content

fix(printer): keep package constraints on the lambda body (#2925) - #2941

Open
MavenRain wants to merge 3 commits into
reasonml:masterfrom
MavenRain:fix/2925-package-constraint-hoist
Open

fix(printer): keep package constraints on the lambda body (#2925)#2941
MavenRain wants to merge 3 commits into
reasonml:masterfrom
MavenRain:fix/2925-package-constraint-hoist

Conversation

@MavenRain

Copy link
Copy Markdown

Fixes #2925.

Problem

refmt hoists a package constraint on a lambda body into a return annotation. For the program in #2925:

|> Option.map((module Foo_with_x: FOO_WITH_X) =>
     (module Foo_with_x: FOO)
   );

one refmt pass produces:

|> Option.map((module Foo_with_x: FOO_WITH_X): (module FOO) =>
     (module Foo_with_x)
   );

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 speculative ES6_FUN token 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_package constraint on the body in the two printing paths that serve expression positions:

  • formatPexpFun (lambdas in argument position and JSX expression containers)
  • the trailing-callback path in formatFunAppl

The output for the issue example becomes:

|> Option.map((module Foo_with_x: FOO_WITH_X) =>
     ((module Foo_with_x): (module FOO))
   );

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.t covers 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 ml output of the formatted issue example compiles with ocamlc. All 77 non-rtop cram tests pass.

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>
Comment thread src/reason-parser/reason_pprint_ast.ml Outdated
Comment on lines 9764 to 9766
| Pexp_constraint (_, { ptyp_desc = Ptyp_package _; _ }) ->
retCb, cbArgs
| Pexp_constraint (a, t) ->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Comment thread src/reason-parser/reason_pprint_ast.ml Outdated
Comment on lines +740 to +742
(* 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). *)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
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

2 participants