Skip to content

toml: parse nested array literals with adjacent brackets - #34004

Open
robobun wants to merge 3 commits into
mainfrom
farm/79c5d798/toml-nested-array-brackets
Open

toml: parse nested array literals with adjacent brackets#34004
robobun wants to merge 3 commits into
mainfrom
farm/79c5d798/toml-nested-array-brackets

Conversation

@robobun

@robobun robobun commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Reproduction

Bun.TOML.parse("a = [[1]]")    // AggregateError: Failed to parse toml
Bun.TOML.parse("a = [[[1]]]")  // Unexpected [[
Bun.TOML.parse("a = [ [1] ]")  // OK: { a: [[1]] }

Any TOML nested array literal with adjacent [ or ] brackets was rejected, while the same document with whitespace between brackets parsed. All of these are valid TOML 1.0.

Cause

The lexer has an allow_double_bracket flag that merges adjacent [[ / ]] into single t_open_bracket_double / t_close_bracket_double tokens, used to distinguish [[array.of.tables]] headers from [table] headers. parse_value_inner forced this flag to true at the top of every call and again after each compound value's closing delimiter, so inside a = [[1]] the ]] was merged into a header-close token (and at depth 3+, the [[ was merged into a header-open token) and the array parse failed on the unexpected token.

Fix

parse_assignment and parse_value_inner now save the caller's flag value on entry, hold it false while lexing tokens inside an array or inline table, and restore the saved value before lexing the token that follows the value. At top level the saved value is true, so a [[header]] on the line after a value is still recognised; at any nested depth the saved value is false, so adjacent brackets stay two tokens.

Verification

  • bun bd test test/js/bun/resolve/toml/ passes (22 tests).
  • New tests fail on the system bun and pass on the debug build.
  • Cross-checked against smol-toml for 12 nested-array and header-after-value cases; all match.

[review] gate passed · iteration 2 · 2 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/resolve/toml/toml-parse.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (7f24b5e42)

