TOML: map date/time values to Temporal, round-trip them through stringify
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::Stringit produces with the date/time kind (toml_datetimeonEString); 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.parseandimport/requireof.tomlconstruct the object through the same JSC code pathsTemporal.*.from(string)uses (new bindings inbindings.cpp).- The bundler lowers the node to a
Temporal.*.from("...")call over a real unboundTemporalsymbol, so the chunk renamer renames a user binding namedTemporalinstead of letting it capture the reference, and the calls are pure-annotated so unused exports tree-shake. bun build --no-bundleof a.tomlfile (which prints the data AST directly, without a symbol table) prints the tagged string as a bareTemporal.*.from("...")call. That path turns each top-level key into a module-scopevar, so a document with both a date/time and a top-level key literally namedTemporalfails at evaluation with aTypeError; renaming without a symbol table was tried and dropped as open-ended.
TOML.stringifyemitsTemporal.Instant/PlainDateTime/PlainDate/PlainTimeas unquoted TOML literals, sostringify(parse(doc))round-trips date/times.Temporal.ZonedDateTimeis 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/Durationhave no TOML form and throw. Values TOML's 4-digit years cannot spell throw likeDatealready did, with one refinement for instants: an offset date-time at a year edge (0000-01-01T00:00:00+01:00is valid TOML but its UTC year is -1) is emitted with the nearest offset whose local year fits, so everythingparseaccepts also stringifies; only instants a day or more outside 0000..9999 throw.Dateoutput now trims trailing fraction zeros (Zrather than.000Z) soDateandInstantspell the same instant identically; otherwiseDatehandling is unchanged.- With
BUN_JSC_useTemporal=0, a date/time value throws aTypeError("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 plaintoEqual(deepEquals learned Temporal objects in #37024). All pass, including theparse(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, lowercaset/z, omitted seconds, leap second), fraction truncation, nesting, bothuseTemporal=0behaviors, 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 emittedTemporal.*.frommodules run and that a uservar Temporalin 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)