Skip to content

Represent array and list literals with spreads with a Pexp_spread node - #8905

Merged
cknitt merged 2 commits into
operand-line-suffixfrom
parsetree-spread
Oct 27, 2026
Merged

cknitt merged 2 commits into
operand-line-suffixfrom
parsetree-spread

Conversation

@cknitt

@cknitt cknitt commented Oct 18, 2026

Copy link
Copy Markdown
Member

Part of #8624 (res.spread item).

The parser used to turn [a, ...b] and list{...a, b} into calls of Primitive_array.spread / Primitive_list.spread with a res.spread attribute on the function. They are now a Pexp_spread (kind, elements) node with items and spreads in source order, typed as Texp_spread, and translated to new Parray_spread / Plist_spread Lambda primitives that compile to the same runtime calls. A list literal whose only spread is its tail (list{a, ...b}) stays a chain of ::, as before.

Effects

  • JS output: unchanged. tests/tests rebuilds byte-identical, and every item/spread sequence up to length 4 (arrays and lists, top level and in functions) compiles to the same JS with the old and new bsc.
  • Formatter: fixes. The old printer read the call back and lost information:
    • [...[1, 2], 3, ...[]] printed as [1, 2, 3, ] (invalid), and list{...xs, ...list{}, 1} as list{...xs, , 1}
    • attributes on spread literals were dropped (@attr [1, ...a] → [1, ...a])
    • comments in list spreads moved to the end of the line
  • Errors: spreading the wrong collection type now reports the spread (This has type: list<int>, but it's expected to have type: array<int>) instead of "This array item has type list…". With a type annotation, the mismatch is reported at the element, as for a plain array literal.
  • PPX (v0 wire): unchanged; to0 emits the old call and from0 decodes it back. The generated item groups get ghost locations, so a group of items can be told apart from a spread array/list literal, and the round trip is exact. A spread call built by a PPX with real locations decodes to spreads only, which concatenates the same values. Calls without the marker stay calls.
  • Editor: pipe completion still types the literal as the runtime call (the same approach as tagged templates); semantic tokens no longer need the attribute check.
  • Magic numbers: ResImpl/ResIntf and cmt bumped. The new Lambda primitives come after the last constant constructor, as Pdict_spread did.

New tests

  • ast-mapping/SpreadLiterals.res and an ounit case for the v0 wire shape, the exact round trip, PPX-built calls and unmarked calls
  • printer cases for literal spreads, attributes, line breaking and list comments
  • super_errors: array_spread_type_mismatch, list_spread_type_mismatch, array_spread_annotation_mismatch
  • analysis: SpreadLiterals.res (AST dump, hints, highlighting, pipe completion)

A stacked follow-up will fix a formatter bug that predates this PR: a line comment inside a literal used as a switch/if/try/loop operand moves on every reformat.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EUpL6dKCiSwAeriDGRHqNr

@codecov

codecov Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.86047% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.42%. Comparing base (364e152) to head (a97a105).

Files with missing lines Patch % Lines
compiler/ml/printast.ml 0.00% 8 Missing ⚠️
tests/ounit_tests/ounit_ast_mapper0_tests.ml 82.85% 6 Missing ⚠️
compiler/ml/printtyped.ml 0.00% 4 Missing ⚠️
compiler/ml/rec_check.ml 0.00% 4 Missing ⚠️
analysis/src/hint.ml 25.00% 3 Missing ⚠️
analysis/reanalyze/src/arnold.ml 0.00% 2 Missing ⚠️
compiler/ml/ast_mapper_from0.ml 93.10% 2 Missing ⚠️
compiler/ml/printlambda.ml 0.00% 2 Missing ⚠️
compiler/ml/typecore.ml 83.33% 2 Missing ⚠️
analysis/src/dump_ast.ml 90.00% 1 Missing ⚠️
... and 5 more
Additional details and impacted files
@@                   Coverage Diff                   @@
##           operand-line-suffix    #8905      +/-   ##
=======================================================
- Coverage                81.44%   81.42%   -0.02%     
=======================================================
  Files                      459      460       +1     
  Lines                    64553    64633      +80     
=======================================================
+ Hits                     52573    52630      +57     
- Misses                   11980    12003      +23     
Files with missing lines Coverage Δ
analysis/reanalyze/src/side_effects.ml 78.46% <100.00%> (+0.33%) ⬆️
analysis/src/completion_front_end.ml 81.61% <100.00%> (+0.04%) ⬆️
analysis/src/semantic_tokens.ml 88.63% <ø> (-0.16%) ⬇️
compiler/core/lam_analysis.ml 78.91% <100.00%> (ø)
compiler/core/lam_compile_primitive.ml 81.36% <100.00%> (+0.11%) ⬆️
compiler/ml/ast_helper.ml 88.25% <100.00%> (+0.03%) ⬆️
compiler/ml/ast_iterator.ml 93.97% <100.00%> (+0.03%) ⬆️
compiler/ml/ast_mapper.ml 98.47% <100.00%> (+0.01%) ⬆️
compiler/ml/ast_mapper_to0.ml 79.96% <100.00%> (+0.29%) ⬆️
compiler/ml/depend.ml 87.04% <100.00%> (+0.07%) ⬆️
... and 23 more

... and 2 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 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: a97a105

@github-actions

Copy link
Copy Markdown

@cknitt

cknitt commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 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-27T08:41:02.323898Z 6562b42 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

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: aa5c53bae8

ℹ️ 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 force-pushed the parsetree-spread branch 3 times, most recently from 2e93ed3 to 798978d Compare October 21, 2026 18:03
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Changelog

🐛 Bug fix

  • Fix the formatter dropping spreads of array and list literals, attributes on literals with spreads, and comments in list literals with spreads.

💅 Polish

  • Report a spread of the wrong collection type, such as a list spread into an array, as a mismatch of the spread instead of an array item.

🏠 Internal

  • Represent array and list literals with spreads with a dedicated parsetree node.

Semantic Diff

2 commits · Full page

Represent array and list literals with spreads with a Pexp_spread node

b93e551 · cknitt + Claude · Oct 18, 2026, 18:22 UTC

Why

The parser turned [a, ...b] and list{...a, b} into calls of Primitive_array.spread and Primitive_list.spread marked with res.spread, so the formatter had to rebuild the literal from the call and got it wrong: spreads of literals were flattened, attributes on the literal were dropped and an empty list spread printed as an empty element.

Pexp_spread keeps the items and spreads in source order, and the type checker gives it a Texp_spread node. Translation emits new Parray_spread and Plist_spread primitives that compile to the same runtime calls. A list literal whose only spread is its tail stays a chain of ::. The parsetree and typed tree layouts change; the release bumps their magic numbers.

AST modified (array literal with a spread)

let x = [1, ...ys]

parse

 expression
-  Pexp_apply
-  expression
-    attribute "res.spread"
-    Pexp_ident "Primitive_array.spread"
-    <arg>
-    Nolabel
+  Pexp_spread array
+    Pspread_item
       expression
-        Pexp_array
-          expression
-            Pexp_array
-              expression
-                Pexp_constant PConst_int (1,None)
-          expression
-            Pexp_ident "ys"
+        Pexp_constant PConst_int (1,None)
+    Pspread
+      expression
+        Pexp_ident "ys"

Typedtree added (Texp_spread)

let ys = [2]
let x = [1, ...ys]

typed · After

expression
  Texp_spread array
    expression
      Texp_array
        expression
          Texp_constant Const_int 1
    expression
      Texp_ident "ys/1001"

Format modified (literals with spreads)

let x = [...[1, 2], 3, ...[]]
let y = @attr list{...xs, ...list{}, 1}

rescript

-let x = [1, 2, 3, ]
-let y = list{...xs, , 1}
+let x = [...[1, 2], 3, ...[]]
+let y = @attr list{...xs, ...list{}, 1}

Review

  • Why do the generated item groups have ghost locations? The v0 encoding has no marker on them, and the ghost flag tells a group of items from an array or list literal that is spread. A call built by a PPX with real locations decodes to spreads only, which concatenates the same values.
  • Why does the typed node hold the concatenated parts rather than the elements? The parts are the arrays and cons chains the runtime call takes, so the items keep the errors of array and list literals and translation needs no list building of its own.
Move the v0 spread encoding into the PPX bridge and test spread literals

a97a105 · cknitt + Claude · Oct 27, 2026, 07:00 UTC

The res.spread call is only the v0 PPX wire format, so it now lives in the bridge: Ast_mapper_to0 builds it (spread_call, spread_marker) and Ast_mapper_from0 decodes it, with the module-name lookup local to the decoder. Ast_spread keeps what the type checker and analysis use: groups, parts and module_name. The ghost locations of the generated parts stay, as for any node the compiler makes up. Behaviour is unchanged.

The new mocha test runs array literals with several spreads, items between them, a leading item and an empty spread, list literals with spreads before the tail and a tail-only spread, and checks that both kinds evaluate their items and spreads left to right; its generated JavaScript pins the Primitive_array.spread and Primitive_list.spread calls and the order of their parts.

@cknitt

cknitt commented Oct 21, 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: 798978d3ad

ℹ️ 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/translcore.ml Resolved
@cknitt

cknitt commented Oct 27, 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: a091db154b

ℹ️ 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 removed this pull request from stack #8907 October 27, 2026 08:04
@cknitt
cknitt changed the base branch from master to operand-line-suffix October 27, 2026 08:05
@cknitt
cknitt added this pull request to stack #9062 October 27, 2026 08:05
@cknitt

cknitt commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 6562b42cde

ℹ️ 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 and others added 2 commits October 27, 2026 10:36
Why:
    The parser turned [a, ...b] and list{...a, b} into calls of
    Primitive_array.spread and Primitive_list.spread marked with res.spread,
    so the formatter had to rebuild the literal from the call and got it
    wrong: spreads of literals were flattened, attributes on the literal
    were dropped and an empty list spread printed as an empty element.

Pexp_spread keeps the items and spreads in source order, and the type
checker gives it a Texp_spread node. Translation emits new Parray_spread
and Plist_spread primitives that compile to the same runtime calls. A list
literal whose only spread is its tail stays a chain of ::. The parsetree
and typed tree layouts change; the release bumps their magic numbers.

AST modified (array literal with a spread):
Example:
    let x = [1, ...ys]
Before (parse):
    expression
      Pexp_apply
      expression
        attribute "res.spread"
        Pexp_ident "Primitive_array.spread"
      [
        <arg>
        Nolabel
          expression
            Pexp_array
            [
              expression
                Pexp_array
                [
                  expression
                    Pexp_constant PConst_int (1,None)
                ]
              expression
                Pexp_ident "ys"
            ]
      ]
After (parse):
    expression
      Pexp_spread array
      [
        Pspread_item
          expression
            Pexp_constant PConst_int (1,None)
        Pspread
          expression
            Pexp_ident "ys"
      ]

Typedtree added (Texp_spread):
Example:
    let ys = [2]
    let x = [1, ...ys]
After (typed):
    expression
      Texp_spread array
      [
        expression
          Texp_array
          [
            expression
              Texp_constant Const_int 1
          ]
        expression
          Texp_ident "ys/1001"
      ]

Format modified (literals with spreads):
Example:
    let x = [...[1, 2], 3, ...[]]
    let y = @attr list{...xs, ...list{}, 1}
Before (rescript):
    let x = [1, 2, 3, ]
    let y = list{...xs, , 1}
After (rescript):
    let x = [...[1, 2], 3, ...[]]
    let y = @attr list{...xs, ...list{}, 1}

Review: Why do the generated item groups have ghost locations? The v0 encoding has no marker on them, and the ghost flag tells a group of items from an array or list literal that is spread. A call built by a PPX with real locations decodes to spreads only, which concatenates the same values.
Review: Why does the typed node hold the concatenated parts rather than the elements? The parts are the arrays and cons chains the runtime call takes, so the items keep the errors of array and list literals and translation needs no list building of its own.

Area: syntax printer typechecker lambda codegen analysis reanalyze tests
Output: unchanged, tests/tests JS byte-identical after a rebuild; a
 combinatorial harness of all item/spread sequences up to length 4 compiles
 to identical JS with the old and new bsc
Formatting: changed spreads of array and list literals and attributes on
 literals with spreads are kept; comments in list spreads stay in place
Formats: ast cmt (layouts changed; magic numbers left to yarn
 changelog:release)
PPX: unchanged wire: the v0 encoding is the res.spread call it was, decoded
 back to Pexp_spread (ast-mapping fixture SpreadLiterals.res)
Editor: unchanged: pipe completion types the literal as the runtime call;
 SpreadLiterals.res covers AST dump, hints, highlighting and completion
Tests: +tests/syntax_tests/data/ast-mapping/SpreadLiterals.res
 +tests/analysis_tests/tests/src/SpreadLiterals.res
 +tests/build_tests/super_errors/fixtures/array_spread_type_mismatch.res
 +tests/build_tests/super_errors/fixtures/list_spread_type_mismatch.res
 +tests/build_tests/super_errors/fixtures/array_spread_annotation_mismatch.res
 ~tests/ounit_tests/ounit_ast_mapper0_tests.ml
 ~tests/syntax_tests/data/printer/expr/array.res
 ~tests/syntax_tests/data/printer/expr/list.res
 ~tests/syntax_tests/data/printer/comments/array.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUpL6dKCiSwAeriDGRHqNr
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
The res.spread call is only the v0 PPX wire format, so it now lives in the
bridge: Ast_mapper_to0 builds it (spread_call, spread_marker) and
Ast_mapper_from0 decodes it, with the module-name lookup local to the
decoder. Ast_spread keeps what the type checker and analysis use: groups,
parts and module_name. The ghost locations of the generated parts stay, as
for any node the compiler makes up. Behaviour is unchanged.

The new mocha test runs array literals with several spreads, items between
them, a leading item and an empty spread, list literals with spreads before
the tail and a tail-only spread, and checks that both kinds evaluate their
items and spreads left to right; its generated JavaScript pins the
Primitive_array.spread and Primitive_list.spread calls and the order of
their parts.

Area: compiler/ml tests
Output: unchanged
Tests: +tests/tests/src/spread_literal_test.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt removed this pull request from stack #9062 October 27, 2026 10:50
@cknitt
cknitt added this pull request to stack #9064 October 27, 2026 10:50
@cknitt
cknitt marked this pull request as ready for review October 27, 2026 11:40
@cknitt
cknitt requested a review from cristianoc October 27, 2026 11:40
@cknitt
cknitt merged commit cd641cb into master Oct 27, 2026
50 of 57 checks passed
@cknitt
cknitt deleted the parsetree-spread branch October 27, 2026 11:56
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