test/js/bun/resolve/toml/toml-parse.test.ts:
(pass) Bun.TOML.parse with non-string input throws [13.39ms]
(pass) Bun.TOML.parse accepts \u{XX} at start of a basic string (#30893) [2.61ms]
(pass) Bun.TOML.parse rejects \x escape in quoted key at file start without panicking (#30893) [3.73ms]
(pass) Bun.TOML.parse rejects \u escape in quoted key at file start without panicking (#30893) [3.73ms]
(pass) Bun.TOML.parse handles trailing backslash-CR in multiline basic string (#30893) [1.87ms]
(pass) Bun.TOML.parse produces correct codepoints for \t and \f escapes [3.90ms]
(pass) Bun.TOML.parse normalizes literal CRLF to LF in multiline basic strings [2.17ms]
(pass) Bun.TOML.parse rejects out-of-range \u{...} escapes 
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (693ae6bde)

test/js/bun/resolve/toml/toml-parse.test.ts:
(pass) Bun.TOML.parse with non-string input throws [0.50ms]
(pass) Bun.TOML.parse accepts \u{XX} at start of a basic string (#30893) [0.10ms]
(pass) Bun.TOML.parse rejects \x escape in quoted key at file start without panicking (#30893) [0.10ms]
(pass) Bun.TOML.parse rejects \u escape in quoted key at file start without panicking (#30893) [0.06ms]
(pass) Bun.TOML.parse handles trailing backslash-CR in multiline basic string (#30893) [0.05ms]
(pass) Bun.TOML.parse produces correct codepoints for \t and \f escapes [0.07ms]
(pass) Bun.TOML.parse normalizes literal CRLF to LF in multiline basic strings [0.04ms]
(pass) Bun.TOML.parse rejects out-of-range \u{...} escapes without overflowing (#30825) [0.18ms]
(pass) Bun.TOML.parse rejects \u{...} escapes with no closing brace (#30825) [0.14ms]
(pass) Bun.TOML.parse still accepts in-range \u{...} escapes (#30825) [0.10ms]
(pass) Bun.TOML.parse rejects array values without comma separators (#31252) [0.25ms]
(pass) Bun.TOML.parse accepts nested array literals with adjacent brackets [0.32ms]
(pass) Bun.TOML.parse still merges [[ / ]] as array-of
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/resolve/toml/toml-parse.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (7f24b5e42)

test/js/bun/resolve/toml/toml-parse.test.ts:
(pass) Bun.TOML.parse with non-string input throws [16.27ms]
(pass) Bun.TOML.parse accepts \u{XX} at start of a basic string (#30893) [2.44ms]
(pass) Bun.TOML.parse rejects \x escape in quoted key at file start without panicking (#30893) [4.13ms]
(pass) Bun.TOML.parse rejects \u escape in quoted key at file start without panicking (#30893) [3.40ms]
(pass) Bun.TOML.parse handles trailing backslash-CR in multiline basic string (#30893) [2.21ms]
(pass) Bun.TOML.parse produces correct codepoints for \t and \f escapes [3.89ms]
(pass) Bun.TOML.parse normalizes literal CRLF to LF in multiline basic strings [1.87ms]
(pass) Bun.TOML.parse rejects out-of-range \u{...} escapes 
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 759ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current version: 1.29.0)
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl
... (truncated)
diff hotspot
src/parsers/toml.rs                         | 18 +++++++-----
 test/js/bun/resolve/toml/toml-parse.test.ts | 43 +++++++++++++++++++++++++++++
 2 files changed, 54 insertions(+), 7 deletions(-)

gate history · 2 passed · 0 rejected · iteration 2

evidence per changed file
file                                         reads  edits  tests
src/parsers/toml.rs                              1      8      0
test/js/bun/resolve/toml/toml-parse.test.ts      1      1      0

Bun.TOML.parse rejected nested array literals with adjacent brackets like
`a = [[1]]` while accepting the whitespace-separated `a = [ [1] ]`. The
lexer's allow_double_bracket flag (which merges `[[`/`]]` into
array-of-tables header tokens) was forced to true at the top of
parse_value_inner and again after every compound value closed, so a `]]`
(or at depth >=3 a `[[`) inside an array was lexed as a single header
token and the parse failed.

parse_assignment and parse_value_inner now save the caller's flag on
entry, hold it false while inside an array or inline table, and restore
the saved value before lexing the token that follows the value. This
keeps `[[header]]` detection working after a top-level value while
treating adjacent brackets inside values as two separate tokens.
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 783b25cd-7690-48a9-84ff-f38afb345e1a

📥 Commits

Reviewing files that changed from the base of the PR and between 8624c2b and 693ae6b.

📒 Files selected for processing (2)
  • src/parsers/toml.rs
  • test/js/bun/resolve/toml/toml-parse.test.ts

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

@robobun

robobun commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:32 AM PT - Jul 12th, 2026

@robobun, your commit 7f24b5e421347b5e0973a8c34463c3a3efe5928a passed in Build #72090! 🎉


🧪   To try this PR locally:

bunx bun-pr 34004

That installs a local version of the PR into your bun-34004 executable, so you can run:

bun-34004 --bun

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Rewrite the TOML parser for v1.1.0 conformance #32953 - Complete TOML parser rewrite for v1.1.0 conformance that replaces src/parsers/toml.rs wholesale, subsuming the allow_double_bracket nested-array fix in this PR

🤖 Generated with Claude Code

@robobun

robobun commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author

Re #32953: that is a full parser rewrite (+12.5k / -2k). This PR is a targeted ~50-line fix for the allow_double_bracket flag being left true in value position, so nested arrays like [[1]] parse today. The test cases added here should pass under the rewrite as well. Happy to close if the rewrite is landing imminently; otherwise this can go in first.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — targeted fix for the allow_double_bracket flag leaking into value position.

What was reviewed:

  • Traced the save/restore flow through every parse_value_inner arm (scalars, t_empty_array, t_open_bracket, t_open_brace) and nested parse_assignment — flag is false for every next() inside a compound value and restored to the caller's value exactly before the token following the closing delimiter is lexed.
  • Confirmed against lexer.rs that allow_double_bracket inits true and expect_assignment() lexes the value's first token with flag=false, so [[ in a = [[1]] correctly tokenizes as two t_open_bracket.
  • Checked the removed post-next() writes (t_empty_array, inline-table loop) were either dead or now subsumed by parse_assignment's own restore.
  • Tests cover both directions: nested arrays at depth 1–3, inside inline tables, and [[header]] recognition after every value shape.
Extended reasoning...

Overview

Changes src/parsers/toml.rs (~18 lines net) to fix Bun.TOML.parse rejecting nested array literals with adjacent brackets (a = [[1]]). The lexer's allow_double_bracket flag — which merges [[ / ]] into array-of-tables header tokens — was being forced to true at the top of every parse_value_inner call and again after each compound value, so ]] inside a value merged into t_close_bracket_double and failed the array's t_close_bracket expect. The fix converts the unconditional writes into a save/restore of the caller's flag: compound-value arms set it false before lexing their body and restore the saved value before lexing the token that follows. parse_assignment gets the same treatment so its restore point propagates the correct outer value into parse_value_inner.

Adds 2 tests (18 assertions) to test/js/bun/resolve/toml/toml-parse.test.ts covering nested arrays at depths 1–3, empty nested arrays, arrays inside inline tables and vice versa, and a regression guard that [[header]] on the line after each value shape (scalar, [1], [], {…}, [[1]]) is still lexed as an array-of-tables header.

Security risks

None. This turns a parse rejection of valid TOML into acceptance; no new input classes reach downstream code that weren't already reachable via the whitespace-separated form (a = [ [1] ]). No allocation, path, or FFI changes.

Level of scrutiny

Low–medium. The parser is user-input-facing but the change is a localized state-flag discipline fix with no unsafe code touched. I traced the flag value at every lexer.next() / lexer.expect() call site in the diff against the actual lexer bracket-handling code (lexer.rs:518–540) and each restore point lands correctly: top-level values restore true so a following [[t]] header merges; nested values restore false so adjacent ] stay single tokens. The removed allow_double_bracket = false after the inline-table parse_assignment call is now redundant because parse_assignment itself restores to its captured outer (which is false inside the brace arm).

Other factors

Test coverage is thorough for a fix this size — both the bug and the inverse regression (header merging after values) are asserted, plus the pre-existing whitespace-separated forms. The PR description shows tests fail on system Bun and pass on the debug build. There is an open full-rewrite PR (#32953) that would subsume this, but this fix is independent and the added tests will apply to the rewrite too. No prior human review comments to address.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — targeted lexer-flag save/restore fix with thorough test coverage.

What was reviewed:

  • Traced allow_double_bracket through parse_assignmentexpect_assignmentparse_value_inner for [[1]], [[[1]]], [[]], {a=[[1]]}, [{b=[[1]]}] — flag is held false at every next() inside a value and restored before the token after the outermost delimiter.
  • Confirmed the moved = false before next() in the t_open_bracket/t_open_brace arms is what stops ]]/[[ from merging mid-value; scalar arms inherit the caller's flag so [[header]] after a top-level value still merges.
  • Checked parse_maybe_trailing_comma and the t_empty_array arm still see false inside compound values after the removed post-assignment reset — parse_assignment now restores to the saved false, so state is equivalent.
Extended reasoning...

Overview

Changes src/parsers/toml.rs (~11 net lines across parse_assignment and parse_value_inner) to save/restore the lexer's allow_double_bracket flag instead of unconditionally forcing it to true, and adds two tests (18 assertions) to test/js/bun/resolve/toml/toml-parse.test.ts. The flag controls whether the lexer merges adjacent [[ / ]] into array-of-tables header tokens; the bug was that it was set true at the top of every parse_value_inner call, so nested array literals like [[1]] had their closing ]] mis-lexed as a header-close token.

Security risks

None. Pure boolean flag manipulation in a recursive-descent parser. No new allocations, no unsafe changes, no untrusted-length arithmetic. The change strictly widens the set of valid TOML that parses — previously-rejected inputs now succeed, and the second test block verifies previously-accepted inputs (headers after values, whitespace-separated brackets) still parse identically.

Level of scrutiny

Low-to-medium. This is a self-contained lexer-mode bugfix in a leaf parser (Bun.TOML.parse and TOML config imports). The state machine is small enough to trace by hand: I walked a = [[1]], a = [[[1]]], a = [[]], t = {a = [[1]]}, a = [{b = [[1]]}], and each header-after-value case through the new flag transitions and confirmed the flag is false at every next() call that could see adjacent brackets inside a value, and true (via the saved outer value) at the next() that lexes the token following the outermost value. The removed = false after parse_assignment in the t_open_brace loop is now redundant because parse_assignment itself restores to the saved value (which is false when called from inside a brace/bracket arm). The removed = true in the t_empty_array arm is safe because that arm no longer needs to force the flag — it inherits whatever the caller set (true at top level via the restore in parse_assignment, false when nested).

Other factors

Test coverage is unusually thorough for a fix this size: it exercises depth 2 and 3, empty nested arrays, mixed nesting with inline tables in both directions, the whitespace-separated forms that already worked, and — critically — a second test that guards the restore path for every value shape (scalar, non-empty array, empty array, inline table, nested array) followed by a [[header]]. The PR description shows the tests fail on system Bun and pass on both debug-ASAN and release builds. The bug-hunting system found no issues. The overlapping full-rewrite PR (#32953) is orthogonal — these tests remain valid regardless.

@robobun

robobun commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: build #72090 has 284 jobs passed, 0 test failures, 0 error annotations. The only red is darwin-14-aarch64-test-bun which expired (agent never picked it up; retries still scheduled). The previous build #72056 failed only on unrelated tests (test-worker-message-port-transfer-terminate.js, s3.leak.test.ts, bun-add.test.ts, next-build.test.ts, test-http-proxy-request-no-proxy.mjs), each on a single lane; the TOML tests passed on every lane in both builds.

The diff is green; ready for review/merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant