Semantic Diff

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

5 commits

0326cb5a3b407a7c5905392d231e4b4962142d48..pr-8961

d4fe75321 · Roland Peelen · Oct 22, 2026

Format a blank line around module items that span several lines

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.

modifiedFormat structure items

module Details = {  let make = () => React.string("details")}module Output = Tool.TextResult.Outputlet failed = Tool.TextResult.failed
rescript
Before
After
module Details = {module Details = { let make = () => React.string("details") let make = () => React.string("details")}} module Output = Tool.TextResult.Outputmodule Output = Tool.TextResult.Outputlet failed = Tool.TextResult.failedlet failed = Tool.TextResult.failed

Format unchanged (one-line items with comments):

let a = 1// about blet b = 2 // trailinglet c = 3
rescript
After
let a = 1// about blet b = 2 // trailinglet 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.
fcda8d73a · Roland Peelen · Oct 23, 2026

Make multi-line item spacing the experimental feature MultilineItemSpacing

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.

modifiedFormat 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

modifiedDiagnostic unknown experimental feature

"experimental-features": {"FooBar": true}
diagnostic
Before
After
Unknown experimental feature 'FooBar'. Available features: LetUnwrapUnknown 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.
13bd5534f · Roland Peelen · Oct 23, 2026

Compare format output without carriage returns in the spacing test

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

2af588080 · Roland Peelen · Oct 23, 2026

Cover multi-line item spacing with unit tests

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.

f209b6654 · Roland Peelen · Oct 23, 2026

Check is_nil on every document constructor

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.