Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
37 changes: 31 additions & 6 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, losslessly (Temporal carries nanosecond precision):
Comment thread
dylan-conway marked this conversation as resolved.
Outdated

```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,17 @@ 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`,
function, and symbol properties are skipped (inside arrays they throw,
since TOML arrays cannot have holes).
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 (its time-zone
annotation has no TOML form and is dropped), and `Date` becomes an offset
date-time. Because TOML cannot represent them, `null` values, `BigInt`,
Comment thread
dylan-conway marked this conversation as resolved.
Outdated
circular structures, `Temporal.PlainYearMonth`, `Temporal.PlainMonthDay`,
and `Temporal.Duration` throw; `undefined`, function, and symbol
properties are skipped (inside arrays they throw, since TOML arrays
cannot have holes).
Comment thread
dylan-conway marked this conversation as resolved.
Outdated

---

Expand Down
23 changes: 16 additions & 7 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,9 +780,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 @@ -798,10 +801,16 @@ 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;
* `undefined`, function, and symbol properties are skipped (inside
* arrays they throw, since TOML arrays cannot have holes).
* `Temporal.Instant`, `Temporal.PlainDateTime`, `Temporal.PlainDate`,
* and `Temporal.PlainTime` values become the corresponding TOML
* date/time literals, `Temporal.ZonedDateTime` becomes an offset
* date-time (dropping its time-zone annotation), and `Date` becomes an
* offset date-time. `null`, `BigInt`, circular structures, 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).
*
* @category Utilities
*
Expand Down
45 changes: 45 additions & 0 deletions src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1583,7 +1583,34 @@
// `data` (the only field needing a static relocation) at offset 0; `align(8)`
// keeps the struct itself 8-aligned. `EString` is arena-stored (never inline
// in `Expr`), so this does not affect `Expr` size.
/// 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",
}
}
}

#[repr(C, align(8))]

Check warning on line 1613 in src/ast/e.rs

View check run for this annotation

Claude / Claude Code Review

TomlDateTimeKind enum inserted between EString's doc comment and the struct

The `TomlDateTimeKind` enum is inserted between `EString`'s doc comment (line 1578 `/// JavaScript string literal type` + the `repr(C, align(8))` rationale) and the `#[repr(C, align(8))] pub struct EString` declaration itself. Since `//` comments are whitespace to rustc, the `///` line now attaches to `TomlDateTimeKind` (concatenating into "JavaScript string literal type Discriminants are shared with…"), the arm64-linker `repr` rationale sits above a `repr(u8)` enum, and `EString` loses its rust
Comment thread
robobun marked this conversation as resolved.
pub struct EString {
// A version of this where `utf8` and `value` are stored in a packed union, with len as a single u32 was attempted.
// It did not improve benchmarks. Neither did converting this from a heap-allocated type to a stack-allocated type.
Expand All @@ -1600,6 +1627,11 @@
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 @@ -1613,6 +1645,7 @@
end: None,
rope_len: 0,
is_utf16: false,
toml_datetime: None,
}
}
}
Expand Down Expand Up @@ -1660,6 +1693,7 @@
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 @@ -1670,6 +1704,16 @@
..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 @@ -1896,6 +1940,7 @@
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
85 changes: 85 additions & 0 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,12 @@ impl<'a> Parser<'a> {

let mut final_expr = expr;

// TOML date/time literals become `Temporal.*.from("...")` calls over
// a real unbound symbol, so the chunk renamer reserves the name
// instead of letting a user `Temporal` binding capture it.
Comment thread
robobun marked this conversation as resolved.
let mut temporal_ref: Option<js_ast::Ref> = None;
lower_date_time_literals(p, &mut final_expr, &mut temporal_ref)?;

// Optionally call a runtime API function to transform the expression
if !runtime_api_call.is_empty() {
let args_slice: &mut [Expr] = p.arena.alloc_slice_fill_with(1, |_| expr);
Expand Down Expand Up @@ -604,7 +610,86 @@ impl<'a> Parser<'a> {
b"",
)?))
}
}

/// Rewrites every `toml_datetime`-tagged `E::String` in `expr` (in place)
/// into a `Temporal.<Class>.from("<text>")` call, declaring the unbound
/// `Temporal` symbol on first use. The calls are pure-annotated so tree
/// shaking may drop unused exports.
Comment thread
robobun marked this conversation as resolved.
Outdated
fn lower_date_time_literals<'a>(
p: &mut JavaScriptParser<'a>,
expr: &mut Expr,
temporal_ref: &mut Option<js_ast::Ref>,
) -> Result<(), Error> {
Comment thread
robobun marked this conversation as resolved.
match expr.data {
js_ast::ExprData::EString(str) if str.toml_datetime.is_some() => {
let ref_ = match *temporal_ref {
Some(ref_) => ref_,
None => {
let ref_ =
p.declare_common_js_symbol(js_ast::symbol::Kind::Unbound, b"Temporal")?;
*temporal_ref = Some(ref_);
ref_
}
};
let (class, text) = {
let str = str.get();
let kind = str.toml_datetime.expect("infallible: guard checked");
(kind.temporal_class(), str.slice8())
};
let loc = expr.loc;
p.record_usage(ref_);
let namespace = p.new_expr(E::Identifier::init(ref_), loc);
let class_dot = p.new_expr(
E::Dot {
target: namespace,
name: E::Str::new(class),
name_loc: loc,
can_be_removed_if_unused: true,
..Default::default()
},
loc,
);
let from_dot = p.new_expr(
E::Dot {
target: class_dot,
name: E::Str::new(b"from"),
name_loc: loc,
can_be_removed_if_unused: true,
..Default::default()
},
loc,
);
let arg = p.new_expr(E::String::init(text), loc);
let args_slice: &mut [Expr] = p.arena.alloc_slice_fill_with(1, |_| arg);
*expr = p.new_expr(
E::Call {
target: from_dot,
args: Vec::from_arena_slice(args_slice),
can_be_unwrapped_if_unused: E::CallUnwrap::IfUnused,
..Default::default()
},
loc,
);
}
js_ast::ExprData::EArray(mut arr) => {
for item in arr.items.slice_mut() {
lower_date_time_literals(p, item, temporal_ref)?;
}
}
js_ast::ExprData::EObject(mut obj) => {
for property in obj.properties.slice_mut() {
if let Some(value) = &mut property.value {
lower_date_time_literals(p, value, temporal_ref)?;
}
}
}
_ => {}
}
Ok(())
}

impl<'a> Parser<'a> {
fn _parse<const TS: bool>(self) -> Result<crate::Result<'a>, Error> {
// `Source.path` is `Path<'static>`, so
// `path.text` satisfies `Action::Parse(&'static [u8])` directly.
Expand Down
18 changes: 17 additions & 1 deletion src/js_parser_jsc/expr_jsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,23 @@ fn data_to_js_with_check(
ExprData::EObject(e) => object_to_js(e, global, stack_check),
ExprData::EObjectJSON(e) => object_json_to_js(e, global, stack_check),
ExprData::EArrayJSON(e) => array_json_to_js(e, global, stack_check),
ExprData::EString(e) => string_to_js(e, global),
ExprData::EString(e) => {
if let Some(kind) = e.toml_datetime {
let text = e.slice8();
// SAFETY: `text` is an arena-owned ASCII slice that outlives
// the call.
return unsafe {
bun_jsc::cpp::Bun__Temporal__fromDateTimeLiteral(
global,
text.as_ptr(),
text.len(),
kind as u8,
)
}
.map_err(js_err);
}
string_to_js(e, global)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ExprData::ENull(_) => Ok(JSValue::NULL),
ExprData::EUndefined(_) => Ok(JSValue::UNDEFINED),
ExprData::EBoolean(boolean) | ExprData::EBranchBoolean(boolean) => Ok(if boolean.value {
Expand Down
22 changes: 22 additions & 0 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3683,6 +3683,28 @@ pub(crate) mod __gated_printer {
}
}
ExprData::EString(e) => {
// A TOML date/time literal prints as the `Temporal.*.from`
// call that reconstructs it (the bundler rewrites these in
// `to_lazy_export_ast` before printing).
Comment thread
robobun marked this conversation as resolved.
Outdated
if let Some(kind) = e.toml_datetime {
let wrap = level.gte(Level::New) || flags.contains(ExprFlag::ForbidCall);
if wrap {
self.print(b"(");
}
self.print_space_before_identifier();
self.add_source_mapping(expr.loc);
self.print(b"Temporal.");
self.print(kind.temporal_class());
self.print(b".from(\"");
// Always ASCII (validated by the TOML scanner); no escaping.
self.print(e.slice8());
self.print(b"\")");
if wrap {
self.print(b")");
}
return;
}

let mut e = *e;
e.resolve_rope_if_needed(self.bump);
self.add_source_mapping(expr.loc);
Expand Down
Loading
Loading