From 3506002380c8c0c7bddd588d12f51644d0f84efa Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 10:59:32 +0000 Subject: [PATCH 01/10] Keep attributes on module expressions when formatting print_mod_expr never printed pmod_attributes, so the formatter dropped attributes such as `module M = @attr F(X)`, `module M = @attr {}` and `include @attr F(X)`. Print them before the module expression, except on functors, whose attributes are already printed on their first parameter. Two placements need care to round-trip: - An attributed functor in an application is parenthesized, `(@attr F)(X)`, and an attributed inner application is no longer flattened into the outer one, `(@attr F(A))(B)`; otherwise the attribute would move to the whole application. - `await` is printed before the attributes, since `@attr await M` does not parse. Signed-off-by: Christoph Knittel Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + compiler/syntax/src/res_parsetree_viewer.ml | 6 ++++- compiler/syntax/src/res_printer.ml | 25 ++++++++++++++++++- .../data/printer/modExpr/attributes.res | 24 ++++++++++++++++++ .../modExpr/expected/attributes.res.txt | 24 ++++++++++++++++++ 5 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests/syntax_tests/data/printer/modExpr/attributes.res create mode 100644 tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f16d7bd44..26aa2ea502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ #### :bug: Bug fix +- Keep attributes on module expressions when formatting, such as `module M = @attr F(X)`, which the formatter used to drop. PR_LINK - Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation, including across full watcher rebuilds. https://github.com/rescript-lang/rescript/pull/8667 - Fix `RescriptTools.binaryPath` failing with `ERR_PACKAGE_IMPORT_NOT_DEFINED` when used from a user project. https://github.com/rescript-lang/rescript/pull/8694 - GenType: type inline-record cases of untagged variants as objects instead of their single field's type. https://github.com/rescript-lang/rescript/pull/8693 diff --git a/compiler/syntax/src/res_parsetree_viewer.ml b/compiler/syntax/src/res_parsetree_viewer.ml index 7604b96b40..bb263ac664 100644 --- a/compiler/syntax/src/res_parsetree_viewer.ml +++ b/compiler/syntax/src/res_parsetree_viewer.ml @@ -600,7 +600,11 @@ let requires_special_callback_printing_first_arg args = let mod_expr_apply mod_expr = let rec loop acc mod_expr = match mod_expr with - | {pmod_desc = Pmod_apply (next, arg)} -> loop (arg :: acc) next + (* An inner application with attributes is kept as the callee, so the + printer can parenthesize it with its attributes *) + | {pmod_desc = Pmod_apply (next, arg); pmod_attributes} + when acc = [] || filter_parsing_attrs pmod_attributes = [] -> + loop (arg :: acc) next | _ -> (acc, mod_expr) in loop [] mod_expr diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 1701bd2ef5..05252289c7 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -6033,10 +6033,20 @@ and print_mod_expr ~state mod_expr cmt_tbl = | [{pmod_desc = Pmod_structure _}] -> true | _ -> false in + let call_expr_doc = + let doc = print_mod_expr ~state call_expr cmt_tbl in + (* Without parens, attributes on the functor would attach to the + whole application *) + match + Parsetree_viewer.filter_parsing_attrs call_expr.pmod_attributes + with + | [] -> doc + | _ -> add_parens doc + in Doc.group (Doc.concat [ - print_mod_expr ~state call_expr cmt_tbl; + call_expr_doc; (if is_unit_sugar then print_mod_apply_arg ~state (List.hd args [@doesNotRaise]) @@ -6076,6 +6086,19 @@ and print_mod_expr ~state mod_expr cmt_tbl = ] | Pmod_functor _ -> print_mod_functor ~state mod_expr cmt_tbl in + let doc = + match mod_expr.pmod_desc with + (* A functor's attributes belong to its first parameter, see + [print_mod_functor] *) + | Pmod_functor _ -> doc + | _ -> + Doc.concat + [ + print_attributes ~state ~inline:true mod_expr.pmod_attributes cmt_tbl; + doc; + ] + in + (* [await] comes first: [@attr await M] does not parse *) let doc = if Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes then match mod_expr.pmod_desc with diff --git a/tests/syntax_tests/data/printer/modExpr/attributes.res b/tests/syntax_tests/data/printer/modExpr/attributes.res new file mode 100644 index 0000000000..2c717e80e7 --- /dev/null +++ b/tests/syntax_tests/data/printer/modExpr/attributes.res @@ -0,0 +1,24 @@ +module A = @attr F +module B = @attr {} +module C = @attr { + let x = 1 +} +module D = @a @b F + +// on a functor application, its functor, or an argument +module E = @attr F({}) +module G = (@attr F)({}) +module H = @attr F(A, B) +module I = (@attr F(A))(B) +module J = F(@attr X) + +module K: T = @attr X +include @attr F({}) +module L = @attr unpack(x) +module M = @attr %ext + +let f = async () => { + module A = await @attr X + module B = await (@attr X: S) + () +} diff --git a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt new file mode 100644 index 0000000000..6ea9f5e09c --- /dev/null +++ b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt @@ -0,0 +1,24 @@ +module A = @attr F +module B = @attr {} +module C = @attr { + let x = 1 +} +module D = @a @b F + +// on a functor application, its functor, or an argument +module E = @attr F() +module G = (@attr F)() +module H = @attr F(A, B) +module I = (@attr F(A))(B) +module J = F(@attr X) + +module K: T = @attr X +include @attr F() +module L = @attr unpack(x) +module M = @attr %ext + +let f = async () => { + module A = await @attr X + module B = await (@attr X: S) + () +} From 44f090189816321d0b3d58d90be0dd1756e4b1e1 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 10:59:53 +0000 Subject: [PATCH 02/10] Add CHANGELOG link for #8735 Signed-off-by: Christoph Knittel Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26aa2ea502..6732436937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ #### :bug: Bug fix -- Keep attributes on module expressions when formatting, such as `module M = @attr F(X)`, which the formatter used to drop. PR_LINK +- Keep attributes on module expressions when formatting, such as `module M = @attr F(X)`, which the formatter used to drop. https://github.com/rescript-lang/rescript/pull/8735 - Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation, including across full watcher rebuilds. https://github.com/rescript-lang/rescript/pull/8667 - Fix `RescriptTools.binaryPath` failing with `ERR_PACKAGE_IMPORT_NOT_DEFINED` when used from a user project. https://github.com/rescript-lang/rescript/pull/8694 - GenType: type inline-record cases of untagged variants as objects instead of their single field's type. https://github.com/rescript-lang/rescript/pull/8693 From c896e6e29584e23076eea5f9a66f31e5db6175b5 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 15:57:48 +0000 Subject: [PATCH 03/10] Keep attributes on module constraints when formatting An attribute on a constraint was printed without parens, so it moved to the constrained module on reparse (`F(@attr (X: S))` printed as `F(@attr X: S)`, and likewise for `include`). On the right-hand side of a module binding it was dropped, because `module M = @attr (X: S)` was printed as `module M: S = X`. Parenthesize attributed constraints, keep them on the right-hand side of a binding, and don't add a second pair of parens after `include`. Signed-off-by: Christoph Knittel Co-Authored-By: Claude Opus 5.5 --- compiler/syntax/src/res_parens.ml | 5 ++- compiler/syntax/src/res_printer.ml | 31 ++++++++++++------- .../data/printer/modExpr/attributes.res | 8 +++++ .../modExpr/expected/attributes.res.txt | 8 +++++ 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 4b8e671027..a82f4ddfcd 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -430,7 +430,10 @@ let braced_expr expr = let include_mod_expr mod_expr = match mod_expr.Parsetree.pmod_desc with - | Parsetree.Pmod_constraint _ -> true + (* An attributed constraint already prints its own parens *) + | Parsetree.Pmod_constraint _ -> + Parsetree_viewer.filter_parsing_attrs mod_expr.pmod_attributes = [] + || Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes | _ -> false let mod_expr_parens mod_expr = diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 05252289c7..eee1abe530 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -798,10 +798,10 @@ and print_module_binding ~state ~is_rec module_binding cmt_tbl i = in let mod_expr_doc, mod_constraint_doc = match module_binding.pmb_expr with - | {pmod_desc = Pmod_constraint (mod_expr, mod_type)} - when not - (Parsetree_viewer.has_await_attribute - module_binding.pmb_expr.pmod_attributes) -> + | {pmod_desc = Pmod_constraint (mod_expr, mod_type); pmod_attributes} + (* [module M: S = E] has no node for the constraint's attributes *) + when (not (Parsetree_viewer.has_await_attribute pmod_attributes)) + && Parsetree_viewer.filter_parsing_attrs pmod_attributes = [] -> ( print_mod_expr ~state mod_expr cmt_tbl, Doc.concat [Doc.text ": "; print_mod_type ~state mod_type cmt_tbl] ) | mod_expr -> (print_mod_expr ~state mod_expr cmt_tbl, Doc.nil) @@ -6077,13 +6077,22 @@ and print_mod_expr ~state mod_expr cmt_tbl = Doc.rparen; ]); ]) - | Pmod_constraint (mod_expr, mod_type) -> - Doc.concat - [ - print_mod_expr ~state mod_expr cmt_tbl; - Doc.text ": "; - print_mod_type ~state mod_type cmt_tbl; - ] + | Pmod_constraint (inner, mod_type) -> + let doc = + Doc.concat + [ + print_mod_expr ~state inner cmt_tbl; + Doc.text ": "; + print_mod_type ~state mod_type cmt_tbl; + ] + in + (* [@attr (M: S)]: without parens the attributes would attach to [M]. + [await] already parenthesizes the constraint. *) + if + Parsetree_viewer.filter_parsing_attrs mod_expr.pmod_attributes <> [] + && not (Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes) + then add_parens doc + else doc | Pmod_functor _ -> print_mod_functor ~state mod_expr cmt_tbl in let doc = diff --git a/tests/syntax_tests/data/printer/modExpr/attributes.res b/tests/syntax_tests/data/printer/modExpr/attributes.res index 2c717e80e7..2312dc83f7 100644 --- a/tests/syntax_tests/data/printer/modExpr/attributes.res +++ b/tests/syntax_tests/data/printer/modExpr/attributes.res @@ -14,6 +14,14 @@ module J = F(@attr X) module K: T = @attr X include @attr F({}) + +// on a module constraint, or on the module it constrains +module N = @attr (X: S) +module O = F(@attr (X: S)) +module P = F((@attr X: S)) +include @attr (X: S) +include (@attr X: S) + module L = @attr unpack(x) module M = @attr %ext diff --git a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt index 6ea9f5e09c..0a09315b2f 100644 --- a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt +++ b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt @@ -14,6 +14,14 @@ module J = F(@attr X) module K: T = @attr X include @attr F() + +// on a module constraint, or on the module it constrains +module N = @attr (X: S) +module O = F(@attr (X: S)) +module P = F(@attr X: S) +include @attr (X: S) +include (@attr X: S) + module L = @attr unpack(x) module M = @attr %ext From 201cb961047120b14ba6ecb6757cd50232819696 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 16:05:24 +0000 Subject: [PATCH 04/10] Simplify the module attribute printing checks Use the existing Parsetree_viewer.has_printable_attributes instead of filtering the attributes and comparing with [], and share the rule that an attributed module constraint prints its own parens between the printer and Parens.include_mod_expr. Signed-off-by: Christoph Knittel Co-Authored-By: Claude Opus 5.5 --- compiler/syntax/src/res_parens.ml | 11 +++++++---- compiler/syntax/src/res_parens.mli | 1 + compiler/syntax/src/res_parsetree_viewer.ml | 2 +- compiler/syntax/src/res_printer.ml | 18 +++++------------- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index a82f4ddfcd..d47c43fdbd 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -428,12 +428,15 @@ let braced_expr expr = | Pexp_constraint _ -> true | _ -> false +(* [@attr (M: S)]: an attributed module constraint prints its own parens, so + the attributes don't attach to [M]. [await] already parenthesizes it. *) +let attributed_mod_constraint (mod_expr : Parsetree.module_expr) = + Parsetree_viewer.has_printable_attributes mod_expr.pmod_attributes + && not (Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes) + let include_mod_expr mod_expr = match mod_expr.Parsetree.pmod_desc with - (* An attributed constraint already prints its own parens *) - | Parsetree.Pmod_constraint _ -> - Parsetree_viewer.filter_parsing_attrs mod_expr.pmod_attributes = [] - || Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes + | Parsetree.Pmod_constraint _ -> not (attributed_mod_constraint mod_expr) | _ -> false let mod_expr_parens mod_expr = diff --git a/compiler/syntax/src/res_parens.mli b/compiler/syntax/src/res_parens.mli index 8d304823f4..60d58bc05f 100644 --- a/compiler/syntax/src/res_parens.mli +++ b/compiler/syntax/src/res_parens.mli @@ -33,6 +33,7 @@ val mod_expr_functor_constraint : Parsetree.module_type -> bool val braced_expr : Parsetree.expression -> bool val call_expr : Parsetree.expression -> kind +val attributed_mod_constraint : Parsetree.module_expr -> bool val include_mod_expr : Parsetree.module_expr -> bool val mod_expr_parens : Parsetree.module_expr -> bool diff --git a/compiler/syntax/src/res_parsetree_viewer.ml b/compiler/syntax/src/res_parsetree_viewer.ml index bb263ac664..e3068449ea 100644 --- a/compiler/syntax/src/res_parsetree_viewer.ml +++ b/compiler/syntax/src/res_parsetree_viewer.ml @@ -603,7 +603,7 @@ let mod_expr_apply mod_expr = (* An inner application with attributes is kept as the callee, so the printer can parenthesize it with its attributes *) | {pmod_desc = Pmod_apply (next, arg); pmod_attributes} - when acc = [] || filter_parsing_attrs pmod_attributes = [] -> + when acc = [] || not (has_printable_attributes pmod_attributes) -> loop (arg :: acc) next | _ -> (acc, mod_expr) in diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index eee1abe530..4b53eb1dcd 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -801,7 +801,7 @@ and print_module_binding ~state ~is_rec module_binding cmt_tbl i = | {pmod_desc = Pmod_constraint (mod_expr, mod_type); pmod_attributes} (* [module M: S = E] has no node for the constraint's attributes *) when (not (Parsetree_viewer.has_await_attribute pmod_attributes)) - && Parsetree_viewer.filter_parsing_attrs pmod_attributes = [] -> + && not (Parsetree_viewer.has_printable_attributes pmod_attributes) -> ( print_mod_expr ~state mod_expr cmt_tbl, Doc.concat [Doc.text ": "; print_mod_type ~state mod_type cmt_tbl] ) | mod_expr -> (print_mod_expr ~state mod_expr cmt_tbl, Doc.nil) @@ -6037,11 +6037,9 @@ and print_mod_expr ~state mod_expr cmt_tbl = let doc = print_mod_expr ~state call_expr cmt_tbl in (* Without parens, attributes on the functor would attach to the whole application *) - match - Parsetree_viewer.filter_parsing_attrs call_expr.pmod_attributes - with - | [] -> doc - | _ -> add_parens doc + if Parsetree_viewer.has_printable_attributes call_expr.pmod_attributes + then add_parens doc + else doc in Doc.group (Doc.concat @@ -6086,13 +6084,7 @@ and print_mod_expr ~state mod_expr cmt_tbl = print_mod_type ~state mod_type cmt_tbl; ] in - (* [@attr (M: S)]: without parens the attributes would attach to [M]. - [await] already parenthesizes the constraint. *) - if - Parsetree_viewer.filter_parsing_attrs mod_expr.pmod_attributes <> [] - && not (Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes) - then add_parens doc - else doc + if Parens.attributed_mod_constraint mod_expr then add_parens doc else doc | Pmod_functor _ -> print_mod_functor ~state mod_expr cmt_tbl in let doc = From c813560db97509e2d2ea3c97cdb0a237ca7ee126 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 16:53:11 +0000 Subject: [PATCH 05/10] Keep attributes on awaited module constraints when formatting `await @attr (X: S)` printed as `await (@attr X: S)`, moving the attribute from the constraint to X: await's parens enclosed the attributes but not the constraint itself. An attributed constraint now always prints its own parens, and await only adds parens around an unattributed one. Signed-off-by: Christoph Knittel Co-Authored-By: Claude Opus 5.5 --- compiler/syntax/src/res_parens.ml | 3 +-- compiler/syntax/src/res_printer.ml | 3 ++- tests/syntax_tests/data/printer/modExpr/attributes.res | 1 + .../data/printer/modExpr/expected/attributes.res.txt | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index d47c43fdbd..b2dc538a38 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -429,10 +429,9 @@ let braced_expr expr = | _ -> false (* [@attr (M: S)]: an attributed module constraint prints its own parens, so - the attributes don't attach to [M]. [await] already parenthesizes it. *) + the attributes don't attach to [M] *) let attributed_mod_constraint (mod_expr : Parsetree.module_expr) = Parsetree_viewer.has_printable_attributes mod_expr.pmod_attributes - && not (Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes) let include_mod_expr mod_expr = match mod_expr.Parsetree.pmod_desc with diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 4b53eb1dcd..d3f39f3c90 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -6103,7 +6103,8 @@ and print_mod_expr ~state mod_expr cmt_tbl = let doc = if Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes then match mod_expr.pmod_desc with - | Pmod_constraint _ -> + | Pmod_constraint _ when not (Parens.attributed_mod_constraint mod_expr) + -> Doc.concat [Doc.text "await "; Doc.lparen; doc; Doc.rparen] | _ -> Doc.concat [Doc.text "await "; doc] else doc diff --git a/tests/syntax_tests/data/printer/modExpr/attributes.res b/tests/syntax_tests/data/printer/modExpr/attributes.res index 2312dc83f7..280ff54f54 100644 --- a/tests/syntax_tests/data/printer/modExpr/attributes.res +++ b/tests/syntax_tests/data/printer/modExpr/attributes.res @@ -28,5 +28,6 @@ module M = @attr %ext let f = async () => { module A = await @attr X module B = await (@attr X: S) + module C = await @attr (X: S) () } diff --git a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt index 0a09315b2f..3c4a6d28e1 100644 --- a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt +++ b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt @@ -28,5 +28,6 @@ module M = @attr %ext let f = async () => { module A = await @attr X module B = await (@attr X: S) + module C = await @attr (X: S) () } From 41ecef7b535783c524a3a197a959377deb9bd490 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 17:36:51 +0000 Subject: [PATCH 06/10] Fix module expression printing paths that drop attributes or await Besides print_mod_expr, several printer paths print or take apart a module expression on their own, and each dropped or moved attributes and await, or printed code that doesn't parse: - a module constraint was hoisted into a local module's binding (`module M = @attr (X: S)` lost @attr) and a functor's result signature, the latter also dropping await - the `include F({type t = ...})` shortcut ignored all attributes - application flattening and callee parens ignored await, so `(await F(A))(B)` printed as `F(A, B)` and `(await F)(A)` as `await F(A)` - nested functors were merged through an awaited inner functor - `F(@attr {})` printed as `F()` - applied constraints, functors and extensions lost their parens, as did constraints after `module M: T =`, `module type of` and inside `module(...)` Found by formatting and re-parsing every combination of 15 contexts and about 450 module expressions, comparing parsetrees. Signed-off-by: Christoph Knittel Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- compiler/syntax/src/res_parsetree_viewer.ml | 14 +++- compiler/syntax/src/res_parsetree_viewer.mli | 1 + compiler/syntax/src/res_printer.ml | 82 ++++++++++++------- .../data/printer/modExpr/attributes.res | 13 +++ .../modExpr/expected/attributes.res.txt | 19 +++++ .../printer/modExpr/expected/parens.res.txt | 15 ++++ .../data/printer/modExpr/parens.res | 15 ++++ 8 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 tests/syntax_tests/data/printer/modExpr/expected/parens.res.txt create mode 100644 tests/syntax_tests/data/printer/modExpr/parens.res diff --git a/CHANGELOG.md b/CHANGELOG.md index 6732436937..0ebd00baaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ #### :bug: Bug fix -- Keep attributes on module expressions when formatting, such as `module M = @attr F(X)`, which the formatter used to drop. https://github.com/rescript-lang/rescript/pull/8735 +- Fix the formatter dropping or moving attributes and `await` on module expressions, such as `module M = @attr F(X)`, and printing invalid code for module constraints and functors in some positions, such as `module M: T = (X: S)`. https://github.com/rescript-lang/rescript/pull/8735 - Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation, including across full watcher rebuilds. https://github.com/rescript-lang/rescript/pull/8667 - Fix `RescriptTools.binaryPath` failing with `ERR_PACKAGE_IMPORT_NOT_DEFINED` when used from a user project. https://github.com/rescript-lang/rescript/pull/8694 - GenType: type inline-record cases of untagged variants as objects instead of their single field's type. https://github.com/rescript-lang/rescript/pull/8693 diff --git a/compiler/syntax/src/res_parsetree_viewer.ml b/compiler/syntax/src/res_parsetree_viewer.ml index e3068449ea..29d2f73665 100644 --- a/compiler/syntax/src/res_parsetree_viewer.ml +++ b/compiler/syntax/src/res_parsetree_viewer.ml @@ -548,6 +548,12 @@ let is_printable_attribute attr = let has_printable_attributes attrs = List.exists is_printable_attribute attrs +(* Attributes or [await] on a module expression: either one prints before it + and binds less tightly than an application or a constraint *) +let mod_expr_has_attributes (mod_expr : Parsetree.module_expr) = + has_printable_attributes mod_expr.pmod_attributes + || has_await_attribute mod_expr.pmod_attributes + let filter_printable_attributes attrs = List.filter is_printable_attribute attrs let partition_printable_attributes attrs = @@ -602,8 +608,8 @@ let mod_expr_apply mod_expr = match mod_expr with (* An inner application with attributes is kept as the callee, so the printer can parenthesize it with its attributes *) - | {pmod_desc = Pmod_apply (next, arg); pmod_attributes} - when acc = [] || not (has_printable_attributes pmod_attributes) -> + | {pmod_desc = Pmod_apply (next, arg)} as apply + when acc = [] || not (mod_expr_has_attributes apply) -> loop (arg :: acc) next | _ -> (acc, mod_expr) in @@ -612,10 +618,12 @@ let mod_expr_apply mod_expr = let mod_expr_functor mod_expr = let rec loop acc mod_expr = match mod_expr with + (* An awaited inner functor is kept as the result, so [await] is printed *) | { pmod_desc = Pmod_functor (lbl, mod_type, return_mod_expr); pmod_attributes = attrs; - } -> + } + when acc = [] || not (has_await_attribute attrs) -> let param = (attrs, lbl, mod_type) in loop (param :: acc) return_mod_expr | return_mod_expr -> (List.rev acc, return_mod_expr) diff --git a/compiler/syntax/src/res_parsetree_viewer.mli b/compiler/syntax/src/res_parsetree_viewer.mli index 509a334557..c89c985bdf 100644 --- a/compiler/syntax/src/res_parsetree_viewer.mli +++ b/compiler/syntax/src/res_parsetree_viewer.mli @@ -96,6 +96,7 @@ val filter_fragile_match_attributes : val should_indent_binary_expr : Parsetree.expression -> bool val should_inline_rhs_binary_expr : Parsetree.expression -> bool val has_printable_attributes : Parsetree.attributes -> bool +val mod_expr_has_attributes : Parsetree.module_expr -> bool val filter_printable_attributes : Parsetree.attributes -> Parsetree.attributes val partition_printable_attributes : Parsetree.attributes -> Parsetree.attributes * Parsetree.attributes diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index d3f39f3c90..78bbf1168e 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -28,6 +28,10 @@ let add_parens doc = Doc.rparen; ]) +(* Module expressions hug their parens, so a multi-line structure or + signature keeps its braces next to them: [(M: {...})] *) +let add_mod_expr_parens doc = Doc.concat [Doc.lparen; doc; Doc.rparen] + let add_braces ?(force_break = false) doc = Doc.breakable_group ~force_break (Doc.concat @@ -798,19 +802,18 @@ and print_module_binding ~state ~is_rec module_binding cmt_tbl i = in let mod_expr_doc, mod_constraint_doc = match module_binding.pmb_expr with - | {pmod_desc = Pmod_constraint (mod_expr, mod_type); pmod_attributes} + | {pmod_desc = Pmod_constraint (mod_expr, mod_type)} as constrained (* [module M: S = E] has no node for the constraint's attributes *) - when (not (Parsetree_viewer.has_await_attribute pmod_attributes)) - && not (Parsetree_viewer.has_printable_attributes pmod_attributes) -> - ( print_mod_expr ~state mod_expr cmt_tbl, + when not (Parsetree_viewer.mod_expr_has_attributes constrained) -> + let mod_expr_doc = + if Parens.mod_expr_parens constrained then + add_mod_expr_parens (print_mod_expr ~state mod_expr cmt_tbl) + else print_mod_expr_constraint_parens ~state mod_expr cmt_tbl + in + ( mod_expr_doc, Doc.concat [Doc.text ": "; print_mod_type ~state mod_type cmt_tbl] ) | mod_expr -> (print_mod_expr ~state mod_expr cmt_tbl, Doc.nil) in - let mod_expr_doc_parens = - if Parens.mod_expr_parens module_binding.pmb_expr then - Doc.concat [Doc.lparen; mod_expr_doc; Doc.rparen] - else mod_expr_doc - in let mod_name = let doc = Doc.text module_binding.pmb_name.Location.txt in print_comments doc cmt_tbl module_binding.pmb_name.loc @@ -824,7 +827,7 @@ and print_module_binding ~state ~is_rec module_binding cmt_tbl i = mod_name; mod_constraint_doc; Doc.text " = "; - mod_expr_doc_parens; + mod_expr_doc; ] in print_comments doc cmt_tbl module_binding.pmb_loc @@ -983,7 +986,10 @@ and print_mod_type ~state mod_type cmt_tbl = ]) | Pmty_typeof mod_expr -> Doc.concat - [Doc.text "module type of "; print_mod_expr ~state mod_expr cmt_tbl] + [ + Doc.text "module type of "; + print_mod_expr_constraint_parens ~state mod_expr cmt_tbl; + ] | Pmty_extension extension -> print_extension ~state ~at_module_lvl:false extension cmt_tbl | Pmty_alias longident -> @@ -1205,8 +1211,9 @@ and print_include_declaration ~state try as oneliner if there is a single type alias declaration *) | Pmod_apply - ( {pmod_desc = Pmod_ident longident_loc}, + ( {pmod_desc = Pmod_ident longident_loc; pmod_attributes = []}, { + pmod_attributes = []; pmod_desc = Pmod_structure [ @@ -1222,7 +1229,8 @@ and print_include_declaration ~state ] ); } as structure_item); ]; - } ) -> + } ) + when include_declaration.pincl_mod.pmod_attributes = [] -> Doc.concat [ print_longident_location longident_loc cmt_tbl; @@ -3670,7 +3678,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = (Doc.concat [ Doc.soft_line; - print_mod_expr ~state mod_expr cmt_tbl; + print_mod_expr_constraint_parens ~state mod_expr cmt_tbl; Doc.text ": "; print_comments (print_package_type ~state @@ -3706,7 +3714,10 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = Doc.text "module("; Doc.indent (Doc.concat - [Doc.soft_line; print_mod_expr ~state mod_expr cmt_tbl]); + [ + Doc.soft_line; + print_mod_expr_constraint_parens ~state mod_expr cmt_tbl; + ]); Doc.soft_line; Doc.rparen; ]) @@ -5554,9 +5565,7 @@ and print_expression_block ~state ~braces expr cmt_tbl = let name, mod_expr = match mod_expr.pmod_desc with | Pmod_constraint (mod_expr2, mod_type) - when not - (Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes) - -> + when not (Parsetree_viewer.mod_expr_has_attributes mod_expr) -> let name = Doc.concat [name; Doc.text ": "; print_mod_type ~state mod_type cmt_tbl] @@ -5570,7 +5579,7 @@ and print_expression_block ~state ~braces expr cmt_tbl = Doc.text "module "; name; Doc.text " = "; - print_mod_expr ~state mod_expr cmt_tbl; + print_mod_expr_constraint_parens ~state mod_expr cmt_tbl; ] in let loc = {expr.pexp_loc with loc_end = mod_expr.pmod_loc.loc_end} in @@ -6025,7 +6034,7 @@ and print_mod_expr ~state mod_expr cmt_tbl = let args, call_expr = Parsetree_viewer.mod_expr_apply mod_expr in let is_unit_sugar = match args with - | [{pmod_desc = Pmod_structure []}] -> true + | [{pmod_desc = Pmod_structure []; pmod_attributes = []}] -> true | _ -> false in let should_hug = @@ -6035,11 +6044,14 @@ and print_mod_expr ~state mod_expr cmt_tbl = in let call_expr_doc = let doc = print_mod_expr ~state call_expr cmt_tbl in + match call_expr.pmod_desc with + | Pmod_constraint _ | Pmod_functor _ | Pmod_extension _ -> + add_mod_expr_parens doc (* Without parens, attributes on the functor would attach to the whole application *) - if Parsetree_viewer.has_printable_attributes call_expr.pmod_attributes - then add_parens doc - else doc + | _ when Parsetree_viewer.mod_expr_has_attributes call_expr -> + add_mod_expr_parens doc + | _ -> doc in Doc.group (Doc.concat @@ -6084,7 +6096,8 @@ and print_mod_expr ~state mod_expr cmt_tbl = print_mod_type ~state mod_type cmt_tbl; ] in - if Parens.attributed_mod_constraint mod_expr then add_parens doc else doc + if Parens.attributed_mod_constraint mod_expr then add_mod_expr_parens doc + else doc | Pmod_functor _ -> print_mod_functor ~state mod_expr cmt_tbl in let doc = @@ -6117,14 +6130,15 @@ and print_mod_functor ~state mod_expr cmt_tbl = in let return_constraint, return_mod_expr = match return_mod_expr.pmod_desc with - | Pmod_constraint (mod_expr, mod_type) -> + | Pmod_constraint (mod_expr, mod_type) + when not (Parsetree_viewer.mod_expr_has_attributes return_mod_expr) -> let constraint_doc = let doc = print_mod_type ~state mod_type cmt_tbl in if Parens.mod_expr_functor_constraint mod_type then add_parens doc else doc in let mod_constraint = Doc.concat [Doc.text ": "; constraint_doc] in - (mod_constraint, print_mod_expr ~state mod_expr cmt_tbl) + (mod_constraint, print_mod_expr_constraint_parens ~state mod_expr cmt_tbl) | _ -> (Doc.nil, print_mod_expr ~state return_mod_expr cmt_tbl) in let parameters_doc = @@ -6185,10 +6199,22 @@ and print_mod_functor_param ~state (attrs, lbl, opt_mod_type) cmt_tbl = print_comments doc cmt_tbl cmt_loc and print_mod_apply_arg ~state mod_expr cmt_tbl = - match mod_expr.pmod_desc with - | Pmod_structure [] -> Doc.text "()" + match mod_expr with + | {pmod_desc = Pmod_structure []; pmod_attributes = []} -> Doc.text "()" | _ -> print_mod_expr ~state mod_expr cmt_tbl +(* A constraint without attributes needs parens where [M: S] would not parse + or would mean something else: after [module M: T =], in a functor's + result, after [module type of] and inside [module(...)]. With attributes + it prints its own. *) +and print_mod_expr_constraint_parens ~state mod_expr cmt_tbl = + let doc = print_mod_expr ~state mod_expr cmt_tbl in + match mod_expr.pmod_desc with + | Pmod_constraint _ + when not (Parsetree_viewer.mod_expr_has_attributes mod_expr) -> + add_mod_expr_parens doc + | _ -> doc + and print_exception_def ~state (constr : Parsetree.extension_constructor) cmt_tbl = let kind = diff --git a/tests/syntax_tests/data/printer/modExpr/attributes.res b/tests/syntax_tests/data/printer/modExpr/attributes.res index 280ff54f54..40091abf23 100644 --- a/tests/syntax_tests/data/printer/modExpr/attributes.res +++ b/tests/syntax_tests/data/printer/modExpr/attributes.res @@ -11,6 +11,10 @@ module G = (@attr F)({}) module H = @attr F(A, B) module I = (@attr F(A))(B) module J = F(@attr X) +module Q = F(@attr {}) +include @attr F({type t = int}) +include (@attr F)({type t = int}) +include F(@attr {type t = int}) module K: T = @attr X include @attr F({}) @@ -22,6 +26,10 @@ module P = F((@attr X: S)) include @attr (X: S) include (@attr X: S) +// on a functor's result +module R = (X) => @attr (Y: S) +module S = (X): S => @attr Y + module L = @attr unpack(x) module M = @attr %ext @@ -29,5 +37,10 @@ let f = async () => { module A = await @attr X module B = await (@attr X: S) module C = await @attr (X: S) + module D = @attr (X: S) + module E = (await F)(A) + module G = (await F(A))(B) + module H = (X: T) => await (Y: S) + module I = (X: T) => await (Y: S) => {} () } diff --git a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt index 3c4a6d28e1..ce72c00eff 100644 --- a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt +++ b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt @@ -11,6 +11,16 @@ module G = (@attr F)() module H = @attr F(A, B) module I = (@attr F(A))(B) module J = F(@attr X) +module Q = F(@attr {}) +include @attr F({ + type t = int +}) +include (@attr F)({ + type t = int +}) +include F(@attr { + type t = int +}) module K: T = @attr X include @attr F() @@ -22,6 +32,10 @@ module P = F(@attr X: S) include @attr (X: S) include (@attr X: S) +// on a functor's result +module R = (_: X) => @attr (Y: S) +module S = (_: X): S => @attr Y + module L = @attr unpack(x) module M = @attr %ext @@ -29,5 +43,10 @@ let f = async () => { module A = await @attr X module B = await (@attr X: S) module C = await @attr (X: S) + module D = @attr (X: S) + module E = (await F)(A) + module G = (await F(A))(B) + module H = (X: T) => await (Y: S) + module I = (X: T) => await (Y: S) => {} () } diff --git a/tests/syntax_tests/data/printer/modExpr/expected/parens.res.txt b/tests/syntax_tests/data/printer/modExpr/expected/parens.res.txt new file mode 100644 index 0000000000..52310bc081 --- /dev/null +++ b/tests/syntax_tests/data/printer/modExpr/expected/parens.res.txt @@ -0,0 +1,15 @@ +// module constraints where `M: S` would not parse or would mean something else +module A: T = (X: S) +module rec B: T = (X: S) +module type C = module type of (X: S) +let d = module((X: S)) +let f = () => { + module M: T = (X: S) + () +} + +// functors that are applied +module G = (X: S)(Z) +module H = ((Y: S) => {})(Z) +module I = ((_: Y): S => W)(Z) +module J = (%ext)(Z) diff --git a/tests/syntax_tests/data/printer/modExpr/parens.res b/tests/syntax_tests/data/printer/modExpr/parens.res new file mode 100644 index 0000000000..eff0010b3c --- /dev/null +++ b/tests/syntax_tests/data/printer/modExpr/parens.res @@ -0,0 +1,15 @@ +// module constraints where `M: S` would not parse or would mean something else +module A: T = (X: S) +module rec B: T = (X: S) +module type C = module type of (X: S) +let d = module((X: S)) +let f = () => { + module M: T = (X: S) + () +} + +// functors that are applied +module G = ((X: S))(Z) +module H = ((Y: S) => {})(Z) +module I = ((Y): S => W)(Z) +module J = (%ext)(Z) From 65043357afea77220e5cb9e02786b23cbf9fe0b2 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 19:15:48 +0000 Subject: [PATCH 07/10] Check module attributes the way they're printed, and simplify - `mod_expr_has_attributes` now uses the same filter as `print_attributes`. It used `has_printable_attributes`, which also excludes `@JSX`, so `(@JSX F)(A)` printed as `@JSX F(A)` and `(@JSX F(A))(B)` lost the attribute. - A functor argument whose printed form starts with a doc comment is parenthesized, since the parser doesn't accept a doc comment there. - A constraint now prints its own parens whenever it has attributes or `await`, which replaces `Parens.attributed_mod_constraint` and the constraint case of `await`. - `print_mod_expr_constraint_parens` covers what `Parens.mod_expr_parens` did for module bindings, so that is removed. Signed-off-by: Christoph Knittel Co-Authored-By: Claude Opus 5.5 --- compiler/syntax/src/res_parens.ml | 26 ++--------- compiler/syntax/src/res_parens.mli | 3 -- compiler/syntax/src/res_parsetree_viewer.ml | 5 ++- compiler/syntax/src/res_printer.ml | 44 +++++++++++++------ .../data/printer/modExpr/attributes.res | 9 ++++ .../modExpr/expected/attributes.res.txt | 15 +++++++ 6 files changed, 61 insertions(+), 41 deletions(-) diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index b2dc538a38..1c7ffb41d7 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -428,31 +428,11 @@ let braced_expr expr = | Pexp_constraint _ -> true | _ -> false -(* [@attr (M: S)]: an attributed module constraint prints its own parens, so - the attributes don't attach to [M] *) -let attributed_mod_constraint (mod_expr : Parsetree.module_expr) = - Parsetree_viewer.has_printable_attributes mod_expr.pmod_attributes - let include_mod_expr mod_expr = match mod_expr.Parsetree.pmod_desc with - | Parsetree.Pmod_constraint _ -> not (attributed_mod_constraint mod_expr) - | _ -> false - -let mod_expr_parens mod_expr = - match mod_expr with - | { - Parsetree.pmod_desc = - Pmod_constraint - ( {Parsetree.pmod_desc = Pmod_structure _}, - {Parsetree.pmty_desc = Pmty_signature [{psig_desc = Psig_module _}]} ); - } -> - false - | { - Parsetree.pmod_desc = - Pmod_constraint - (_, {Parsetree.pmty_desc = Pmty_signature [{psig_desc = Psig_module _}]}); - } -> - true + (* With attributes or [await], a constraint prints its own parens *) + | Parsetree.Pmod_constraint _ -> + not (Parsetree_viewer.mod_expr_has_attributes mod_expr) | _ -> false let arrow_return_typ_expr typ_expr = diff --git a/compiler/syntax/src/res_parens.mli b/compiler/syntax/src/res_parens.mli index 60d58bc05f..5cfbd5c342 100644 --- a/compiler/syntax/src/res_parens.mli +++ b/compiler/syntax/src/res_parens.mli @@ -33,11 +33,8 @@ val mod_expr_functor_constraint : Parsetree.module_type -> bool val braced_expr : Parsetree.expression -> bool val call_expr : Parsetree.expression -> kind -val attributed_mod_constraint : Parsetree.module_expr -> bool val include_mod_expr : Parsetree.module_expr -> bool -val mod_expr_parens : Parsetree.module_expr -> bool - val arrow_return_typ_expr : Parsetree.core_type -> bool val pattern_record_row_rhs : Parsetree.pattern -> bool diff --git a/compiler/syntax/src/res_parsetree_viewer.ml b/compiler/syntax/src/res_parsetree_viewer.ml index 29d2f73665..185bce942a 100644 --- a/compiler/syntax/src/res_parsetree_viewer.ml +++ b/compiler/syntax/src/res_parsetree_viewer.ml @@ -549,9 +549,10 @@ let is_printable_attribute attr = let has_printable_attributes attrs = List.exists is_printable_attribute attrs (* Attributes or [await] on a module expression: either one prints before it - and binds less tightly than an application or a constraint *) + and binds less tightly than an application or a constraint. The attributes + are the ones [Res_printer.print_attributes] prints. *) let mod_expr_has_attributes (mod_expr : Parsetree.module_expr) = - has_printable_attributes mod_expr.pmod_attributes + filter_parsing_attrs mod_expr.pmod_attributes <> [] || has_await_attribute mod_expr.pmod_attributes let filter_printable_attributes attrs = List.filter is_printable_attribute attrs diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 78bbf1168e..b49dac32e8 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -32,6 +32,26 @@ let add_parens doc = signature keeps its braces next to them: [(M: {...})] *) let add_mod_expr_parens doc = Doc.concat [Doc.lparen; doc; Doc.rparen] +(* Whether a module expression's printed form starts with a doc comment: + doc comments print before other attributes, and an unparenthesized + constraint or application starts with its leftmost module *) +let rec mod_expr_starts_with_doc_comment (mod_expr : Parsetree.module_expr) = + let attrs = Parsetree_viewer.filter_parsing_attrs mod_expr.pmod_attributes in + let doc_comments, _ = + Parsetree_viewer.partition_doc_comment_attributes attrs + in + if doc_comments <> [] then true + else if Parsetree_viewer.mod_expr_has_attributes mod_expr then false + else + match mod_expr.pmod_desc with + | Pmod_constraint (inner, _) -> mod_expr_starts_with_doc_comment inner + | Pmod_apply _ -> ( + let _, call_expr = Parsetree_viewer.mod_expr_apply mod_expr in + match call_expr.pmod_desc with + | Pmod_constraint _ | Pmod_functor _ | Pmod_extension _ -> false + | _ -> mod_expr_starts_with_doc_comment call_expr) + | _ -> false + let add_braces ?(force_break = false) doc = Doc.breakable_group ~force_break (Doc.concat @@ -805,12 +825,7 @@ and print_module_binding ~state ~is_rec module_binding cmt_tbl i = | {pmod_desc = Pmod_constraint (mod_expr, mod_type)} as constrained (* [module M: S = E] has no node for the constraint's attributes *) when not (Parsetree_viewer.mod_expr_has_attributes constrained) -> - let mod_expr_doc = - if Parens.mod_expr_parens constrained then - add_mod_expr_parens (print_mod_expr ~state mod_expr cmt_tbl) - else print_mod_expr_constraint_parens ~state mod_expr cmt_tbl - in - ( mod_expr_doc, + ( print_mod_expr_constraint_parens ~state mod_expr cmt_tbl, Doc.concat [Doc.text ": "; print_mod_type ~state mod_type cmt_tbl] ) | mod_expr -> (print_mod_expr ~state mod_expr cmt_tbl, Doc.nil) in @@ -6096,7 +6111,10 @@ and print_mod_expr ~state mod_expr cmt_tbl = print_mod_type ~state mod_type cmt_tbl; ] in - if Parens.attributed_mod_constraint mod_expr then add_mod_expr_parens doc + (* [@attr (M: S)], [await (M: S)]: without parens, attributes and + [await] would apply to [M] *) + if Parsetree_viewer.mod_expr_has_attributes mod_expr then + add_mod_expr_parens doc else doc | Pmod_functor _ -> print_mod_functor ~state mod_expr cmt_tbl in @@ -6115,11 +6133,7 @@ and print_mod_expr ~state mod_expr cmt_tbl = (* [await] comes first: [@attr await M] does not parse *) let doc = if Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes then - match mod_expr.pmod_desc with - | Pmod_constraint _ when not (Parens.attributed_mod_constraint mod_expr) - -> - Doc.concat [Doc.text "await "; Doc.lparen; doc; Doc.rparen] - | _ -> Doc.concat [Doc.text "await "; doc] + Doc.concat [Doc.text "await "; doc] else doc in print_comments doc cmt_tbl mod_expr.pmod_loc @@ -6201,7 +6215,11 @@ and print_mod_functor_param ~state (attrs, lbl, opt_mod_type) cmt_tbl = and print_mod_apply_arg ~state mod_expr cmt_tbl = match mod_expr with | {pmod_desc = Pmod_structure []; pmod_attributes = []} -> Doc.text "()" - | _ -> print_mod_expr ~state mod_expr cmt_tbl + | _ -> + let doc = print_mod_expr ~state mod_expr cmt_tbl in + (* The parser doesn't accept a doc comment at the start of an argument *) + if mod_expr_starts_with_doc_comment mod_expr then add_mod_expr_parens doc + else doc (* A constraint without attributes needs parens where [M: S] would not parse or would mean something else: after [module M: T =], in a functor's diff --git a/tests/syntax_tests/data/printer/modExpr/attributes.res b/tests/syntax_tests/data/printer/modExpr/attributes.res index 40091abf23..e8bc65cb37 100644 --- a/tests/syntax_tests/data/printer/modExpr/attributes.res +++ b/tests/syntax_tests/data/printer/modExpr/attributes.res @@ -30,6 +30,15 @@ include (@attr X: S) module R = (X) => @attr (Y: S) module S = (X): S => @attr Y +// @JSX is printed like any other attribute +module T = (@JSX F)(A) +module U = (@JSX F(A))(B) +module V = @JSX (X: S) + +// a functor argument can't start with a doc comment +module W = H(@w /** doc */ X) +module Y = H((/** doc */ X: S)) + module L = @attr unpack(x) module M = @attr %ext diff --git a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt index ce72c00eff..fbffc66817 100644 --- a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt +++ b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt @@ -36,6 +36,21 @@ include (@attr X: S) module R = (_: X) => @attr (Y: S) module S = (_: X): S => @attr Y +// @JSX is printed like any other attribute +module T = (@JSX F)(A) +module U = (@JSX F(A))(B) +module V = @JSX (X: S) + +// a functor argument can't start with a doc comment +module W = H( + (/** doc */ + @w X), +) +module Y = H( + (/** doc */ + X: S), +) + module L = @attr unpack(x) module M = @attr %ext From 81f9ab58a9a176dfd373f00ecba54fe76bb3ad30 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 21:30:45 +0000 Subject: [PATCH 08/10] Restore Parens.mod_expr_parens for module bindings Removing it changed how existing code is formatted: after a signature holding a single module, `} = (M')` lost its parens and printed as `} = M'`. Both parse the same, but this PR shouldn't reformat existing code (tests/tests/src/coercion_module_alias_test.res failed the format check). Signed-off-by: Christoph Knittel Co-Authored-By: Claude Opus 5.5 --- compiler/syntax/src/res_parens.ml | 17 +++++++++++++++++ compiler/syntax/src/res_parens.mli | 2 ++ compiler/syntax/src/res_printer.ml | 7 ++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 1c7ffb41d7..660846eecc 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -435,6 +435,23 @@ let include_mod_expr mod_expr = not (Parsetree_viewer.mod_expr_has_attributes mod_expr) | _ -> false +let mod_expr_parens mod_expr = + match mod_expr with + | { + Parsetree.pmod_desc = + Pmod_constraint + ( {Parsetree.pmod_desc = Pmod_structure _}, + {Parsetree.pmty_desc = Pmty_signature [{psig_desc = Psig_module _}]} ); + } -> + false + | { + Parsetree.pmod_desc = + Pmod_constraint + (_, {Parsetree.pmty_desc = Pmty_signature [{psig_desc = Psig_module _}]}); + } -> + true + | _ -> false + let arrow_return_typ_expr typ_expr = match typ_expr.Parsetree.ptyp_desc with | Ptyp_arrow _ -> true diff --git a/compiler/syntax/src/res_parens.mli b/compiler/syntax/src/res_parens.mli index 5cfbd5c342..8d304823f4 100644 --- a/compiler/syntax/src/res_parens.mli +++ b/compiler/syntax/src/res_parens.mli @@ -35,6 +35,8 @@ val call_expr : Parsetree.expression -> kind val include_mod_expr : Parsetree.module_expr -> bool +val mod_expr_parens : Parsetree.module_expr -> bool + val arrow_return_typ_expr : Parsetree.core_type -> bool val pattern_record_row_rhs : Parsetree.pattern -> bool diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index b49dac32e8..d12097e1ce 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -825,7 +825,12 @@ and print_module_binding ~state ~is_rec module_binding cmt_tbl i = | {pmod_desc = Pmod_constraint (mod_expr, mod_type)} as constrained (* [module M: S = E] has no node for the constraint's attributes *) when not (Parsetree_viewer.mod_expr_has_attributes constrained) -> - ( print_mod_expr_constraint_parens ~state mod_expr cmt_tbl, + let mod_expr_doc = + if Parens.mod_expr_parens constrained then + add_mod_expr_parens (print_mod_expr ~state mod_expr cmt_tbl) + else print_mod_expr_constraint_parens ~state mod_expr cmt_tbl + in + ( mod_expr_doc, Doc.concat [Doc.text ": "; print_mod_type ~state mod_type cmt_tbl] ) | mod_expr -> (print_mod_expr ~state mod_expr cmt_tbl, Doc.nil) in From fa5b8f936ef8e88b617314153c581c265990b360 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 8 Oct 2026 04:40:20 +0000 Subject: [PATCH 09/10] Check module attributes without building lists mod_expr_has_attributes filtered the attributes only to test whether the result was empty, and the doc comment check filtered and partitioned them. Factor out is_parsing_attr and is_doc_comment_attribute and use List.exists instead. Signed-off-by: Christoph Knittel Co-Authored-By: Claude Opus 5.5 --- compiler/syntax/src/res_parsetree_viewer.ml | 61 +++++++++++--------- compiler/syntax/src/res_parsetree_viewer.mli | 1 + compiler/syntax/src/res_printer.ml | 9 ++- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/compiler/syntax/src/res_parsetree_viewer.ml b/compiler/syntax/src/res_parsetree_viewer.ml index 185bce942a..8d0bd99fde 100644 --- a/compiler/syntax/src/res_parsetree_viewer.ml +++ b/compiler/syntax/src/res_parsetree_viewer.ml @@ -225,19 +225,20 @@ let rec unwrap_braces expr = | Pexp_braces {expr = inner} -> unwrap_braces inner | _ -> expr +(* Attributes the parser adds to encode syntax; they are never printed *) +let is_parsing_attr (attr : Parsetree.attribute) = + match attr with + | ( { + Location.txt = + ( "res.iflet" | "res.ternary" | "res.await" | "res.patVariantSpread" + | "res.dictPattern" | "res.dictSpread" ); + }, + _ ) -> + true + | _ -> false + let filter_parsing_attrs attrs = - List.filter - (fun attr -> - match attr with - | ( { - Location.txt = - ( "res.iflet" | "res.ternary" | "res.await" - | "res.patVariantSpread" | "res.dictPattern" | "res.dictSpread" ); - }, - _ ) -> - false - | _ -> true) - attrs + List.filter (fun attr -> not (is_parsing_attr attr)) attrs let is_block_expr expr = match (unwrap_braces expr).pexp_desc with @@ -552,29 +553,33 @@ let has_printable_attributes attrs = List.exists is_printable_attribute attrs and binds less tightly than an application or a constraint. The attributes are the ones [Res_printer.print_attributes] prints. *) let mod_expr_has_attributes (mod_expr : Parsetree.module_expr) = - filter_parsing_attrs mod_expr.pmod_attributes <> [] - || has_await_attribute mod_expr.pmod_attributes + List.exists + (fun attr -> + match attr with + | {Location.txt = "res.await"}, _ -> true + | _ -> not (is_parsing_attr attr)) + mod_expr.pmod_attributes let filter_printable_attributes attrs = List.filter is_printable_attribute attrs let partition_printable_attributes attrs = List.partition is_printable_attribute attrs +let is_doc_comment_attribute ((id, payload) : Parsetree.attribute) = + match (id, payload) with + | ( {txt = "res.doc"}, + PStr + [ + { + pstr_desc = + Pstr_eval ({pexp_desc = Pexp_constant (Pconst_string _)}, _); + }; + ] ) -> + true + | _ -> false + let partition_doc_comment_attributes attrs = - List.partition - (fun ((id, payload) : Parsetree.attribute) -> - match (id, payload) with - | ( {txt = "res.doc"}, - PStr - [ - { - pstr_desc = - Pstr_eval ({pexp_desc = Pexp_constant (Pconst_string _)}, _); - }; - ] ) -> - true - | _ -> false) - attrs + List.partition is_doc_comment_attribute attrs let rec is_fun_expr expr = match expr.pexp_desc with diff --git a/compiler/syntax/src/res_parsetree_viewer.mli b/compiler/syntax/src/res_parsetree_viewer.mli index c89c985bdf..deb4f1f250 100644 --- a/compiler/syntax/src/res_parsetree_viewer.mli +++ b/compiler/syntax/src/res_parsetree_viewer.mli @@ -100,6 +100,7 @@ val mod_expr_has_attributes : Parsetree.module_expr -> bool val filter_printable_attributes : Parsetree.attributes -> Parsetree.attributes val partition_printable_attributes : Parsetree.attributes -> Parsetree.attributes * Parsetree.attributes +val is_doc_comment_attribute : Parsetree.attribute -> bool val partition_doc_comment_attributes : Parsetree.attributes -> Parsetree.attributes * Parsetree.attributes diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index d12097e1ce..965ff26412 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -36,11 +36,10 @@ let add_mod_expr_parens doc = Doc.concat [Doc.lparen; doc; Doc.rparen] doc comments print before other attributes, and an unparenthesized constraint or application starts with its leftmost module *) let rec mod_expr_starts_with_doc_comment (mod_expr : Parsetree.module_expr) = - let attrs = Parsetree_viewer.filter_parsing_attrs mod_expr.pmod_attributes in - let doc_comments, _ = - Parsetree_viewer.partition_doc_comment_attributes attrs - in - if doc_comments <> [] then true + if + List.exists Parsetree_viewer.is_doc_comment_attribute + mod_expr.pmod_attributes + then true else if Parsetree_viewer.mod_expr_has_attributes mod_expr then false else match mod_expr.pmod_desc with From 469cbbe960682812b4842c0f720f8f19ddf6a790 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 8 Oct 2026 05:12:53 +0000 Subject: [PATCH 10/10] Fix typed packs of constraints and extension callees, simplify From another review pass: - `(module((X: S1)): module(S2))` printed as `module((X: S1): S2)`, which parses as a functor. A typed pack of a constraint now keeps the general form. - An applied extension with a payload got parens it doesn't need: `%ext(A)(B)` printed as `(%ext(A))(B)`. Only `(%ext)(B)` needs them. - A module expression's attributes and doc comments stay on one line with it, instead of a doc comment or a long attribute list breaking onto unindented lines. - One rule for when an applied module needs parens (`Parens.mod_apply_callee`) and one for when a constraint does (`Parens.mod_constraint`, formerly `include_mod_expr`), used everywhere. The doc comment check uses the former and accounts for functors and `await`, which print first, so it no longer adds redundant parens. Signed-off-by: Christoph Knittel Co-Authored-By: Claude Opus 5.5 --- compiler/syntax/src/res_parens.ml | 14 +- compiler/syntax/src/res_parens.mli | 3 +- compiler/syntax/src/res_printer.ml | 136 ++++++++++-------- .../data/printer/modExpr/attributes.res | 2 + .../modExpr/expected/attributes.res.txt | 12 +- .../printer/modExpr/expected/parens.res.txt | 2 + .../data/printer/modExpr/parens.res | 2 + 7 files changed, 104 insertions(+), 67 deletions(-) diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 660846eecc..cf76c63f1b 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -428,13 +428,23 @@ let braced_expr expr = | Pexp_constraint _ -> true | _ -> false -let include_mod_expr mod_expr = +(* A constraint that needs parens where [M: S] would not parse or would mean + something else, e.g. after [include] or [module type of]. With attributes + or [await] it prints its own. *) +let mod_constraint mod_expr = match mod_expr.Parsetree.pmod_desc with - (* With attributes or [await], a constraint prints its own parens *) | Parsetree.Pmod_constraint _ -> not (Parsetree_viewer.mod_expr_has_attributes mod_expr) | _ -> false +(* An applied module expression that needs parens: [(M: S)(X)], + [((Y) => M)(X)], [(%ext)(X)], and with attributes or [await], which + would otherwise apply to the whole application *) +let mod_apply_callee callee = + match callee.Parsetree.pmod_desc with + | Pmod_constraint _ | Pmod_functor _ | Pmod_extension (_, PStr []) -> true + | _ -> Parsetree_viewer.mod_expr_has_attributes callee + let mod_expr_parens mod_expr = match mod_expr with | { diff --git a/compiler/syntax/src/res_parens.mli b/compiler/syntax/src/res_parens.mli index 8d304823f4..593fa40986 100644 --- a/compiler/syntax/src/res_parens.mli +++ b/compiler/syntax/src/res_parens.mli @@ -33,7 +33,8 @@ val mod_expr_functor_constraint : Parsetree.module_type -> bool val braced_expr : Parsetree.expression -> bool val call_expr : Parsetree.expression -> kind -val include_mod_expr : Parsetree.module_expr -> bool +val mod_constraint : Parsetree.module_expr -> bool +val mod_apply_callee : Parsetree.module_expr -> bool val mod_expr_parens : Parsetree.module_expr -> bool diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 965ff26412..7eb4fc48e5 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -36,20 +36,22 @@ let add_mod_expr_parens doc = Doc.concat [Doc.lparen; doc; Doc.rparen] doc comments print before other attributes, and an unparenthesized constraint or application starts with its leftmost module *) let rec mod_expr_starts_with_doc_comment (mod_expr : Parsetree.module_expr) = - if - List.exists Parsetree_viewer.is_doc_comment_attribute - mod_expr.pmod_attributes - then true - else if Parsetree_viewer.mod_expr_has_attributes mod_expr then false - else - match mod_expr.pmod_desc with - | Pmod_constraint (inner, _) -> mod_expr_starts_with_doc_comment inner - | Pmod_apply _ -> ( - let _, call_expr = Parsetree_viewer.mod_expr_apply mod_expr in - match call_expr.pmod_desc with - | Pmod_constraint _ | Pmod_functor _ | Pmod_extension _ -> false - | _ -> mod_expr_starts_with_doc_comment call_expr) - | _ -> false + match mod_expr.pmod_desc with + (* [await] and a functor's parameter list come first *) + | _ when Parsetree_viewer.has_await_attribute mod_expr.pmod_attributes -> + false + | Pmod_functor _ -> false + | _ + when List.exists Parsetree_viewer.is_doc_comment_attribute + mod_expr.pmod_attributes -> + true + | _ when Parsetree_viewer.mod_expr_has_attributes mod_expr -> false + | Pmod_constraint (inner, _) -> mod_expr_starts_with_doc_comment inner + | Pmod_apply _ -> + let _, call_expr = Parsetree_viewer.mod_expr_apply mod_expr in + (not (Parens.mod_apply_callee call_expr)) + && mod_expr_starts_with_doc_comment call_expr + | _ -> false let add_braces ?(force_break = false) doc = Doc.breakable_group ~force_break @@ -1224,32 +1226,35 @@ and print_include_declaration ~state print_attributes ~state include_declaration.pincl_attributes cmt_tbl; Doc.text "include "; (let include_doc = - match include_declaration.pincl_mod.pmod_desc with + match include_declaration.pincl_mod with (* include Module.Name({ type t = t }) try as oneliner if there is a single type alias declaration *) - | Pmod_apply - ( {pmod_desc = Pmod_ident longident_loc; pmod_attributes = []}, - { - pmod_attributes = []; - pmod_desc = - Pmod_structure - [ - ({ - pstr_desc = - Pstr_type - ( _, - [ - { - ptype_kind = Ptype_abstract; - ptype_manifest = Some _; - }; - ] ); - } as structure_item); - ]; - } ) - when include_declaration.pincl_mod.pmod_attributes = [] -> + | { + pmod_attributes = []; + pmod_desc = + Pmod_apply + ( {pmod_desc = Pmod_ident longident_loc; pmod_attributes = []}, + { + pmod_attributes = []; + pmod_desc = + Pmod_structure + [ + ({ + pstr_desc = + Pstr_type + ( _, + [ + { + ptype_kind = Ptype_abstract; + ptype_manifest = Some _; + }; + ] ); + } as structure_item); + ]; + } ); + } -> Doc.concat [ print_longident_location longident_loc cmt_tbl; @@ -1271,7 +1276,7 @@ and print_include_declaration ~state ] | _ -> print_mod_expr ~state include_declaration.pincl_mod cmt_tbl in - if Parens.include_mod_expr include_declaration.pincl_mod then + if Parens.mod_constraint include_declaration.pincl_mod then add_parens include_doc else include_doc); ] @@ -3686,6 +3691,23 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = Doc.space; print_expression_block ~state ~braces:true body cmt_tbl; ]) + | Pexp_constraint + ( {pexp_desc = Pexp_pack ({pmod_desc = Pmod_constraint _} as mod_expr)}, + {ptyp_desc = Ptyp_package package_type; ptyp_loc} ) -> + (* [module((M: S1): S2)] would parse as a functor, so this keeps the + general form [(module((M: S1)): module(S2))] *) + Doc.concat + [ + Doc.lparen; + Doc.text "module("; + print_mod_expr_constraint_parens ~state mod_expr cmt_tbl; + Doc.text "): "; + print_comments + (print_package_type ~state ~print_module_keyword_and_parens:true + package_type cmt_tbl) + cmt_tbl ptyp_loc; + Doc.rparen; + ] | Pexp_constraint ( {pexp_desc = Pexp_pack mod_expr}, {ptyp_desc = Ptyp_package package_type; ptyp_loc} ) -> @@ -6063,14 +6085,8 @@ and print_mod_expr ~state mod_expr cmt_tbl = in let call_expr_doc = let doc = print_mod_expr ~state call_expr cmt_tbl in - match call_expr.pmod_desc with - | Pmod_constraint _ | Pmod_functor _ | Pmod_extension _ -> - add_mod_expr_parens doc - (* Without parens, attributes on the functor would attach to the - whole application *) - | _ when Parsetree_viewer.mod_expr_has_attributes call_expr -> - add_mod_expr_parens doc - | _ -> doc + if Parens.mod_apply_callee call_expr then add_mod_expr_parens doc + else doc in Doc.group (Doc.concat @@ -6127,12 +6143,7 @@ and print_mod_expr ~state mod_expr cmt_tbl = (* A functor's attributes belong to its first parameter, see [print_mod_functor] *) | Pmod_functor _ -> doc - | _ -> - Doc.concat - [ - print_attributes ~state ~inline:true mod_expr.pmod_attributes cmt_tbl; - doc; - ] + | _ -> Doc.concat [print_mod_expr_attributes ~state mod_expr cmt_tbl; doc] in (* [await] comes first: [@attr await M] does not parse *) let doc = @@ -6216,6 +6227,23 @@ and print_mod_functor_param ~state (attrs, lbl, opt_mod_type) cmt_tbl = in print_comments doc cmt_tbl cmt_loc +(* A module expression's attributes, doc comments first, on one line with + it: unlike before a declaration, a line break here would leave the module + at the start of the next line *) +and print_mod_expr_attributes ~state (mod_expr : Parsetree.module_expr) cmt_tbl + = + match Parsetree_viewer.filter_parsing_attrs mod_expr.pmod_attributes with + | [] -> Doc.nil + | attrs -> + let doc_comments, attrs = + Parsetree_viewer.partition_doc_comment_attributes attrs + in + Doc.concat + (List.map + (fun attr -> + Doc.concat [fst (print_attribute ~state attr cmt_tbl); Doc.space]) + (doc_comments @ attrs)) + and print_mod_apply_arg ~state mod_expr cmt_tbl = match mod_expr with | {pmod_desc = Pmod_structure []; pmod_attributes = []} -> Doc.text "()" @@ -6231,11 +6259,7 @@ and print_mod_apply_arg ~state mod_expr cmt_tbl = it prints its own. *) and print_mod_expr_constraint_parens ~state mod_expr cmt_tbl = let doc = print_mod_expr ~state mod_expr cmt_tbl in - match mod_expr.pmod_desc with - | Pmod_constraint _ - when not (Parsetree_viewer.mod_expr_has_attributes mod_expr) -> - add_mod_expr_parens doc - | _ -> doc + if Parens.mod_constraint mod_expr then add_mod_expr_parens doc else doc and print_exception_def ~state (constr : Parsetree.extension_constructor) cmt_tbl = diff --git a/tests/syntax_tests/data/printer/modExpr/attributes.res b/tests/syntax_tests/data/printer/modExpr/attributes.res index e8bc65cb37..c39dfa18f4 100644 --- a/tests/syntax_tests/data/printer/modExpr/attributes.res +++ b/tests/syntax_tests/data/printer/modExpr/attributes.res @@ -38,6 +38,8 @@ module V = @JSX (X: S) // a functor argument can't start with a doc comment module W = H(@w /** doc */ X) module Y = H((/** doc */ X: S)) +module Z = /** doc */ X +include /** doc */ X module L = @attr unpack(x) module M = @attr %ext diff --git a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt index fbffc66817..ccc639f7bc 100644 --- a/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt +++ b/tests/syntax_tests/data/printer/modExpr/expected/attributes.res.txt @@ -42,14 +42,10 @@ module U = (@JSX F(A))(B) module V = @JSX (X: S) // a functor argument can't start with a doc comment -module W = H( - (/** doc */ - @w X), -) -module Y = H( - (/** doc */ - X: S), -) +module W = H((/** doc */ @w X)) +module Y = H((/** doc */ X: S)) +module Z = /** doc */ X +include /** doc */ X module L = @attr unpack(x) module M = @attr %ext diff --git a/tests/syntax_tests/data/printer/modExpr/expected/parens.res.txt b/tests/syntax_tests/data/printer/modExpr/expected/parens.res.txt index 52310bc081..4a8d821af6 100644 --- a/tests/syntax_tests/data/printer/modExpr/expected/parens.res.txt +++ b/tests/syntax_tests/data/printer/modExpr/expected/parens.res.txt @@ -3,6 +3,7 @@ module A: T = (X: S) module rec B: T = (X: S) module type C = module type of (X: S) let d = module((X: S)) +let e = (module((X: S1)): module(S2)) let f = () => { module M: T = (X: S) () @@ -13,3 +14,4 @@ module G = (X: S)(Z) module H = ((Y: S) => {})(Z) module I = ((_: Y): S => W)(Z) module J = (%ext)(Z) +module K = %ext(A)(Z) diff --git a/tests/syntax_tests/data/printer/modExpr/parens.res b/tests/syntax_tests/data/printer/modExpr/parens.res index eff0010b3c..61f0c4c4cb 100644 --- a/tests/syntax_tests/data/printer/modExpr/parens.res +++ b/tests/syntax_tests/data/printer/modExpr/parens.res @@ -3,6 +3,7 @@ module A: T = (X: S) module rec B: T = (X: S) module type C = module type of (X: S) let d = module((X: S)) +let e = (module((X: S1)): module(S2)) let f = () => { module M: T = (X: S) () @@ -13,3 +14,4 @@ module G = ((X: S))(Z) module H = ((Y: S) => {})(Z) module I = ((Y): S => W)(Z) module J = (%ext)(Z) +module K = %ext(A)(Z)