Skip to content

Represent await on module expressions with a Pmod_await node - #8738

Merged
cknitt merged 11 commits into
masterfrom
module-await-node
Oct 8, 2026
Merged

cknitt merged 11 commits into
masterfrom
module-await-node

Conversation

@cknitt

@cknitt cknitt commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Resolves the "res.await on module expressions (await M)" item of #8624, and #7383.

await M on a module expression was stored as a res.await attribute on M, while expressions have had a Pexp_await node since #7368 (whose changelog entry noted that the attribute was still used for modules). This adds Pmod_await of module_expr, so res.await now only appears in the v0 PPX wire representation.

Changes

  • Parsetree: Pmod_await, with Ast_helper.Mod.await, the mapper and iterator, printast, pprintast, depend and the syntax debugger. The current AST magic numbers are bumped.

  • Parser: produces Pmod_await; its location includes the await keyword. The attributes written after await belong to the await node, like those of @w (await M), and the module keeps its own: await @b (@a M).

  • v0 bridge: PPXs see the same attributes, in the same order, as with the old attribute encoding. Ast_mapper_to0 writes the inner module's attributes, then the res.await marker (carrying the await's location), then the await node's attributes, e.g. [@a; res.await; @b] for await @b (@a M); Ast_mapper_from0 splits them at the last marker.

  • Frontend: Ast_await.awaited_module_path recognizes await M, await (M: S) and (await M: S) structurally, also with repeated awaits. It's used for the dynamic import rewrite and for the "not in an async context" check. Ast_await.is_await and Ast_attributes.has_await_payload are removed; they also accepted a user-written @await attribute for that check, which no longer means anything.

  • Builtin ppx: like Pexp_await, Pmod_await doesn't reach the type checker. After the dynamic import rewrite, any other await is removed, with its attributes kept on the module it wraps, after the module's own. That keeps the current behavior: such an await has no effect, e.g. module M = await F(X) is just F(X), and the type checker sees the same trees as on master. Making that an error would be a separate, user-visible change.

  • Type checker: module type of now looks up its module path inside the scope of the module's @warning attributes, so module type of @warning("-3") Old and a dynamic import of a deprecated module with @warning("-3") no longer report warning 3 (CHANGELOG entry).

  • Printer: await is printed by its own node instead of being special-cased among the attributes, removing most of the await handling Fix module expressions losing attributes, await or parens when formatting #8735 needed:

    • await @w M, with the await node's attributes after await, and parens around an attributed, constrained or awaited operand: await @w (@a M), await (await M);
    • (await F)(X) when applied.

    await (@a X) and await @w (await X), which Fix module expressions losing attributes, await or parens when formatting #8735 could not round-trip because they were stored on one node, now print as written.

Behavior

  • Compiling every combination of 12 module await forms and 5 contexts (top-level and local, sync and async) gives the same JavaScript and the same errors as master; this covers dynamic imports, the async-context error and the no-op cases. The only difference is that errors on an awaited module now point at the whole await M rather than at M.
  • Formatting the repo's .res/.resi files gives the same output as master.

Tests

  • ast-mapping/ModuleAwait.res: every await form, with attributes on the await node and on the inner module, and a nested await. The parsetree after the v0 round trip is identical to the original.
  • printer/modExpr/attributes.res: @w (await X), await @w (await X) and an awaited structure as a functor argument.
  • The parser snapshot for expressions/await.res now shows the await node.
  • ounit_ast_mapper0_tests.ml: the v0 attribute order for parsed sources, the await location through the v0 round trip, a v0 marker without a location, and nested awaits as dynamic imports.
  • super_errors/warning_3_module_type_of_scope.res: @warning("-3") on module type of and on a dynamic import.
  • tests/tests/src/module_await_noop.res: module type of await M doesn't strengthen, and G(await {}) applies a generative functor.

🤖 Generated with Claude Code

@cknitt
cknitt added this pull request to stack #8739 October 8, 2026 06:31
cknitt added a commit that referenced this pull request Oct 8, 2026
Signed-off-by: Christoph Knittel <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cknitt
cknitt marked this pull request as ready for review October 8, 2026 06:43
@cknitt

cknitt commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 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-08T13:55:59.715689Z 9a5861d 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.

@codecov

codecov Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.01923% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.97%. Comparing base (ac96983) to head (8d66864).

