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.
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.
modifiedFormatmulti-line item spacing, opt-in
moduleA = {let x = 1}let y = 2
rescript
Before and After are equal always, since the previous commit"experimental-features": {"MultilineItemSpacing": true}
moduleA = {let x = 1}let y = 2
Format unchanged (default):
moduleA = {let x = 1}let y = 2
rescript
After without the feature
moduleA = {let x = 1}let y = 2
modifiedDiagnosticunknown 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.