Skip to content

Experimental formatter feature MultilineItemSpacing: blank lines around multi-line module items - #8961

Draft
rolandpeelen wants to merge 5 commits into
masterfrom
formatter-multiline-item-spacing
Draft

rolandpeelen wants to merge 5 commits into
masterfrom
formatter-multiline-item-spacing

Conversation

@rolandpeelen

@rolandpeelen rolandpeelen commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Closes #8960.

Adds the experimental feature MultilineItemSpacing. When a project enables it, rescript format separates a structure or signature item that prints on more than one line from its neighbours with a blank line. One-line items stay together, and blocks inside expressions keep their layout. Like let? (LetUnwrap), it is opt-in:

"experimental-features": { "MultilineItemSpacing": true }
// Without the feature (unchanged)
module Details = {
  let make = () => React.string("details")
}
module Output = Tool.TextResult.Output
let failed = Tool.TextResult.failed

// With MultilineItemSpacing
module Details = {
  let make = () => React.string("details")
}

module Output = Tool.TextResult.Output
let failed = Tool.TextResult.failed

Without the feature the formatter output is byte-for-byte unchanged: every existing syntax snapshot matches master, and no repository files need reformatting.

How

  • Printer: Res_doc.item_separator ~prev ~next is a hard line, plus a blank line when either neighbour spans several lines in the output. It is measured with the printer's own layout decisions, so formatting twice gives the same text. Res_doc.comment keeps leading comments out of that measurement. print_structure/print_signature turn it on when Experimental_features.MultilineItemSpacing is enabled.
  • Compiler: new Experimental_features.MultilineItemSpacing. bsc -enable-experimental MultilineItemSpacing -format file.res formats with it; the flag must come before -format.
  • Build systems (Rust rewatch and OCaml rewatch-ocaml): the config accepts the feature, and rescript format (files and --stdin) passes the root project's experimental flags to bsc -format. Outside a project, format runs without them, as before.
  • Docs: build-schema.json and CompilerConfigurationSpec.md list the feature.

Testing

  • New fixtures in tests/syntax_tests/data/experimental/MultilineItemSpacing/. res_parser accepts -enable-experimental, and scripts/test_syntax.sh prints and round-trips each experimental/<Feature>/ directory with that feature enabled.
  • New shared test rewatch/tests/format/05-format-experimental-multiline-item-spacing.sh (opt-in check); 01-invalid-experimental-key.sh now expects both features in the error.
  • Local: make test, make test-syntax + roundtrip, make checkformat (ocamlformat 0.29), the rewatch suite against both rescript.exe and rescript-rust.exe, dune runtest for tests/ounit_tests and tests/rewatch_ounit_tests, and cargo clippy -D warnings all pass.

Checklist

  • Added a changelog fragment (see changes/README.md), or no release notes are needed and a maintainer should apply changelog:skip.
  • Added tests for the fix or feature.
  • Signed off every commit for the DCO (git commit -s).

Why:
    Dense code, often written by AI agents, runs multi-line modules and
    functions together, which makes a file hard to scan (#8960). The
    formatter already collapses several blank lines into one; it now also
    separates an item that prints on more than one line from its
    neighbours in structures and signatures. Blocks inside expressions
    keep their layout.

Format modified (structure items):
Example:
    module Details = {
      let make = () => React.string("details")
    }
    module Output = Tool.TextResult.Output
    let failed = Tool.TextResult.failed
Before (rescript):
    module Details = {
      let make = () => React.string("details")
    }
    module Output = Tool.TextResult.Output
    let failed = Tool.TextResult.failed
After (rescript):
    module Details = {
      let make = () => React.string("details")
    }

    module Output = Tool.TextResult.Output
    let failed = Tool.TextResult.failed

Format unchanged (one-line items with comments):
Example:
    let a = 1
    // about b
    let b = 2 // trailing
    let c = 3
After (rescript):
    let a = 1
    // about b
    let b = 2 // trailing
    let c = 3

Review: Is "multi-line" decided on the source or on the output? On the output: the new Res_doc.item_separator measures the items on each side with the printer's own layout decisions, stopping at the first line break, so an item that only breaks at the line width counts and formatting twice gives the same text. All 1025 printer and idempotency fixtures are stable across two passes.
Review: Why a Res_doc.comment marker? Leading comments print inside the item's document (a value binding carries them), so a `//` line above a one-line item would make it multi-line. The marker keeps comments out of the measurement; doc comments are attributes and still count.
Review: Why move own-line trailing comments below the blank line? A comment inside the previous item's source, such as `f(a // c => 1)`, prints on its own line after the item. Formatting again makes it a leading comment of the next item, after the blank line, so the first pass puts it there too.
Review: Does this reformat existing code? Yes: 346 of the 1870 .res/.resi files `make format` covers gain blank lines. They are not reformatted in this commit, so `make checkformat` reports them.

Signed-off-by: Roland Peelen <roland@magic.dev>
@github-actions

Copy link
Copy Markdown

Semantic Diff

1 commit · Full page

Format a blank line around module items that span several lines

d4fe753 · Roland Peelen · Oct 22, 2026

Why

Dense code, often written by AI agents, runs multi-line modules and functions together, which makes a file hard to scan (#8960). The formatter already collapses several blank lines into one; it now also separates an item that prints on more than one line from its neighbours in structures and signatures. Blocks inside expressions keep their layout.

Format modified (structure items)

module Details = {
  let make = () => React.string("details")
}
module Output = Tool.TextResult.Output
let failed = Tool.TextResult.failed

rescript

 module Details = {
   let make = () => React.string("details")
 }
+
 module Output = Tool.TextResult.Output
 let failed = Tool.TextResult.failed

Format unchanged (one-line items with comments):

let a = 1
// about b
let b = 2 // trailing
let c = 3

rescript · After

let a = 1
// about b
let b = 2 // trailing
let c = 3

Review

  • Is "multi-line" decided on the source or on the output? On the output: the new Res_doc.item_separator measures the items on each side with the printer's own layout decisions, stopping at the first line break, so an item that only breaks at the line width counts and formatting twice gives the same text. All 1025 printer and idempotency fixtures are stable across two passes.
  • Why a Res_doc.comment marker? Leading comments print inside the item's document (a value binding carries them), so a // line above a one-line item would make it multi-line. The marker keeps comments out of the measurement; doc comments are attributes and still count.
  • Why move own-line trailing comments below the blank line? A comment inside the previous item's source, such as f(a // c => 1), prints on its own line after the item. Formatting again makes it a leading comment of the next item, after the blank line, so the first pass puts it there too.
  • Does this reformat existing code? Yes: 346 of the 1870 .res/.resi files make format covers gain blank lines. They are not reformatted in this commit, so make checkformat reports them.

…acing

Why:
    Spacing multi-line module items changes the output of `rescript
    format` for most existing projects (341 files in this repository
    alone), so it is opt-in, like `let?` (LetUnwrap). Without the feature
    the formatter prints exactly as before, and the snapshots of the
    previous commit return to their master state.

Format modified (multi-line item spacing, opt-in):
Example:
    module A = {
      let x = 1
    }
    let y = 2
Before (rescript): always, since the previous commit
    module A = {
      let x = 1
    }

    let y = 2
After (rescript): "experimental-features": {"MultilineItemSpacing": true}
    module A = {
      let x = 1
    }

    let y = 2

Format unchanged (default):
Example:
    module A = {
      let x = 1
    }
    let y = 2
After (rescript): without the feature
    module A = {
      let x = 1
    }
    let y = 2

Diagnostic modified (unknown experimental feature):
Example:
    "experimental-features": {"FooBar": true}
Before (diagnostic): Unknown experimental feature 'FooBar'. Available features: LetUnwrap
After (diagnostic): Unknown experimental feature 'FooBar'. Available features: LetUnwrap, MultilineItemSpacing

Review: How does `rescript format` learn about the feature? Both build systems pass the root project's experimental flags, the same ones a build passes, to `bsc` before `-format`; `-format` runs as soon as bsc reads it, so the order matters. Outside a project, format runs without them, as before. Covered by rewatch/tests/format/05-format-experimental-multiline-item-spacing.sh, which passes for both rescript.exe and rescript-rust.exe.
Review: How do the syntax tests enable it? `res_parser` accepts `-enable-experimental`, and scripts/test_syntax.sh prints and round-trips each file under tests/syntax_tests/data/experimental/<Feature>/ with that feature enabled.
Review: What about tools that call `bsc -format` directly instead of `rescript format`? They format without the feature unless they pass `-enable-experimental MultilineItemSpacing` before `-format`.

Signed-off-by: Roland Peelen <roland@magic.dev>
@rolandpeelen rolandpeelen changed the title Format a blank line around module items that span several lines Experimental formatter feature MultilineItemSpacing: blank lines around multi-line module items Oct 23, 2026
@github-actions

Copy link
Copy Markdown

Semantic Diff

1 new commit (2 total) · Full page

Make multi-line item spacing the experimental feature MultilineItemSpacing

fcda8d7 · Roland Peelen · Oct 23, 2026

Why

Spacing multi-line module items changes the output of rescript format for most existing projects (341 files in this repository alone), so it is opt-in, like let? (LetUnwrap). Without the feature the formatter prints exactly as before, and the snapshots of the previous commit return to their master state.

Format modified (multi-line item spacing, opt-in)

module A = {
  let x = 1
}
let y = 2

rescript · Before and After are equal · always, since the previous commit · "experimental-features": {"MultilineItemSpacing": true}

module A = {
  let x = 1
}

let y = 2

Format unchanged (default):

module A = {
  let x = 1
}
let y = 2

rescript · After · without the feature

module A = {
  let x = 1
}
let y = 2

Diagnostic modified (unknown experimental feature)

"experimental-features": {"FooBar": true}

diagnostic

-Unknown experimental feature 'FooBar'. Available features: LetUnwrap
+Unknown experimental feature 'FooBar'. Available features: LetUnwrap, MultilineItemSpacing

Review

  • How does rescript format learn about the feature? Both build systems pass the root project's experimental flags, the same ones a build passes, to bsc before -format; -format runs as soon as bsc reads it, so the order matters. Outside a project, format runs without them, as before. Covered by rewatch/tests/format/05-format-experimental-multiline-item-spacing.sh, which passes for both rescript.exe and rescript-rust.exe.
  • How do the syntax tests enable it? res_parser accepts -enable-experimental, and scripts/test_syntax.sh prints and round-trips each file under tests/syntax_tests/data/experimental/<Feature>/ with that feature enabled.
  • What about tools that call bsc -format directly instead of rescript format? They format without the feature unless they pass -enable-experimental MultilineItemSpacing before -format.

@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.31%. Comparing base (0326cb5) to head (f209b66).
⚠️ Report is 17 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8961      +/-   ##
==========================================
+ Coverage   81.21%   81.31%   +0.09%     
==========================================
  Files         457      456       -1     
  Lines       63853    63886      +33     
==========================================
+ Hits        51859    51947      +88     
+ Misses      11994    11939      -55     
Files with missing lines Coverage Δ
compiler/bsc/rescript_compiler_driver.ml 81.22% <ø> (ø)
compiler/ml/experimental_features.ml 86.66% <100.00%> (+9.74%) ⬆️
compiler/syntax/cli/res_cli.ml 73.33% <ø> (ø)
compiler/syntax/src/res_doc.ml 80.08% <100.00%> (+11.85%) ⬆️
compiler/syntax/src/res_printer.ml 94.06% <100.00%> (+0.01%) ⬆️

... and 28 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.

The formatter writes CRLF on Windows, so the new rewatch format test failed
there on line endings alone; it now strips them before comparing.

Signed-off-by: Roland Peelen <roland@magic.dev>
@github-actions

Copy link
Copy Markdown

Semantic Diff

1 new commit (3 total) · Full page

Compare format output without carriage returns in the spacing test

13bd553 · Roland Peelen · Oct 23, 2026

The formatter writes CRLF on Windows, so the new rewatch format test failed there on line endings alone; it now strips them before comparing.

@pkg-pr-new

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

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: f209b66

@github-actions

Copy link
Copy Markdown

Codecov reported 15 changed lines without coverage: branches of the
separator's measurement that the fixtures never reach first, the
trailing comments it moves, the new cases of will_break, is_nil and
debug, and Experimental_features.to_string. tests/syntax_tests/res_test.ml
now prints documents built for each case and checks the output, and
formats a module with the feature off and on through the printer API.
The fixture adds a multi-line template literal and a trailing comment on
a multi-line item.

print_list matched an accumulator that cannot be empty after the first
item; it now matches the one case that adds a separator and treats
every other one as the first item.

Signed-off-by: Roland Peelen <roland@magic.dev>
@github-actions

Copy link
Copy Markdown

Semantic Diff

1 new commit (4 total) · Full page

Cover multi-line item spacing with unit tests

2af5880 · Roland Peelen · Oct 23, 2026

Codecov reported 15 changed lines without coverage: branches of the separator's measurement that the fixtures never reach first, the trailing comments it moves, the new cases of will_break, is_nil and debug, and Experimental_features.to_string. tests/syntax_tests/res_test.ml now prints documents built for each case and checks the output, and formats a module with the feature off and on through the printer API. The fixture adds a multi-line template literal and a trailing comment on a multi-line item.

print_list matched an accumulator that cannot be empty after the first item; it now matches the one case that adds a separator and treats every other one as the first item.

Codecov still reported the last line of the is_nil match: its Group,
CustomLayout and BreakParent alternatives share the line with the new
ones, and no test passed them. The test now checks every constructor.

Signed-off-by: Roland Peelen <roland@magic.dev>
@github-actions

Copy link
Copy Markdown

Semantic Diff

1 new commit (5 total) · Full page

Check is_nil on every document constructor

f209b66 · Roland Peelen · Oct 23, 2026

Codecov still reported the last line of the is_nil match: its Group, CustomLayout and BreakParent alternatives share the line with the new ones, and no test passed them. The test now checks every constructor.

@cknitt

cknitt commented Oct 23, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 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-23T14:33:12.696863Z f209b66 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. 🎉

Reviewed commit: f209b66548

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

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.

Have formatter insert newlines around multi-line expressions

2 participants