Files with missing lines Patch % Lines
compiler/ml/ast_mapper_from0.ml 78.78% 7 Missing ⚠️
tests/ounit_tests/ounit_ast_mapper0_tests.ml 91.95% 7 Missing ⚠️
compiler/syntax/src/res_parsetree_viewer.ml 25.00% 3 Missing ⚠️
compiler/ml/printast.ml 0.00% 2 Missing ⚠️
compiler/ml/typemod.ml 33.33% 2 Missing ⚠️
compiler/syntax/src/res_printer.ml 85.71% 2 Missing ⚠️
compiler/ml/ast_iterator.ml 0.00% 1 Missing ⚠️
compiler/ml/depend.ml 0.00% 1 Missing ⚠️
compiler/ml/typecore.ml 0.00% 1 Missing ⚠️
compiler/syntax/src/res_comments_table.ml 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8738      +/-   ##
==========================================
+ Coverage   79.88%   79.97%   +0.09%     
==========================================
  Files         464      464              
  Lines       63126    63236     +110     
==========================================
+ Hits        50428    50573     +145     
+ Misses      12698    12663      -35     
Files with missing lines Coverage Δ
compiler/frontend/ast_attributes.ml 91.78% <ø> (-0.12%) ⬇️
compiler/frontend/bs_builtin_ppx.ml 90.84% <100.00%> (-0.36%) ⬇️
compiler/ml/ast_await.ml 100.00% <100.00%> (+5.88%) ⬆️
compiler/ml/ast_helper.ml 82.68% <100.00%> (+0.06%) ⬆️
compiler/ml/ast_mapper.ml 90.15% <100.00%> (+0.02%) ⬆️
compiler/ml/ast_mapper_to0.ml 75.49% <100.00%> (+3.55%) ⬆️
compiler/ml/pprintast.ml 92.11% <100.00%> (+<0.01%) ⬆️
compiler/syntax/src/res_ast_debugger.ml 98.11% <100.00%> (+<0.01%) ⬆️
compiler/syntax/src/res_core.ml 91.95% <100.00%> (+<0.01%) ⬆️
compiler/syntax/src/res_parens.ml 83.76% <100.00%> (+0.06%) ⬆️
... and 10 more

... and 4 files with indirect coverage changes

🚀 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 8, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/darwin-x64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: 8d66864

@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: 27a3592c97

ℹ️ 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 Outdated
@cknitt

cknitt commented Oct 8, 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: 51c9a0dfaa

ℹ️ 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/ml/ast_mapper_to0.ml Outdated
Comment thread compiler/ml/ast_await.ml Outdated
@cknitt

cknitt commented Oct 8, 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: 148ba90d12

ℹ️ 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/ml/ast_await.ml Outdated
Base automatically changed from print-module-expr-attributes to master October 8, 2026 08:28
@cknitt
cknitt force-pushed the module-await-node branch from 148ba90 to 52ca529 Compare October 8, 2026 08:28
cknitt and others added 5 commits October 8, 2026 08:34
`await M` on a module expression was stored as a `res.await` attribute
on M, while expressions already have `Pexp_await`. Add `Pmod_await` and
have the parser produce it, so `res.await` only remains in the v0 PPX
wire representation: `Ast_mapper_to0` writes it like for `Pexp_await`
(the await node's attributes, the marker, then the inner module's) and
`Ast_mapper_from0` splits them again.

The frontend recognizes awaited module paths structurally
(`Ast_await.awaited_module_path`) for the dynamic import rewrite and the
async context check. Any other `await` on a module still has no effect:
the type checker types `Pmod_await M` as `M`. Errors on an awaited
module now point at the whole `await M`.

The printer no longer special-cases a `res.await` attribute: `await` is
printed by its own node, which also lets `await @w (await X)` and
`@w (await X)` round-trip. Formatting of existing code is unchanged.

Bump the current AST magic numbers for the new constructor.

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>
An await node's own attributes print before it, so a doc comment on it starts the printed argument: F((/** doc */ (await X))) printed as F(/** doc */ (await X)), which doesn't parse. Only a functor's attributes print elsewhere (on its first parameter).

Signed-off-by: Christoph Knittel <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v0 bridge folds an await node and its operand into one node, marked
with `res.await`. The marker was written without a location, so after a
round trip through an external PPX the await node took its operand's
location and lost the span of the `await` keyword. Store the await
node's location on the marker, like `res.braces` does, and restore it,
falling back to the node's location for markers without one. This also
applies to `Pexp_await`, which had the same problem.

The dynamic import rewrite replaced an await node with its operand and
dropped the await node's attributes, e.g. a `@warning` in
`(@warning("-3") (await List): ListT)` as a PPX may produce it. Keep them
on the imported module, as when await was an attribute.

Signed-off-by: Christoph Knittel <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`module M = await (await List)` used to be a dynamic import: both awaits
became `res.await` attributes on the same node. With `Pmod_await` it is
two nested nodes, and the dynamic import forms only looked through one,
so it silently became a static module reference and skipped the async
context check. `Ast_await.awaited_module_path` now looks through any
number of awaits around the module path and around the constraint, and
the toplevel and local module type pre-scans use it too.
`create_await_module_expression` removes nested awaits as well.

Compiling 19 module await forms in 5 contexts gives the same JavaScript
and errors as master.

Signed-off-by: Christoph Knittel <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cknitt
cknitt force-pushed the module-await-node branch from 52ca529 to b599968 Compare October 8, 2026 08:35
@cknitt

cknitt commented Oct 8, 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: b599968499

ℹ️ 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/ml/ast_await.ml Outdated
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

cknitt and others added 2 commits October 8, 2026 09:39
`awaited_module_path` and `create_await_module_expression` each removed
the awaits around a dynamic import, the second time keeping their
attributes. `awaited_module_path` now returns the module to import with
its awaits removed and their attributes kept, and
`create_await_module_expression` just packs it.

The toplevel and local module cases each declared
`module type __List__ = module type of List` once per module with the
same code; it is now `local_module_type_decl`. Both callers bind its
result before mapping the rest, since a later import of the same module
must find it already declared.

Signed-off-by: Christoph Knittel <christoph@knittel.cc>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`type_module_type_of` looked a module path up without the warning scope
of the module's attributes, unlike `type_module`, so
`module type of @warning("-3") DeprecatedModule` still reported warning 3.
That includes the `module type __M__ = module type of M` a dynamic import
declares, so `module M = @warning("-3") (await DeprecatedModule)` reported
it too (also before `Pmod_await`). Look the module up in that scope.

Signed-off-by: Christoph Knittel <christoph@knittel.cc>

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

cknitt commented Oct 8, 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: 8d52a7a708

ℹ️ 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/ml/typemod.ml
cknitt and others added 2 commits October 8, 2026 10:03
Outside the dynamic import forms, `await` on a module has no effect; as
a `res.await` attribute it was invisible to the type checker. As a
`Pmod_await` node it hid the module's shape where the type checker
treats shapes specially: `module type of await M` was strengthened
(abstract types became `M.t`) instead of taking the unstrengthened
module path, and `F(await {})` was rejected for a generative functor.

Like `Pexp_await`, which never reaches the type checker, the builtin
ppx now removes the remaining awaits after the dynamic import rewrite,
keeping their attributes on the module they wrap. The type checker's
`Pmod_await` case only remains for trees that bypass it.

Type checking every input of the module expression round-trip harness
(7950 cases) gives the same exit codes, JavaScript and errors as master.

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

cknitt commented Oct 8, 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: 29f7bc5bff

ℹ️ 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/ml/ast_mapper_to0.ml Outdated
PPXs saw await on a module as the res.await attribute, after the module's
own attributes and before those written after await or around the await:
[await @b (@A M)] was [@A; res.await; @b]. The attributes after await now
belong to the Pmod_await node, as those of [@b (await M)] already did, and
the v0 bridge emits them in that order. The printer prints an await's
attributes after await and parenthesizes an attributed operand.

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

cknitt commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 9a5861d0a2

ℹ️ 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".

@cknitt
cknitt requested a review from cristianoc October 8, 2026 14:24
The trailing underscore in Ast_helper is for OCaml keywords, as in
Mod.functor_ and Mod.constraint_; await isn't one, and the expression helper
is Exp.await.

Signed-off-by: Christoph Knittel <christoph@knittel.cc>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cknitt
cknitt merged commit 23a4197 into master Oct 8, 2026
24 checks passed
@cknitt
cknitt deleted the module-await-node branch October 8, 2026 16:58
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.

2 participants