Skip to content

fmt: accept struct-update spread in object literals (B-900) - #4364

Merged
aaronvg merged 3 commits into
canaryfrom
aaron/b-900
Aug 11, 2026
Merged

aaronvg merged 3 commits into
canaryfrom
aaron/b-900

Conversation

@aaronvg

@aaronvg aaronvg commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes B-900.

Problem

baml fmt rejected any object literal containing a ...spread member:

error: while formatting: Expected token/node OBJECT_FIELD or R_BRACE, but found SPREAD_ELEMENT

The syntax parses and type-checks, so baml check and baml test passed on files baml fmt refused to format at all. Two-line repro:

class P { name: string, score: int }
function f(p: P) -> P { P { ...p, score: 1 } }

Cause

ObjectInitializer::from_cst in baml_fmt only admitted OBJECT_FIELD nodes, so a SPREAD_ELEMENT sibling fell through to the catch-all error arm.

Fix

Added an ObjectMember enum (Field | Spread) and a SpreadElement strong-AST node, and widened the object-literal member list to hold either.

Members stay in one ordered list rather than being split into "spreads" and "fields", because order is semantic — later members win at runtime, so P { score: 9, ...p } evaluates to 1 while P { ...p, score: 9 } evaluates to 9. Splitting them would silently change program behavior. ... prints tight against its operand.

The change is scoped to OBJECT_LITERAL, the only node where the parser emits SPREAD_ELEMENT (parse_object_literal_body is its sole caller). Map and array literals reject spread at parse time, so they keep using ObjectField directly.

Tests

Four regression tests in object_spread_tests, each asserting idempotency:

  • single-line spread round-trips as P { ...p, score: 1 }
  • order preserved across ...p, score, score, ...p, ...a, ...b, and ...a, score, ...b
  • multi-line path with a comment above the spread member
  • spread of a call expression (...base()), not just a bare name

Full suite: 124 passed, 0 failed — 120 pre-existing plus these 4, including the 1024-case formatter_scenario_matrix.

Verification

Formatting a messy input now produces stable output, and a second pass is a no-op:

function tight(p: Config) -> Config {
    Config { ...p, alpha: "x" }
}

function wide(base: Config) -> Config {
    Config {
        // inherit everything from base first
        ...base,
        alpha: "a much longer override value that forces the multi-line path here",
    }
}

Also confirmed against the case that surfaced this: Example 2 of plugins/baml/skills/core/SKILL.md in the baml-skill repo now passes baml fmt, baml check, and baml test. That example demonstrates spread while the same skill instructs agents to run baml fmt, so the guidance was self-contradicting until this landed.

🤖 Generated with Claude Code

https://claude.ai/code/session_017WeFQbEGpSJjzM79LHJ8du


Note

Low Risk
Formatter-only change scoped to object-literal AST/printing; no language semantics, auth, or data-handling paths are modified.

Overview
baml fmt now formats object literals that contain ...spread members, which previously failed with an unexpected SPREAD_ELEMENT error even though the syntax already parsed and type-checked.

ObjectInitializer members are modeled as an ordered ObjectMember list (Field | Spread) so field/spread order is preserved — later members win at runtime. Spreads print tight against their operand (...expr), including call expressions, on both single- and multi-line layouts.

Adds regression tests covering order preservation, multi-line comments, call operands, and formatter idempotency.

Reviewed by Cursor Bugbot for commit e9058c1. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added support for object spread syntax (...expression) in object initializers.
    • Preserved the original order of fields, spread elements, and comments when formatting.
    • Supported spreads across single-line and multi-line object formatting.
    • Enabled arbitrary spread expressions, including function calls.
  • Bug Fixes

    • Improved validation messages for object initializer members.
    • Ensured formatting remains consistent when run repeatedly.

`baml fmt` rejected any object literal containing a `...spread` member:

    error: while formatting: Expected token/node OBJECT_FIELD or R_BRACE,
    but found SPREAD_ELEMENT

The syntax parses and type-checks fine, so `baml check` and `baml test`
both passed on files that `baml fmt` refused to format at all.

`ObjectInitializer::from_cst` only admitted OBJECT_FIELD nodes. Introduce
an `ObjectMember` enum (Field | Spread) plus a `SpreadElement` strong-AST
node, and widen the object-literal member list to hold either.

Member order is semantic — later members win at runtime, so
`P { score: 9, ...p }` and `P { ...p, score: 9 }` evaluate differently.
Members are kept in a single ordered list so source order round-trips
verbatim; `...` is printed tight against its operand.

Scoped to OBJECT_LITERAL, the only node where the parser emits
SPREAD_ELEMENT (`parse_object_literal_body` is its sole caller). Map and
array literals reject spread at parse time, so they keep using
ObjectField directly.

Tests: single-line, multi-line, comment retention, spread of a call
expression, and order preservation across four spread/field arrangements
— each asserting idempotency.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_017WeFQbEGpSJjzM79LHJ8du
@linear

linear Bot commented Aug 11, 2026

Copy link
Copy Markdown

B-900

@vercel

vercel Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview Aug 11, 2026 10:54pm
promptfiddle2 Ready Ready Preview Aug 11, 2026 10:54pm

Request Review

@aaronvg
aaronvg enabled auto-merge August 11, 2026 22:30
@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@vercel
vercel Bot temporarily deployed to Preview – beps August 11, 2026 22:30 Inactive
@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The formatter adds ObjectMember and SpreadElement support for object initializers. It preserves field and spread order, handles trivia and multiline output, and adds regression tests for formatting and idempotence.

Changes

Object spread formatting

Layer / File(s) Summary
AST parsing and printing
baml_language/crates/baml_fmt/src/ast/expressions.rs
Object initializers accept fields and spread elements through ObjectMember. SpreadElement parses, sizes, preserves trivia, and prints ...expr.
Formatting regression coverage
baml_language/crates/baml_fmt/src/lib.rs
Tests cover spread syntax, member order, comments, multiline formatting, call expressions, and idempotence.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ObjectInitializerParser
  participant ObjectMember
  participant SpreadElementPrinter
  ObjectInitializerParser->>ObjectMember: Parse field or spread element
  ObjectMember->>SpreadElementPrinter: Format spread expression
  SpreadElementPrinter-->>ObjectMember: Return formatted ...expr
  ObjectMember-->>ObjectInitializerParser: Preserve ordered members
Loading

Possibly related PRs

  • BoundaryML/baml#4107: This PR also changes formatter AST and printing logic in expressions.rs for another expression syntax.
  • BoundaryML/baml#4136: This PR also modifies ObjectField representation in the formatter AST.
  • BoundaryML/baml#4308: This PR also changes formatter behavior and regression tests in expressions.rs and lib.rs.

Poem

A rabbit found three dots in line,
And made each spread element shine.
Fields stayed put, comments stayed near,
Repeated formatting brought no fear.
“Hop!” said the formatter, “all order is clear.”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: support for struct-update spread in object literals.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aaron/b-900

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Silences clippy::doc_markdown (pedantic, warn-by-default in this
workspace) on the new object_spread_tests module.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_017WeFQbEGpSJjzM79LHJ8du
@vercel
vercel Bot temporarily deployed to Preview – beps August 11, 2026 22:35 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 August 11, 2026 22:43 Inactive
Both failures on the previous run were infrastructure flakes unrelated to
this change (which only touches baml_fmt):
  - Release bridge_cffi (x86_64-unknown-linux-gnu): runner TLS failure in
    the git checkout step, before any code was built.
  - cargo test (aarch64-apple-darwin): random SIGABRT in a bex_engine
    async-runtime test. canary hits the same job with a different random
    bex_engine SIGABRT, and the test passes 12/12 locally on macOS.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_017WeFQbEGpSJjzM79LHJ8du
@vercel
vercel Bot temporarily deployed to Preview – beps August 11, 2026 22:46 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 August 11, 2026 22:54 Inactive
@aaronvg
aaronvg added this pull request to the merge queue Aug 11, 2026
@github-actions

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 26.4 MB 11.2 MB file 27.4 MB -947.8 KB (-3.5%) OK
✅ packed-program Linux 🔒 17.0 MB 6.9 MB file 18.6 MB -1.6 MB (-8.6%) OK
✅ baml-cli macOS 🔒 20.6 MB 9.8 MB file 21.3 MB -698.5 KB (-3.3%) OK
✅ packed-program macOS 🔒 13.4 MB 6.1 MB file 14.5 MB -1.1 MB (-7.7%) OK
✅ baml-cli Windows 🔒 22.1 MB 10.0 MB file 23.0 MB -842.3 KB (-3.7%) OK
✅ packed-program Windows 🔒 14.2 MB 6.1 MB file 15.5 MB -1.4 MB (-8.8%) OK
✅ bridge_wasm WASM 15.8 MB 🔒 4.3 MB gzip 4.6 MB -347.7 KB (-7.5%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

Merged via the queue into canary with commit 5e6edc8 Aug 11, 2026
84 checks passed
@aaronvg
aaronvg deleted the aaron/b-900 branch August 11, 2026 23:09

This branch was previously deployed

2 inactive deployments
Preview – promptfiddle2 — e9058c10 Deployed Aug 11, 2026 by vercel[bot]
Preview – beps — e9058c10 Deployed Aug 11, 2026 by vercel[bot]
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.

1 participant