Skip to content

Fix module expressions losing attributes, await or parens when formatting - #8735

Open
cknitt wants to merge 8 commits into
masterfrom
print-module-expr-attributes
Open

cknitt wants to merge 8 commits into
masterfrom
print-module-expr-attributes

Conversation

@cknitt

@cknitt cknitt commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

The formatter dropped or moved attributes and await on module expressions, and printed code that doesn't parse for module constraints and functors in some positions.

print_mod_expr never printed pmod_attributes, and several other printer paths print or take apart a module expression on their own:

Source Formatted before this PR
module M = @attr F({}) module M = F()
module M = (@attr F)({}) module M = F()
module M = @attr (X: S) module M: S = X
F(@attr (X: S)) F(@attr X: S) (attribute moves to X)
local module M = @attr (X: S) module M: S = X
(X) => @attr (Y: S) (_: X): S => Y
(Z: T) => await (X: S) (Z: T): S => X (await dropped)
include @attr F({type t = int}) include F({type t = int})
(await F(A))(B) F(A, B) (await dropped)
(await F)(A) await F(A)
H(@attr {}) H()
module M: T = (X: S) module M: T = X: S (doesn't parse)
((X: S))(Z), ((Y: S) => {})(Z), (%ext)(Z) parens dropped
module type of (X: S), module((X: S)) parens dropped

Found while adding a fixture for #8734: module M = @inlined F({}) couldn't be kept in a formatted test file.

Changes

  • Print a module expression's attributes before it. Functors are the exception: their attributes are printed on the first parameter, as before.
  • Parsetree_viewer.mod_expr_has_attributes: the attributes print_attributes prints, or await. It guards every place that hoists a constraint (module bindings, local modules, functor results), flattens applications or merges nested functors, so those only apply to unannotated nodes.
  • A constraint with attributes or await prints its own parens: @attr (X: S), await @attr (X: S). Without them, the position decides: after module M: T =, in a functor's result, after module type of and inside module(...).- Applied functors are parenthesized when they're a constraint, a functor, an extension, or carry attributes or await.
  • A functor argument whose printed form starts with a doc comment is parenthesized, since the parser doesn't accept one there.
  • The include F({type t = ...}) shortcut and the F() shorthand only apply without attributes.
  • New parens hug the module expression, (M: {...}), like the existing binding parens.

No existing syntax snapshot changes.

How it was checked

Beyond the syntax tests, I formatted every combination of 15 contexts (bindings, module rec, local modules, include, functor bodies and arguments, applied functors, module(...), module type of, await) and about 450 module expressions with attributes and await on different nodes, nested two levels deep: 6750 cases. For each, the reparsed tree must equal the original and formatting must be idempotent.

  • master fails 4541 of them; this PR fails none that master passes.
  • The same holds with @JSX, @attr("payload"), @a.b and a doc comment in place of @attr.
  • Ignoring attribute order on a node, 6 remain: await written twice on one module, await @w (await X), which the parser stores as two res.await on the same node. That can't type-check and is left alone.
  • A separate check with comments in each changed position keeps all comments.

Tests

  • printer/modExpr/attributes.res: attributes and await on each kind of module expression and in each of the positions above.
  • printer/modExpr/parens.res: unattributed constraints and functors in positions that need parens.

🤖 Generated with Claude Code

cknitt and others added 2 commits October 7, 2026 10:59
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 <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Christoph Knittel <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.38710% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 79.87%. Comparing base (d7ffff5) to head (45b832a).

Files with missing lines Patch % Lines
compiler/syntax/src/res_printer.ml 98.18% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8735      +/-   ##
==========================================
+ Coverage   79.86%   79.87%   +0.01%     
==========================================
  Files         464      464              
  Lines       63090    63127      +37     
==========================================
+ Hits        50388    50424      +36     
- Misses      12702    12703       +1     
Files with missing lines Coverage Δ
compiler/syntax/src/res_parens.ml 83.52% <100.00%> (+0.06%) ⬆️
compiler/syntax/src/res_parsetree_viewer.ml 92.33% <100.00%> (+0.11%) ⬆️
compiler/syntax/src/res_printer.ml 93.52% <98.18%> (+0.03%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new

pkg-pr-new Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@8735

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@8735

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@8735

@rescript/darwin-x64

npm i https://pkg.pr.new/@rescript/darwin-x64@8735

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@8735

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@8735

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@8735

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@8735

commit: 45b832a

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

@cknitt
cknitt marked this pull request as ready for review October 7, 2026 15:47
@cknitt

cknitt commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T18:08:58.693964Z fba1d12 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7d925133e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/syntax/src/res_printer.ml
cknitt and others added 2 commits October 7, 2026 15:57
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 <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cknitt

cknitt commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77e9cb704e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/syntax/src/res_parens.ml Outdated
`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 <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cknitt

cknitt commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8329512020

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/syntax/src/res_parsetree_viewer.ml Outdated
Comment thread compiler/syntax/src/res_printer.ml Outdated
Comment thread compiler/syntax/src/res_printer.ml
Comment thread compiler/syntax/src/res_printer.ml
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 <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cknitt cknitt changed the title Keep attributes on module expressions when formatting Fix module expressions losing attributes, await or parens when formatting Oct 7, 2026
@cknitt

cknitt commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fba1d121ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/syntax/src/res_parsetree_viewer.ml
cknitt and others added 2 commits October 7, 2026 19:15
- `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 <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant