Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c820de1
TOML: parse date/time values as Temporal objects, stringify them back…
robobun Aug 6, 2026
da85b78
Tag EString with the TOML date/time kind instead of adding an AST node
robobun Aug 6, 2026
aafc927
Tighten code comments
robobun Aug 6, 2026
9ac6b36
Route the transform-path Temporal reference through globalThis
robobun Aug 6, 2026
4a6ffe8
Share the Temporal literal FFI dispatch, cover the sub-minute offset …
robobun Aug 6, 2026
e52bf10
Keep EString's doc comment attached to the struct
robobun Aug 6, 2026
258b0c6
Address review: typed discriminants, classify once, Date precision pa…
robobun Aug 6, 2026
e966563
Mangle a globalThis TOML key's var on the transform path
robobun Aug 6, 2026
4af3b09
Share one Expr to JSValue converter between TOML.parse and imports
robobun Aug 6, 2026
b0f49a0
Walk the TOML AST iteratively; fail lazy-export parse errors gracefully
robobun Aug 6, 2026
27274c7
Use an absolute entry path and drain stderr in the deep-header test
robobun Aug 6, 2026
5be6ab7
Merge main to pick up Temporal-aware deepEquals (#37024)
robobun Aug 6, 2026
7978392
Compare conformance-suite date/times with plain toEqual
robobun Aug 6, 2026
627f3e3
Convert the remaining lazy-export unwraps; gate the datetime walk to …
robobun Aug 6, 2026
b7a674d
Merge remote-tracking branch 'origin/main' into farm/76e35552/toml-te…
dylan-conway Aug 12, 2026
ca941ea
Gate the datetime lowering walk to TOML lazy exports; downcast pre-cl…
dylan-conway Aug 13, 2026
25187c8
TOML.stringify: spell year-edge instants with an in-range offset inst…
dylan-conway Aug 13, 2026
1dd8b4f
Address review: single out-of-range sentinel from toTOMLDateTime, tig…
dylan-conway Aug 13, 2026
c45d52f
Drop the --no-bundle globalThis key mangling; convert the last lazy-e…
dylan-conway Aug 13, 2026
902aef3
Print TOML date/times on the --no-bundle path as bare Temporal.*.from…
dylan-conway Aug 13, 2026
0a4b019
Classify Temporal values through JSC::temporalType and a shared bun_j…
dylan-conway Aug 13, 2026
a3168a5
Merge remote-tracking branch 'origin/main' into farm/76e35552/toml-te…
dylan-conway Aug 14, 2026
48b6af9
Merge remote-tracking branch 'origin/main' into farm/76e35552/toml-te…
dylan-conway Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions docs/runtime/toml.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Bun's TOML parser implements the full [TOML v1.1.0 specification](https://github
- **Integers**: decimal, hex (`0x`), octal (`0o`), and binary (`0b`). Integers that cannot be represented losslessly as a JavaScript number — outside ±(2^53 - 1) — throw
- **Floats**: including `inf` and `nan`
- **Booleans**: `true` and `false`
- **Date/times**: offset date-time, local date-time, local date, and local time, returned as strings of their source text
- **Date/times**: returned as [Temporal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal) objects — offset date-time as `Temporal.Instant`, local date-time as `Temporal.PlainDateTime`, local date as `Temporal.PlainDate`, and local time as `Temporal.PlainTime`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **Arrays**: including mixed types and nested arrays
- **Tables**: standard (`[table]`) and inline (`{ key = "value" }`), including TOML 1.1 multi-line inline tables
- **Array of tables**: `[[array]]`
Expand Down Expand Up @@ -81,6 +81,25 @@ role = "backend"
`);
```

#### Date/times

Each of TOML's four date/time types maps 1:1 onto a Temporal type. Temporal carries nanosecond precision; as the TOML spec permits, fractional seconds beyond nine digits are truncated:

```ts
const doc = Bun.TOML.parse(`
created = 1979-05-27T00:32:00-07:00 # offset date-time
meeting = 1979-05-27T07:32:00 # local date-time
birthday = 1979-05-27 # local date
opens = 07:32:00 # local time
`);

doc.created; // Temporal.Instant (an offset date-time specifies an instant;
// the written offset normalizes away: 1979-05-27T07:32:00Z)
doc.meeting; // Temporal.PlainDateTime
doc.birthday; // Temporal.PlainDate
doc.opens; // Temporal.PlainTime
```

#### Error Handling

`Bun.TOML.parse()` throws a `SyntaxError` if the TOML is invalid:
Expand Down Expand Up @@ -118,11 +137,20 @@ Bun.TOML.stringify({
// x = 2
```

The top-level value must be an object — a TOML document is a table. `Date`
values become TOML offset date-times. Because TOML cannot represent them,
`null` values, `BigInt`, and circular structures throw; `undefined`,
The top-level value must be an object — a TOML document is a table.
`Temporal.Instant`, `Temporal.PlainDateTime`, `Temporal.PlainDate`, and
`Temporal.PlainTime` values become the corresponding TOML date/time
literals, so `stringify(parse(doc))` round-trips date/time types.
`Temporal.ZonedDateTime` becomes an offset date-time and `Date` becomes
an offset date-time in UTC. TOML has no syntax for time-zone or calendar
annotations, so those are dropped (the ISO fields are written), and its
years are four digits, so date values outside 0000–9999 and invalid
`Date`s throw. Because TOML cannot represent them, `null` values,
`BigInt`, circular structures, `Temporal.PlainYearMonth`,
`Temporal.PlainMonthDay`, and `Temporal.Duration` also throw; `undefined`,
function, and symbol properties are skipped (inside arrays they throw,
since TOML arrays cannot have holes).
since TOML arrays cannot have holes), and passing one of those as the
top-level value returns `undefined`, as `JSON.stringify` does.

---

Expand Down
20 changes: 15 additions & 5 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,9 +792,12 @@ declare module "bun" {
/**
* Parse a TOML (v1.1.0) document into a JavaScript object.
*
* Date/time values parse as strings of their source text. Integers
* outside `Number.MAX_SAFE_INTEGER` throw, since they cannot be
* represented losslessly as JavaScript numbers.
* Date/time values parse as Temporal objects: offset date-times as
* `Temporal.Instant`, local date-times as `Temporal.PlainDateTime`,
* local dates as `Temporal.PlainDate`, and local times as
* `Temporal.PlainTime`. Integers outside `Number.MAX_SAFE_INTEGER`
* throw, since they cannot be represented losslessly as JavaScript
* numbers.
*
* @category Utilities
*
Expand All @@ -810,8 +813,15 @@ declare module "bun" {
* Serialize a JavaScript object to a TOML document.
*
* The top-level value must be an object (a TOML document is a table).
* `Date` values become TOML offset date-times. `null`, `BigInt`, and
* circular structures throw, since TOML cannot represent them;
* `Temporal.Instant`, `Temporal.PlainDateTime`, `Temporal.PlainDate`,
* and `Temporal.PlainTime` values become the corresponding TOML
* date/time literals, `Temporal.ZonedDateTime` becomes an offset
* date-time, and `Date` becomes an offset date-time in UTC; time-zone
* and calendar annotations are dropped, since TOML has no syntax for
* them. `null`, `BigInt`, circular structures, invalid `Date`s, date
* values outside years 0000–9999, and Temporal types with no TOML form
* (`Temporal.PlainYearMonth`, `Temporal.PlainMonthDay`,
* `Temporal.Duration`) throw, since TOML cannot represent them;
* `undefined`, function, and symbol properties are skipped (inside
* arrays they throw, since TOML arrays cannot have holes).
*
Expand Down
45 changes: 45 additions & 0 deletions src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1643,6 +1643,33 @@ pub struct Spread {
pub value: ExprNodeIndex,
}

/// Discriminants are shared with the C++ switch in
/// `Bun__Temporal__fromDateTimeLiteral`.
Comment thread
robobun marked this conversation as resolved.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(u8)]
pub enum TomlDateTimeKind {
/// `1979-05-27T00:32:00-07:00` → `Temporal.Instant`
OffsetDateTime = 1,
/// `1979-05-27T07:32:00` → `Temporal.PlainDateTime`
LocalDateTime = 2,
/// `1979-05-27` → `Temporal.PlainDate`
LocalDate = 3,
/// `07:32:00` → `Temporal.PlainTime`
LocalTime = 4,
}

impl TomlDateTimeKind {
/// Unqualified Temporal class name (`Instant`, `PlainDateTime`, …).
pub fn temporal_class(self) -> &'static [u8] {
match self {
TomlDateTimeKind::OffsetDateTime => b"Instant",
TomlDateTimeKind::LocalDateTime => b"PlainDateTime",
TomlDateTimeKind::LocalDate => b"PlainDate",
TomlDateTimeKind::LocalTime => b"PlainTime",
}
}
}

/// JavaScript string literal type
// repr(C, align(8)): `StoreStr`/`StoreRef` are `packed(4)`, so under
// `repr(Rust)` the `data.ptr: NonNull<u8>` lands at a 4-but-not-8-aligned
Expand All @@ -1668,6 +1695,11 @@ pub struct EString {
pub rope_len: u32,
pub prefer_template: bool,
pub is_utf16: bool,
/// Set only by the TOML parser on a date/time literal (`data` is its
/// ASCII source text). The TOML AST never enters the JS visit passes;
/// the sinks that materialize or print it check this tag and produce a
/// Temporal value instead of a string.
Comment thread
robobun marked this conversation as resolved.
pub toml_datetime: Option<TomlDateTimeKind>,
}
// Also exported as `String`; `EString` avoids colliding with bun_core::String.
pub use EString as String;
Expand All @@ -1681,6 +1713,7 @@ impl Default for EString {
end: None,
rope_len: 0,
is_utf16: false,
toml_datetime: None,
}
}
}
Expand Down Expand Up @@ -1728,6 +1761,7 @@ impl EString {
end: None,
rope_len: 0,
is_utf16: false,
toml_datetime: None,
}
}
/// `data` is arena-owned (source text or `Expr.Data.Store` / bump arena)
Expand All @@ -1738,6 +1772,16 @@ impl EString {
..Default::default()
}
}

/// A TOML date/time literal; `data` must be ASCII text `Temporal.*.from`
/// accepts verbatim.
Comment thread
robobun marked this conversation as resolved.
pub fn init_toml_datetime(data: &[u8], kind: TomlDateTimeKind) -> Self {
Self {
data: Str::new(data),
toml_datetime: Some(kind),
..Default::default()
}
}
/// Construct from a UTF-16 slice (arena-owned). The `data` slice's `.len()`
/// stores the **u16 element count** (not byte count); `slice16()` and
/// friends rely on this. The pointer is reinterpreted to `*const u8` for
Expand Down Expand Up @@ -1964,6 +2008,7 @@ impl EString {
end: self.end,
rope_len: self.rope_len,
is_utf16: self.is_utf16,
toml_datetime: self.toml_datetime,
}
}

Expand Down
1 change: 1 addition & 0 deletions src/ast/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2153,6 +2153,7 @@ impl Data {
end: el.end,
rope_len: el.rope_len,
is_utf16: el.is_utf16,
toml_datetime: el.toml_datetime,
});
Ok(Data::EString(StoreRef::from_bump(item)))
}
Expand Down
30 changes: 16 additions & 14 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,8 @@ pub mod parse_worker {
// is disjoint from any other field the caller may hold a pointer to.
let define = unsafe { &mut (*transpiler).options.define };
let mut ast = JSAst::init(
js_parser::new_lazy_export_ast(bump, define, opts, log, root, source, b"")?.unwrap(),
js_parser::new_lazy_export_ast(bump, define, opts, log, root, source, b"")?
.ok_or(AnyError::ParserError)?,
Comment thread
robobun marked this conversation as resolved.
);
ast.css = Some(crate::bundled_ast::CssAstRef::from_bump(
bump.alloc(bun_css::BundlerStyleSheet::empty()),
Expand All @@ -626,7 +627,8 @@ pub mod parse_worker {
// SAFETY: see `get_empty_css_ast` — disjoint field of a live `*mut Transpiler`.
let define = unsafe { &mut (*transpiler).options.define };
Ok(JSAst::init(
js_parser::new_lazy_export_ast(bump, define, opts, log, root, source, b"")?.unwrap(),
js_parser::new_lazy_export_ast(bump, define, opts, log, root, source, b"")?
.ok_or(AnyError::ParserError)?,
))
}

Expand Down Expand Up @@ -763,7 +765,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap(),
.ok_or(AnyError::ParserError)?,
));
}
Loader::Toml => {
Expand All @@ -786,7 +788,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap(),
.ok_or(AnyError::ParserError)?,
))
})();
let _ = temp_log.clone_to_with_recycled(log, true);
Expand All @@ -812,7 +814,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap(),
.ok_or(AnyError::ParserError)?,
))
})();
let _ = temp_log.clone_to_with_recycled(log, true);
Expand All @@ -834,7 +836,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap(),
.ok_or(AnyError::ParserError)?,
Comment thread
dylan-conway marked this conversation as resolved.
))
})();
let _ = temp_log.clone_to_with_recycled(log, true);
Expand Down Expand Up @@ -865,7 +867,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap(),
.ok_or(AnyError::ParserError)?,
))
})();
let _ = temp_log.clone_to_with_recycled(log, true);
Expand All @@ -889,7 +891,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap(),
.ok_or(AnyError::ParserError)?,
);
ast.add_url_for_css(
bump,
Expand Down Expand Up @@ -930,7 +932,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap(),
.ok_or(AnyError::ParserError)?,
);
ast.add_url_for_css(
bump,
Expand Down Expand Up @@ -1059,7 +1061,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap(),
.ok_or(AnyError::ParserError)?,
));
}
Loader::Napi => {
Expand Down Expand Up @@ -1128,7 +1130,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap(),
.ok_or(AnyError::ParserError)?,
));
}
Loader::Html => {
Expand All @@ -1154,7 +1156,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap();
.ok_or(AnyError::ParserError)?;
ast.import_records = bun_alloc::vec_from_iter_in(import_records, bump);

// We're banning import default of html loader files for now.
Expand Down Expand Up @@ -1280,7 +1282,7 @@ pub mod parse_worker {
symbols,
);
let _ = temp_log.append_to_maybe_recycled(log, source);
let mut ast = JSAst::init(lazy?.unwrap());
let mut ast = JSAst::init(lazy?.ok_or(AnyError::ParserError)?);
let css_ast_heap = crate::bundled_ast::CssAstRef::from_bump(bump.alloc(css_ast));
ast.css = Some(css_ast_heap);
ast.import_records = bun_alloc::vec_from_iter_in(import_records, bump);
Expand Down Expand Up @@ -1349,7 +1351,7 @@ pub mod parse_worker {
source,
b"",
)?
.unwrap(),
.ok_or(AnyError::ParserError)?,
);
ast.add_url_for_css(
bump,
Expand Down
2 changes: 1 addition & 1 deletion src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6911,7 +6911,7 @@ pub mod bv2_impl {
// We replace this runtime API call's ref later via .link on the Symbol.
b"__jsonParse",
)?
.unwrap(),
.ok_or(Error::ParserError)?,
);

let fake_input_file = crate::Graph::InputFile {
Expand Down
1 change: 1 addition & 0 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,7 @@ impl<'a> Transpiler<'a> {
lower_import_meta_main_for_node_js: false,
framework: None,
repl_mode: self.options.repl_mode,
lower_toml_datetimes: false,
};

opts.features.emit_decorator_metadata = this_parse.emit_decorator_metadata;
Expand Down
1 change: 1 addition & 0 deletions src/codegen/cppbind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ const rustSharedTypes: Record<string, string> = {

// JSC / Bun
"BunString": "bun_core::String",
"JSC::TemporalType": "crate::TemporalType",
"JSC::EncodedJSValue": "crate::JSValue",
"EncodedJSValue": "crate::JSValue",
"JSC::JSGlobalObject": "crate::JSGlobalObject",
Expand Down
Loading