Skip to content

TOML: map date/time values to Temporal, round-trip them through stringify - #37018

Merged
dylan-conway merged 23 commits into
mainfrom
farm/76e35552/toml-temporal-dates
Aug 14, 2026
Merged

TOML: map date/time values to Temporal, round-trip them through stringify#37018
dylan-conway merged 23 commits into
mainfrom
farm/76e35552/toml-temporal-dates

Conversation

@robobun

@robobun robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

TOML.parse returned all four TOML date/time types as strings of their source text. They now map 1:1 and losslessly onto Temporal, which is enabled by default on main:

TOML JS
offset date-time (1979-05-27T00:32:00-07:00) Temporal.Instant
local date-time (1979-05-27T07:32:00, 1.1's 1979-05-27 07:32) Temporal.PlainDateTime
local date (1979-05-27) Temporal.PlainDate
local time (07:32:00, 07:32) Temporal.PlainTime

Per the spec an offset date-time "specifies an instant", so it is Temporal.Instant and the written offset normalizes to UTC. Sub-second digits are preserved (Temporal carries nanoseconds; fractional seconds beyond 9 digits are truncated as the TOML spec directs, since Temporal rejects them). Leap-second :60 clamps to :59 through Temporal's own ISO parsing.

const doc = Bun.TOML.parse("d = 1979-05-27T00:32:00-07:00");
doc.d instanceof Temporal.Instant; // true
doc.d.toString(); // "1979-05-27T07:32:00Z"
Bun.TOML.stringify(doc); // "d = 1979-05-27T07:32:00Z\n" (unquoted date-time literal)

How

  • The TOML parser tags the E::String it produces with the date/time kind (toml_datetime on EString); the lexer already distinguished the four kinds. The TOML AST never enters the JS visit/transform passes, so only the TOML sinks check the tag.
  • All three sinks of the TOML AST stay consistent:
    • Bun.TOML.parse and import/require of .toml construct the object through the same JSC code paths Temporal.*.from(string) uses (new bindings in bindings.cpp).
    • The bundler lowers the node to a Temporal.*.from("...") call over a real unbound Temporal symbol, so the chunk renamer renames a user binding named Temporal instead of letting it capture the reference, and the calls are pure-annotated so unused exports tree-shake.
    • bun build --no-bundle of a .toml file (which prints the data AST directly, without a symbol table) prints the tagged string as a bare Temporal.*.from("...") call. That path turns each top-level key into a module-scope var, so a document with both a date/time and a top-level key literally named Temporal fails at evaluation with a TypeError; renaming without a symbol table was tried and dropped as open-ended.
  • TOML.stringify emits Temporal.Instant/PlainDateTime/PlainDate/PlainTime as unquoted TOML literals, so stringify(parse(doc)) round-trips date/times. Temporal.ZonedDateTime is also accepted and emits the offset at that instant, dropping the [Time/Zone] annotation (TOML has no zone syntax); non-ISO [u-ca=...] calendar annotations are dropped the same way (the stored ISO fields are emitted). PlainYearMonth/PlainMonthDay/Duration have no TOML form and throw. Values TOML's 4-digit years cannot spell throw like Date already did, with one refinement for instants: an offset date-time at a year edge (0000-01-01T00:00:00+01:00 is valid TOML but its UTC year is -1) is emitted with the nearest offset whose local year fits, so everything parse accepts also stringifies; only instants a day or more outside 0000..9999 throw. Date output now trims trailing fraction zeros (Z rather than .000Z) so Date and Instant spell the same instant identically; otherwise Date handling is unchanged.
  • With BUN_JSC_useTemporal=0, a date/time value throws a TypeError ("Date/time values require Temporal, which is disabled in this process"); the module-import path previously would have panicked on any conversion error and now fails the load with the pending exception.
  • bunfig.toml uses no date values, so config parsing is unaffected.

Everything else about parse/stringify (error messages, layout, numbers) is byte-for-byte unchanged.

Tests

  • Regenerated toml-test-suite.test.ts (708 cases) from the same pinned toml-test commit: datetime expectations construct the Temporal value and compare with plain toEqual (deepEquals learned Temporal objects in Compare Temporal objects by value in Bun.deepEquals and toEqual #37024). All pass, including the parse(stringify(parse(input))) lap each valid case asserts.
  • Hand-written coverage in toml.test.ts: the four mappings, instant semantics, TOML spellings Temporal does not print (space separator, lowercase t/z, omitted seconds, leap second), fraction truncation, nesting, both useTemporal=0 behaviors, stringify of all eight Temporal types, year bounds, calendar/zone annotation dropping, array layout.
  • Import fixtures (test/js/bun/resolve/toml) extended with a [dates] table; bundler tests assert the emitted Temporal.*.from modules run and that a user var Temporal in the same bundle is renamed instead of captured.

Types and docs updated (bun.d.ts JSDoc, docs/runtime/toml.mdx).


[review] gate passed · iteration 3 · 25 files touched

fails on main (without fix)
ASAN without fix: 53 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_loader.test.ts test/js/bun/resolve/toml/toml.test.js test/js/bun/toml/toml-test-suite.test.ts test/js/bun/toml/toml.test.ts
bun test v1.4.0 (48b6af953)

test/bundler/bundler_loader.test.ts:
(pass) bundler > bun loader > bun/loader-yaml-file [1013.43ms]
(pass) bundler > bun loader > bun/loader-text-file [412.02ms]
(pass) bundler > bun loader > bun/loader-json-file [489.14ms]
(pass) bundler > bun loader > bun/loader-toml-file [429.12ms]
(pass) bundler > bun loader > bun/loader-toml-datetime-shadowed-temporal-global [413.44ms]
runtime failed file: /tmp/bun-build-tests/bun-avOXit/bun/loader-toml-datetime-imported-temporal-binding/out.js
stdout output:
polyfill false 1979-05-27
---
expected stdout:
polyfill true 1979-05-27
---
1843 |               console.log(`---`);
1844 |               console.log(`expected ${name}:`);
1845 |               console.log(expected);
1846 |               console.log(`---`);
1847 |             }
1848 |             expect(result).toBe(expected);
                                  ^
error: exp
... (truncated)

release without fix: 53 FAILED
bun test v1.4.0-canary.1 (b7a043103)

test/bundler/bundler_loader.test.ts:
(pass) bundler > bun loader > bun/loader-yaml-file [31.84ms]
(pass) bundler > bun loader > bun/loader-text-file [14.09ms]
(pass) bundler > bun loader > bun/loader-json-file [18.21ms]
(pass) bundler > bun loader > bun/loader-toml-file [13.89ms]
(pass) bundler > bun loader > bun/loader-toml-datetime-shadowed-temporal-global [22.56ms]
runtime failed file: /tmp/bun-build-tests/bun-Rboeq6/bun/loader-toml-datetime-imported-temporal-binding/out.js
stdout output:
polyfill false 1979-05-27
---
expected stdout:
polyfill true 1979-05-27
---
1843 |               console.log(`---`);
1844 |               console.log(`expected ${name}:`);
1845 |               console.log(expected);
1846 |               console.log(`---`);
1847 |             }
1848 |             expect(result).toBe(expected);
                                  ^
error: expect(received).toBe(expected)

Expected: "polyfill true 1979-05-27"
Received: "polyfill false 1979-05-27"

      at <anonymous> (/workspace/bun/test/bundler/expectBundled.ts:1848:28)
(fail) bundler > bun loader > bun/loader-toml-datetime-imported-temporal-binding [18.93ms]
93
... (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/bundler/bundler_loader.test.ts test/js/bun/resolve/toml/toml.test.js test/js/bun/toml/toml-test-suite.test.ts test/js/bun/toml/toml.test.ts
bun test v1.4.0 (48b6af953)

test/bundler/bundler_loader.test.ts:
(pass) bundler > bun loader > bun/loader-yaml-file [1011.62ms]
(pass) bundler > bun loader > bun/loader-text-file [508.58ms]
(pass) bundler > bun loader > bun/loader-json-file [360.53ms]
(pass) bundler > bun loader > bun/loader-toml-file [381.91ms]
(pass) bundler > bun loader > bun/loader-toml-datetime-shadowed-temporal-global [387.83ms]
(pass) bundler > bun loader > bun/loader-toml-datetime-imported-temporal-binding [377.21ms]
(pass) bundler > bun loader > bun/loader-toml-datetime-no-bundle [464.49ms]
(pass) bundler > bun loader > bun/loader-toml-datetime [433.16ms]
(pass) bundler > bun loader > bun/loader-text-file [398.02ms]
(pass) bundler > bun loader > bun/loader-xml-file [432.77ms]
(pass) bundler > node loader > bun/loader-yaml-file [427.98ms]
(pass) bundler > node loader > bun/loader-text-file [385.65ms]
(pass) bundler > 
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 652ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/23] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 240 extern-C blocks audited
[2/23] gen cpp.rs (cppbind)
[3/23] gen JS modules (bundle-modules)
Preprocess modules (8539ms)
Bundle modules (64ms)
Postprocesss modules (324ms)
Bundle Functions (858ms)
Generate Code (29ms)

[9.83s] Bundled "src/js" for production
  2632 kb
  197 internal modules
  13 native modules
  91 internal functions across 17 files
[3/8] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_ast v0.0.0 (/workspace/bun/src/ast)
�[1m�[92m   Compiling�[0m bun_install_types v0.0.0 (/workspace/bun/src/install_types)
�[1m�[92m   Compiling�[0m bun_parsers v0.0.0 (/workspace/bun/src/parsers)
�[1m�[92m   Compiling�[0m bun_react_compiler v0.0.0 (/workspace/bun/src/react_compiler)
�[1m�[92m   Compiling�[0m bun_css v0.0.0 (/workspace/bun/src/
... (truncated)
diff hotspot
docs/runtime/toml.mdx                          |  38 +-
 packages/bun-types/bun.d.ts                    |  20 +-
 src/ast/e.rs                                   |  45 ++
 src/ast/expr.rs                                |   1 +
 src/bundler/ParseTask.rs                       |  30 +-
 src/bundler/bundle_v2.rs                       |   2 +-
 src/bundler/transpiler.rs                      |   1 +
 src/codegen/cppbind.ts                         |   1 +
 src/js_parser/parse/parse_entry.rs             | 117 +++
 src/js_parser_jsc/expr_jsc.rs                  |  26 +-
 src/js_parser_jsc/lib.rs                       |   3 +-
 src/js_printer/lib.rs                          |  21 +
 src/jsc/JSValue.rs                             |  19 +
 src/jsc/bindings/bindings.cpp                  | 155 ++++
 src/jsc/lib.rs                                 |   1 +
 src/parsers/toml.rs                            | 129 ++--
 src/runtime/api.rs                             |  63 +-
 src/runtime/api/TOMLObject.rs                  | 150 +++-
 test/bundler/bundler_loader.test.ts            |  67 ++
 test/js/bun/resolve/toml/toml-fixture.toml     |   6 +
 test/js/bun/resolve/toml/toml-fixture.toml.txt |   6 +
 test/js/bun/resolve/toml/toml.test.js          |   9 +
 test/js/bun/toml/generate_toml_test_suite.ts   |  93 +--
 test/js/bun/toml/toml-test-suite.test.ts       | 951 ++++++++++++-------------
 test/js/bun/toml/toml.test.ts                  | 297 ++++++--
 25 files changed, 1494 insertions(+), 757 deletions(-)

gate history · 7 passed · 0 rejected · iteration 3

evidence per changed file
file                                reads  edits  tests
docs/runtime/toml.mdx                   1      1      0
packages/bun-types/bun.d.ts             1      1      0
src/ast/e.rs                            6      8      0
src/ast/expr.rs                         5      6      0
src/bundler/ParseTask.rs                2      1      0
src/bundler/bundle_v2.rs                1      1      0
src/bundler/transpiler.rs               5      7      0
src/codegen/cppbind.ts                  0      0      0
src/js_parser/parse/parse_entry.rs      4     10      0
src/js_parser_jsc/expr_jsc.rs           4      7      0
src/js_parser_jsc/lib.rs                1      2      0
src/js_printer/lib.rs                   3      7      0
src/jsc/JSValue.rs                      2      4      0
src/jsc/bindings/bindings.cpp           7     11      0
src/jsc/lib.rs                          0      0      0
src/parsers/toml.rs                     5      7      0
(+ 9 more files)

… as date/time literals

TOML.parse previously returned all four TOML date/time types as strings
of their source text. They now map 1:1 onto Temporal:

- offset date-time -> Temporal.Instant (an offset date-time specifies an
  instant; the written offset normalizes to UTC)
- local date-time -> Temporal.PlainDateTime (including TOML 1.1's space
  separator and omitted seconds)
- local date -> Temporal.PlainDate
- local time -> Temporal.PlainTime

The parser now produces a dedicated E::DateTime AST node carrying the
kind and source text (fractional seconds truncated to Temporal's 9-digit
limit, as the TOML spec directs). All three sinks of the TOML AST stay
consistent: Bun.TOML.parse and import/require construct the Temporal
object through the same JSC code paths Temporal.*.from(string) uses, and
the bundler lowers the node to a Temporal.*.from("...") call over a real
unbound Temporal symbol so chunk renaming protects the global reference
and unused date exports stay tree-shakable.

TOML.stringify now emits Temporal.Instant, PlainDateTime, PlainDate, and
PlainTime as unquoted TOML date/time literals, so stringify(parse(doc))
round-trips date/times instead of re-quoting them as strings.
Temporal.ZonedDateTime emits its offset form (the time-zone annotation
has no TOML representation), non-ISO calendar annotations are dropped
the same way, and values outside TOML's 4-digit years throw like Date
already did. PlainYearMonth, PlainMonthDay, and Duration have no TOML
form and throw. Date is unchanged.

With BUN_JSC_useTemporal=0 a date/time value now throws a TypeError
(and a TOML module import fails with that exception instead of
panicking).

The toml-test conformance suite is regenerated: expectations compare
Temporal class + canonical toString, since Temporal instances have no
own properties for toEqual to see.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

TOML date/time literals now retain their specific kind, parse into corresponding Temporal objects, and serialize back to TOML-compatible values. Bundler output, runtime conversion, error propagation, tests, and documentation were updated for the new behavior.

Changes

TOML Temporal date/time handling

Layer / File(s) Summary
Date/time classification and AST metadata
src/parsers/toml.rs, src/ast/e.rs, src/ast/expr.rs
The parser classifies the four TOML date/time forms, truncates excess fractional precision, and preserves the kind through AST cloning.
Runtime Temporal conversion
src/jsc/bindings/bindings.cpp, src/js_parser_jsc/..., src/runtime/api.rs
Tagged literals convert to Temporal.Instant, Temporal.PlainDateTime, Temporal.PlainDate, or Temporal.PlainTime. Conversion errors propagate through the JavaScript runtime.
Bundler and printed module output
src/js_parser/parse/parse_entry.rs, src/js_printer/lib.rs, src/bundler/transpiler.rs
Nested TOML date/time values emit globalThis.Temporal reconstruction calls. Bundler name collisions are handled without capturing Temporal references.
Lazy AST error propagation
src/bundler/ParseTask.rs, src/bundler/bundle_v2.rs
Loader and generated HTML paths return parser errors when lazy AST generation produces no AST.
Temporal TOML serialization
src/runtime/api/TOMLObject.rs, src/jsc/bindings/bindings.cpp
TOML.stringify serializes supported Temporal types and Date, normalizes offsets and precision, and rejects unsupported types, placements, and years.
Behavioral test coverage
test/js/bun/toml/*, test/bundler/bundler_loader.test.ts, test/js/bun/resolve/toml/*
Tests cover Temporal type mapping, canonicalization, nested values, bundling, disabled Temporal support, serialization errors, and round trips.
Documentation and declarations
docs/runtime/toml.mdx, packages/bun-types/bun.d.ts
TOML parsing and stringification documentation describes Temporal mappings, supported values, normalization, and rejection rules.

Suggested reviewers: alii, dylan-conway, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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 The description clearly explains the change, implementation details, scope, edge cases, and verification results, despite using different section headings than the template.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: mapping TOML date/time values to Temporal and supporting stringify round trips.

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

@github-actions github-actions Bot added the claude label Aug 6, 2026
The TOML AST never enters the JS visit/transform passes (SLazyExport is
not visited; the runtime import and Bun.TOML.parse convert the raw expr
directly), so a dedicated expression variant is not needed: a
toml_datetime tag on EString carries the same information, and only the
TOML sinks (expr_to_js, data_to_js, to_lazy_export_ast, the printer's
EString arm) check it. The bundler still rewrites tagged strings into
Temporal.*.from calls over an unbound Temporal symbol for rename safety
and tree shaking; behavior is unchanged and the test suite is identical.
Comment thread src/ast/e.rs Outdated
Comment thread src/ast/e.rs Outdated
Comment thread src/ast/e.rs Outdated
Comment thread src/js_parser/parse/parse_entry.rs Outdated
Comment thread src/js_parser/parse/parse_entry.rs Outdated
Comment thread src/js_printer/lib.rs Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/parsers/toml.rs
Comment thread src/parsers/toml.rs Outdated
Comment thread src/runtime/api/TOMLObject.rs Outdated
Comment thread src/runtime/api/TOMLObject.rs Outdated
Comment thread src/runtime/api/TOMLObject.rs Outdated
Comment thread src/runtime/api/TOMLObject.rs Outdated
Comment thread src/runtime/jsc_hooks.rs Outdated
Comment thread src/ast/e.rs
Comment thread src/ast/e.rs
Comment thread src/ast/e.rs
Comment thread src/js_parser/parse/parse_entry.rs
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Comment thread src/js_parser/parse/parse_entry.rs Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bundler/ParseTask.rs (1)

768-839: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle the XML loader with the same error conversion.

The Loader::Xml branch at Line 870 still calls .unwrap() on new_lazy_export_ast(...). The upstream contract in src/js_parser/parser.rs:1709-1756 returns Ok(None) after logging parser failures. An XML parser failure can therefore panic instead of returning ParserError. The panic also prevents the temporary log from flushing at Line 873.

As per coding guidelines, user-reachable failures must be recoverable errors rather than panics or unreachable assertions.

Proposed fix
-                        .unwrap()
+                        .ok_or(AnyError::ParserError)?,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bundler/ParseTask.rs` around lines 768 - 839, Update the Loader::Xml
branch to handle new_lazy_export_ast(...) without unwrap: propagate its error
and convert Ok(None) to AnyError::ParserError, matching the TOML/YAML/JSON5
branches. Keep the temporary-log closure and ensure all XML failure paths return
through the existing temp_log flush before returning.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/runtime/toml.mdx`:
- Around line 144-146: The TOML serialization documentation and public
declaration must consistently state that invalid Date values are rejected and
supported Date or Temporal values outside years 0000–9999 are rejected. Update
the relevant serialization descriptions in docs/runtime/toml.mdx lines 144-146
and packages/bun-types/bun.d.ts lines 816-825, preserving the existing
supported-type behavior and wording consistency across both sites.
- Around line 144-146: Document that calendar annotations are normalized to
TOML-supported fields and not preserved alongside the existing time-zone
behavior in docs/runtime/toml.mdx lines 144-146 and packages/bun-types/bun.d.ts
lines 816-820; update both affected descriptions consistently without changing
the surrounding serialization behavior.
- Line 86: Update the date/time type description near the TOML-to-Temporal
mapping statement to remove the claim of universal losslessness. State that
Temporal preserves fractional seconds up to nanosecond precision and that the
TOML scanner truncates fractions beyond nine digits.
- Around line 140-150: Update the TOML runtime documentation around the
top-level object requirement to explicitly state that top-level undefined,
function, and symbol inputs return undefined, while those values remain skipped
when used as properties and throw inside arrays. Preserve the existing behavior
descriptions for other unsupported values.
- Line 54: The TOML date/time documentation must state that parsing fails when
Temporal is disabled. In docs/runtime/toml.mdx lines 54-54, add this condition
beside the Temporal type mappings; in packages/bun-types/bun.d.ts lines 795-800,
include the same failure behavior in the parse error documentation.

In `@src/bundler/transpiler.rs`:
- Around line 1942-1964: Update the candidate collision check in the
mangled_global_this construction to compare candidate against each property key
after applying the same normalization as ensure_valid_identifier. Preserve the
existing grow-loop behavior so normalized collisions append underscores before
allocating the final name.

In `@test/js/bun/toml/toml.test.ts`:
- Around line 341-346: Update the Bun.spawn invocation in the affected test to
pass an absolute entry path by joining or resolving dir with index.ts, matching
the approach used by the sibling test while preserving the existing cwd and
process options.

---

Outside diff comments:
In `@src/bundler/ParseTask.rs`:
- Around line 768-839: Update the Loader::Xml branch to handle
new_lazy_export_ast(...) without unwrap: propagate its error and convert
Ok(None) to AnyError::ParserError, matching the TOML/YAML/JSON5 branches. Keep
the temporary-log closure and ensure all XML failure paths return through the
existing temp_log flush before returning.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 24893e72-9e74-420f-abd1-da115dd9f515

📥 Commits

Reviewing files that changed from the base of the PR and between f426a8e and ca941ea.

📒 Files selected for processing (22)
  • docs/runtime/toml.mdx
  • packages/bun-types/bun.d.ts
  • src/ast/e.rs
  • src/ast/expr.rs
  • src/bundler/ParseTask.rs
  • src/bundler/bundle_v2.rs
  • src/bundler/transpiler.rs
  • src/js_parser/parse/parse_entry.rs
  • src/js_parser_jsc/expr_jsc.rs
  • src/js_parser_jsc/lib.rs
  • src/js_printer/lib.rs
  • src/jsc/bindings/bindings.cpp
  • src/parsers/toml.rs
  • src/runtime/api.rs
  • src/runtime/api/TOMLObject.rs
  • test/bundler/bundler_loader.test.ts
  • test/js/bun/resolve/toml/toml-fixture.toml
  • test/js/bun/resolve/toml/toml-fixture.toml.txt
  • test/js/bun/resolve/toml/toml.test.js
  • test/js/bun/toml/generate_toml_test_suite.ts
  • test/js/bun/toml/toml-test-suite.test.ts
  • test/js/bun/toml/toml.test.ts

Comment thread docs/runtime/toml.mdx
Comment thread docs/runtime/toml.mdx Outdated
Comment thread docs/runtime/toml.mdx Outdated
Comment thread docs/runtime/toml.mdx Outdated
Comment thread src/bundler/transpiler.rs Outdated
Comment thread test/js/bun/toml/toml.test.ts
…ead of throwing

An offset date-time at the edge of TOML's four-digit years (e.g.
0000-01-01T00:00:00+01:00) parses to an Instant whose UTC year is -1 or
10000. stringify printed every Instant with Z and so rejected a value
that TOML.parse itself produced. Pick the offset closest to Z (or to a
ZonedDateTime's own offset) whose local year fits; the instant is
unchanged to the nanosecond. Only instants a day or more outside
0000..9999, which TOML cannot spell at all, still throw.
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/runtime/api/TOMLObject.rs Outdated
Comment thread src/bundler/ParseTask.rs
…hter comments, docs/types wording for truncation, annotations, year bounds
Comment thread src/jsc/bindings/bindings.cpp
Comment thread src/jsc/bindings/bindings.cpp Outdated
Comment thread src/jsc/bindings/bindings.cpp
…xport unwrap

parse_data_loader hand-builds the module without a symbol table, so a
top-level TOML key that shadows a global the printed code needs cannot
be handled the way the bundler does. Renaming vars by string comparison
was open-ended; a key named globalThis next to a date/time on this path
now fails at evaluation with a TypeError instead.
Comment thread src/js_printer/lib.rs Outdated
Comment thread src/js_printer/lib.rs
…sc::TemporalType

Main's WebKit already provides JSC::TemporalType / JSC::temporalType(), so
drop the hand-rolled inherits<> chain and the TOML-private enum in favor
of Bun__JSValue__temporalType -> bun_jsc::TemporalType, the same surface
the Temporal console formatting work uses.
Comment thread src/jsc/bindings/bindings.cpp

@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.

I reviewed the latest revision (through 0a4b019) and the automated bug-hunting pass found no issues; my earlier inline nits on ParseTask.rs and the deep-header test have all been addressed. Given the scope — new user-facing API surface (breaking change to TOML.parse return types, Date output format tweak), new C++ Temporal bindings with uncheckedDowncast, and the deliberate --no-bundle limitation for a top-level Temporal key — a maintainer sign-off is still warranted.

What was reviewed:

  • Exception handling in Bun__Temporal__fromDateTimeLiteral / toTOMLDateTime — throw scopes and RETURN_IF_EXCEPTION are placed correctly; uncheckedDowncast is guarded by the caller's temporalType classification.
  • The 64-byte stack buffer vs. worst-case Temporal string length (35 bytes for a 9-frac-digit offset date-time) — RELEASE_ASSERT(length <= bufLen) holds.
  • tomlOffsetForInstant Int128 arithmetic and the year-edge offset selection round-trip cases.
  • EString layout change (new Option<TomlDateTimeKind> field) propagated through Default, init, and the expr.rs clone path.
Extended reasoning...

Overview

This PR changes Bun.TOML.parse to return Temporal objects for TOML's four date/time types instead of source-text strings, and teaches TOML.stringify to emit Temporal (and Date) values as unquoted TOML date/time literals so parse↔stringify round-trips. It touches 25 files across the TOML parser (src/parsers/toml.rs), AST (src/ast/e.rs, expr.rs), three consumption sinks (runtime expr_jsc.rs, bundler parse_entry.rs, printer lib.rs), new C++ JSC bindings (bindings.cppBun__Temporal__fromDateTimeLiteral, Bun__Temporal__toTOMLDateTime, Bun__JSValue__temporalType, tomlOffsetForInstant), the TOML.stringify implementation, docs, types, and ~1000 lines of test changes including a regenerated 708-case conformance suite.

Security risks

None identified. Input is TOML source text validated by the existing scanner before reaching Temporal construction; the new truncate_fractional_seconds bounds its allocation by input length. The C++ toTOMLDateTime writes into a caller-supplied 64-byte stack buffer with a RELEASE_ASSERT(length <= bufLen) guard; the longest possible output (9999-12-31T23:59:59.999999999-23:59, 35 bytes) fits comfortably. uncheckedDowncast on Temporal cells is safe because temporalType was already computed on the same value and has_toml_form gates the switch arms.

Level of scrutiny

High. This is a breaking API change (date/times were strings, now Temporal objects), adds new C++↔Rust FFI surface with manual buffer handling, embeds an API design decision (offset date-time → Instant rather than ZonedDateTime; year-edge instants pick a fitting offset rather than throwing; .000ZZ for Date), and accepts a known --no-bundle limitation (top-level Temporal key shadows the printed global). These are exactly the kind of decisions REVIEW.md says need maintainer agreement, not automated approval.

Other factors

The PR has been through extensive iteration: earlier rounds fixed iterative-walk stack safety, converted all new_lazy_export_ast .unwrap() sites, dropped an open-ended globalThis mangling scheme, and addressed docs/types wording. All 40+ review threads are marked resolved, and dylan-conway has been actively engaged resolving the latest round. Test coverage is thorough (conformance suite round-trip lap, bundler symbol-collision tests, both useTemporal=0 paths, GC stress, year-edge boundary cases). No bugs were found on this revision. The remaining question is maintainer sign-off on the API shape and the --no-bundle trade-off, which is not something an automated review should decide.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 0a4b019 (build 93779): no failing job. The red status is two darwin 26 aarch64 - test-bun shards that were canceled without ever being assigned an agent (runnable 01:03, canceled 01:45; the release-tier=latest aarch64 queue is one box with two slots and was backlogged; the same lane passes on main's build 94234 when it gets an agent). Every test listed in the annotations passed on retry (complex-workspace, napi, sourcetextmodule-leak in the parallel batch, next-pages ssr and bun-install-registry on Windows aarch64, grpc-js test-tonic on darwin 14); none of them touch TOML, Temporal, the lazy-export path, or the printer. All other lanes, including darwin 14 aarch64 and darwin 14 x64, are green.

@dylan-conway
dylan-conway merged commit 056491f into main Aug 14, 2026
11 of 12 checks passed
@dylan-conway
dylan-conway deleted the farm/76e35552/toml-temporal-dates branch August 14, 2026 23:34

@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.

I reviewed this PR and the bug-hunting pass found no issues. Given it's a user-facing API change (TOML date/times now return Temporal objects instead of strings) that spans the parser, printer, bundler lazy-export path, and new JSC C++ bindings, a maintainer sign-off on the API mapping and the --no-bundle shadowing trade-off would still be worthwhile.

What was reviewed:

  • The three sinks (JSC materialization, bundler lowering, printer) stay consistent; the unbound Temporal symbol lets the chunk renamer handle collisions.
  • Bun__Temporal__toTOMLDateTime buffer bound: longest output (9999-12-31T23:59:59.999999999-23:59, 35 bytes) fits the 64-byte stack buffer, guarded by RELEASE_ASSERT.
  • tomlOffsetForInstant Int128 arithmetic and the year-edge offset selection round-trip against the parse-side test cases.
  • Exception handling in the new C++ bindings uses RETURN_IF_EXCEPTION after every fallible Temporal call; the useTemporal=0 path throws instead of reaching the missing structures.
Extended reasoning...

Overview

This PR changes Bun.TOML.parse to return Temporal objects (Instant/PlainDateTime/PlainDate/PlainTime) for TOML's four date/time types instead of source-text strings, and teaches TOML.stringify to emit all serializable Temporal types as unquoted TOML literals. It touches 25 files across the AST (toml_datetime tag on EString), the TOML parser, the JS printer, the bundler's lazy-export lowering (lower_date_time_literals in parse_entry.rs), the runtime expr_to_js converter, and ~130 lines of new C++ in bindings.cpp (Bun__Temporal__fromDateTimeLiteral, Bun__Temporal__toTOMLDateTime, tomlOffsetForInstant). The net diff is ~+1540/-770, plus a regenerated 708-case conformance suite and extensive hand-written tests.

Security risks

None identified. The only untrusted-input surface is the TOML scanner, which already validated date/time syntax; the new truncate_fractional_seconds operates on that validated ASCII slice. The C++ toTOMLDateTime writes into a caller-provided stack buffer with a RELEASE_ASSERT(length <= bufLen) bound; the longest possible output is well under 64 bytes. No auth, crypto, or filesystem paths are involved.

Level of scrutiny

High. This is a user-facing behavioral change to a documented API — code that previously received strings from TOML.parse will now receive Temporal objects. The PR encodes several API design decisions a maintainer should ratify: offset date-time → Instant (offset normalizes away) rather than preserving the written offset; ZonedDateTime and non-ISO calendars silently drop annotations on stringify; the --no-bundle path deliberately fails at runtime with a TypeError if a top-level TOML key is literally Temporal (the earlier string-mangling approach was removed in c45d52f after three review rounds). It also adds new C++ JSC bindings that use uncheckedDowncast after a Rust-side type discrimination — correct as written, but the kind of code REVIEW.md flags for careful review.

Other factors

The PR has been through many review iterations: eight prior inline findings from this system (all resolved with code changes — iterative worklist instead of recursion, .ok_or(ParserError)? on all 14 new_lazy_export_ast sites, concurrent stderr draining in tests), plus CodeRabbit and comment-cop passes. dylan-conway has been actively pushing fixes and resolving threads, so a human is already engaged, but no independent approval is on the thread. Test coverage is thorough (conformance suite round-trips, boundary years, sub-minute LMT offsets, GC stress, useTemporal=0 on both parse and import paths, bundler symbol-renaming tests). CI is green on all lanes that ran. Given the API-design surface and the C++ bindings, deferring rather than auto-approving.

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.

2 participants