From c820de1a674109fc872c65de7c21eafcf795b7d6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:11:35 +0000 Subject: [PATCH 01/19] TOML: parse date/time values as Temporal objects, stringify them back 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. --- docs/runtime/toml.mdx | 37 ++- packages/bun-types/bun.d.ts | 23 +- src/ast/e.rs | 61 +++++ src/ast/expr.rs | 9 + src/js_parser/parse/parse_entry.rs | 88 +++++++ src/js_parser_jsc/expr_jsc.rs | 14 ++ src/js_printer/lib.rs | 19 ++ src/jsc/bindings/bindings.cpp | 140 +++++++++++ src/parsers/toml.rs | 130 +++++++---- src/react_compiler/lowering/build_hir/expr.rs | 2 + .../lowering/find_context_identifiers.rs | 3 +- src/runtime/api.rs | 13 ++ src/runtime/api/TOMLObject.rs | 89 ++++++- src/runtime/jsc_hooks.rs | 15 +- test/bundler/bundler_loader.test.ts | 37 +++ test/js/bun/resolve/toml/toml-fixture.toml | 6 + .../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 | 59 +++-- test/js/bun/toml/toml-test-suite.test.ts | 53 +++-- test/js/bun/toml/toml.test.ts | 220 ++++++++++++++---- 21 files changed, 887 insertions(+), 146 deletions(-) diff --git a/docs/runtime/toml.mdx b/docs/runtime/toml.mdx index 8414d9ae1774..d583924e56b8 100644 --- a/docs/runtime/toml.mdx +++ b/docs/runtime/toml.mdx @@ -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` - **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]]` @@ -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): + +```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: @@ -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`, +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). --- diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 87d0f34d25d6..9637a3fe3acf 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -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 * @@ -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 * diff --git a/src/ast/e.rs b/src/ast/e.rs index 4067d111f55e..8c8cb711b8fd 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -1975,6 +1975,67 @@ impl fmt::Display for EString { } } +/// Which of the four TOML date/time kinds a `DateTime` literal is, and the +/// Temporal class it materializes as. Discriminants cross the FFI boundary +/// (`Bun__Temporal__fromDateTimeLiteral`) — keep them in sync with the C++ +/// switch. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(u8)] +pub enum DateTimeKind { + /// `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 DateTimeKind { + /// Unqualified Temporal class name (`Instant`, `PlainDateTime`, …). + pub fn temporal_class(self) -> &'static [u8] { + match self { + DateTimeKind::OffsetDateTime => b"Instant", + DateTimeKind::LocalDateTime => b"PlainDateTime", + DateTimeKind::LocalDate => b"PlainDate", + DateTimeKind::LocalTime => b"PlainTime", + } + } +} + +/// A date/time literal that materializes as a Temporal object. Produced only +/// by the TOML parser; JavaScript has no such literal. +pub struct DateTime { + /// Source text of the literal. Always ASCII, already validated by the + /// producing parser, and accepted verbatim by `Temporal.*.from` (fractional + /// seconds are pre-truncated to the 9 digits Temporal carries). + pub data: Str, + pub kind: DateTimeKind, +} + +impl DateTime { + /// `data` is arena-owned (source text or bump arena) and bulk-freed; + /// `StoreStr` records it under the `StoreRef` contract. + pub fn init(data: &[u8], kind: DateTimeKind) -> Self { + Self { + data: Str::new(data), + kind, + } + } + + #[inline] + pub fn slice(&self) -> &[u8] { + self.data.slice() + } +} + +impl fmt::Display for DateTime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "E.DateTime({})", bstr::BStr::new(&self.data)) + } +} + // value is in the Node pub struct TemplatePart { pub value: ExprNodeIndex, diff --git a/src/ast/expr.rs b/src/ast/expr.rs index 7fff5ed55968..8f1b6f6b768a 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -954,6 +954,7 @@ impl_into_expr_data_boxed! { If => EIf, Import => EImport, InlinedEnum => EInlinedEnum, + DateTime => EDateTime, } impl_into_expr_data_inline! { @@ -1139,6 +1140,7 @@ pub enum Tag { ENumber, EBigInt, EString, + EDateTime, ERequireString, ERequireResolveString, ERequireCallTarget, @@ -1216,6 +1218,7 @@ impl Tag { Tag::EMissing => "", Tag::ENumber => "number", Tag::EBigInt => "BigInt", + Tag::EDateTime => "date-time", Tag::EObject | Tag::EObjectJSON => "object", Tag::ESpread => "...", Tag::ETemplate => "template", @@ -1479,6 +1482,7 @@ pub enum Data { ENumber(E::Number), EBigInt(StoreRef), EString(StoreRef), + EDateTime(StoreRef), ERequireString(E::RequireString), ERequireResolveString(E::RequireResolveString), @@ -2312,6 +2316,10 @@ impl Data { } hasher.update(b"\x00"); } + Data::EDateTime(e) => { + raw(hasher, e.kind as u8); + hasher.update(e.slice()); + } Data::ERequireString(e) => { raw(hasher, e.import_record_index); // preferably, i'd like to write the filepath } @@ -2894,6 +2902,7 @@ crate::new_store!( E::PrivateIdentifier, E::BigInt, E::EString, + E::DateTime, E::InlinedEnum, E::NameOfSymbol, ], diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 87c89136c8a7..9ec5b56a38eb 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -559,6 +559,14 @@ impl<'a> Parser<'a> { let mut final_expr = expr; + // Date/time literals (produced by the TOML parser) materialize as + // `Temporal.*.from("...")` calls. Bundled modules share one scope, so + // the reference must be a real unbound `Temporal` symbol: the chunk + // renamer then reserves the name and renames a user binding called + // `Temporal` instead of letting it capture these calls. + let mut temporal_ref: Option = 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); @@ -604,7 +612,87 @@ impl<'a> Parser<'a> { b"", )?)) } +} + +/// Rewrites every `E::DateTime` in `expr` (in place) into the +/// `Temporal..from("")` call it prints as, referencing an +/// unbound `Temporal` symbol declared on first use. The calls are annotated +/// as removable-if-unused: constructing a Temporal value from a validated +/// literal has no observable side effects, so tree shaking may drop unused +/// exports. +fn lower_date_time_literals<'a>( + p: &mut JavaScriptParser<'a>, + expr: &mut Expr, + temporal_ref: &mut Option, +) -> Result<(), Error> { + match expr.data { + js_ast::ExprData::EDateTime(dt) => { + 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 dt = dt.get(); + (dt.kind.temporal_class(), dt.slice()) + }; + 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(self) -> Result, Error> { // `Source.path` is `Path<'static>`, so // `path.text` satisfies `Action::Parse(&'static [u8])` directly. diff --git a/src/js_parser_jsc/expr_jsc.rs b/src/js_parser_jsc/expr_jsc.rs index 745c9da0b586..16845318765e 100644 --- a/src/js_parser_jsc/expr_jsc.rs +++ b/src/js_parser_jsc/expr_jsc.rs @@ -71,6 +71,20 @@ fn data_to_js_with_check( }), ExprData::ENumber(e) => Ok(number_to_js(*e)), // ExprData::EBigInt(e) => e.to_js(ctx, exception), + ExprData::EDateTime(e) => { + let e = e.get(); + let text = e.slice(); + // SAFETY: `text` is an arena-owned ASCII slice that outlives the call. + unsafe { + bun_jsc::cpp::Bun__Temporal__fromDateTimeLiteral( + global, + text.as_ptr(), + text.len(), + e.kind as u8, + ) + } + .map_err(js_err) + } ExprData::EInlinedEnum(inlined) => { data_to_js_with_check(&inlined.value.data, global, stack_check) } diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 86627cc0dd8a..f48bb72a3272 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -3215,6 +3215,25 @@ pub(crate) mod __gated_printer { self.print(b")"); } } + ExprData::EDateTime(e) => { + // A date/time literal has no JS literal form; it prints as + // the `Temporal.*.from` call that reconstructs the value. + 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(e.kind.temporal_class()); + self.print(b".from(\""); + // Always ASCII (validated by the TOML scanner); no escaping. + self.print(e.slice()); + self.print(b"\")"); + if wrap { + self.print(b")"); + } + } ExprData::ERequireMain => { self.print_space_before_identifier(); self.add_source_mapping(expr.loc); diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index d99045a75ad4..cb4ddcea3790 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -97,8 +97,17 @@ #include "JavaScriptCore/IntlObject.h" #include "JavaScriptCore/ISO8601.h" #include "JavaScriptCore/JSCTimeZone.h" +#include "JavaScriptCore/InstantCore.h" #include "JavaScriptCore/TemporalCoreTypes.h" +#include "JavaScriptCore/TemporalDuration.h" #include "JavaScriptCore/TemporalEnums.h" +#include "JavaScriptCore/TemporalInstant.h" +#include "JavaScriptCore/TemporalPlainDate.h" +#include "JavaScriptCore/TemporalPlainDateTime.h" +#include "JavaScriptCore/TemporalPlainMonthDay.h" +#include "JavaScriptCore/TemporalPlainTime.h" +#include "JavaScriptCore/TemporalPlainYearMonth.h" +#include "JavaScriptCore/TemporalZonedDateTime.h" #include "JavaScriptCore/TimeZoneICUBridge.h" #include "JavaScriptCore/FunctionPrototype.h" @@ -5834,6 +5843,137 @@ extern "C" [[ZIG_EXPORT(nothrow)]] double Bun__gregorianDateTimeToMSInZone(JSC:: return static_cast(r->epochMilliseconds()); } +// Materializes a parsed date/time literal as a Temporal object, through the +// same paths `Temporal.*.from(string)` takes. `kind` mirrors the Rust +// `bun_ast::E::DateTimeKind` discriminants. `text` is ASCII, pre-validated by +// the producing parser, and within Temporal's 9-digit fraction limit. +extern "C" [[ZIG_EXPORT(zero_is_throw)]] EncodedJSValue Bun__Temporal__fromDateTimeLiteral(JSC::JSGlobalObject* globalObject, const uint8_t* text, size_t len, uint8_t kind) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!JSC::Options::useTemporal()) [[unlikely]] { + // The Temporal structures on the global object only exist when the + // option is on; reaching for them would crash. + JSC::throwTypeError(globalObject, scope, "Date/time values require Temporal, which is disabled in this process"_s); + return {}; + } + + WTF::String string { std::span(reinterpret_cast(text), len) }; + JSC::JSValue item = JSC::jsString(vm, string); + + JSC::JSObject* result = nullptr; + switch (kind) { + case 1: + result = JSC::TemporalInstant::toInstant(globalObject, item); + break; + case 2: + result = JSC::TemporalPlainDateTime::from(globalObject, item, JSC::jsUndefined()); + break; + case 3: + result = JSC::TemporalPlainDate::from(globalObject, item, JSC::jsUndefined()); + break; + case 4: + result = JSC::TemporalPlainTime::from(globalObject, item, JSC::jsUndefined()); + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(result); + return JSValue::encode(result); +} + +// Classifies a JSValue as one of the Temporal object types, or 0 for +// everything else. The discriminants are shared with `TOMLObject.rs`: +// 1 Instant, 2 PlainDateTime, 3 PlainDate, 4 PlainTime, 5 ZonedDateTime, +// 6 PlainYearMonth, 7 PlainMonthDay, 8 Duration (1-4 mirror `DateTimeKind`). +extern "C" [[ZIG_EXPORT(nothrow)]] uint8_t Bun__JSValue__temporalObjectType(JSC::EncodedJSValue encodedValue) +{ + JSC::JSValue value = JSC::JSValue::decode(encodedValue); + if (!value.isCell()) + return 0; + JSC::JSCell* cell = value.asCell(); + // Every Temporal class is a plain ObjectType cell; anything else + // (JSFinalObject, arrays, dates, functions, …) short-circuits here. + if (cell->type() != JSC::ObjectType) + return 0; + if (cell->inherits()) + return 1; + if (cell->inherits()) + return 2; + if (cell->inherits()) + return 3; + if (cell->inherits()) + return 4; + if (cell->inherits()) + return 5; + if (cell->inherits()) + return 6; + if (cell->inherits()) + return 7; + if (cell->inherits()) + return 8; + return 0; +} + +// Formats a Temporal object (`temporalType` from +// `Bun__JSValue__temporalObjectType`, 1-5 only) as a TOML date/time literal +// into `buf`. Returns the length written, or -1 if it cannot fit. The ISO +// fields are formatted directly so no `[u-ca=...]` calendar annotation is +// emitted (the ISO date itself is what TOML can carry); a ZonedDateTime +// emits its offset form, dropping the `[Time/Zone]` annotation. +extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, uint8_t temporalType, uint8_t* buf, size_t bufLen) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSC::JSCell* cell = JSC::JSValue::decode(encodedValue).asCell(); + constexpr JSC::PrecisionData autoPrecision { { JSC::Precision::Auto, 0 }, JSC::TemporalUnit::Nanosecond, 1 }; + + WTF::String string; + switch (temporalType) { + case 1: + string = JSC::TemporalCore::instantToString(dynamicDowncast(cell)->exactTime(), std::nullopt, autoPrecision); + break; + case 2: { + auto* dateTime = dynamicDowncast(cell); + string = JSC::ISO8601::temporalDateTimeToString(dateTime->plainDate(), dateTime->plainTime(), { JSC::Precision::Auto, 0 }); + break; + } + case 3: + string = JSC::ISO8601::temporalDateToString(dynamicDowncast(cell)->plainDate()); + break; + case 4: + string = JSC::ISO8601::temporalTimeToString(dynamicDowncast(cell)->plainTime(), { JSC::Precision::Auto, 0 }); + break; + case 5: { + auto* zoned = dynamicDowncast(cell); + std::optional offsetNs = zoned->getOffsetNanoseconds(globalObject); + RETURN_IF_EXCEPTION(scope, -1); + ASSERT(offsetNs); + // TOML offsets are `HH:MM` only; a historic sub-minute offset + // (e.g. pre-1972 Africa/Monrovia) falls back to the equivalent + // UTC instant rather than emitting an offset TOML cannot parse. + if (offsetNs && *offsetNs % 60000000000ll != 0) + offsetNs = std::nullopt; + string = JSC::TemporalCore::instantToString(zoned->exactTime(), offsetNs, autoPrecision); + break; + } + default: + RELEASE_ASSERT_NOT_REACHED(); + } + + unsigned length = string.length(); + if (length > bufLen) [[unlikely]] + return -1; + for (unsigned i = 0; i < length; i++) { + ASSERT(isASCII(string[i])); + buf[i] = static_cast(string[i]); + } + return static_cast(length); +} + extern "C" EncodedJSValue JSC__JSValue__dateInstanceFromNumber(JSC::JSGlobalObject* globalObject, double unixTimestamp) { auto& vm = JSC::getVM(globalObject); diff --git a/src/parsers/toml.rs b/src/parsers/toml.rs index aff96cfad4fb..98616e438f55 100644 --- a/src/parsers/toml.rs +++ b/src/parsers/toml.rs @@ -16,7 +16,10 @@ //! - integers parse as `f64` but are validated as 64-bit integers first; //! values outside `Number.MAX_SAFE_INTEGER` are errors (TOML requires //! lossless handling or an error) -//! - date/time values (all four kinds) become strings of their source text +//! - date/time values become `E::DateTime` nodes that materialize as Temporal +//! objects (offset date-time → `Temporal.Instant`, local date-time → +//! `Temporal.PlainDateTime`, local date → `Temporal.PlainDate`, local time +//! → `Temporal.PlainTime`) //! - strings are UTF-8; non-ASCII content is re-encoded to UTF-16 EStrings //! so both the JS conversion and the printer paths agree @@ -133,8 +136,11 @@ enum ValueData<'a> { is_ascii: bool, }, Number(f64), - /// All four TOML date/time kinds, as their source text (always ASCII). - DateTime(&'a [u8]), + /// One of the four TOML date/time kinds, as its source text (always ASCII). + DateTime { + text: &'a [u8], + kind: E::DateTimeKind, + }, Boolean(bool), ArrayOpen, InlineOpen, @@ -184,6 +190,29 @@ const MAX_SAFE_INTEGER: i64 = (1 << 53) - 1; const BARE_CR: &[u8] = b"Bare carriage return is not allowed; use \\r\\n or \\n"; const UNDERSCORE_IN_NUMBER: &[u8] = b"Underscores in numbers must be surrounded by digits"; +/// TOML allows unlimited fractional-second digits and says extra precision +/// "should be truncated, not rounded"; Temporal carries nanoseconds and +/// rejects more than 9 digits, so drop anything past the ninth here. +fn truncate_fractional_seconds<'a>(text: &'a [u8], bump: &'a Bump) -> &'a [u8] { + let Some(dot) = text.iter().position(|&b| b == b'.') else { + return text; + }; + let frac_start = dot + 1; + let mut frac_end = frac_start; + while frac_end < text.len() && text[frac_end].is_ascii_digit() { + frac_end += 1; + } + if frac_end - frac_start <= 9 { + return text; + } + let keep = frac_start + 9; + let mut out: ArenaVec<'a, u8> = + ArenaVec::with_capacity_in(keep + (text.len() - frac_end), bump); + out.extend_from_slice(&text[..keep]); + out.extend_from_slice(&text[frac_end..]); + out.into_bump_slice() +} + fn is_bare_key_char(c: u8) -> bool { c.is_ascii_alphanumeric() || c == b'-' || c == b'_' } @@ -634,15 +663,18 @@ impl<'a, 'log> Scanner<'a, 'log> { if self.peek().is_ascii_digit() { let d1 = self.digit_run_len(self.pos); if d1 == 4 && self.peek_at(self.pos + 4) == b'-' { - let text = self.scan_datetime_from_date()?; + let (text, kind) = self.scan_datetime_from_date()?; self.expect_value_terminator()?; - return Ok(ValueData::DateTime(text)); + return Ok(ValueData::DateTime { text, kind }); } if d1 == 2 && self.peek_at(self.pos + 2) == b':' { let start = self.pos; self.scan_time_digits()?; self.expect_value_terminator()?; - return Ok(ValueData::DateTime(&self.src[start..self.pos])); + return Ok(ValueData::DateTime { + text: &self.src[start..self.pos], + kind: E::DateTimeKind::LocalTime, + }); } } @@ -672,8 +704,8 @@ impl<'a, 'log> Scanner<'a, 'log> { } /// `YYYY-MM-DD` and everything that may follow it (time, offset). - /// Returns the full source text of the literal. - fn scan_datetime_from_date(&mut self) -> PResult<&'a [u8]> { + /// Returns the full source text of the literal and which kind it is. + fn scan_datetime_from_date(&mut self) -> PResult<(&'a [u8], E::DateTimeKind)> { let start = self.pos; let year = self.read_digits(4, b"Invalid date: expected a 4-digit year")?; @@ -724,44 +756,53 @@ impl<'a, 'log> Scanner<'a, 'log> { _ => false, }; - if has_time { - self.scan_time_digits()?; - // Optional offset. - match self.peek() { - b'Z' | b'z' => { - self.pos += 1; + if !has_time { + return Ok((&self.src[start..self.pos], E::DateTimeKind::LocalDate)); + } + + self.scan_time_digits()?; + // Optional offset. + let has_offset = match self.peek() { + b'Z' | b'z' => { + self.pos += 1; + true + } + b'+' | b'-' => { + self.pos += 1; + let hour = + self.read_digits(2, b"Invalid date-time offset: expected 2-digit hours")?; + if self.peek() != b':' { + return Err(self.err( + self.pos, + b"Invalid date-time offset: expected ':' between hours and minutes", + )); } - b'+' | b'-' => { - self.pos += 1; - let hour = - self.read_digits(2, b"Invalid date-time offset: expected 2-digit hours")?; - if self.peek() != b':' { - return Err(self.err( - self.pos, - b"Invalid date-time offset: expected ':' between hours and minutes", - )); - } - self.pos += 1; - let minute = - self.read_digits(2, b"Invalid date-time offset: expected 2-digit minutes")?; - if hour > 23 { - return Err(self.err( - start, - b"Invalid date-time offset: hours must be between 00 and 23", - )); - } - if minute > 59 { - return Err(self.err( - start, - b"Invalid date-time offset: minutes must be between 00 and 59", - )); - } + self.pos += 1; + let minute = + self.read_digits(2, b"Invalid date-time offset: expected 2-digit minutes")?; + if hour > 23 { + return Err(self.err( + start, + b"Invalid date-time offset: hours must be between 00 and 23", + )); } - _ => {} + if minute > 59 { + return Err(self.err( + start, + b"Invalid date-time offset: minutes must be between 00 and 59", + )); + } + true } - } + _ => false, + }; - Ok(&self.src[start..self.pos]) + let kind = if has_offset { + E::DateTimeKind::OffsetDateTime + } else { + E::DateTimeKind::LocalDateTime + }; + Ok((&self.src[start..self.pos], kind)) } /// `HH:MM[:SS[.frac]]` — seconds are optional in TOML 1.1. @@ -1713,7 +1754,10 @@ impl<'a, 'log> Parser<'a, 'log> { match token.data { ValueData::String { text, is_ascii } => Ok(self.string_expr(text, is_ascii, loc)), ValueData::Number(n) => Ok(Expr::init(E::Number::new(n), loc)), - ValueData::DateTime(text) => Ok(Expr::init(E::String::init(text), loc)), + ValueData::DateTime { text, kind } => { + let text = truncate_fractional_seconds(text, self.bump); + Ok(Expr::init(E::DateTime::init(text, kind), loc)) + } ValueData::Boolean(b) => Ok(Expr::init(E::Boolean { value: b }, loc)), ValueData::ArrayOpen => self.parse_array(token.pos), ValueData::InlineOpen => self.parse_inline_table(token.pos), diff --git a/src/react_compiler/lowering/build_hir/expr.rs b/src/react_compiler/lowering/build_hir/expr.rs index 69596394f38b..4908d6b704ae 100644 --- a/src/react_compiler/lowering/build_hir/expr.rs +++ b/src/react_compiler/lowering/build_hir/expr.rs @@ -347,6 +347,8 @@ pub(crate) fn lower_expression( Data::EMissing(_) => Err(todo_err("EMissing", loc)), Data::ECommonjsExportIdentifier(_) => Err(todo_err("ECommonjsExportIdentifier", loc)), Data::ENameOfSymbol(_) => Err(todo_err("ENameOfSymbol", loc)), + // Produced only by the TOML parser; never occurs in JavaScript source. + Data::EDateTime(_) => Err(todo_err("EDateTime", loc)), } } diff --git a/src/react_compiler/lowering/find_context_identifiers.rs b/src/react_compiler/lowering/find_context_identifiers.rs index cda024b07435..b309e383f390 100644 --- a/src/react_compiler/lowering/find_context_identifiers.rs +++ b/src/react_compiler/lowering/find_context_identifiers.rs @@ -536,7 +536,8 @@ impl<'a> ContextIdentifierVisitor<'a> { | Data::EImportMetaMain(_) | Data::ERequireMain | Data::ESpecial(_) - | Data::ENameOfSymbol(_) => {} + | Data::ENameOfSymbol(_) + | Data::EDateTime(_) => {} } } } diff --git a/src/runtime/api.rs b/src/runtime/api.rs index dabd44dc1ef7..5faaf138ed18 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -313,6 +313,19 @@ fn expr_to_js_with_check( ExprData::EBoolean(boolean) => Ok(JSValue::from(boolean.value)), ExprData::ENumber(number) => Ok(JSValue::js_number(number.value())), ExprData::EString(str) => estring_to_js(str.get(), global), + ExprData::EDateTime(dt) => { + let dt = dt.get(); + let text = dt.slice(); + // SAFETY: `text` is an arena-owned ASCII slice that outlives the call. + unsafe { + bun_jsc::cpp::Bun__Temporal__fromDateTimeLiteral( + global, + text.as_ptr(), + text.len(), + dt.kind as u8, + ) + } + } ExprData::EArray(arr) => { JSValue::create_array_from_iter(global, arr.slice().iter(), |item| { expr_to_js_with_check(*item, global, stack_check) diff --git a/src/runtime/api/TOMLObject.rs b/src/runtime/api/TOMLObject.rs index 1dc12136f876..99ed54c2d5ca 100644 --- a/src/runtime/api/TOMLObject.rs +++ b/src/runtime/api/TOMLObject.rs @@ -70,7 +70,11 @@ fn stringify(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { } let unwrapped = value.unwrap_boxed_primitive(global)?; - if !unwrapped.is_object() || unwrapped.is_array() || unwrapped.is_date() { + if !unwrapped.is_object() + || unwrapped.is_array() + || unwrapped.is_date() + || temporal_object_type(unwrapped) != 0 + { return Err(global.throw(format_args!( "TOML.stringify expects an object at the top level (a TOML document is a table)" ))); @@ -176,13 +180,18 @@ impl Stringifier { } while let Some(item) = iter.next()? { let item = item.unwrap_boxed_primitive(global)?; - if !item.is_object() || item.is_array() || item.is_date() || item.is_function() { + if !item.is_object() + || item.is_array() + || item.is_date() + || item.is_function() + || temporal_object_type(item) != 0 + { return Ok(Layout::Keyval); } } return Ok(Layout::ArrayOfTables); } - if value.is_object() && !value.is_date() { + if value.is_object() && !value.is_date() && temporal_object_type(value) == 0 { return Ok(Layout::Table); } Ok(Layout::Keyval) @@ -257,6 +266,7 @@ impl Stringifier { || item.is_array() || item.is_date() || item.is_function() + || temporal_object_type(item) != 0 { self.path.pop(); return Err(self.err_changed(global)); @@ -321,6 +331,11 @@ impl Stringifier { return self.append_datetime(global, value); } + let temporal_type = temporal_object_type(value); + if temporal_type != 0 { + return self.append_temporal(global, value, temporal_type); + } + if value.is_array() { self.mark_visiting(global, value)?; self.builder.append_lchar(b'['); @@ -473,6 +488,50 @@ impl Stringifier { Ok(()) } + /// A Temporal object as the TOML date/time literal of its type: `Instant` + /// and `ZonedDateTime` emit offset date-times (the latter dropping its + /// time-zone annotation), `PlainDateTime`/`PlainDate`/`PlainTime` their + /// local forms. `PlainYearMonth`/`PlainMonthDay`/`Duration` have no TOML + /// representation and throw. + fn append_temporal( + &mut self, + global: &JSGlobalObject, + value: JSValue, + temporal_type: u8, + ) -> StringifyResult<()> { + if temporal_type > TEMPORAL_ZONED_DATE_TIME { + return Err(global + .throw(format_args!( + "TOML.stringify cannot serialize {} (it has no TOML representation)", + temporal_type_name(temporal_type) + )) + .into()); + } + let mut buf = [0u8; 64]; + // SAFETY: `buf` is a live stack buffer for the duration of the call. + let len = unsafe { + jsc::cpp::Bun__Temporal__toTOMLDateTime( + global, + value, + temporal_type, + buf.as_mut_ptr(), + buf.len(), + ) + }?; + // The expanded-year form (leading `+`/`-`) has a 6-digit year, which + // TOML's 4-digit `date-fullyear` cannot carry. + if len < 1 || !buf[0].is_ascii_digit() { + return Err(global + .throw(format_args!( + "TOML.stringify cannot serialize a {} outside years 0000-9999", + temporal_type_name(temporal_type) + )) + .into()); + } + self.builder.append_latin1(&buf[..len as usize]); + Ok(()) + } + // ── errors ───────────────────────────────────────────────────────────── fn err_null_value(&mut self, global: &JSGlobalObject, key: &BunString) -> StringifyError { @@ -509,6 +568,30 @@ impl Stringifier { } } +/// The `Bun__Temporal__toTOMLDateTime` discriminant for `ZonedDateTime`, the +/// last Temporal type with a TOML representation (1-5; 6-8 have none). +const TEMPORAL_ZONED_DATE_TIME: u8 = 5; + +/// Classifies `value` via `Bun__JSValue__temporalObjectType`: 0 for anything +/// that is not a Temporal object, else the 1-8 discriminant +/// `temporal_type_name` describes. +fn temporal_object_type(value: JSValue) -> u8 { + jsc::cpp::Bun__JSValue__temporalObjectType(value) +} + +fn temporal_type_name(temporal_type: u8) -> &'static str { + match temporal_type { + 1 => "Temporal.Instant", + 2 => "Temporal.PlainDateTime", + 3 => "Temporal.PlainDate", + 4 => "Temporal.PlainTime", + 5 => "Temporal.ZonedDateTime", + 6 => "Temporal.PlainYearMonth", + 7 => "Temporal.PlainMonthDay", + _ => "Temporal.Duration", + } +} + fn is_bare_key(name: &BunString) -> bool { if name.length() == 0 { return false; diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 1a5f86b7ec84..9c878ce39e5d 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -2737,12 +2737,19 @@ fn transpile_source_code_inner( // `SExpr` part; anything else is a parser bug. unreachable!("JSON/TOML/YAML parse result is always SExpr") }; - bun_js_parser_jsc::expr_to_js(&s_expr.value, global).unwrap_or_else(|e| { - bun_core::Output::panic(format_args!( + match bun_js_parser_jsc::expr_to_js(&s_expr.value, global) { + Ok(value) => value, + // A thrown exception (e.g. constructing a Temporal + // date/time while Temporal is disabled) fails the + // module load with that exception pending. + Err(bun_ast::ToJSError::JSError | bun_ast::ToJSError::JSTerminated) => { + return Err(crate::Error::JSError); + } + Err(e) => bun_core::Output::panic(format_args!( "Unexpected JS error: {}", <&'static str>::from(e) - )) - }) + )), + } }; return Ok(OwnedResolvedSource::from(ResolvedSource { specifier: input_specifier.dupe_ref(), diff --git a/test/bundler/bundler_loader.test.ts b/test/bundler/bundler_loader.test.ts index daeccfa1acfe..e633c752fbf3 100644 --- a/test/bundler/bundler_loader.test.ts +++ b/test/bundler/bundler_loader.test.ts @@ -54,6 +54,43 @@ describe("bundler", async () => { }, run: { stdout: '{"hello":"world"}' }, }); + // The Temporal reference is a real unbound symbol: a user binding named + // Temporal in the same bundle gets renamed instead of capturing the + // `Temporal.*.from` calls the TOML module compiles to. + itBundled("bun/loader-toml-datetime-shadowed-temporal-global", { + target, + files: { + "/entry.ts": /* js */ ` + import cfg from './config.toml'; + var Temporal = "shadowed"; + console.write(Temporal + " " + cfg.ld.toString()); + `, + "/config.toml": `ld = 1979-05-27`, + }, + run: { stdout: "shadowed 1979-05-27" }, + }); + // TOML date/time values bundle as Temporal construction calls; the + // bundled module yields the same values Bun.TOML.parse returns. + itBundled("bun/loader-toml-datetime", { + target, + files: { + "/entry.ts": /* js */ ` + import cfg, { lt } from './config.toml'; + console.write(JSON.stringify([ + cfg.odt instanceof Temporal.Instant, cfg.odt.toString(), + cfg.ldt instanceof Temporal.PlainDateTime, cfg.ldt.toString(), + cfg.ld instanceof Temporal.PlainDate, cfg.ld.toString(), + lt instanceof Temporal.PlainTime, lt.toString(), + cfg.tbl.arr[0].toString(), + ])); + `, + "/config.toml": `odt = 1979-05-27T00:32:00-07:00\nldt = 1979-05-27 07:32\nld = 1979-05-27\nlt = 07:32:00.500\n[tbl]\narr = [ 07:32:00 ]`, + }, + run: { + stdout: + '[true,"1979-05-27T07:32:00Z",true,"1979-05-27T07:32:00",true,"1979-05-27",true,"07:32:00.5","07:32:00"]', + }, + }); itBundled("bun/loader-text-file", { target, files: { diff --git a/test/js/bun/resolve/toml/toml-fixture.toml b/test/js/bun/resolve/toml/toml-fixture.toml index bed28762b3e9..20ce461d7d0b 100644 --- a/test/js/bun/resolve/toml/toml-fixture.toml +++ b/test/js/bun/resolve/toml/toml-fixture.toml @@ -40,3 +40,9 @@ entry_one = "three" [[array.nested]] entry_one = "four" + +[dates] +odt = 1979-05-27T00:32:00-07:00 +ldt = 1979-05-27T07:32:00 +ld = 1979-05-27 +lt = 07:32:00 diff --git a/test/js/bun/resolve/toml/toml-fixture.toml.txt b/test/js/bun/resolve/toml/toml-fixture.toml.txt index 5b7df33af2b5..7154794b410e 100644 --- a/test/js/bun/resolve/toml/toml-fixture.toml.txt +++ b/test/js/bun/resolve/toml/toml-fixture.toml.txt @@ -40,3 +40,9 @@ entry_one = "three" [[array.nested]] entry_one = "four" + +[dates] +odt = 1979-05-27T00:32:00-07:00 +ldt = 1979-05-27T07:32:00 +ld = 1979-05-27 +lt = 07:32:00 diff --git a/test/js/bun/resolve/toml/toml.test.js b/test/js/bun/resolve/toml/toml.test.js index c244e1be31ef..e679da1048b5 100644 --- a/test/js/bun/resolve/toml/toml.test.js +++ b/test/js/bun/resolve/toml/toml.test.js @@ -20,6 +20,15 @@ function checkToml(toml) { expect(toml.install.scopes["@mybigcompany3"].three).toBe(4); expect(toml.install.cache.dir).toBe("C:\\Windows\\System32"); expect(toml.install.cache.dir2).toBe("C:\\Windows\\System32\\🏳️‍🌈"); + // Imported date/time values are the same Temporal objects TOML.parse returns. + expect(toml.dates.odt).toBeInstanceOf(Temporal.Instant); + expect(toml.dates.odt.toString()).toBe("1979-05-27T07:32:00Z"); + expect(toml.dates.ldt).toBeInstanceOf(Temporal.PlainDateTime); + expect(toml.dates.ldt.toString()).toBe("1979-05-27T07:32:00"); + expect(toml.dates.ld).toBeInstanceOf(Temporal.PlainDate); + expect(toml.dates.ld.toString()).toBe("1979-05-27"); + expect(toml.dates.lt).toBeInstanceOf(Temporal.PlainTime); + expect(toml.dates.lt.toString()).toBe("07:32:00"); } it("via dynamic import", async () => { diff --git a/test/js/bun/toml/generate_toml_test_suite.ts b/test/js/bun/toml/generate_toml_test_suite.ts index e0ba0b62c358..3dd1826c7be3 100644 --- a/test/js/bun/toml/generate_toml_test_suite.ts +++ b/test/js/bun/toml/generate_toml_test_suite.ts @@ -18,8 +18,10 @@ * - integers -> number; values outside Number.MAX_SAFE_INTEGER throw * (TOML requires lossless handling or an error; mixed number/BigInt * output is not acceptable API) - * - datetime/datetime-local/date-local/time-local -> string (source text), - * compared via separator/fraction normalization (see generated helper) + * - datetime -> Temporal.Instant, datetime-local -> Temporal.PlainDateTime, + * date-local -> Temporal.PlainDate, time-local -> Temporal.PlainTime; + * compared by class + canonical toString (Temporal instances have no own + * properties, so a bare toEqual would compare nothing) * - invalid documents -> SyntaxError, with the exact full message asserted * when the in-tree parser produced a SyntaxError at generation time */ @@ -253,10 +255,11 @@ let output = `// Tests generated from the official toml-lang/toml-test conforman // TOML type encoding asserted by these tests: // - integer: number; values outside Number.MAX_SAFE_INTEGER throw (TOML // requires lossless handling or an error — see the out-of-range block) -// - datetime, datetime-local, date-local, time-local: string (source text); -// compared after normalizing the date/time separator to "T", uppercasing -// "Z", padding omitted seconds to ":00", and trimming trailing zeros from -// fractional seconds +// - datetime: Temporal.Instant; datetime-local: Temporal.PlainDateTime; +// date-local: Temporal.PlainDate; time-local: Temporal.PlainTime. +// Temporal instances have no own properties, so toEqual alone would +// compare nothing: both sides normalize to "" tag strings +// (class + canonical toString) before the single toEqual // - invalid documents throw SyntaxError; the exact full message is asserted // where the in-tree parser produced a SyntaxError at generation time // @@ -275,20 +278,29 @@ function dt(kind: TomlDateTime["kind"], value: string): TomlDateTime { return new TomlDateTime(kind, value); } -function normalizeDateTime(s: string): string { - return s - .replace(/^(\\d{4}-\\d{2}-\\d{2})[ tT]/, "$1T") - .replace(/[zZ]$/, "Z") - .replace(/(^|T)(\\d{2}:\\d{2})(?=[Z+-]|$)/, "$1$2:00") - .replace(/\\.(\\d+)/, (_, frac: string) => { - const trimmed = frac.replace(/0+$/, ""); - return trimmed === "" ? "" : "." + trimmed; - }); +const TEMPORAL_CLASS = { + "datetime": "Instant", + "datetime-local": "PlainDateTime", + "date-local": "PlainDate", + "time-local": "PlainTime", +} as const; + +function temporalTag(className: string, iso: string): string { + return \`\`; } -// Datetime markers become normalized strings; everything else is unchanged. +// The corpus value is TOML source text; TOML.parse truncates fractional +// seconds to Temporal's 9-digit limit, and Temporal.*.from accepts the rest +// of TOML's spellings (space separator, lowercase t/z, omitted seconds) as is. +function expectedTemporalTag(marker: TomlDateTime): string { + const className = TEMPORAL_CLASS[marker.kind]; + const text = marker.value.replace(/\\.(\\d{9})\\d+/, ".$1"); + return temporalTag(className, (Temporal as any)[className].from(text).toString()); +} + +// Datetime markers become "" tags; everything else is unchanged. function normalizeExpected(expected: unknown): unknown { - if (expected instanceof TomlDateTime) return normalizeDateTime(expected.value); + if (expected instanceof TomlDateTime) return expectedTemporalTag(expected); if (Array.isArray(expected)) return expected.map(normalizeExpected); if (expected !== null && typeof expected === "object") { const out: Record = Object.create(null); @@ -299,10 +311,15 @@ function normalizeExpected(expected: unknown): unknown { } // Normalize the positions of \`actual\` that \`expected\` marks as datetimes, in -// lockstep, so a single toEqual compares everything else exactly. +// lockstep, so a single toEqual compares everything else exactly. A value of +// the wrong class is left as is and shows up as the toEqual mismatch. function normalizeActual(actual: unknown, expected: unknown): unknown { if (expected instanceof TomlDateTime) { - return typeof actual === "string" ? normalizeDateTime(actual) : actual; + const className = TEMPORAL_CLASS[expected.kind]; + if (actual instanceof (Temporal as any)[className]) { + return temporalTag(className, (actual as any).toString()); + } + return actual; } if (Array.isArray(expected) && Array.isArray(actual)) { return actual.map((a, i) => normalizeActual(a, expected[i])); @@ -328,8 +345,8 @@ function expectTomlEqual(parsed: unknown, expected: unknown): void { output += `\n// Each case also asserts that parse(stringify(parse(input))) produces the same // value: stringify must never emit a document its own parse rejects or reads -// back differently. The TOML text may change (date/times come back as quoted -// strings), but the JS value is a fixed point after one lap. +// back differently. The TOML text may change (layout, normalized date/time +// spellings), but the JS value is a fixed point after one lap. describe("toml-test/valid", () => {\n`; for (const tc of validCases) { output += ` test(${jsString(tc.name)}, () => {\n`; diff --git a/test/js/bun/toml/toml-test-suite.test.ts b/test/js/bun/toml/toml-test-suite.test.ts index 30d9ae025da6..c2ec7b604af4 100644 --- a/test/js/bun/toml/toml-test-suite.test.ts +++ b/test/js/bun/toml/toml-test-suite.test.ts @@ -6,10 +6,11 @@ // TOML type encoding asserted by these tests: // - integer: number; values outside Number.MAX_SAFE_INTEGER throw (TOML // requires lossless handling or an error — see the out-of-range block) -// - datetime, datetime-local, date-local, time-local: string (source text); -// compared after normalizing the date/time separator to "T", uppercasing -// "Z", padding omitted seconds to ":00", and trimming trailing zeros from -// fractional seconds +// - datetime: Temporal.Instant; datetime-local: Temporal.PlainDateTime; +// date-local: Temporal.PlainDate; time-local: Temporal.PlainTime. +// Temporal instances have no own properties, so toEqual alone would +// compare nothing: both sides normalize to "" tag strings +// (class + canonical toString) before the single toEqual // - invalid documents throw SyntaxError; the exact full message is asserted // where the in-tree parser produced a SyntaxError at generation time // @@ -28,20 +29,29 @@ function dt(kind: TomlDateTime["kind"], value: string): TomlDateTime { return new TomlDateTime(kind, value); } -function normalizeDateTime(s: string): string { - return s - .replace(/^(\d{4}-\d{2}-\d{2})[ tT]/, "$1T") - .replace(/[zZ]$/, "Z") - .replace(/(^|T)(\d{2}:\d{2})(?=[Z+-]|$)/, "$1$2:00") - .replace(/\.(\d+)/, (_, frac: string) => { - const trimmed = frac.replace(/0+$/, ""); - return trimmed === "" ? "" : "." + trimmed; - }); +const TEMPORAL_CLASS = { + "datetime": "Instant", + "datetime-local": "PlainDateTime", + "date-local": "PlainDate", + "time-local": "PlainTime", +} as const; + +function temporalTag(className: string, iso: string): string { + return ``; +} + +// The corpus value is TOML source text; TOML.parse truncates fractional +// seconds to Temporal's 9-digit limit, and Temporal.*.from accepts the rest +// of TOML's spellings (space separator, lowercase t/z, omitted seconds) as is. +function expectedTemporalTag(marker: TomlDateTime): string { + const className = TEMPORAL_CLASS[marker.kind]; + const text = marker.value.replace(/\.(\d{9})\d+/, ".$1"); + return temporalTag(className, (Temporal as any)[className].from(text).toString()); } -// Datetime markers become normalized strings; everything else is unchanged. +// Datetime markers become "" tags; everything else is unchanged. function normalizeExpected(expected: unknown): unknown { - if (expected instanceof TomlDateTime) return normalizeDateTime(expected.value); + if (expected instanceof TomlDateTime) return expectedTemporalTag(expected); if (Array.isArray(expected)) return expected.map(normalizeExpected); if (expected !== null && typeof expected === "object") { const out: Record = Object.create(null); @@ -52,10 +62,15 @@ function normalizeExpected(expected: unknown): unknown { } // Normalize the positions of `actual` that `expected` marks as datetimes, in -// lockstep, so a single toEqual compares everything else exactly. +// lockstep, so a single toEqual compares everything else exactly. A value of +// the wrong class is left as is and shows up as the toEqual mismatch. function normalizeActual(actual: unknown, expected: unknown): unknown { if (expected instanceof TomlDateTime) { - return typeof actual === "string" ? normalizeDateTime(actual) : actual; + const className = TEMPORAL_CLASS[expected.kind]; + if (actual instanceof (Temporal as any)[className]) { + return temporalTag(className, (actual as any).toString()); + } + return actual; } if (Array.isArray(expected) && Array.isArray(actual)) { return actual.map((a, i) => normalizeActual(a, expected[i])); @@ -80,8 +95,8 @@ function expectTomlEqual(parsed: unknown, expected: unknown): void { // Each case also asserts that parse(stringify(parse(input))) produces the same // value: stringify must never emit a document its own parse rejects or reads -// back differently. The TOML text may change (date/times come back as quoted -// strings), but the JS value is a fixed point after one lap. +// back differently. The TOML text may change (layout, normalized date/time +// spellings), but the JS value is a fixed point after one lap. describe("toml-test/valid", () => { test("valid/array/array-subtables", () => { const input: string = "[[arr]]\n[arr.subtab]\nval=1\n\n[[arr]]\n[arr.subtab]\nval=2\n"; diff --git a/test/js/bun/toml/toml.test.ts b/test/js/bun/toml/toml.test.ts index 4426fcbee1ac..803c99593bb6 100644 --- a/test/js/bun/toml/toml.test.ts +++ b/test/js/bun/toml/toml.test.ts @@ -1,5 +1,6 @@ import { TOML } from "bun"; import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; // Hand-written coverage beyond the official conformance suite // (toml-test-suite.test.ts): the JS-facing API surface, JS value mapping, @@ -225,23 +226,29 @@ describe("numbers", () => { }); }); -describe("date/times return their source text", () => { - test("all four kinds", () => { +describe("date/times return Temporal objects", () => { + test("all four kinds map to their Temporal type", () => { const o = TOML.parse( ["odt = 1979-05-27T07:32:00Z", "ldt = 1979-05-27T07:32:00", "ld = 1979-05-27", "lt = 07:32:00"].join("\n"), ) as any; - expect(o).toEqual({ - odt: "1979-05-27T07:32:00Z", - ldt: "1979-05-27T07:32:00", - ld: "1979-05-27", - lt: "07:32:00", - }); - for (const key of ["odt", "ldt", "ld", "lt"]) { - expect(typeof o[key]).toBe("string"); - } + expect(o.odt).toBeInstanceOf(Temporal.Instant); + expect(o.ldt).toBeInstanceOf(Temporal.PlainDateTime); + expect(o.ld).toBeInstanceOf(Temporal.PlainDate); + expect(o.lt).toBeInstanceOf(Temporal.PlainTime); + expect(o.odt.toString()).toBe("1979-05-27T07:32:00Z"); + expect(o.ldt.toString()).toBe("1979-05-27T07:32:00"); + expect(o.ld.toString()).toBe("1979-05-27"); + expect(o.lt.toString()).toBe("07:32:00"); + }); + + test("an offset date-time specifies an instant; the written offset normalizes away", () => { + const o = TOML.parse("a = 1979-05-27T00:32:00-07:00\nb = 1979-05-27T07:32:00Z") as any; + expect(o.a.epochMilliseconds).toBe(296638320000); + expect(o.a.toString()).toBe("1979-05-27T07:32:00Z"); + expect(o.a.equals(o.b)).toBe(true); }); - test("source spelling is preserved verbatim", () => { + test("TOML spellings Temporal does not print survive losslessly", () => { const o = TOML.parse( [ "lower = 1979-05-27t07:32:00.500z", @@ -249,18 +256,69 @@ describe("date/times return their source text", () => { "frac = 07:32:00.999999999", "noseconds = 07:32", "datenoseconds = 1979-05-27T07:32Z", + "leap = 1990-12-31T23:59:60Z", // RFC 3339 leap second; Temporal clamps to :59 ].join("\n"), ) as any; - expect(o.lower).toBe("1979-05-27t07:32:00.500z"); - expect(o.space).toBe("1979-05-27 07:32:00+13:00"); - expect(o.frac).toBe("07:32:00.999999999"); - expect(o.noseconds).toBe("07:32"); - expect(o.datenoseconds).toBe("1979-05-27T07:32Z"); + expect(o.lower.toString()).toBe("1979-05-27T07:32:00.5Z"); + expect(o.space.toString()).toBe("1979-05-26T18:32:00Z"); + expect(o.frac.toString()).toBe("07:32:00.999999999"); + expect(o.noseconds.toString()).toBe("07:32:00"); + expect(o.datenoseconds.toString()).toBe("1979-05-27T07:32:00Z"); + expect(o.leap.toString()).toBe("1990-12-31T23:59:59Z"); + }); + + test("fractional seconds past Temporal's 9 digits are truncated, not rounded", () => { + // TOML: "excess precision should be truncated, not rounded". + const o = TOML.parse("a = 07:32:00.123456789999\nb = 1979-05-27T07:32:00.999999999999Z") as any; + expect(o.a.toString()).toBe("07:32:00.123456789"); + expect(o.b.toString()).toBe("1979-05-27T07:32:00.999999999Z"); + }); + + test("date/times nest in arrays and inline tables", () => { + const o = TOML.parse("arr = [ 1979-05-27, 07:32:00 ]\ntbl = { d = 1979-05-27T07:32:00 }") as any; + expect(o.arr[0]).toBeInstanceOf(Temporal.PlainDate); + expect(o.arr[1]).toBeInstanceOf(Temporal.PlainTime); + expect(o.tbl.d).toBeInstanceOf(Temporal.PlainDateTime); + }); + + test("parse throws when Temporal is disabled", async () => { + // The Temporal mapping cannot exist without Temporal; a date/time value + // is then an error rather than a silently different type. + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `try { Bun.TOML.parse("a = 1979-05-27"); console.log("no error"); } catch (e) { console.log(e.constructor.name + ": " + e.message); }` + + `console.log(JSON.stringify(Bun.TOML.parse("b = 'still works'")));`, + ], + env: { ...bunEnv, BUN_JSC_useTemporal: "0" }, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toBe( + 'TypeError: Date/time values require Temporal, which is disabled in this process\n{"b":"still works"}\n', + ); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); }); - test("offset date-times feed directly into Date and Temporal-style consumers", () => { - const odt = (TOML.parse("a = 1979-05-27T00:32:00-07:00") as any).a; - expect(new Date(odt).getTime()).toBe(296638320000); + test("importing a TOML module with date/times throws when Temporal is disabled", async () => { + // The import path converts the same way; the load must fail with the + // TypeError, not crash. + using dir = tempDir("toml-dates-no-temporal", { + "config.toml": "a = 1979-05-27\n", + "index.ts": `try { await import("./config.toml"); console.log("no error"); } catch (e) { console.log(e.constructor.name + ": " + e.message); }`, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + env: { ...bunEnv, BUN_JSC_useTemporal: "0" }, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toBe("TypeError: Date/time values require Temporal, which is disabled in this process\n"); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); }); }); @@ -466,13 +524,15 @@ describe("robustness", () => { test("values survive garbage collection", () => { const doc = 'a = "héllo wörld 🌍"\nb = [1, 2.5, true, "x"]\n[t]\nc = 1979-05-27\n'; - const results: unknown[] = []; + const results: any[] = []; for (let i = 0; i < 100; i++) { results.push(TOML.parse(doc)); } Bun.gc(true); for (const o of results) { - expect(o).toEqual({ a: "héllo wörld 🌍", b: [1, 2.5, true, "x"], t: { c: "1979-05-27" } }); + expect(o).toEqual({ a: "héllo wörld 🌍", b: [1, 2.5, true, "x"], t: { c: expect.anything() } }); + expect(o.t.c).toBeInstanceOf(Temporal.PlainDate); + expect(o.t.c.toString()).toBe("1979-05-27"); } }); }); @@ -630,8 +690,8 @@ describe("TOML.stringify", () => { test("Date becomes a TOML offset date-time", () => { const d = new Date(Date.UTC(1979, 4, 27, 7, 32, 0, 999)); expect(TOML.stringify({ d })).toBe("d = 1979-05-27T07:32:00.999Z\n"); - // parse returns datetimes as source-text strings. - expect(TOML.parse(TOML.stringify({ d }))).toEqual({ d: "1979-05-27T07:32:00.999Z" }); + // It reads back as the Temporal.Instant of the same moment. + expect(TOML.parse(TOML.stringify({ d })).d.epochMilliseconds).toBe(d.getTime()); expect(TOML.stringify({ d: new Date(0) })).toBe("d = 1970-01-01T00:00:00.000Z\n"); // The 4-digit-year bounds (`Date.UTC(0, ...)` remaps year 0 to 1900, so raw ms). expect(TOML.stringify({ d: new Date(-62167219200000) })).toBe("d = 0000-01-01T00:00:00.000Z\n"); @@ -651,6 +711,79 @@ describe("TOML.stringify", () => { ); }); + test("the four Temporal date/time types become unquoted TOML literals", () => { + expect( + TOML.stringify({ + odt: Temporal.Instant.from("1979-05-27T00:32:00-07:00"), + ldt: Temporal.PlainDateTime.from("1979-05-27T07:32:00"), + ld: Temporal.PlainDate.from("1979-05-27"), + lt: Temporal.PlainTime.from("07:32:00"), + }), + ).toBe( + [ + "odt = 1979-05-27T07:32:00Z", // an Instant prints in UTC + "ldt = 1979-05-27T07:32:00", + "ld = 1979-05-27", + "lt = 07:32:00", + ].join("\n") + "\n", + ); + }); + + test("Temporal values keep sub-second precision with trailing zeros trimmed", () => { + expect( + TOML.stringify({ + half: Temporal.Instant.from("1979-05-27T07:32:00.500Z"), + nanos: Temporal.PlainTime.from("07:32:00.123456789"), + }), + ).toBe("half = 1979-05-27T07:32:00.5Z\nnanos = 07:32:00.123456789\n"); + }); + + test("ZonedDateTime emits its offset and drops the time-zone annotation", () => { + const zdt = Temporal.ZonedDateTime.from("2024-06-15T12:34:56+02:00[Europe/Berlin]"); + expect(TOML.stringify({ zdt })).toBe("zdt = 2024-06-15T12:34:56+02:00\n"); + // The same instant comes back, as an Instant. + expect(TOML.parse(TOML.stringify({ zdt })).zdt.equals(zdt.toInstant())).toBe(true); + }); + + test("a non-ISO calendar date emits its ISO fields without the calendar annotation", () => { + const hebrew = Temporal.PlainDate.from("2024-01-01[u-ca=hebrew]"); + expect(hebrew.toString()).toBe("2024-01-01[u-ca=hebrew]"); // what TOML cannot carry + expect(TOML.stringify({ d: hebrew })).toBe("d = 2024-01-01\n"); + }); + + test("Temporal values in arrays stay inline instead of becoming [[table]] sections", () => { + expect(TOML.stringify({ a: [Temporal.PlainDate.from("1979-05-27"), Temporal.PlainTime.from("07:32:00")] })).toBe( + "a = [1979-05-27, 07:32:00]\n", + ); + }); + + test("Temporal types with no TOML representation throw", () => { + expect(stringifyError({ x: Temporal.PlainYearMonth.from("2024-01") }).message).toBe( + "TOML.stringify cannot serialize Temporal.PlainYearMonth (it has no TOML representation)", + ); + expect(stringifyError({ x: Temporal.PlainMonthDay.from("01-01") }).message).toBe( + "TOML.stringify cannot serialize Temporal.PlainMonthDay (it has no TOML representation)", + ); + expect(stringifyError({ x: Temporal.Duration.from("PT1H") }).message).toBe( + "TOML.stringify cannot serialize Temporal.Duration (it has no TOML representation)", + ); + }); + + test("Temporal values outside years 0000-9999 throw like Date does", () => { + expect(stringifyError({ d: Temporal.PlainDate.from({ year: 10000, month: 1, day: 1 }) }).message).toBe( + "TOML.stringify cannot serialize a Temporal.PlainDate outside years 0000-9999", + ); + expect(stringifyError({ d: Temporal.PlainDate.from({ year: -1, month: 12, day: 31 }) }).message).toBe( + "TOML.stringify cannot serialize a Temporal.PlainDate outside years 0000-9999", + ); + expect(stringifyError({ i: Temporal.Instant.from("+010000-01-01T00:00:00Z") }).message).toBe( + "TOML.stringify cannot serialize a Temporal.Instant outside years 0000-9999", + ); + // The boundary years themselves are fine. + expect(TOML.stringify({ d: Temporal.PlainDate.from("0000-01-01") })).toBe("d = 0000-01-01\n"); + expect(TOML.stringify({ d: Temporal.PlainDate.from("9999-12-31") })).toBe("d = 9999-12-31\n"); + }); + test("null values throw with the offending key", () => { expect(stringifyError({ a: { broken: null } }).message).toBe( "TOML cannot represent null (key 'broken'); remove the key or use a sentinel value", @@ -679,6 +812,7 @@ describe("TOML.stringify", () => { expect(stringifyError("str").message).toBe(msg); expect(stringifyError(5).message).toBe(msg); expect(stringifyError(new Date(0)).message).toBe(msg); + expect(stringifyError(Temporal.PlainDate.from("1979-05-27")).message).toBe(msg); expect(TOML.stringify(undefined)).toBeUndefined(); }); @@ -723,30 +857,32 @@ describe("TOML.stringify", () => { // The TOML.stringify suite above covers parse(stringify(jsValue)). These cover // the other direction, stringify of a value produced by parse (read, modify, -// write back), where the four date/time types lose their TOML type. +// write back). describe("stringify(parse) round-trips", () => { - test("all four date/time types become quoted strings on the way back out", () => { - // parse returns a date/time literal as the string of its source text, so - // stringify sees a plain string and must quote it. The TOML type changes, - // but the JS value is a fixed point after one stringify/parse lap. + test("all four date/time types come back out as the same TOML type", () => { + // parse maps a date/time literal to its Temporal type, and stringify maps + // each Temporal type back to the literal it came from: the TOML type + // survives the lap (the spelling normalizes to canonical ISO form). const cases: [string, string][] = [ - ["d = 1979-05-27T07:32:00Z", 'd = "1979-05-27T07:32:00Z"\n'], - ["d = 1979-05-27T07:32:00", 'd = "1979-05-27T07:32:00"\n'], - ["d = 1979-05-27", 'd = "1979-05-27"\n'], - ["d = 07:32:00", 'd = "07:32:00"\n'], + ["d = 1979-05-27T07:32:00Z", "d = 1979-05-27T07:32:00Z\n"], + ["d = 1979-05-27T00:32:00-07:00", "d = 1979-05-27T07:32:00Z\n"], // instant; offset normalizes + ["d = 1979-05-27T07:32:00", "d = 1979-05-27T07:32:00\n"], + ["d = 1979-05-27", "d = 1979-05-27\n"], + ["d = 07:32:00", "d = 07:32:00\n"], + ["d = 07:32", "d = 07:32:00\n"], // TOML 1.1 omitted seconds ]; - for (const [doc, requoted] of cases) { - const once = TOML.parse(doc); - expect(TOML.stringify(once)).toBe(requoted); - expect(TOML.parse(TOML.stringify(once))).toEqual(once as any); + for (const [doc, out] of cases) { + const once = TOML.parse(doc) as any; + expect(TOML.stringify(once)).toBe(out); + expect((TOML.parse(TOML.stringify(once)) as any).d.toString()).toBe(once.d.toString()); } }); - test("a date literal and a string of the same text are indistinguishable after parse", () => { - // This is why the previous test cannot preserve the TOML type: both - // documents produce the identical JS value, so stringify has nothing to go on. - expect(TOML.parse("a = 1979-05-27")).toEqual({ a: "1979-05-27" }); - expect(TOML.parse('a = "1979-05-27"')).toEqual({ a: "1979-05-27" }); + test("a date literal and a string of the same text stay distinct", () => { + expect((TOML.parse("a = 1979-05-27") as any).a).toBeInstanceOf(Temporal.PlainDate); + expect((TOML.parse('a = "1979-05-27"') as any).a).toBe("1979-05-27"); + expect(TOML.stringify(TOML.parse("a = 1979-05-27"))).toBe("a = 1979-05-27\n"); + expect(TOML.stringify(TOML.parse('a = "1979-05-27"'))).toBe('a = "1979-05-27"\n'); }); test("nan, inf, -inf, and signed zero round-trip as values, not just as text", () => { From da85b78056fd9d018eac1d38985b2d48847f91cc Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:30:08 +0000 Subject: [PATCH 02/19] Tag EString with the TOML date/time kind instead of adding an AST node 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. --- src/ast/e.rs | 110 ++++++++---------- src/ast/expr.rs | 10 +- src/js_parser/parse/parse_entry.rs | 19 +-- src/js_parser_jsc/expr_jsc.rs | 32 ++--- src/js_printer/lib.rs | 42 ++++--- src/parsers/toml.rs | 22 ++-- src/react_compiler/lowering/build_hir/expr.rs | 2 - .../lowering/find_context_identifiers.rs | 3 +- src/runtime/api.rs | 27 +++-- 9 files changed, 127 insertions(+), 140 deletions(-) diff --git a/src/ast/e.rs b/src/ast/e.rs index 8c8cb711b8fd..acd5d9184ab2 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -1583,6 +1583,35 @@ pub struct Spread { // `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. +/// Which of the four TOML date/time kinds a tagged `EString` literal is, and +/// the Temporal class it materializes as. Discriminants cross the FFI +/// boundary (`Bun__Temporal__fromDateTimeLiteral`) — keep them in sync with +/// the C++ switch. +#[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))] 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. @@ -1600,6 +1629,12 @@ 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 whose `data` is + /// the (ASCII, pre-validated) source text. The TOML AST never enters the + /// JS visit/transform passes; the sinks that materialize or print it + /// (`expr_to_js`, `data_to_js`, `to_lazy_export_ast`, the printer) check + /// this tag and produce a Temporal value instead of a string. + pub toml_datetime: Option, } // Also exported as `String`; `EString` avoids colliding with bun_core::String. pub use EString as String; @@ -1613,6 +1648,7 @@ impl Default for EString { end: None, rope_len: 0, is_utf16: false, + toml_datetime: None, } } } @@ -1660,6 +1696,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) @@ -1670,6 +1707,17 @@ impl EString { ..Default::default() } } + + /// A TOML date/time literal: `data` is its ASCII source text (fractional + /// seconds pre-truncated to the 9 digits Temporal carries), accepted + /// verbatim by `Temporal.*.from`. + 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 @@ -1896,6 +1944,7 @@ impl EString { end: self.end, rope_len: self.rope_len, is_utf16: self.is_utf16, + toml_datetime: self.toml_datetime, } } @@ -1975,67 +2024,6 @@ impl fmt::Display for EString { } } -/// Which of the four TOML date/time kinds a `DateTime` literal is, and the -/// Temporal class it materializes as. Discriminants cross the FFI boundary -/// (`Bun__Temporal__fromDateTimeLiteral`) — keep them in sync with the C++ -/// switch. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[repr(u8)] -pub enum DateTimeKind { - /// `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 DateTimeKind { - /// Unqualified Temporal class name (`Instant`, `PlainDateTime`, …). - pub fn temporal_class(self) -> &'static [u8] { - match self { - DateTimeKind::OffsetDateTime => b"Instant", - DateTimeKind::LocalDateTime => b"PlainDateTime", - DateTimeKind::LocalDate => b"PlainDate", - DateTimeKind::LocalTime => b"PlainTime", - } - } -} - -/// A date/time literal that materializes as a Temporal object. Produced only -/// by the TOML parser; JavaScript has no such literal. -pub struct DateTime { - /// Source text of the literal. Always ASCII, already validated by the - /// producing parser, and accepted verbatim by `Temporal.*.from` (fractional - /// seconds are pre-truncated to the 9 digits Temporal carries). - pub data: Str, - pub kind: DateTimeKind, -} - -impl DateTime { - /// `data` is arena-owned (source text or bump arena) and bulk-freed; - /// `StoreStr` records it under the `StoreRef` contract. - pub fn init(data: &[u8], kind: DateTimeKind) -> Self { - Self { - data: Str::new(data), - kind, - } - } - - #[inline] - pub fn slice(&self) -> &[u8] { - self.data.slice() - } -} - -impl fmt::Display for DateTime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "E.DateTime({})", bstr::BStr::new(&self.data)) - } -} - // value is in the Node pub struct TemplatePart { pub value: ExprNodeIndex, diff --git a/src/ast/expr.rs b/src/ast/expr.rs index 8f1b6f6b768a..e61f8fa11a02 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -954,7 +954,6 @@ impl_into_expr_data_boxed! { If => EIf, Import => EImport, InlinedEnum => EInlinedEnum, - DateTime => EDateTime, } impl_into_expr_data_inline! { @@ -1140,7 +1139,6 @@ pub enum Tag { ENumber, EBigInt, EString, - EDateTime, ERequireString, ERequireResolveString, ERequireCallTarget, @@ -1218,7 +1216,6 @@ impl Tag { Tag::EMissing => "", Tag::ENumber => "number", Tag::EBigInt => "BigInt", - Tag::EDateTime => "date-time", Tag::EObject | Tag::EObjectJSON => "object", Tag::ESpread => "...", Tag::ETemplate => "template", @@ -1482,7 +1479,6 @@ pub enum Data { ENumber(E::Number), EBigInt(StoreRef), EString(StoreRef), - EDateTime(StoreRef), ERequireString(E::RequireString), ERequireResolveString(E::RequireResolveString), @@ -2157,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))) } @@ -2316,10 +2313,6 @@ impl Data { } hasher.update(b"\x00"); } - Data::EDateTime(e) => { - raw(hasher, e.kind as u8); - hasher.update(e.slice()); - } Data::ERequireString(e) => { raw(hasher, e.import_record_index); // preferably, i'd like to write the filepath } @@ -2902,7 +2895,6 @@ crate::new_store!( E::PrivateIdentifier, E::BigInt, E::EString, - E::DateTime, E::InlinedEnum, E::NameOfSymbol, ], diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 9ec5b56a38eb..cb492ea14990 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -614,19 +614,19 @@ impl<'a> Parser<'a> { } } -/// Rewrites every `E::DateTime` in `expr` (in place) into the -/// `Temporal..from("")` call it prints as, referencing an -/// unbound `Temporal` symbol declared on first use. The calls are annotated -/// as removable-if-unused: constructing a Temporal value from a validated -/// literal has no observable side effects, so tree shaking may drop unused -/// exports. +/// Rewrites every `toml_datetime`-tagged `E::String` in `expr` (in place) +/// into the `Temporal..from("")` call it prints as, referencing +/// an unbound `Temporal` symbol declared on first use. The calls are +/// annotated as removable-if-unused: constructing a Temporal value from a +/// validated literal has no observable side effects, so tree shaking may +/// drop unused exports. fn lower_date_time_literals<'a>( p: &mut JavaScriptParser<'a>, expr: &mut Expr, temporal_ref: &mut Option, ) -> Result<(), Error> { match expr.data { - js_ast::ExprData::EDateTime(dt) => { + js_ast::ExprData::EString(str) if str.toml_datetime.is_some() => { let ref_ = match *temporal_ref { Some(ref_) => ref_, None => { @@ -637,8 +637,9 @@ fn lower_date_time_literals<'a>( } }; let (class, text) = { - let dt = dt.get(); - (dt.kind.temporal_class(), dt.slice()) + 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_); diff --git a/src/js_parser_jsc/expr_jsc.rs b/src/js_parser_jsc/expr_jsc.rs index 16845318765e..5fa360521dff 100644 --- a/src/js_parser_jsc/expr_jsc.rs +++ b/src/js_parser_jsc/expr_jsc.rs @@ -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) + } ExprData::ENull(_) => Ok(JSValue::NULL), ExprData::EUndefined(_) => Ok(JSValue::UNDEFINED), ExprData::EBoolean(boolean) | ExprData::EBranchBoolean(boolean) => Ok(if boolean.value { @@ -71,20 +87,6 @@ fn data_to_js_with_check( }), ExprData::ENumber(e) => Ok(number_to_js(*e)), // ExprData::EBigInt(e) => e.to_js(ctx, exception), - ExprData::EDateTime(e) => { - let e = e.get(); - let text = e.slice(); - // SAFETY: `text` is an arena-owned ASCII slice that outlives the call. - unsafe { - bun_jsc::cpp::Bun__Temporal__fromDateTimeLiteral( - global, - text.as_ptr(), - text.len(), - e.kind as u8, - ) - } - .map_err(js_err) - } ExprData::EInlinedEnum(inlined) => { data_to_js_with_check(&inlined.value.data, global, stack_check) } diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index f48bb72a3272..61ff4c10e5e3 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -3215,25 +3215,6 @@ pub(crate) mod __gated_printer { self.print(b")"); } } - ExprData::EDateTime(e) => { - // A date/time literal has no JS literal form; it prints as - // the `Temporal.*.from` call that reconstructs the value. - 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(e.kind.temporal_class()); - self.print(b".from(\""); - // Always ASCII (validated by the TOML scanner); no escaping. - self.print(e.slice()); - self.print(b"\")"); - if wrap { - self.print(b")"); - } - } ExprData::ERequireMain => { self.print_space_before_identifier(); self.add_source_mapping(expr.loc); @@ -3702,6 +3683,29 @@ pub(crate) mod __gated_printer { } } ExprData::EString(e) => { + // A TOML date/time literal has no JS literal form; it + // prints as the `Temporal.*.from` call that reconstructs + // the value. (The bundler path rewrites these in + // `to_lazy_export_ast` instead and never gets here.) + 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); diff --git a/src/parsers/toml.rs b/src/parsers/toml.rs index 98616e438f55..0a7e90c13151 100644 --- a/src/parsers/toml.rs +++ b/src/parsers/toml.rs @@ -16,10 +16,10 @@ //! - integers parse as `f64` but are validated as 64-bit integers first; //! values outside `Number.MAX_SAFE_INTEGER` are errors (TOML requires //! lossless handling or an error) -//! - date/time values become `E::DateTime` nodes that materialize as Temporal -//! objects (offset date-time → `Temporal.Instant`, local date-time → -//! `Temporal.PlainDateTime`, local date → `Temporal.PlainDate`, local time -//! → `Temporal.PlainTime`) +//! - date/time values become `E::String`s tagged with `toml_datetime`, which +//! materialize as Temporal objects (offset date-time → `Temporal.Instant`, +//! local date-time → `Temporal.PlainDateTime`, local date → +//! `Temporal.PlainDate`, local time → `Temporal.PlainTime`) //! - strings are UTF-8; non-ASCII content is re-encoded to UTF-16 EStrings //! so both the JS conversion and the printer paths agree @@ -139,7 +139,7 @@ enum ValueData<'a> { /// One of the four TOML date/time kinds, as its source text (always ASCII). DateTime { text: &'a [u8], - kind: E::DateTimeKind, + kind: E::TomlDateTimeKind, }, Boolean(bool), ArrayOpen, @@ -673,7 +673,7 @@ impl<'a, 'log> Scanner<'a, 'log> { self.expect_value_terminator()?; return Ok(ValueData::DateTime { text: &self.src[start..self.pos], - kind: E::DateTimeKind::LocalTime, + kind: E::TomlDateTimeKind::LocalTime, }); } } @@ -705,7 +705,7 @@ impl<'a, 'log> Scanner<'a, 'log> { /// `YYYY-MM-DD` and everything that may follow it (time, offset). /// Returns the full source text of the literal and which kind it is. - fn scan_datetime_from_date(&mut self) -> PResult<(&'a [u8], E::DateTimeKind)> { + fn scan_datetime_from_date(&mut self) -> PResult<(&'a [u8], E::TomlDateTimeKind)> { let start = self.pos; let year = self.read_digits(4, b"Invalid date: expected a 4-digit year")?; @@ -757,7 +757,7 @@ impl<'a, 'log> Scanner<'a, 'log> { }; if !has_time { - return Ok((&self.src[start..self.pos], E::DateTimeKind::LocalDate)); + return Ok((&self.src[start..self.pos], E::TomlDateTimeKind::LocalDate)); } self.scan_time_digits()?; @@ -798,9 +798,9 @@ impl<'a, 'log> Scanner<'a, 'log> { }; let kind = if has_offset { - E::DateTimeKind::OffsetDateTime + E::TomlDateTimeKind::OffsetDateTime } else { - E::DateTimeKind::LocalDateTime + E::TomlDateTimeKind::LocalDateTime }; Ok((&self.src[start..self.pos], kind)) } @@ -1756,7 +1756,7 @@ impl<'a, 'log> Parser<'a, 'log> { ValueData::Number(n) => Ok(Expr::init(E::Number::new(n), loc)), ValueData::DateTime { text, kind } => { let text = truncate_fractional_seconds(text, self.bump); - Ok(Expr::init(E::DateTime::init(text, kind), loc)) + Ok(Expr::init(E::String::init_toml_datetime(text, kind), loc)) } ValueData::Boolean(b) => Ok(Expr::init(E::Boolean { value: b }, loc)), ValueData::ArrayOpen => self.parse_array(token.pos), diff --git a/src/react_compiler/lowering/build_hir/expr.rs b/src/react_compiler/lowering/build_hir/expr.rs index 4908d6b704ae..69596394f38b 100644 --- a/src/react_compiler/lowering/build_hir/expr.rs +++ b/src/react_compiler/lowering/build_hir/expr.rs @@ -347,8 +347,6 @@ pub(crate) fn lower_expression( Data::EMissing(_) => Err(todo_err("EMissing", loc)), Data::ECommonjsExportIdentifier(_) => Err(todo_err("ECommonjsExportIdentifier", loc)), Data::ENameOfSymbol(_) => Err(todo_err("ENameOfSymbol", loc)), - // Produced only by the TOML parser; never occurs in JavaScript source. - Data::EDateTime(_) => Err(todo_err("EDateTime", loc)), } } diff --git a/src/react_compiler/lowering/find_context_identifiers.rs b/src/react_compiler/lowering/find_context_identifiers.rs index b309e383f390..cda024b07435 100644 --- a/src/react_compiler/lowering/find_context_identifiers.rs +++ b/src/react_compiler/lowering/find_context_identifiers.rs @@ -536,8 +536,7 @@ impl<'a> ContextIdentifierVisitor<'a> { | Data::EImportMetaMain(_) | Data::ERequireMain | Data::ESpecial(_) - | Data::ENameOfSymbol(_) - | Data::EDateTime(_) => {} + | Data::ENameOfSymbol(_) => {} } } } diff --git a/src/runtime/api.rs b/src/runtime/api.rs index 5faaf138ed18..ebb6ec5b713a 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -312,19 +312,22 @@ fn expr_to_js_with_check( ExprData::ENull(_) => Ok(JSValue::NULL), ExprData::EBoolean(boolean) => Ok(JSValue::from(boolean.value)), ExprData::ENumber(number) => Ok(JSValue::js_number(number.value())), - ExprData::EString(str) => estring_to_js(str.get(), global), - ExprData::EDateTime(dt) => { - let dt = dt.get(); - let text = dt.slice(); - // SAFETY: `text` is an arena-owned ASCII slice that outlives the call. - unsafe { - bun_jsc::cpp::Bun__Temporal__fromDateTimeLiteral( - global, - text.as_ptr(), - text.len(), - dt.kind as u8, - ) + ExprData::EString(str) => { + let str = str.get(); + if let Some(kind) = str.toml_datetime { + let text = str.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, + ) + }; } + estring_to_js(str, global) } ExprData::EArray(arr) => { JSValue::create_array_from_iter(global, arr.slice().iter(), |item| { From aafc927422d2b285be044f72fbc3cf7c99401e62 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:33:38 +0000 Subject: [PATCH 03/19] Tighten code comments --- src/ast/e.rs | 20 ++++++++----------- src/js_parser/parse/parse_entry.rs | 16 ++++++--------- src/js_printer/lib.rs | 7 +++---- src/jsc/bindings/bindings.cpp | 32 +++++++++++++----------------- src/parsers/toml.rs | 5 ++--- src/runtime/api/TOMLObject.rs | 13 ++++-------- src/runtime/jsc_hooks.rs | 5 ++--- 7 files changed, 39 insertions(+), 59 deletions(-) diff --git a/src/ast/e.rs b/src/ast/e.rs index acd5d9184ab2..d8477d026752 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -1583,10 +1583,8 @@ pub struct Spread { // `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. -/// Which of the four TOML date/time kinds a tagged `EString` literal is, and -/// the Temporal class it materializes as. Discriminants cross the FFI -/// boundary (`Bun__Temporal__fromDateTimeLiteral`) — keep them in sync with -/// the C++ switch. +/// Discriminants are shared with the C++ switch in +/// `Bun__Temporal__fromDateTimeLiteral`. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[repr(u8)] pub enum TomlDateTimeKind { @@ -1629,11 +1627,10 @@ 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 whose `data` is - /// the (ASCII, pre-validated) source text. The TOML AST never enters the - /// JS visit/transform passes; the sinks that materialize or print it - /// (`expr_to_js`, `data_to_js`, `to_lazy_export_ast`, the printer) check - /// this tag and produce a Temporal value instead of a string. + /// 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. pub toml_datetime: Option, } // Also exported as `String`; `EString` avoids colliding with bun_core::String. @@ -1708,9 +1705,8 @@ impl EString { } } - /// A TOML date/time literal: `data` is its ASCII source text (fractional - /// seconds pre-truncated to the 9 digits Temporal carries), accepted - /// verbatim by `Temporal.*.from`. + /// A TOML date/time literal; `data` must be ASCII text `Temporal.*.from` + /// accepts verbatim. pub fn init_toml_datetime(data: &[u8], kind: TomlDateTimeKind) -> Self { Self { data: Str::new(data), diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index cb492ea14990..b7b55054040d 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -559,11 +559,9 @@ impl<'a> Parser<'a> { let mut final_expr = expr; - // Date/time literals (produced by the TOML parser) materialize as - // `Temporal.*.from("...")` calls. Bundled modules share one scope, so - // the reference must be a real unbound `Temporal` symbol: the chunk - // renamer then reserves the name and renames a user binding called - // `Temporal` instead of letting it capture these calls. + // 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. let mut temporal_ref: Option = None; lower_date_time_literals(p, &mut final_expr, &mut temporal_ref)?; @@ -615,11 +613,9 @@ impl<'a> Parser<'a> { } /// Rewrites every `toml_datetime`-tagged `E::String` in `expr` (in place) -/// into the `Temporal..from("")` call it prints as, referencing -/// an unbound `Temporal` symbol declared on first use. The calls are -/// annotated as removable-if-unused: constructing a Temporal value from a -/// validated literal has no observable side effects, so tree shaking may -/// drop unused exports. +/// into a `Temporal..from("")` call, declaring the unbound +/// `Temporal` symbol on first use. The calls are pure-annotated so tree +/// shaking may drop unused exports. fn lower_date_time_literals<'a>( p: &mut JavaScriptParser<'a>, expr: &mut Expr, diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 61ff4c10e5e3..04161f10d2f7 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -3683,10 +3683,9 @@ pub(crate) mod __gated_printer { } } ExprData::EString(e) => { - // A TOML date/time literal has no JS literal form; it - // prints as the `Temporal.*.from` call that reconstructs - // the value. (The bundler path rewrites these in - // `to_lazy_export_ast` instead and never gets here.) + // 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). if let Some(kind) = e.toml_datetime { let wrap = level.gte(Level::New) || flags.contains(ExprFlag::ForbidCall); if wrap { diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index cb4ddcea3790..c842a14cb861 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5843,18 +5843,17 @@ extern "C" [[ZIG_EXPORT(nothrow)]] double Bun__gregorianDateTimeToMSInZone(JSC:: return static_cast(r->epochMilliseconds()); } -// Materializes a parsed date/time literal as a Temporal object, through the -// same paths `Temporal.*.from(string)` takes. `kind` mirrors the Rust -// `bun_ast::E::DateTimeKind` discriminants. `text` is ASCII, pre-validated by -// the producing parser, and within Temporal's 9-digit fraction limit. +// Materializes a date/time literal as a Temporal object through the same +// paths `Temporal.*.from(string)` takes. `kind` mirrors the Rust +// `bun_ast::E::TomlDateTimeKind` discriminants. extern "C" [[ZIG_EXPORT(zero_is_throw)]] EncodedJSValue Bun__Temporal__fromDateTimeLiteral(JSC::JSGlobalObject* globalObject, const uint8_t* text, size_t len, uint8_t kind) { auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); + // The Temporal structures on the global object only exist when the + // option is on; reaching for them would crash. if (!JSC::Options::useTemporal()) [[unlikely]] { - // The Temporal structures on the global object only exist when the - // option is on; reaching for them would crash. JSC::throwTypeError(globalObject, scope, "Date/time values require Temporal, which is disabled in this process"_s); return {}; } @@ -5885,9 +5884,9 @@ extern "C" [[ZIG_EXPORT(zero_is_throw)]] EncodedJSValue Bun__Temporal__fromDateT } // Classifies a JSValue as one of the Temporal object types, or 0 for -// everything else. The discriminants are shared with `TOMLObject.rs`: -// 1 Instant, 2 PlainDateTime, 3 PlainDate, 4 PlainTime, 5 ZonedDateTime, -// 6 PlainYearMonth, 7 PlainMonthDay, 8 Duration (1-4 mirror `DateTimeKind`). +// everything else. Discriminants are shared with `TOMLObject.rs`: 1 Instant, +// 2 PlainDateTime, 3 PlainDate, 4 PlainTime, 5 ZonedDateTime, +// 6 PlainYearMonth, 7 PlainMonthDay, 8 Duration. extern "C" [[ZIG_EXPORT(nothrow)]] uint8_t Bun__JSValue__temporalObjectType(JSC::EncodedJSValue encodedValue) { JSC::JSValue value = JSC::JSValue::decode(encodedValue); @@ -5917,12 +5916,10 @@ extern "C" [[ZIG_EXPORT(nothrow)]] uint8_t Bun__JSValue__temporalObjectType(JSC: return 0; } -// Formats a Temporal object (`temporalType` from -// `Bun__JSValue__temporalObjectType`, 1-5 only) as a TOML date/time literal -// into `buf`. Returns the length written, or -1 if it cannot fit. The ISO -// fields are formatted directly so no `[u-ca=...]` calendar annotation is -// emitted (the ISO date itself is what TOML can carry); a ZonedDateTime -// emits its offset form, dropping the `[Time/Zone]` annotation. +// Formats a Temporal object (`temporalType` 1-5 from the classifier above) +// as a TOML date/time literal into `buf`; returns the length written, or -1 +// if it cannot fit. The ISO fields are formatted directly, dropping the +// `[u-ca=...]` and `[Time/Zone]` annotations TOML cannot carry. extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, uint8_t temporalType, uint8_t* buf, size_t bufLen) { auto& vm = JSC::getVM(globalObject); @@ -5952,9 +5949,8 @@ extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC: std::optional offsetNs = zoned->getOffsetNanoseconds(globalObject); RETURN_IF_EXCEPTION(scope, -1); ASSERT(offsetNs); - // TOML offsets are `HH:MM` only; a historic sub-minute offset - // (e.g. pre-1972 Africa/Monrovia) falls back to the equivalent - // UTC instant rather than emitting an offset TOML cannot parse. + // TOML offsets are `HH:MM` only; a historic sub-minute offset falls + // back to the equivalent UTC instant. if (offsetNs && *offsetNs % 60000000000ll != 0) offsetNs = std::nullopt; string = JSC::TemporalCore::instantToString(zoned->exactTime(), offsetNs, autoPrecision); diff --git a/src/parsers/toml.rs b/src/parsers/toml.rs index 0a7e90c13151..46fd61f82ae3 100644 --- a/src/parsers/toml.rs +++ b/src/parsers/toml.rs @@ -190,9 +190,8 @@ const MAX_SAFE_INTEGER: i64 = (1 << 53) - 1; const BARE_CR: &[u8] = b"Bare carriage return is not allowed; use \\r\\n or \\n"; const UNDERSCORE_IN_NUMBER: &[u8] = b"Underscores in numbers must be surrounded by digits"; -/// TOML allows unlimited fractional-second digits and says extra precision -/// "should be truncated, not rounded"; Temporal carries nanoseconds and -/// rejects more than 9 digits, so drop anything past the ninth here. +/// TOML says excess fractional-second precision "should be truncated, not +/// rounded"; Temporal rejects more than its 9 digits, so drop the rest here. fn truncate_fractional_seconds<'a>(text: &'a [u8], bump: &'a Bump) -> &'a [u8] { let Some(dot) = text.iter().position(|&b| b == b'.') else { return text; diff --git a/src/runtime/api/TOMLObject.rs b/src/runtime/api/TOMLObject.rs index 99ed54c2d5ca..bdc5b800cb32 100644 --- a/src/runtime/api/TOMLObject.rs +++ b/src/runtime/api/TOMLObject.rs @@ -488,11 +488,8 @@ impl Stringifier { Ok(()) } - /// A Temporal object as the TOML date/time literal of its type: `Instant` - /// and `ZonedDateTime` emit offset date-times (the latter dropping its - /// time-zone annotation), `PlainDateTime`/`PlainDate`/`PlainTime` their - /// local forms. `PlainYearMonth`/`PlainMonthDay`/`Duration` have no TOML - /// representation and throw. + /// A Temporal object as the TOML date/time literal of its type; + /// `PlainYearMonth`/`PlainMonthDay`/`Duration` have no TOML form and throw. fn append_temporal( &mut self, global: &JSGlobalObject, @@ -568,12 +565,10 @@ impl Stringifier { } } -/// The `Bun__Temporal__toTOMLDateTime` discriminant for `ZonedDateTime`, the -/// last Temporal type with a TOML representation (1-5; 6-8 have none). +/// The last discriminant with a TOML representation (1-5; 6-8 have none). const TEMPORAL_ZONED_DATE_TIME: u8 = 5; -/// Classifies `value` via `Bun__JSValue__temporalObjectType`: 0 for anything -/// that is not a Temporal object, else the 1-8 discriminant +/// 0 for anything that is not a Temporal object, else the 1-8 discriminant /// `temporal_type_name` describes. fn temporal_object_type(value: JSValue) -> u8 { jsc::cpp::Bun__JSValue__temporalObjectType(value) diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 9c878ce39e5d..ef1b148a143e 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -2739,9 +2739,8 @@ fn transpile_source_code_inner( }; match bun_js_parser_jsc::expr_to_js(&s_expr.value, global) { Ok(value) => value, - // A thrown exception (e.g. constructing a Temporal - // date/time while Temporal is disabled) fails the - // module load with that exception pending. + // A thrown exception fails the module load with + // that exception pending. Err(bun_ast::ToJSError::JSError | bun_ast::ToJSError::JSTerminated) => { return Err(crate::Error::JSError); } From 9ac6b3614e72d6173a41c2e591c8fda060647ac1 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:46:45 +0000 Subject: [PATCH 04/19] Route the transform-path Temporal reference through globalThis The bundler protects the printed Temporal reference with an unbound symbol and the chunk renamer, but the transform path (--no-bundle) has no renamer and a top-level TOML key named Temporal becomes a module-scope var that would capture it. The printer arm only serves that unrenamed path, so prefix the reference with globalThis. --- src/js_printer/lib.rs | 9 ++++++--- test/bundler/bundler_loader.test.ts | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 04161f10d2f7..68c45fb83b57 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -3684,8 +3684,11 @@ 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). + // call that reconstructs it. The bundler rewrites these in + // `to_lazy_export_ast` before printing; this arm serves + // the unrenamed transform paths, where `globalThis.` keeps + // a same-module `var Temporal` (a TOML key of that name) + // from capturing the reference. if let Some(kind) = e.toml_datetime { let wrap = level.gte(Level::New) || flags.contains(ExprFlag::ForbidCall); if wrap { @@ -3693,7 +3696,7 @@ pub(crate) mod __gated_printer { } self.print_space_before_identifier(); self.add_source_mapping(expr.loc); - self.print(b"Temporal."); + self.print(b"globalThis.Temporal."); self.print(kind.temporal_class()); self.print(b".from(\""); // Always ASCII (validated by the TOML scanner); no escaping. diff --git a/test/bundler/bundler_loader.test.ts b/test/bundler/bundler_loader.test.ts index e633c752fbf3..c4d8d6365092 100644 --- a/test/bundler/bundler_loader.test.ts +++ b/test/bundler/bundler_loader.test.ts @@ -69,6 +69,22 @@ describe("bundler", async () => { }, run: { stdout: "shadowed 1979-05-27" }, }); + // The transform path (--no-bundle) has no renamer, and a top-level TOML + // key named Temporal becomes a module-scope var; the printed reference + // goes through globalThis so that var cannot capture it. + itBundled("bun/loader-toml-datetime-no-bundle-temporal-key", { + target, + bundling: false, + entryPoints: ["/config.toml"], + files: { + "/config.toml": `Temporal = "x"\nd = 1979-05-27`, + }, + run: true, + onAfterBundle(api) { + const code = api.readFile("/out.js"); + expect(code).toContain('globalThis.Temporal.PlainDate.from("1979-05-27")'); + }, + }); // TOML date/time values bundle as Temporal construction calls; the // bundled module yields the same values Bun.TOML.parse returns. itBundled("bun/loader-toml-datetime", { From 4a6ffe88b2c78ee6eeddecf2b2bd0d73c6a8e1c5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:59:23 +0000 Subject: [PATCH 05/19] Share the Temporal literal FFI dispatch, cover the sub-minute offset fallback Both to-JS sinks called Bun__Temporal__fromDateTimeLiteral with the same unsafe block; hoist it into JSValue::from_toml_datetime_literal. Add a stringify test for a pre-standard-time ZonedDateTime (Europe/Berlin 1800, LMT +00:53:28), whose sub-minute offset TOML cannot carry and which falls back to the UTC instant form. --- src/js_parser_jsc/expr_jsc.rs | 14 ++------------ src/jsc/JSValue.rs | 14 ++++++++++++++ src/runtime/api.rs | 12 +----------- test/js/bun/toml/toml.test.ts | 9 +++++++++ 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/js_parser_jsc/expr_jsc.rs b/src/js_parser_jsc/expr_jsc.rs index 5fa360521dff..d9acffa4161f 100644 --- a/src/js_parser_jsc/expr_jsc.rs +++ b/src/js_parser_jsc/expr_jsc.rs @@ -63,18 +63,8 @@ fn data_to_js_with_check( ExprData::EArrayJSON(e) => array_json_to_js(e, global, stack_check), 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); + return JSValue::from_toml_datetime_literal(global, e.slice8(), kind as u8) + .map_err(js_err); } string_to_js(e, global) } diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 1ffefc5fa1bc..296afab27fae 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -655,6 +655,20 @@ impl JSValue { pub fn from_date_number(global: &JSGlobalObject, value: f64) -> JSValue { JSC__JSValue__dateInstanceFromNumber(global, value) } + /// A TOML date/time literal as the Temporal object of its kind (a + /// `bun_ast::E::TomlDateTimeKind` discriminant). `text` must be ASCII + /// that `Temporal.*.from` accepts verbatim. + pub fn from_toml_datetime_literal( + global: &JSGlobalObject, + text: &[u8], + kind: u8, + ) -> JsResult { + debug_assert!(text.is_ascii()); + // SAFETY: `text` is a live slice for the duration of the call. + unsafe { + crate::cpp::Bun__Temporal__fromDateTimeLiteral(global, text.as_ptr(), text.len(), kind) + } + } #[track_caller] pub fn from_int64_no_truncate(global: &JSGlobalObject, i: i64) -> JsResult { host_fn::from_js_host_call(global, || JSC__JSValue__fromInt64NoTruncate(global, i)) diff --git a/src/runtime/api.rs b/src/runtime/api.rs index ebb6ec5b713a..9d94d06b7832 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -315,17 +315,7 @@ fn expr_to_js_with_check( ExprData::EString(str) => { let str = str.get(); if let Some(kind) = str.toml_datetime { - let text = str.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, - ) - }; + return JSValue::from_toml_datetime_literal(global, str.slice8(), kind as u8); } estring_to_js(str, global) } diff --git a/test/js/bun/toml/toml.test.ts b/test/js/bun/toml/toml.test.ts index 803c99593bb6..42c3bed16b17 100644 --- a/test/js/bun/toml/toml.test.ts +++ b/test/js/bun/toml/toml.test.ts @@ -745,6 +745,15 @@ describe("TOML.stringify", () => { expect(TOML.parse(TOML.stringify({ zdt })).zdt.equals(zdt.toInstant())).toBe(true); }); + test("a ZonedDateTime with a sub-minute offset falls back to the UTC instant form", () => { + // Pre-standard-time zones use LMT: Europe/Berlin in 1800 is +00:53:28, + // which TOML's HH:MM offset grammar cannot carry. + const zdt = Temporal.ZonedDateTime.from("1800-01-01T00:00[Europe/Berlin]"); + expect(zdt.offsetNanoseconds % 60_000_000_000).not.toBe(0); + expect(TOML.stringify({ zdt })).toBe(`zdt = ${zdt.toInstant().toString()}\n`); + expect(TOML.parse(TOML.stringify({ zdt })).zdt.equals(zdt.toInstant())).toBe(true); + }); + test("a non-ISO calendar date emits its ISO fields without the calendar annotation", () => { const hebrew = Temporal.PlainDate.from("2024-01-01[u-ca=hebrew]"); expect(hebrew.toString()).toBe("2024-01-01[u-ca=hebrew]"); // what TOML cannot carry From e52bf105ef59d3565e6a8ac38271f80a063d8e3a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 06:07:22 +0000 Subject: [PATCH 06/19] Keep EString's doc comment attached to the struct The TomlDateTimeKind enum landed between the doc comment and the struct declaration, so the rustdoc attached to the enum instead. --- src/ast/e.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ast/e.rs b/src/ast/e.rs index d8477d026752..8555370b27e9 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -1575,14 +1575,6 @@ pub struct Spread { pub value: ExprNodeIndex, } -/// JavaScript string literal type -// repr(C, align(8)): `StoreStr`/`StoreRef` are `packed(4)`, so under -// `repr(Rust)` the `data.ptr: NonNull` lands at a 4-but-not-8-aligned -// offset, and a `static EString = EString::from_static(b"...")` then emits an -// `ARM64_RELOC_UNSIGNED` at that offset which arm64 ld rejects. `repr(C)` pins -// `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`. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -1610,6 +1602,14 @@ impl TomlDateTimeKind { } } +/// JavaScript string literal type +// repr(C, align(8)): `StoreStr`/`StoreRef` are `packed(4)`, so under +// `repr(Rust)` the `data.ptr: NonNull` lands at a 4-but-not-8-aligned +// offset, and a `static EString = EString::from_static(b"...")` then emits an +// `ARM64_RELOC_UNSIGNED` at that offset which arm64 ld rejects. `repr(C)` pins +// `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. #[repr(C, align(8))] 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. From 258b0c6458d34353c2ad57aaceb71ff4c82d4325 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 06:30:15 +0000 Subject: [PATCH 07/19] Address review: typed discriminants, classify once, Date precision parity - The 0-8 classifier result is now a #[repr(u8)] TemporalObjectType enum with exhaustive matches (no magic numbers or wildcard naming), and the 1-4 construction kind crosses as TomlDateTimeKind: the only u8 casts left are at the extern call sites. The construction helper moves to bun_js_parser_jsc, which can see the bun_ast enum. - layout_of carries its classification through Layout::TemporalKeyval so emission does not re-classify the same value. - Date now prints with auto fraction precision (trailing zeros trimmed), matching how Temporal.Instant spells the same instant. - Bundler test for the realistic collision: an imported Temporal polyfill binding in the same chunk gets renamed while the TOML module's calls resolve to the native global. --- src/js_parser_jsc/expr_jsc.rs | 22 ++++- src/js_parser_jsc/lib.rs | 4 +- src/jsc/JSValue.rs | 14 --- src/runtime/api.rs | 2 +- src/runtime/api/TOMLObject.rs | 143 ++++++++++++++++++++-------- test/bundler/bundler_loader.test.ts | 16 ++++ test/js/bun/toml/toml.test.ts | 10 +- 7 files changed, 149 insertions(+), 62 deletions(-) diff --git a/src/js_parser_jsc/expr_jsc.rs b/src/js_parser_jsc/expr_jsc.rs index d9acffa4161f..f5eb49994154 100644 --- a/src/js_parser_jsc/expr_jsc.rs +++ b/src/js_parser_jsc/expr_jsc.rs @@ -63,8 +63,7 @@ fn data_to_js_with_check( ExprData::EArrayJSON(e) => array_json_to_js(e, global, stack_check), ExprData::EString(e) => { if let Some(kind) = e.toml_datetime { - return JSValue::from_toml_datetime_literal(global, e.slice8(), kind as u8) - .map_err(js_err); + return toml_datetime_to_js(global, e.slice8(), kind).map_err(js_err); } string_to_js(e, global) } @@ -202,6 +201,25 @@ fn json_value_to_js( }) } +/// A TOML date/time literal as the Temporal object of its kind. `text` must +/// be ASCII that `Temporal.*.from` accepts verbatim. +pub fn toml_datetime_to_js( + global: &JSGlobalObject, + text: &[u8], + kind: E::TomlDateTimeKind, +) -> bun_jsc::JsResult { + debug_assert!(text.is_ascii()); + // SAFETY: `text` is a live slice for the duration of the call. + unsafe { + bun_jsc::cpp::Bun__Temporal__fromDateTimeLiteral( + global, + text.as_ptr(), + text.len(), + kind as u8, + ) + } +} + fn utf8_bytes_to_js(bytes: &[u8], global: &JSGlobalObject) -> Result { if bytes.is_empty() { let empty = BunString::EMPTY; diff --git a/src/js_parser_jsc/lib.rs b/src/js_parser_jsc/lib.rs index ba924b24c9c9..5d0de89104c2 100644 --- a/src/js_parser_jsc/lib.rs +++ b/src/js_parser_jsc/lib.rs @@ -11,4 +11,6 @@ pub mod expr_jsc; // Re-export the foreign `Expr` alongside its JSC extension trait so downstream // callers can write `bun_js_parser_jsc::Expr` / `expr.to_js(global)` without // also depending on `bun_js_parser` directly. -pub use expr_jsc::{ExprJsc, data_to_js, expr_to_js, string_to_js, value_string_to_js}; +pub use expr_jsc::{ + ExprJsc, data_to_js, expr_to_js, string_to_js, toml_datetime_to_js, value_string_to_js, +}; diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 296afab27fae..1ffefc5fa1bc 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -655,20 +655,6 @@ impl JSValue { pub fn from_date_number(global: &JSGlobalObject, value: f64) -> JSValue { JSC__JSValue__dateInstanceFromNumber(global, value) } - /// A TOML date/time literal as the Temporal object of its kind (a - /// `bun_ast::E::TomlDateTimeKind` discriminant). `text` must be ASCII - /// that `Temporal.*.from` accepts verbatim. - pub fn from_toml_datetime_literal( - global: &JSGlobalObject, - text: &[u8], - kind: u8, - ) -> JsResult { - debug_assert!(text.is_ascii()); - // SAFETY: `text` is a live slice for the duration of the call. - unsafe { - crate::cpp::Bun__Temporal__fromDateTimeLiteral(global, text.as_ptr(), text.len(), kind) - } - } #[track_caller] pub fn from_int64_no_truncate(global: &JSGlobalObject, i: i64) -> JsResult { host_fn::from_js_host_call(global, || JSC__JSValue__fromInt64NoTruncate(global, i)) diff --git a/src/runtime/api.rs b/src/runtime/api.rs index 9d94d06b7832..60a9d1e80011 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -315,7 +315,7 @@ fn expr_to_js_with_check( ExprData::EString(str) => { let str = str.get(); if let Some(kind) = str.toml_datetime { - return JSValue::from_toml_datetime_literal(global, str.slice8(), kind as u8); + return bun_js_parser_jsc::toml_datetime_to_js(global, str.slice8(), kind); } estring_to_js(str, global) } diff --git a/src/runtime/api/TOMLObject.rs b/src/runtime/api/TOMLObject.rs index bdc5b800cb32..579e5520f70f 100644 --- a/src/runtime/api/TOMLObject.rs +++ b/src/runtime/api/TOMLObject.rs @@ -73,7 +73,7 @@ fn stringify(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { if !unwrapped.is_object() || unwrapped.is_array() || unwrapped.is_date() - || temporal_object_type(unwrapped) != 0 + || temporal_object_type(unwrapped).is_some() { return Err(global.throw(format_args!( "TOML.stringify expects an object at the top level (a TOML document is a table)" @@ -120,6 +120,9 @@ const MAX_SAFE_INTEGER_F: f64 = 9007199254740991.0; enum Layout { /// `key = value` on the current table's line block. Keyval, + /// `key = value` whose value is a Temporal object; carries the + /// classification so emission does not re-ask. + TemporalKeyval(TemporalObjectType), /// `[path.key]` section. Table, /// `[[path.key]]` section per element. @@ -184,14 +187,17 @@ impl Stringifier { || item.is_array() || item.is_date() || item.is_function() - || temporal_object_type(item) != 0 + || temporal_object_type(item).is_some() { return Ok(Layout::Keyval); } } return Ok(Layout::ArrayOfTables); } - if value.is_object() && !value.is_date() && temporal_object_type(value) == 0 { + if value.is_object() && !value.is_date() { + if let Some(temporal_type) = temporal_object_type(value) { + return Ok(Layout::TemporalKeyval(temporal_type)); + } return Ok(Layout::Table); } Ok(Layout::Keyval) @@ -226,17 +232,20 @@ impl Stringifier { if value.is_null() { return Err(self.err_null_value(global, &prop_name)); } - if let Layout::Keyval = self.layout_of(global, value)? { - if header_pending { - header_pending = false; - self.append_header(false); - } - self.append_key_segment(&prop_name); - self.builder.append_latin1(b" = "); - self.stringify_inline_value(global, value)?; - self.builder.append_lchar(b'\n'); - self.wrote = true; + let known_temporal = match self.layout_of(global, value)? { + Layout::Keyval => None, + Layout::TemporalKeyval(temporal_type) => Some(temporal_type), + Layout::Table | Layout::ArrayOfTables | Layout::Skip => continue, + }; + if header_pending { + header_pending = false; + self.append_header(false); } + self.append_key_segment(&prop_name); + self.builder.append_latin1(b" = "); + self.stringify_inline_value(global, value, known_temporal)?; + self.builder.append_lchar(b'\n'); + self.wrote = true; } // Pass 2: sections. Values are re-read; an array-of-tables element @@ -246,7 +255,7 @@ impl Stringifier { while let Some(prop_name) = iter.next()? { let value = iter.value.unwrap_boxed_primitive(global)?; match self.layout_of(global, value)? { - Layout::Keyval | Layout::Skip => {} + Layout::Keyval | Layout::TemporalKeyval(_) | Layout::Skip => {} Layout::Table => { header_pending = false; self.mark_visiting(global, value)?; @@ -266,7 +275,7 @@ impl Stringifier { || item.is_array() || item.is_date() || item.is_function() - || temporal_object_type(item) != 0 + || temporal_object_type(item).is_some() { self.path.pop(); return Err(self.err_changed(global)); @@ -291,11 +300,13 @@ impl Stringifier { } /// One value on the right-hand side of `=` (or inside an inline - /// array/table). `value` is already unboxed. + /// array/table). `value` is already unboxed; `known_temporal` is the + /// classification `layout_of` already computed for it, if any. fn stringify_inline_value( &mut self, global: &JSGlobalObject, value: JSValue, + known_temporal: Option, ) -> StringifyResult<()> { if !self.stack_check.is_safe_to_recurse() { return Err(StringifyError::StackOverflow); @@ -331,8 +342,7 @@ impl Stringifier { return self.append_datetime(global, value); } - let temporal_type = temporal_object_type(value); - if temporal_type != 0 { + if let Some(temporal_type) = known_temporal.or_else(|| temporal_object_type(value)) { return self.append_temporal(global, value, temporal_type); } @@ -350,7 +360,7 @@ impl Stringifier { if item.is_null() || item.is_undefined() || item.is_symbol() || item.is_function() { return Err(self.err_in_array(global, item)); } - self.stringify_inline_value(global, item)?; + self.stringify_inline_value(global, item, None)?; } self.builder.append_lchar(b']'); self.visiting.remove(&value); @@ -382,7 +392,7 @@ impl Stringifier { first = false; self.append_key_segment(&prop_name); self.builder.append_latin1(b" = "); - self.stringify_inline_value(global, prop_value)?; + self.stringify_inline_value(global, prop_value, None)?; } self.builder .append_latin1(if first { b"{}" } else { b" }" }); @@ -484,7 +494,19 @@ impl Stringifier { )) .into()); } - self.builder.append_latin1(iso); + // `toISOString` always prints three fraction digits; trim trailing + // zeros (and a bare `.`) so `Date` and `Temporal.Instant` spell the + // same instant identically. + debug_assert!(iso.len() == 24 && iso[19] == b'.' && iso[23] == b'Z'); + let mut end = 23; + while end > 20 && iso[end - 1] == b'0' { + end -= 1; + } + if end == 20 { + end = 19; + } + self.builder.append_latin1(&iso[..end]); + self.builder.append_lchar(b'Z'); Ok(()) } @@ -494,13 +516,13 @@ impl Stringifier { &mut self, global: &JSGlobalObject, value: JSValue, - temporal_type: u8, + temporal_type: TemporalObjectType, ) -> StringifyResult<()> { - if temporal_type > TEMPORAL_ZONED_DATE_TIME { + if !temporal_type.has_toml_form() { return Err(global .throw(format_args!( "TOML.stringify cannot serialize {} (it has no TOML representation)", - temporal_type_name(temporal_type) + temporal_type.name() )) .into()); } @@ -510,7 +532,7 @@ impl Stringifier { jsc::cpp::Bun__Temporal__toTOMLDateTime( global, value, - temporal_type, + temporal_type as u8, buf.as_mut_ptr(), buf.len(), ) @@ -521,7 +543,7 @@ impl Stringifier { return Err(global .throw(format_args!( "TOML.stringify cannot serialize a {} outside years 0000-9999", - temporal_type_name(temporal_type) + temporal_type.name() )) .into()); } @@ -565,25 +587,62 @@ impl Stringifier { } } -/// The last discriminant with a TOML representation (1-5; 6-8 have none). -const TEMPORAL_ZONED_DATE_TIME: u8 = 5; +/// Mirror of the `Bun__JSValue__temporalObjectType` discriminants (0, not a +/// Temporal object, maps to `None` in `temporal_object_type`). +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +enum TemporalObjectType { + Instant = 1, + PlainDateTime = 2, + PlainDate = 3, + PlainTime = 4, + ZonedDateTime = 5, + PlainYearMonth = 6, + PlainMonthDay = 7, + Duration = 8, +} -/// 0 for anything that is not a Temporal object, else the 1-8 discriminant -/// `temporal_type_name` describes. -fn temporal_object_type(value: JSValue) -> u8 { - jsc::cpp::Bun__JSValue__temporalObjectType(value) +impl TemporalObjectType { + /// Whether TOML has a date/time literal for this type. + fn has_toml_form(self) -> bool { + match self { + TemporalObjectType::Instant + | TemporalObjectType::PlainDateTime + | TemporalObjectType::PlainDate + | TemporalObjectType::PlainTime + | TemporalObjectType::ZonedDateTime => true, + TemporalObjectType::PlainYearMonth + | TemporalObjectType::PlainMonthDay + | TemporalObjectType::Duration => false, + } + } + + fn name(self) -> &'static str { + match self { + TemporalObjectType::Instant => "Temporal.Instant", + TemporalObjectType::PlainDateTime => "Temporal.PlainDateTime", + TemporalObjectType::PlainDate => "Temporal.PlainDate", + TemporalObjectType::PlainTime => "Temporal.PlainTime", + TemporalObjectType::ZonedDateTime => "Temporal.ZonedDateTime", + TemporalObjectType::PlainYearMonth => "Temporal.PlainYearMonth", + TemporalObjectType::PlainMonthDay => "Temporal.PlainMonthDay", + TemporalObjectType::Duration => "Temporal.Duration", + } + } } -fn temporal_type_name(temporal_type: u8) -> &'static str { - match temporal_type { - 1 => "Temporal.Instant", - 2 => "Temporal.PlainDateTime", - 3 => "Temporal.PlainDate", - 4 => "Temporal.PlainTime", - 5 => "Temporal.ZonedDateTime", - 6 => "Temporal.PlainYearMonth", - 7 => "Temporal.PlainMonthDay", - _ => "Temporal.Duration", +fn temporal_object_type(value: JSValue) -> Option { + match jsc::cpp::Bun__JSValue__temporalObjectType(value) { + 0 => None, + 1 => Some(TemporalObjectType::Instant), + 2 => Some(TemporalObjectType::PlainDateTime), + 3 => Some(TemporalObjectType::PlainDate), + 4 => Some(TemporalObjectType::PlainTime), + 5 => Some(TemporalObjectType::ZonedDateTime), + 6 => Some(TemporalObjectType::PlainYearMonth), + 7 => Some(TemporalObjectType::PlainMonthDay), + 8 => Some(TemporalObjectType::Duration), + _ => unreachable!("Bun__JSValue__temporalObjectType returns 0-8"), } } diff --git a/test/bundler/bundler_loader.test.ts b/test/bundler/bundler_loader.test.ts index c4d8d6365092..d65a2eb6d449 100644 --- a/test/bundler/bundler_loader.test.ts +++ b/test/bundler/bundler_loader.test.ts @@ -69,6 +69,22 @@ describe("bundler", async () => { }, run: { stdout: "shadowed 1979-05-27" }, }); + // The realistic collision: another module in the chunk imports a + // Temporal polyfill binding. The import gets renamed and the TOML + // module's calls still resolve to the native global. + itBundled("bun/loader-toml-datetime-imported-temporal-binding", { + target, + files: { + "/entry.ts": /* js */ ` + import { Temporal } from './polyfill.js'; + import cfg from './config.toml'; + console.write(Temporal.tag + " " + (cfg.ld instanceof globalThis.Temporal.PlainDate) + " " + cfg.ld.toString()); + `, + "/polyfill.js": `export const Temporal = { tag: "polyfill" };`, + "/config.toml": `ld = 1979-05-27`, + }, + run: { stdout: "polyfill true 1979-05-27" }, + }); // The transform path (--no-bundle) has no renamer, and a top-level TOML // key named Temporal becomes a module-scope var; the printed reference // goes through globalThis so that var cannot capture it. diff --git a/test/js/bun/toml/toml.test.ts b/test/js/bun/toml/toml.test.ts index 42c3bed16b17..eb18b1dc1119 100644 --- a/test/js/bun/toml/toml.test.ts +++ b/test/js/bun/toml/toml.test.ts @@ -692,9 +692,15 @@ describe("TOML.stringify", () => { expect(TOML.stringify({ d })).toBe("d = 1979-05-27T07:32:00.999Z\n"); // It reads back as the Temporal.Instant of the same moment. expect(TOML.parse(TOML.stringify({ d })).d.epochMilliseconds).toBe(d.getTime()); - expect(TOML.stringify({ d: new Date(0) })).toBe("d = 1970-01-01T00:00:00.000Z\n"); + // Fraction uses auto precision (trailing zeros trimmed), so a Date and a + // Temporal.Instant spell the same instant identically. + expect(TOML.stringify({ d: new Date(0) })).toBe("d = 1970-01-01T00:00:00Z\n"); + expect(TOML.stringify({ d: new Date(500) })).toBe("d = 1970-01-01T00:00:00.5Z\n"); + expect(TOML.stringify({ a: new Date(0), b: Temporal.Instant.fromEpochMilliseconds(0) })).toBe( + "a = 1970-01-01T00:00:00Z\nb = 1970-01-01T00:00:00Z\n", + ); // The 4-digit-year bounds (`Date.UTC(0, ...)` remaps year 0 to 1900, so raw ms). - expect(TOML.stringify({ d: new Date(-62167219200000) })).toBe("d = 0000-01-01T00:00:00.000Z\n"); + expect(TOML.stringify({ d: new Date(-62167219200000) })).toBe("d = 0000-01-01T00:00:00Z\n"); expect(TOML.stringify({ d: new Date(Date.UTC(9999, 11, 31, 23, 59, 59, 999)) })).toBe( "d = 9999-12-31T23:59:59.999Z\n", ); From e966563fcc8910779af48c5473365d56b1bdfba6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 06:36:13 +0000 Subject: [PATCH 08/19] Mangle a globalThis TOML key's var on the transform path The printed date/time calls reference the global through globalThis and no renamer runs on this path, so a top-level key of that name would capture them. The named export keeps the original alias. --- src/bundler/transpiler.rs | 50 ++++++++++++++++++++++------- test/bundler/bundler_loader.test.ts | 8 +++-- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 682929fa4d3a..cc31a4912227 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1910,6 +1910,7 @@ fn parse_data_loader<'a>( }]); } + let contains_datetime = expr_contains_toml_datetime(&expr); if let Some(obj) = expr.data.e_object_mut() { let properties: &mut [bun_ast::G::Property] = obj.properties.slice_mut(); if !properties.is_empty() { @@ -1967,18 +1968,29 @@ fn parse_data_loader<'a>( *visited.value_ptr = count as u32; symbols[count] = bun_ast::Symbol { - original_name: match bun_core::MutableString::ensure_valid_identifier(name) - { - // The identifier lives in the - // per-parse arena. Arena-copy the - // owned `Box<[u8]>` so it is freed - // with the arena instead of leaking - // (PORTING.md §Forbidden patterns - // bars `heap::alloc` for `&'static`). - // SAFETY: ARENA — `arena` outlives - // the returned `ParseResult.ast`. - Ok(boxed) => bun_ast::StoreStr::new(arena.alloc_slice_copy(&boxed)), - Err(_) => return None, + original_name: { + let valid = match bun_core::MutableString::ensure_valid_identifier(name) + { + Ok(boxed) => boxed, + Err(_) => return None, + }; + // Date/time values print as `globalThis.Temporal.*` + // calls and no renamer runs on this path, so a var + // named `globalThis` would capture them. The named + // export keeps the original alias. + if contains_datetime && &*valid == b"globalThis" { + bun_ast::StoreStr::new(b"globalThis_") + } else { + // The identifier lives in the + // per-parse arena. Arena-copy the + // owned `Box<[u8]>` so it is freed + // with the arena instead of leaking + // (PORTING.md §Forbidden patterns + // bars `heap::alloc` for `&'static`). + // SAFETY: ARENA — `arena` outlives + // the returned `ParseResult.ast`. + bun_ast::StoreStr::new(arena.alloc_slice_copy(&valid)) + } }, ..Default::default() }; @@ -2074,6 +2086,20 @@ fn parse_data_loader<'a>( }); } +/// Whether any value in a data-format AST is a TOML date/time literal. +fn expr_contains_toml_datetime(expr: &bun_ast::Expr) -> bool { + match expr.data { + bun_ast::ExprData::EString(str) => str.toml_datetime.is_some(), + bun_ast::ExprData::EArray(arr) => arr.items.slice().iter().any(expr_contains_toml_datetime), + bun_ast::ExprData::EObject(obj) => obj + .properties + .slice() + .iter() + .any(|prop| prop.value.as_ref().is_some_and(expr_contains_toml_datetime)), + _ => false, + } +} + #[cold] #[inline(never)] fn parse_text_loader<'a>( diff --git a/test/bundler/bundler_loader.test.ts b/test/bundler/bundler_loader.test.ts index d65a2eb6d449..9b0efbe6f2fe 100644 --- a/test/bundler/bundler_loader.test.ts +++ b/test/bundler/bundler_loader.test.ts @@ -87,18 +87,22 @@ describe("bundler", async () => { }); // The transform path (--no-bundle) has no renamer, and a top-level TOML // key named Temporal becomes a module-scope var; the printed reference - // goes through globalThis so that var cannot capture it. + // goes through globalThis so that var cannot capture it. A key named + // globalThis in turn gets its var mangled (the named export keeps the + // original alias). itBundled("bun/loader-toml-datetime-no-bundle-temporal-key", { target, bundling: false, entryPoints: ["/config.toml"], files: { - "/config.toml": `Temporal = "x"\nd = 1979-05-27`, + "/config.toml": `Temporal = "x"\nglobalThis = "y"\nd = 1979-05-27`, }, run: true, onAfterBundle(api) { const code = api.readFile("/out.js"); expect(code).toContain('globalThis.Temporal.PlainDate.from("1979-05-27")'); + expect(code).toContain('globalThis_ = "y"'); + expect(code).toContain("globalThis_ as globalThis"); }, }); // TOML date/time values bundle as Temporal construction calls; the From 4af3b094122281a1d4accdea649a1883078a4768 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 06:56:01 +0000 Subject: [PATCH 09/19] Share one Expr to JSValue converter between TOML.parse and imports Bun.TOML.parse and JSON5.parse previously used their own converter in runtime/api.rs while import/require of data files used bun_js_parser_jsc::data_to_js; the duplication became load-bearing with the Temporal mapping (miss one and the two paths return different types for the same document). api.rs now delegates to the shared converter and maps the error type. Both paths already agreed observably, including __proto__ keys becoming own properties and numeric keys. --- src/runtime/api.rs | 77 ++++++++-------------------------------------- 1 file changed, 13 insertions(+), 64 deletions(-) diff --git a/src/runtime/api.rs b/src/runtime/api.rs index 60a9d1e80011..df914b462bdc 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -273,72 +273,21 @@ fn with_text_format_source( // ─── shared Expr → JS conversion for the text-format parsers ───────────────── -fn estring_to_js( - str: &bun_ast::E::EString, - global: &bun_jsc::JSGlobalObject, -) -> bun_jsc::JsResult { - use bun_jsc::StringJsc as _; - // NOTE: the text-format parsers never build ropes, so the simple - // slice → JS path is sufficient. - if str.is_utf16 { - let zig = bun_core::ZigString::init_utf16(str.slice16()); - let bun_s = bun_core::String::init(zig); - bun_s.to_js(global) - } else { - bun_jsc::bun_string_jsc::create_utf8_for_js(global, str.slice8()) - } -} - +/// `Expr` → `JSValue` for the text-format parsers (TOML, JSON5), through the +/// same converter the module loader uses for imported data files, so +/// `Bun.TOML.parse` and `import "./x.toml"` cannot drift apart. fn expr_to_js( expr: bun_ast::Expr, global: &bun_jsc::JSGlobalObject, ) -> bun_jsc::JsResult { - expr_to_js_with_check(expr, global, bun_core::StackCheck::init()) -} - -fn expr_to_js_with_check( - expr: bun_ast::Expr, - global: &bun_jsc::JSGlobalObject, - stack_check: bun_core::StackCheck, -) -> bun_jsc::JsResult { - use bun_ast::expr::Data as ExprData; - use bun_collections::VecExt as _; - use bun_jsc::JSValue; - - if !stack_check.is_safe_to_recurse() { - return Err(global.throw_stack_overflow()); - } - match expr.data { - ExprData::ENull(_) => Ok(JSValue::NULL), - ExprData::EBoolean(boolean) => Ok(JSValue::from(boolean.value)), - ExprData::ENumber(number) => Ok(JSValue::js_number(number.value())), - ExprData::EString(str) => { - let str = str.get(); - if let Some(kind) = str.toml_datetime { - return bun_js_parser_jsc::toml_datetime_to_js(global, str.slice8(), kind); - } - estring_to_js(str, global) - } - ExprData::EArray(arr) => { - JSValue::create_array_from_iter(global, arr.slice().iter(), |item| { - expr_to_js_with_check(*item, global, stack_check) - }) - } - ExprData::EObject(obj) => { - let js_obj = JSValue::create_empty_object(global, obj.properties.len_u32() as usize); - for prop in obj.properties.slice() { - let key_expr = prop.key.expect("infallible: prop has key"); - let value = expr_to_js_with_check( - prop.value.expect("infallible: prop has value"), - global, - stack_check, - )?; - let key_js = expr_to_js_with_check(key_expr, global, stack_check)?; - let key_str = bun_core::OwnedString::new(key_js.to_bun_string(global)?); - js_obj.put_may_be_index(global, &key_str, value)?; - } - Ok(js_obj) - } - _ => Ok(JSValue::UNDEFINED), - } + use bun_ast::ToJSError; + bun_js_parser_jsc::expr_to_js(&expr, global).map_err(|e| match e { + ToJSError::JSError => bun_jsc::JsError::Thrown, + ToJSError::OutOfMemory => bun_jsc::JsError::OutOfMemory, + ToJSError::JSTerminated => bun_jsc::JsError::Terminated, + // The data-format parsers only produce nodes the converter handles. + ToJSError::CannotConvertArgumentTypeToJS + | ToJSError::CannotConvertIdentifierToJS + | ToJSError::MacroError => global.throw(format_args!("Cannot convert value to JS")), + }) } From b0f49a06432b4ecd38034c6daabc159b2c6b00d9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 07:31:29 +0000 Subject: [PATCH 10/19] Walk the TOML AST iteratively; fail lazy-export parse errors gracefully Deep dotted TOML headers nest objects without bounding recursion in the parser (they are built iteratively there), so the date/time lowering and the transform path's scan stack-overflowed on adversarial documents. Both walks now use an explicit worklist, preserving the pre-existing behavior for deep documents. The lazy-export callers in ParseTask also stop unwrapping the Ok(None) case new_lazy_export_ast returns for a logged parse failure, and the globalThis var mangle on the transform path grows past a colliding key of the same name. --- src/bundler/ParseTask.rs | 18 +++++---- src/bundler/transpiler.rs | 63 ++++++++++++++++++++++++----- src/js_parser/parse/parse_entry.rs | 50 +++++++++++++++++------ test/bundler/bundler_loader.test.ts | 8 ++-- test/js/bun/toml/toml.test.ts | 24 +++++++++++ 5 files changed, 128 insertions(+), 35 deletions(-) diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index fb71210f27b1..75fb89fcbde5 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -610,7 +610,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)?, ); ast.css = Some(crate::bundled_ast::CssAstRef::from_bump( bump.alloc(bun_css::BundlerStyleSheet::empty()), @@ -629,7 +630,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)?, )) } @@ -766,7 +768,7 @@ pub mod parse_worker { source, b"", )? - .unwrap(), + .ok_or(AnyError::ParserError)?, )); } Loader::Toml => { @@ -789,7 +791,7 @@ pub mod parse_worker { source, b"", )? - .unwrap(), + .ok_or(AnyError::ParserError)?, )) })(); let _ = temp_log.clone_to_with_recycled(log, true); @@ -810,7 +812,7 @@ pub mod parse_worker { source, b"", )? - .unwrap(), + .ok_or(AnyError::ParserError)?, )) })(); let _ = temp_log.clone_to_with_recycled(log, true); @@ -832,7 +834,7 @@ pub mod parse_worker { source, b"", )? - .unwrap(), + .ok_or(AnyError::ParserError)?, )) })(); let _ = temp_log.clone_to_with_recycled(log, true); @@ -856,7 +858,7 @@ pub mod parse_worker { source, b"", )? - .unwrap(), + .ok_or(AnyError::ParserError)?, ); ast.add_url_for_css( bump, @@ -897,7 +899,7 @@ pub mod parse_worker { source, b"", )? - .unwrap(), + .ok_or(AnyError::ParserError)?, ); ast.add_url_for_css( bump, diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index cc31a4912227..8953ad56ec67 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1915,6 +1915,32 @@ fn parse_data_loader<'a>( let properties: &mut [bun_ast::G::Property] = obj.properties.slice_mut(); if !properties.is_empty() { let n = properties.len(); + // The var name replacing a `globalThis` key (see the mangle + // below) must not collide with another key, which could + // itself be named `globalThis_`. + let mangled_global_this: &[u8] = if contains_datetime { + let mut candidate: Vec = b"globalThis_".to_vec(); + 'grow: loop { + for property in properties.iter_mut() { + let key: &[u8] = property + .key + .as_mut() + .expect("infallible: prop has key") + .data + .e_string_mut() + .expect("infallible: variant checked") + .slice(arena); + if key == candidate { + candidate.push(b'_'); + continue 'grow; + } + } + break; + } + arena.alloc_slice_copy(&candidate) + } else { + b"globalThis_" + }; // The loop below writes sparsely at index `i` and // `continue`s on `"default"` / duplicate keys, so // some slots are never assigned. In Rust an uninit @@ -1979,7 +2005,7 @@ fn parse_data_loader<'a>( // named `globalThis` would capture them. The named // export keeps the original alias. if contains_datetime && &*valid == b"globalThis" { - bun_ast::StoreStr::new(b"globalThis_") + bun_ast::StoreStr::new(mangled_global_this) } else { // The identifier lives in the // per-parse arena. Arena-copy the @@ -2087,17 +2113,32 @@ fn parse_data_loader<'a>( } /// Whether any value in a data-format AST is a TOML date/time literal. -fn expr_contains_toml_datetime(expr: &bun_ast::Expr) -> bool { - match expr.data { - bun_ast::ExprData::EString(str) => str.toml_datetime.is_some(), - bun_ast::ExprData::EArray(arr) => arr.items.slice().iter().any(expr_contains_toml_datetime), - bun_ast::ExprData::EObject(obj) => obj - .properties - .slice() - .iter() - .any(|prop| prop.value.as_ref().is_some_and(expr_contains_toml_datetime)), - _ => false, +/// Iterative: deep dotted TOML headers nest objects far beyond safe +/// recursion depth. +fn expr_contains_toml_datetime(root: &bun_ast::Expr) -> bool { + let mut work: Vec = vec![root.data]; + while let Some(data) = work.pop() { + match data { + bun_ast::ExprData::EString(str) => { + if str.toml_datetime.is_some() { + return true; + } + } + bun_ast::ExprData::EArray(arr) => { + work.extend(arr.items.slice().iter().map(|item| item.data)); + } + bun_ast::ExprData::EObject(obj) => { + work.extend( + obj.properties + .slice() + .iter() + .filter_map(|prop| prop.value.as_ref().map(|value| value.data)), + ); + } + _ => {} + } } + false } #[cold] diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index b7b55054040d..a94241e97a76 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -612,14 +612,48 @@ impl<'a> Parser<'a> { } } +/// A container queued by `lower_date_time_literals`' worklist. +enum DateTimeLowerContainer { + Object(js_ast::StoreRef), + Array(js_ast::StoreRef), +} + /// Rewrites every `toml_datetime`-tagged `E::String` in `expr` (in place) /// into a `Temporal..from("")` call, declaring the unbound /// `Temporal` symbol on first use. The calls are pure-annotated so tree -/// shaking may drop unused exports. +/// shaking may drop unused exports. Iterative: deep dotted TOML headers nest +/// objects far beyond safe recursion depth. fn lower_date_time_literals<'a>( p: &mut JavaScriptParser<'a>, expr: &mut Expr, temporal_ref: &mut Option, +) -> Result<(), Error> { + let mut work: Vec = Vec::new(); + lower_one_date_time_literal(p, expr, temporal_ref, &mut work)?; + while let Some(container) = work.pop() { + match container { + DateTimeLowerContainer::Object(mut obj) => { + for property in obj.properties.slice_mut() { + if let Some(value) = &mut property.value { + lower_one_date_time_literal(p, value, temporal_ref, &mut work)?; + } + } + } + DateTimeLowerContainer::Array(mut arr) => { + for item in arr.items.slice_mut() { + lower_one_date_time_literal(p, item, temporal_ref, &mut work)?; + } + } + } + } + Ok(()) +} + +fn lower_one_date_time_literal<'a>( + p: &mut JavaScriptParser<'a>, + expr: &mut Expr, + temporal_ref: &mut Option, + work: &mut Vec, ) -> Result<(), Error> { match expr.data { js_ast::ExprData::EString(str) if str.toml_datetime.is_some() => { @@ -672,18 +706,8 @@ fn lower_date_time_literals<'a>( 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)?; - } - } - } + js_ast::ExprData::EArray(arr) => work.push(DateTimeLowerContainer::Array(arr)), + js_ast::ExprData::EObject(obj) => work.push(DateTimeLowerContainer::Object(obj)), _ => {} } Ok(()) diff --git a/test/bundler/bundler_loader.test.ts b/test/bundler/bundler_loader.test.ts index 9b0efbe6f2fe..be900d5e2463 100644 --- a/test/bundler/bundler_loader.test.ts +++ b/test/bundler/bundler_loader.test.ts @@ -95,14 +95,16 @@ describe("bundler", async () => { bundling: false, entryPoints: ["/config.toml"], files: { - "/config.toml": `Temporal = "x"\nglobalThis = "y"\nd = 1979-05-27`, + "/config.toml": `Temporal = "x"\nglobalThis = "y"\nglobalThis_ = "z"\nd = 1979-05-27`, }, run: true, onAfterBundle(api) { const code = api.readFile("/out.js"); expect(code).toContain('globalThis.Temporal.PlainDate.from("1979-05-27")'); - expect(code).toContain('globalThis_ = "y"'); - expect(code).toContain("globalThis_ as globalThis"); + // The mangled var dodges the real globalThis_ key. + expect(code).toContain('globalThis__ = "y"'); + expect(code).toContain("globalThis__ as globalThis"); + expect(code).toContain('globalThis_ = "z"'); }, }); // TOML date/time values bundle as Temporal construction calls; the diff --git a/test/js/bun/toml/toml.test.ts b/test/js/bun/toml/toml.test.ts index eb18b1dc1119..3de01f6d959a 100644 --- a/test/js/bun/toml/toml.test.ts +++ b/test/js/bun/toml/toml.test.ts @@ -302,6 +302,30 @@ describe("date/times return Temporal objects", () => { expect(exitCode).toBe(0); }); + test("deep dotted headers bundle without crashing", async () => { + // Dotted headers nest objects beyond safe recursion depth (the TOML + // parser builds them iteratively); the bundler's date/time lowering and + // the transform path's scan walk the tree iteratively too. A clean + // bundler error is acceptable; death by stack overflow is not. + using dir = tempDir("toml-deep-header", { + "deep.toml": "[" + Buffer.alloc(200_000, "a.").toString() + "a]\nd = 1979-05-27\n", + }); + for (const args of [ + ["build", "./deep.toml"], + ["build", "--no-bundle", "./deep.toml"], + ]) { + await using proc = Bun.spawn({ + cmd: [bunExe(), ...args], + env: bunEnv, + cwd: String(dir), + stdout: "ignore", + stderr: "pipe", + }); + const exitCode = await proc.exited; + expect([0, 1]).toContain(exitCode); + } + }); + test("importing a TOML module with date/times throws when Temporal is disabled", async () => { // The import path converts the same way; the load must fail with the // TypeError, not crash. From 27274c7f33c00b065df87848ac768d0bf5b50860 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 07:39:15 +0000 Subject: [PATCH 11/19] Use an absolute entry path and drain stderr in the deep-header test --- test/js/bun/toml/toml.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/js/bun/toml/toml.test.ts b/test/js/bun/toml/toml.test.ts index 3de01f6d959a..83c40aa955c2 100644 --- a/test/js/bun/toml/toml.test.ts +++ b/test/js/bun/toml/toml.test.ts @@ -1,6 +1,7 @@ import { TOML } from "bun"; import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, tempDir } from "harness"; +import { join } from "node:path"; // Hand-written coverage beyond the official conformance suite // (toml-test-suite.test.ts): the JS-facing API surface, JS value mapping, @@ -310,9 +311,10 @@ describe("date/times return Temporal objects", () => { using dir = tempDir("toml-deep-header", { "deep.toml": "[" + Buffer.alloc(200_000, "a.").toString() + "a]\nd = 1979-05-27\n", }); + const entry = join(String(dir), "deep.toml"); for (const args of [ - ["build", "./deep.toml"], - ["build", "--no-bundle", "./deep.toml"], + ["build", entry], + ["build", "--no-bundle", entry], ]) { await using proc = Bun.spawn({ cmd: [bunExe(), ...args], @@ -321,7 +323,10 @@ describe("date/times return Temporal objects", () => { stdout: "ignore", stderr: "pipe", }); - const exitCode = await proc.exited; + // Drain stderr concurrently so the child cannot block on a full pipe. + // Exit 0 is today's behavior and a clean diagnostic exit would also be + // fine; death by signal (stack overflow) is neither. + const [exitCode] = await Promise.all([proc.exited, proc.stderr.text()]); expect([0, 1]).toContain(exitCode); } }); From 79783922b0103b316499cc6c2cf88a45b7de0370 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:04:53 +0000 Subject: [PATCH 12/19] Compare conformance-suite date/times with plain toEqual Now that deepEquals understands Temporal objects, the generator no longer needs to normalize both sides to class-plus-toString tag strings; dt() constructs the Temporal value directly and each case is a single toEqual. --- test/js/bun/toml/generate_toml_test_suite.ts | 86 +- test/js/bun/toml/toml-test-suite.test.ts | 944 +++++++++---------- 2 files changed, 454 insertions(+), 576 deletions(-) diff --git a/test/js/bun/toml/generate_toml_test_suite.ts b/test/js/bun/toml/generate_toml_test_suite.ts index 3dd1826c7be3..3559e30d2403 100644 --- a/test/js/bun/toml/generate_toml_test_suite.ts +++ b/test/js/bun/toml/generate_toml_test_suite.ts @@ -20,8 +20,8 @@ * output is not acceptable API) * - datetime -> Temporal.Instant, datetime-local -> Temporal.PlainDateTime, * date-local -> Temporal.PlainDate, time-local -> Temporal.PlainTime; - * compared by class + canonical toString (Temporal instances have no own - * properties, so a bare toEqual would compare nothing) + * compared with plain toEqual (deepEquals compares Temporal objects by + * class and value) * - invalid documents -> SyntaxError, with the exact full message asserted * when the in-tree parser produced a SyntaxError at generation time */ @@ -245,8 +245,6 @@ function valueToJS(val: unknown, indent: number = 0): string { // --------------------------------------------------------------------------- // 6. Generate the test file // --------------------------------------------------------------------------- -const kindUnion = DATETIME_KINDS.map(k => JSON.stringify(k)).join(" | "); - let output = `// Tests generated from the official toml-lang/toml-test conformance suite // Generated from toml-test commit: ${commit} // Scope: TOML v1.1.0 manifest (tests/files-toml-1.1.0): ${validCases.length} valid + ${outOfRangeCases.length} out-of-range-integer + ${invalidCases.length} invalid + ${invalidEncodingCases.length} invalid-encoding cases @@ -256,10 +254,9 @@ let output = `// Tests generated from the official toml-lang/toml-test conforman // - integer: number; values outside Number.MAX_SAFE_INTEGER throw (TOML // requires lossless handling or an error — see the out-of-range block) // - datetime: Temporal.Instant; datetime-local: Temporal.PlainDateTime; -// date-local: Temporal.PlainDate; time-local: Temporal.PlainTime. -// Temporal instances have no own properties, so toEqual alone would -// compare nothing: both sides normalize to "" tag strings -// (class + canonical toString) before the single toEqual +// date-local: Temporal.PlainDate; time-local: Temporal.PlainTime, +// compared with plain toEqual (deepEquals compares Temporal objects by +// class and value) // - invalid documents throw SyntaxError; the exact full message is asserted // where the in-tree parser produced a SyntaxError at generation time // @@ -268,16 +265,6 @@ let output = `// Tests generated from the official toml-lang/toml-test conforman import { TOML } from "bun"; import { describe, expect, test } from "bun:test"; -class TomlDateTime { - constructor( - public kind: ${kindUnion}, - public value: string, - ) {} -} -function dt(kind: TomlDateTime["kind"], value: string): TomlDateTime { - return new TomlDateTime(kind, value); -} - const TEMPORAL_CLASS = { "datetime": "Instant", "datetime-local": "PlainDateTime", @@ -285,61 +272,12 @@ const TEMPORAL_CLASS = { "time-local": "PlainTime", } as const; -function temporalTag(className: string, iso: string): string { - return \`\`; -} - // The corpus value is TOML source text; TOML.parse truncates fractional -// seconds to Temporal's 9-digit limit, and Temporal.*.from accepts the rest -// of TOML's spellings (space separator, lowercase t/z, omitted seconds) as is. -function expectedTemporalTag(marker: TomlDateTime): string { - const className = TEMPORAL_CLASS[marker.kind]; - const text = marker.value.replace(/\\.(\\d{9})\\d+/, ".$1"); - return temporalTag(className, (Temporal as any)[className].from(text).toString()); -} - -// Datetime markers become "" tags; everything else is unchanged. -function normalizeExpected(expected: unknown): unknown { - if (expected instanceof TomlDateTime) return expectedTemporalTag(expected); - if (Array.isArray(expected)) return expected.map(normalizeExpected); - if (expected !== null && typeof expected === "object") { - const out: Record = Object.create(null); - for (const [k, v] of Object.entries(expected)) out[k] = normalizeExpected(v); - return out; - } - return expected; -} - -// Normalize the positions of \`actual\` that \`expected\` marks as datetimes, in -// lockstep, so a single toEqual compares everything else exactly. A value of -// the wrong class is left as is and shows up as the toEqual mismatch. -function normalizeActual(actual: unknown, expected: unknown): unknown { - if (expected instanceof TomlDateTime) { - const className = TEMPORAL_CLASS[expected.kind]; - if (actual instanceof (Temporal as any)[className]) { - return temporalTag(className, (actual as any).toString()); - } - return actual; - } - if (Array.isArray(expected) && Array.isArray(actual)) { - return actual.map((a, i) => normalizeActual(a, expected[i])); - } - if ( - expected !== null && - typeof expected === "object" && - actual !== null && - typeof actual === "object" && - !Array.isArray(actual) - ) { - const out: Record = Object.create(null); - for (const [k, v] of Object.entries(actual)) out[k] = normalizeActual(v, (expected as any)[k]); - return out; - } - return actual; -} - -function expectTomlEqual(parsed: unknown, expected: unknown): void { - expect(normalizeActual(parsed, expected)).toEqual(normalizeExpected(expected) as any); +// seconds to Temporal's 9-digit limit (truncated, not rounded), and +// Temporal.*.from accepts the rest of TOML's spellings (space separator, +// lowercase t/z, omitted seconds) as is. +function dt(kind: keyof typeof TEMPORAL_CLASS, value: string) { + return (Temporal as any)[TEMPORAL_CLASS[kind]].from(value.replace(/\\.(\\d{9})\\d+/, ".$1")); } `; @@ -352,8 +290,8 @@ for (const tc of validCases) { output += ` test(${jsString(tc.name)}, () => {\n`; output += ` const input: string = ${jsString(tc.input)};\n`; output += ` const expected: any = ${valueToJS(tc.expected, 2)};\n`; - output += ` expectTomlEqual(TOML.parse(input), expected);\n`; - output += ` expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected);\n`; + output += ` expect(TOML.parse(input)).toEqual(expected);\n`; + output += ` expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected);\n`; output += ` });\n\n`; } output += `});\n`; diff --git a/test/js/bun/toml/toml-test-suite.test.ts b/test/js/bun/toml/toml-test-suite.test.ts index c2ec7b604af4..8d72d569eed1 100644 --- a/test/js/bun/toml/toml-test-suite.test.ts +++ b/test/js/bun/toml/toml-test-suite.test.ts @@ -7,10 +7,9 @@ // - integer: number; values outside Number.MAX_SAFE_INTEGER throw (TOML // requires lossless handling or an error — see the out-of-range block) // - datetime: Temporal.Instant; datetime-local: Temporal.PlainDateTime; -// date-local: Temporal.PlainDate; time-local: Temporal.PlainTime. -// Temporal instances have no own properties, so toEqual alone would -// compare nothing: both sides normalize to "" tag strings -// (class + canonical toString) before the single toEqual +// date-local: Temporal.PlainDate; time-local: Temporal.PlainTime, +// compared with plain toEqual (deepEquals compares Temporal objects by +// class and value) // - invalid documents throw SyntaxError; the exact full message is asserted // where the in-tree parser produced a SyntaxError at generation time // @@ -19,16 +18,6 @@ import { TOML } from "bun"; import { describe, expect, test } from "bun:test"; -class TomlDateTime { - constructor( - public kind: "datetime" | "datetime-local" | "date-local" | "time-local", - public value: string, - ) {} -} -function dt(kind: TomlDateTime["kind"], value: string): TomlDateTime { - return new TomlDateTime(kind, value); -} - const TEMPORAL_CLASS = { "datetime": "Instant", "datetime-local": "PlainDateTime", @@ -36,61 +25,12 @@ const TEMPORAL_CLASS = { "time-local": "PlainTime", } as const; -function temporalTag(className: string, iso: string): string { - return ``; -} - // The corpus value is TOML source text; TOML.parse truncates fractional -// seconds to Temporal's 9-digit limit, and Temporal.*.from accepts the rest -// of TOML's spellings (space separator, lowercase t/z, omitted seconds) as is. -function expectedTemporalTag(marker: TomlDateTime): string { - const className = TEMPORAL_CLASS[marker.kind]; - const text = marker.value.replace(/\.(\d{9})\d+/, ".$1"); - return temporalTag(className, (Temporal as any)[className].from(text).toString()); -} - -// Datetime markers become "" tags; everything else is unchanged. -function normalizeExpected(expected: unknown): unknown { - if (expected instanceof TomlDateTime) return expectedTemporalTag(expected); - if (Array.isArray(expected)) return expected.map(normalizeExpected); - if (expected !== null && typeof expected === "object") { - const out: Record = Object.create(null); - for (const [k, v] of Object.entries(expected)) out[k] = normalizeExpected(v); - return out; - } - return expected; -} - -// Normalize the positions of `actual` that `expected` marks as datetimes, in -// lockstep, so a single toEqual compares everything else exactly. A value of -// the wrong class is left as is and shows up as the toEqual mismatch. -function normalizeActual(actual: unknown, expected: unknown): unknown { - if (expected instanceof TomlDateTime) { - const className = TEMPORAL_CLASS[expected.kind]; - if (actual instanceof (Temporal as any)[className]) { - return temporalTag(className, (actual as any).toString()); - } - return actual; - } - if (Array.isArray(expected) && Array.isArray(actual)) { - return actual.map((a, i) => normalizeActual(a, expected[i])); - } - if ( - expected !== null && - typeof expected === "object" && - actual !== null && - typeof actual === "object" && - !Array.isArray(actual) - ) { - const out: Record = Object.create(null); - for (const [k, v] of Object.entries(actual)) out[k] = normalizeActual(v, (expected as any)[k]); - return out; - } - return actual; -} - -function expectTomlEqual(parsed: unknown, expected: unknown): void { - expect(normalizeActual(parsed, expected)).toEqual(normalizeExpected(expected) as any); +// seconds to Temporal's 9-digit limit (truncated, not rounded), and +// Temporal.*.from accepts the rest of TOML's spellings (space separator, +// lowercase t/z, omitted seconds) as is. +function dt(kind: keyof typeof TEMPORAL_CLASS, value: string) { + return (Temporal as any)[TEMPORAL_CLASS[kind]].from(value.replace(/\.(\d{9})\d+/, ".$1")); } // Each case also asserts that parse(stringify(parse(input))) produces the same @@ -101,8 +41,8 @@ describe("toml-test/valid", () => { test("valid/array/array-subtables", () => { const input: string = "[[arr]]\n[arr.subtab]\nval=1\n\n[[arr]]\n[arr.subtab]\nval=2\n"; const expected: any = { arr: [{ subtab: { val: 1 } }, { subtab: { val: 2 } }] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/array", () => { @@ -120,22 +60,22 @@ describe("toml-test/valid", () => { ints: [1, 2, 3], strings: ["a", "b", "c"], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/bool", () => { const input: string = "a = [true, false]\n"; const expected: any = { a: [true, false] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/empty", () => { const input: string = "thevoid = [[[[[]]]]]\n"; const expected: any = { thevoid: [[[[[]]]]] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/hetergeneous", () => { @@ -147,29 +87,29 @@ describe("toml-test/valid", () => { [1.1, 2.1], ], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/mixed-int-array", () => { const input: string = 'arrays-and-ints = [1, ["Arrays are not integers."]]\n'; const expected: any = { "arrays-and-ints": [1, ["Arrays are not integers."]] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/mixed-int-float", () => { const input: string = "ints-and-floats = [1, 1.1]\n"; const expected: any = { "ints-and-floats": [1, 1.1] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/mixed-int-string", () => { const input: string = 'strings-and-ints = ["hi", 42]\n'; const expected: any = { "strings-and-ints": ["hi", 42] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/mixed-string-table", () => { @@ -186,120 +126,120 @@ describe("toml-test/valid", () => { ], mixed: [{ k: "a" }, "b", 1], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/nested-double", () => { const input: string = 'nest = [\n\t[\n\t\t["a"],\n\t\t[1, 2, [3]]\n\t]\n]\n'; const expected: any = { nest: [[["a"], [1, 2, [3]]]] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/nested-inline-table", () => { const input: string = "a = [ { b = {} } ]\n"; const expected: any = { a: [{ b: {} }] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/nested", () => { const input: string = 'nest = [["a"], ["b"]]\n'; const expected: any = { nest: [["a"], ["b"]] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/nospaces", () => { const input: string = "ints = [1,2,3]\n"; const expected: any = { ints: [1, 2, 3] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/open-parent-table", () => { const input: string = "[[parent-table.arr]]\n[[parent-table.arr]]\n[parent-table]\nnot-arr = 1\n"; const expected: any = { "parent-table": { "not-arr": 1, arr: [{}, {}] } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/string-quote-comma-01", () => { const input: string = 'title = [\n"Client: \\"XXXX\\", Job: XXXX",\n"Code: XXXX"\n]\n'; const expected: any = { title: ['Client: "XXXX", Job: XXXX', "Code: XXXX"] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/string-quote-comma-02", () => { const input: string = 'title = [ " \\", ",]\n'; const expected: any = { title: [' ", '] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/string-with-comma-01", () => { const input: string = 'title = [\n"Client: XXXX, Job: XXXX",\n"Code: XXXX"\n]\n'; const expected: any = { title: ["Client: XXXX, Job: XXXX", "Code: XXXX"] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/string-with-comma-02", () => { const input: string = 'title = [\n"""Client: XXXX,\nJob: XXXX""",\n"Code: XXXX"\n]\n'; const expected: any = { title: ["Client: XXXX,\nJob: XXXX", "Code: XXXX"] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/strings", () => { const input: string = "string_array = [ \"all\", 'strings', \"\"\"are the same\"\"\", '''type''']\n"; const expected: any = { string_array: ["all", "strings", "are the same", "type"] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/table-array-string-backslash", () => { const input: string = 'foo = [ { bar="\\"{{baz}}\\""} ]\n'; const expected: any = { foo: [{ bar: '"{{baz}}"' }] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/array/trailing-comma", () => { const input: string = "arr-1 = [1,]\n\narr-2 = [2,3,]\n\narr-3 = [4,\n]\n\narr-4 = [\n\t5,\n\t6,\n]\n"; const expected: any = { "arr-1": [1], "arr-3": [4], "arr-2": [2, 3], "arr-4": [5, 6] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/bool/bool", () => { const input: string = "t = true\nf = false\n"; const expected: any = { f: false, t: true }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/comment/after-literal-no-ws", () => { const input: string = "inf=inf#infinity\nnan=nan#not a number\ntrue=true#true\nfalse=false#false\n"; const expected: any = { false: false, inf: Infinity, nan: NaN, true: true }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/comment/at-eof", () => { const input: string = '# This is a full-line comment\nkey = "value" # This is a comment at the end of a line\n'; const expected: any = { key: "value" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/comment/at-eof2", () => { const input: string = '# This is a full-line comment\nkey = "value" # This is a comment at the end of a line\n'; const expected: any = { key: "value" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/comment/everywhere", () => { @@ -314,22 +254,22 @@ describe("toml-test/valid", () => { more: [42, 42], }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/comment/noeol", () => { const input: string = "# single comment without any eol characters"; const expected: any = {}; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/comment/nonascii", () => { const input: string = "# ~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿\n"; const expected: any = {}; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/comment/tricky", () => { @@ -355,8 +295,8 @@ describe("toml-test/valid", () => { two: "22#", }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/datetime/datetime", () => { @@ -366,8 +306,8 @@ describe("toml-test/valid", () => { lower: dt("datetime", "1987-07-05T17:45:00Z"), space: dt("datetime", "1987-07-05T17:45:00Z"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/datetime/edge", () => { @@ -381,15 +321,15 @@ describe("toml-test/valid", () => { "last-local": dt("datetime-local", "9999-12-31T23:59:59"), "last-offset": dt("datetime", "9999-12-31T23:59:59Z"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/datetime/invalid-date-in-string", () => { const input: string = "s = '2020-01-01x'\n"; const expected: any = { s: "2020-01-01x" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/datetime/leap-year", () => { @@ -403,15 +343,15 @@ describe("toml-test/valid", () => { "2024-datetime": dt("datetime", "2024-02-29T15:15:15Z"), "2024-datetime-local": dt("datetime-local", "2024-02-29T15:15:15"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/datetime/local-date", () => { const input: string = "bestdayever = 1987-07-05\n"; const expected: any = { bestdayever: dt("date-local", "1987-07-05") }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/datetime/local-time", () => { @@ -420,8 +360,8 @@ describe("toml-test/valid", () => { besttimeever: dt("time-local", "17:45:00"), milliseconds: dt("time-local", "10:32:00.555"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/datetime/local", () => { @@ -431,8 +371,8 @@ describe("toml-test/valid", () => { milli: dt("datetime-local", "1977-12-21T10:32:00.555"), space: dt("datetime-local", "1987-07-05T17:45:00"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/datetime/milliseconds", () => { @@ -444,8 +384,8 @@ describe("toml-test/valid", () => { wita1: dt("datetime", "1987-07-05T17:45:56.123+08:00"), wita2: dt("datetime", "1987-07-05T17:45:56.600+08:00"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/datetime/no-seconds", () => { @@ -457,8 +397,8 @@ describe("toml-test/valid", () => { "without-seconds-3": dt("datetime", "1979-05-27T07:32:00-07:00"), "without-seconds-4": dt("datetime-local", "1979-05-27T07:32:00"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/datetime/timezone", () => { @@ -470,43 +410,43 @@ describe("toml-test/valid", () => { pdt: dt("datetime", "1987-07-05T17:45:56-05:00"), utc: dt("datetime", "1987-07-05T17:45:56Z"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/empty-crlf", () => { const input: string = "\r\n"; const expected: any = {}; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/empty-lf", () => { const input: string = "\n"; const expected: any = {}; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/empty-nothing", () => { const input: string = ""; const expected: any = {}; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/empty-space", () => { const input: string = " "; const expected: any = {}; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/empty-tab", () => { const input: string = "\t"; const expected: any = {}; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/example", () => { @@ -516,8 +456,8 @@ describe("toml-test/valid", () => { "best-day-ever": dt("datetime", "1987-07-05T17:45:00Z"), numtheory: { boring: false, perfection: [6, 28, 496] }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/float/exponent-upper", () => { @@ -533,8 +473,8 @@ describe("toml-test/valid", () => { "zero-exp": 3, "zero-plus": 0, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/float/exponent", () => { @@ -550,8 +490,8 @@ describe("toml-test/valid", () => { "zero-exp": 3, "zero-plus": 0, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/float/float", () => { @@ -564,8 +504,8 @@ describe("toml-test/valid", () => { "zero-intpart": 0.123, "leading-zero-fractional": 0.0123, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/float/inf-and-nan", () => { @@ -579,30 +519,30 @@ describe("toml-test/valid", () => { nan_neg: NaN, nan_plus: NaN, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/float/long", () => { const input: string = "longpi = 3.141592653589793\nneglongpi = -3.141592653589793\n"; const expected: any = { longpi: 3.141592653589793, neglongpi: -3.141592653589793 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/float/max-int", () => { const input: string = "# Maximum and minimum safe natural numbers.\nmax_float = 9_007_199_254_740_991.0\nmin_float = -9_007_199_254_740_991.0\n"; const expected: any = { max_float: 9007199254740991, min_float: -9007199254740991 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/float/underscore", () => { const input: string = "before = 3_141.5927\nafter = 3141.592_7\nexponent = 3e1_4\n"; const expected: any = { after: 3141.5927, before: 3141.5927, exponent: 300000000000000 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/float/zero", () => { @@ -617,29 +557,29 @@ describe("toml-test/valid", () => { "signed-pos": 0, zero: 0, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/implicit-and-explicit-after", () => { const input: string = "[a.b.c]\nanswer = 42\n\n[a]\nbetter = 43\n"; const expected: any = { a: { better: 43, b: { c: { answer: 42 } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/implicit-and-explicit-before", () => { const input: string = "[a]\nbetter = 43\n\n[a.b.c]\nanswer = 42\n"; const expected: any = { a: { better: 43, b: { c: { answer: 42 } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/implicit-groups", () => { const input: string = "[a.b.c]\nanswer = 42\n"; const expected: any = { a: { b: { c: { answer: 42 } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/array-01", () => { @@ -653,30 +593,30 @@ describe("toml-test/valid", () => { { first_name: "Bob", last_name: "Seger" }, ], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/array-02", () => { const input: string = '# "No newlines are allowed between the curly braces unless they are valid within\n# a value"\n\na = { a = [\n]}\n'; const expected: any = { a: { a: [] } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/array-03", () => { const input: string = "b = { a = [\n\t\t1,\n\t\t2,\n\t], b = [\n\t\t3,\n\t\t4,\n\t]}\n"; const expected: any = { b: { a: [1, 2], b: [3, 4] } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/bool", () => { const input: string = "a = {a = true, b = false}\n"; const expected: any = { a: { a: true, b: false } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/empty", () => { @@ -691,15 +631,15 @@ describe("toml-test/valid", () => { many_empty: [{}, {}, {}], nested_empty: { empty: {} }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/end-in-bool", () => { const input: string = 'black = { python=">3.6", version=">=18.9b0", allow_prereleases=true }\n'; const expected: any = { black: { allow_prereleases: true, python: ">3.6", version: ">=18.9b0" } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/inline-table", () => { @@ -712,8 +652,8 @@ describe("toml-test/valid", () => { "str-key": { a: 1 }, "table-array": [{ a: 1 }, { b: 2 }], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/key-dotted-01", () => { @@ -726,8 +666,8 @@ describe("toml-test/valid", () => { d: { a: { b: 1 } }, e: { a: { b: 1 } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/key-dotted-02", () => { @@ -735,8 +675,8 @@ describe("toml-test/valid", () => { const expected: any = { many: { dots: { here: { dot: { dot: { dot: { a: { b: { c: 1, d: 2 } } } } } } } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/key-dotted-03", () => { @@ -744,8 +684,8 @@ describe("toml-test/valid", () => { const expected: any = { tbl: { a: { b: { c: { d: { e: 1 } } } }, x: { a: { b: { c: { d: { e: 1 } } } } } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/key-dotted-04", () => { @@ -756,8 +696,8 @@ describe("toml-test/valid", () => { { T: { a: { b: 2 } }, t: { a: { b: 2 } } }, ], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/key-dotted-05", () => { @@ -769,8 +709,8 @@ describe("toml-test/valid", () => { "arr-3": [{ a: { b: 1 } }, { a: { b: 2 } }], "arr-4": ["str", { a: { b: 1 } }, { a: { b: 2 } }], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/key-dotted-06", () => { @@ -778,23 +718,23 @@ describe("toml-test/valid", () => { const expected: any = { top: { dot: { dot: [{ dot: { dot: { dot: 1 } } }, { dot: { dot: { dot: 2 } } }] } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/key-dotted-07", () => { const input: string = "arr = [\n\t{a.b = [{c.d = 1}]}\n]\n"; const expected: any = { arr: [{ a: { b: [{ c: { d: 1 } }] } }] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/multiline", () => { const input: string = 'tbl_multiline = { a = 1, b = """\nmultiline\n""", c = """and yet\nanother line""", d = 4 }\n'; const expected: any = { tbl_multiline: { a: 1, b: "multiline\n", c: "and yet\nanother line", d: 4 } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/nest", () => { @@ -809,8 +749,8 @@ describe("toml-test/valid", () => { tbl_tbl_empty: { tbl_0: {} }, tbl_tbl_val: { tbl_1: { one: 1 } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/newline-comment", () => { @@ -822,8 +762,8 @@ describe("toml-test/valid", () => { "trailing-comma-1": { c: 1 }, "trailing-comma-2": { c: 1 }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/newline", () => { @@ -837,8 +777,8 @@ describe("toml-test/valid", () => { "trailing-comma-1": { c: 1 }, "trailing-comma-2": { c: 1 }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/inline-table/spaces", () => { @@ -848,23 +788,23 @@ describe("toml-test/valid", () => { "clap-1": { version: "4", features: ["derive", "cargo"] }, "clap-2": { version: "4", features: ["derive", "cargo"], nest: { a: "x", b: [1.5, 9] } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/integer/float64-max", () => { const input: string = "# Maximum and minimum safe float64 natural numbers. Mainly here for\n# -int-as-float.\nmax_int = 9_007_199_254_740_991\nmin_int = -9_007_199_254_740_991\n"; const expected: any = { max_int: 9007199254740991, min_int: -9007199254740991 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/integer/integer", () => { const input: string = "answer = 42\nposanswer = +42\nneganswer = -42\nzero = 0\n"; const expected: any = { answer: 42, neganswer: -42, posanswer: 42, zero: 0 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/integer/literals", () => { @@ -881,15 +821,15 @@ describe("toml-test/valid", () => { oct2: 493, oct3: 501, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/integer/underscore", () => { const input: string = "kilo = 1_000\nx = 1_1_1_1\n"; const expected: any = { kilo: 1000, x: 1111 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/integer/zero", () => { @@ -909,8 +849,8 @@ describe("toml-test/valid", () => { h3: 0, o1: 0, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/alphanum", () => { @@ -928,8 +868,8 @@ describe("toml-test/valid", () => { "2018_10": { "001": 1 }, "a-a-a": { _: false }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/case-sensitive", () => { @@ -945,8 +885,8 @@ describe("toml-test/valid", () => { }, section: { NAME: "upper", Name: "capitalized", name: "lower" }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/dotted-01", () => { @@ -955,8 +895,8 @@ describe("toml-test/valid", () => { many: { dots: { dot: { dot: { dot: 42 } } } }, name: { first: "Arthur", last: "Dent" }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/dotted-02", () => { @@ -965,8 +905,8 @@ describe("toml-test/valid", () => { const expected: any = { count: { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11, l: 12 }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/dotted-03", () => { @@ -977,8 +917,8 @@ describe("toml-test/valid", () => { tbl: { a: { b: { c: 42.666 } } }, top: { key: 1 }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/dotted-04", () => { @@ -987,8 +927,8 @@ describe("toml-test/valid", () => { arr: [{ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 3, d: 4 } } }], top: { key: 1 }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/dotted-empty", () => { @@ -998,36 +938,36 @@ describe("toml-test/valid", () => { a: { "": { "": "empty.empty" } }, x: { "": "x.empty" }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/empty-01", () => { const input: string = '"" = "blank"\n'; const expected: any = { "": "blank" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/empty-02", () => { const input: string = "'' = \"blank\"\n"; const expected: any = { "": "blank" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/empty-03", () => { const input: string = "''=0\n"; const expected: any = { "": 0 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/equals-nospace", () => { const input: string = "answer=42\n"; const expected: any = { answer: 42 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/escapes", () => { @@ -1042,8 +982,8 @@ describe("toml-test/valid", () => { '"quoted"': { quote: true }, "a.b": { "À": {} }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/like-date", () => { @@ -1061,64 +1001,64 @@ describe("toml-test/valid", () => { "2002-01-02": { k: 10, "2024-01-03": { k: 11 } }, a: { "2001-02-08": 7, "2001-02-09": { "2001-02-10": 8 } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/numeric-01", () => { const input: string = "1 = true\n"; const expected: any = { "1": true }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/numeric-02", () => { const input: string = "1.2 = true\n"; const expected: any = { "1": { "2": true } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/numeric-03", () => { const input: string = "0123 = true\n"; const expected: any = { "0123": true }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/numeric-04", () => { const input: string = "01.23 = true\n"; const expected: any = { "01": { "23": true } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/numeric-05", () => { const input: string = "23.01 = true\n"; const expected: any = { "23": { "01": true } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/numeric-06", () => { const input: string = "-1 = true\n"; const expected: any = { "-1": true }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/numeric-07", () => { const input: string = "-01 = true\n"; const expected: any = { "-01": true }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/numeric-08", () => { const input: string = "1 = 'one'\n01 = 'zero one'\n"; const expected: any = { "1": "one", "01": "zero one" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/quoted-dots", () => { @@ -1130,8 +1070,8 @@ describe("toml-test/valid", () => { plain_table: { plain: 3, "with.dot": 4 }, table: { withdot: { "escaped.dot": 7, "key.with.dots": 6, plain: 5 } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/quoted-unicode", () => { @@ -1144,8 +1084,8 @@ describe("toml-test/valid", () => { "l ~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿": "literal key", "~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿": "basic key", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/space", () => { @@ -1157,22 +1097,22 @@ describe("toml-test/valid", () => { "a b": 1, " tbl ": { "\ttab\ttab\t": "tab" }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/special-chars", () => { const input: string = '"=~!@$^&*()_+-`1234567890[]|/?><.,;:\'=" = 1\n'; const expected: any = { "=~!@$^&*()_+-`1234567890[]|/?><.,;:'=": 1 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/special-word", () => { const input: string = 'false = false\ntrue = 1\ninf = 100000000\nnan = "ceci n\'est pas un nombre"\n\n'; const expected: any = { false: false, inf: 100000000, nan: "ceci n'est pas un nombre", true: 1 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/start", () => { @@ -1190,15 +1130,15 @@ describe("toml-test/valid", () => { _key: { _key: 2 }, inline: { "---": { "111": 12, "---": 10, ___: 11 } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/key/zero", () => { const input: string = "0=0\n"; const expected: any = { "0": 0 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/multibyte", () => { @@ -1219,37 +1159,37 @@ describe("toml-test/valid", () => { }, }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/newline-crlf", () => { const input: string = 'os = "DOS"\r\nnewline = "crlf"\r\n'; const expected: any = { newline: "crlf", os: "DOS" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/newline-lf", () => { const input: string = 'os = "unix"\nnewline = "lf"\n'; const expected: any = { newline: "lf", os: "unix" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-0", () => { const input: string = '# This is a full-line comment\nkey = "value" # This is a comment at the end of a line\nanother = "# This is not a comment"\n'; const expected: any = { another: "# This is not a comment", key: "value" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-1", () => { const input: string = 'key = "value"\n'; const expected: any = { key: "value" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-10", () => { @@ -1259,29 +1199,29 @@ describe("toml-test/valid", () => { apple: { color: "red", skin: "thin", type: "fruit" }, orange: { color: "orange", skin: "thick", type: "fruit" }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-11", () => { const input: string = '3.14159 = "pi"\n'; const expected: any = { "3": { "14159": "pi" } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-12", () => { const input: string = 'str = "I\'m a string. \\"You can quote me\\". Name\\tJos\\xE9\\nLocation\\tSF."\n'; const expected: any = { str: 'I\'m a string. "You can quote me". Name\tJosé\nLocation\tSF.' }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-13", () => { const input: string = 'str1 = """\nRoses are red\nViolets are blue"""\n'; const expected: any = { str1: "Roses are red\nViolets are blue" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-14", () => { @@ -1291,8 +1231,8 @@ describe("toml-test/valid", () => { str2: "Roses are red\nViolets are blue", str3: "Roses are red\r\nViolets are blue", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-15", () => { @@ -1303,8 +1243,8 @@ describe("toml-test/valid", () => { str2: "The quick brown fox jumps over the lazy dog.", str3: "The quick brown fox jumps over the lazy dog.", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-16", () => { @@ -1316,8 +1256,8 @@ describe("toml-test/valid", () => { str6: 'Here are fifteen quotation marks: """"""""""""""".', str7: '"This," she said, "is just a pointless statement."', }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-17", () => { @@ -1329,8 +1269,8 @@ describe("toml-test/valid", () => { winpath: "C:\\Users\\nodejs\\templates", winpath2: "\\\\ServerX\\admin$\\system32\\", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-18", () => { @@ -1340,8 +1280,8 @@ describe("toml-test/valid", () => { lines: "The first newline is\ntrimmed in literal strings.\n All other whitespace\n is preserved.\n", regex2: "I [dw]on't need \\d{2} apples", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-19", () => { @@ -1352,23 +1292,23 @@ describe("toml-test/valid", () => { quot15: 'Here are fifteen quotation marks: """""""""""""""', str: "'That,' she said, 'is still pointless.'", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-20", () => { const input: string = "int1 = +99\nint2 = 42\nint3 = 0\nint4 = -17\n"; const expected: any = { int1: 99, int2: 42, int3: 0, int4: -17 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-21", () => { const input: string = "int5 = 1_000\nint6 = 5_349_221\nint7 = 53_49_221 # Indian number system grouping\nint8 = 1_2_3_4_5 # VALID but discouraged\n"; const expected: any = { int5: 1000, int6: 5349221, int7: 5349221, int8: 12345 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-22", () => { @@ -1382,8 +1322,8 @@ describe("toml-test/valid", () => { oct1: 342391, oct2: 493, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-23", () => { @@ -1398,30 +1338,30 @@ describe("toml-test/valid", () => { flt6: -0.02, flt7: 6.626e-34, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-24", () => { const input: string = "flt8 = 224_617.445_991_228\n"; const expected: any = { flt8: 224617.445991228 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-25", () => { const input: string = "# infinity\nsf1 = inf # positive infinity\nsf2 = +inf # positive infinity\nsf3 = -inf # negative infinity\n\n# not a number\nsf4 = nan # actual sNaN/qNaN encoding is implementation-specific\nsf5 = +nan # same as `nan`\nsf6 = -nan # valid, actual encoding is implementation-specific\n"; const expected: any = { sf1: Infinity, sf2: Infinity, sf3: -Infinity, sf4: NaN, sf5: NaN, sf6: NaN }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-26", () => { const input: string = "bool1 = true\nbool2 = false\n"; const expected: any = { bool1: true, bool2: false }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-27", () => { @@ -1433,15 +1373,15 @@ describe("toml-test/valid", () => { odt3: dt("datetime", "1979-05-27T00:32:00.5-07:00"), odt4: dt("datetime", "1979-05-27T00:32:00.999-07:00"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-28", () => { const input: string = "odt4 = 1979-05-27 07:32:00Z\n"; const expected: any = { odt4: dt("datetime", "1979-05-27T07:32:00Z") }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-29", () => { @@ -1450,15 +1390,15 @@ describe("toml-test/valid", () => { odt5: dt("datetime", "1979-05-27T07:32:00Z"), odt6: dt("datetime", "1979-05-27T07:32:00-07:00"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-3", () => { const input: string = 'key = "value"\nbare_key = "value"\nbare-key = "value"\n1234 = "value"\n'; const expected: any = { "1234": "value", "bare-key": "value", bare_key: "value", key: "value" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-30", () => { @@ -1468,22 +1408,22 @@ describe("toml-test/valid", () => { ldt2: dt("datetime-local", "1979-05-27T07:32:00.5"), ldt3: dt("datetime-local", "1979-05-27T00:32:00.999"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-31", () => { const input: string = "ldt3 = 1979-05-27T07:32\n"; const expected: any = { ldt3: dt("datetime-local", "1979-05-27T07:32:00") }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-32", () => { const input: string = "ld1 = 1979-05-27\n"; const expected: any = { ld1: dt("date-local", "1979-05-27") }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-33", () => { @@ -1493,15 +1433,15 @@ describe("toml-test/valid", () => { lt2: dt("time-local", "00:32:00.5"), lt3: dt("time-local", "00:32:00.999"), }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-34", () => { const input: string = "lt3 = 07:32\n"; const expected: any = { lt3: dt("time-local", "07:32:00") }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-35", () => { @@ -1529,22 +1469,22 @@ describe("toml-test/valid", () => { numbers: [0.1, 0.2, 0.5, 1, 2, 5], string_array: ["all", "strings", "are the same", "type"], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-36", () => { const input: string = "integers2 = [\n 1, 2, 3\n]\n\nintegers3 = [\n 1,\n 2, # this is ok\n]\n"; const expected: any = { integers2: [1, 2, 3], integers3: [1, 2] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-37", () => { const input: string = "[table]\n"; const expected: any = { table: {} }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-38", () => { @@ -1554,15 +1494,15 @@ describe("toml-test/valid", () => { "table-1": { key1: "some string", key2: 123 }, "table-2": { key1: "another string", key2: 456 }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-39", () => { const input: string = '[dog."tater.man"]\ntype.name = "pug"\n'; const expected: any = { dog: { "tater.man": { type: { name: "pug" } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-4", () => { @@ -1575,8 +1515,8 @@ describe("toml-test/valid", () => { 'quoted "value"': "value", "ʎǝʞ": "value", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-40", () => { @@ -1588,30 +1528,30 @@ describe("toml-test/valid", () => { g: { h: { i: {} } }, j: { "ʞ": { l: {} } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-41", () => { const input: string = "# [x] you\n# [x.y] don't\n# [x.y.z] need these\n[x.y.z.w] # for this to work\n\n[x] # defining a super-table afterward is ok\n"; const expected: any = { x: { y: { z: { w: {} } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-42", () => { const input: string = "# VALID BUT DISCOURAGED\n[fruit.apple]\n[animal]\n[fruit.orange]\n"; const expected: any = { animal: {}, fruit: { apple: {}, orange: {} } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-43", () => { const input: string = "# RECOMMENDED\n[fruit.apple]\n[fruit.orange]\n[animal]\n"; const expected: any = { animal: {}, fruit: { apple: {}, orange: {} } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-44", () => { @@ -1622,16 +1562,16 @@ describe("toml-test/valid", () => { name: "Fido", owner: { member_since: dt("date-local", "1999-08-04"), name: "Regina Dogman" }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-45", () => { const input: string = 'fruit.apple.color = "red"\n# Defines a table named fruit\n# Defines a table named fruit.apple\n\nfruit.apple.taste.sweet = true\n# Defines a table named fruit.apple.taste\n# fruit and fruit.apple were already created\n'; const expected: any = { fruit: { apple: { color: "red", taste: { sweet: true } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-46", () => { @@ -1640,8 +1580,8 @@ describe("toml-test/valid", () => { const expected: any = { fruit: { apple: { color: "red", taste: { sweet: true }, texture: { smooth: true } } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-47", () => { @@ -1656,8 +1596,8 @@ describe("toml-test/valid", () => { name: { first: "Tom", last: "Preston-Werner" }, point: { x: 1, y: 2 }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-48", () => { @@ -1672,22 +1612,22 @@ describe("toml-test/valid", () => { name: { first: "Tom", last: "Preston-Werner" }, point: { x: 1, y: 2 }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-49", () => { const input: string = '[product]\ntype = { name = "Nail" }\n# type.edible = false # INVALID\n'; const expected: any = { product: { type: { name: "Nail" } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-50", () => { const input: string = '[product]\ntype.name = "Nail"\n# type = { edible = false } # INVALID\n'; const expected: any = { product: { type: { name: "Nail" } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-51", () => { @@ -1696,8 +1636,8 @@ describe("toml-test/valid", () => { const expected: any = { product: [{ name: "Hammer", sku: 738594937 }, {}, { color: "gray", name: "Nail", sku: 284758393 }], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-52", () => { @@ -1713,8 +1653,8 @@ describe("toml-test/valid", () => { { name: "banana", varieties: [{ name: "plantain" }] }, ], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-53", () => { @@ -1727,8 +1667,8 @@ describe("toml-test/valid", () => { { x: 2, y: 4, z: 8 }, ], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-6", () => { @@ -1739,24 +1679,24 @@ describe("toml-test/valid", () => { physical: { color: "orange", shape: "round" }, site: { "google.com": true }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-7", () => { const input: string = 'fruit.name = "banana" # this is best practice\nfruit. color = "yellow" # same as fruit.color\nfruit . flavor = "banana" # same as fruit.flavor\n'; const expected: any = { fruit: { color: "yellow", flavor: "banana", name: "banana" } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-8", () => { const input: string = '# This makes the key "fruit" into a table.\nfruit.apple.smooth = true\n\n# So then you can add to the table "fruit" like so:\nfruit.orange = 2\n'; const expected: any = { fruit: { orange: 2, apple: { smooth: true } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-1.1.0/common-9", () => { @@ -1766,8 +1706,8 @@ describe("toml-test/valid", () => { apple: { color: "red", skin: "thin", type: "fruit" }, orange: { color: "orange", skin: "thick", type: "fruit" }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-example-1-compact", () => { @@ -1794,8 +1734,8 @@ describe("toml-test/valid", () => { beta: { dc: "eqdc10", ip: "10.0.0.2" }, }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/spec-example-1", () => { @@ -1822,50 +1762,50 @@ describe("toml-test/valid", () => { beta: { dc: "eqdc10", ip: "10.0.0.2" }, }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/basic-escape-01", () => { const input: string = '# Escape "\ntest = "\\"one\\""\n'; const expected: any = { test: '"one"' }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/basic-escape-02", () => { const input: string = '# Escape \\ and then "\ntest = "\\\\\\"one"\n'; const expected: any = { test: '\\"one' }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/basic-escape-03", () => { const input: string = '# Escape \\ four times and then "\ntest = "\\\\\\\\\\\\\\\\\\"one"\n'; const expected: any = { test: '\\\\\\\\"one' }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/empty", () => { const input: string = 'answer = ""\n'; const expected: any = { answer: "" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/ends-in-whitespace-escape", () => { const input: string = 'beee = """\nheeee\ngeeee\\ \n\n\n """\n'; const expected: any = { beee: "heeee\ngeeee" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/escape-esc", () => { const input: string = 'esc = "\\e There is no escape! \\e"\n'; const expected: any = { esc: "\u001b There is no escape! \u001b" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/escape-tricky", () => { @@ -1880,15 +1820,15 @@ describe("toml-test/valid", () => { multiline_not_unicode: "\\u0041", multiline_unicode: " ", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/escaped-escape", () => { const input: string = 'answer = "\\\\x64"\n'; const expected: any = { answer: "\\x64" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/escapes", () => { @@ -1909,8 +1849,8 @@ describe("toml-test/valid", () => { tab: "|\t.", unitseparator: "|\u001f.", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/hex-escape", () => { @@ -1926,8 +1866,8 @@ describe("toml-test/valid", () => { nul: "\u0000", whitespace: " \t \u001b \r\n", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/multibyte-escape", () => { @@ -1939,8 +1879,8 @@ describe("toml-test/valid", () => { "basic-2": "ɑ € 𐫱 ɑ€𐫱", "ml-basic-2": "ɑ € 𐫱 ɑ€𐫱", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/multibyte", () => { @@ -1952,24 +1892,24 @@ describe("toml-test/valid", () => { "ml-raw": "ɑ € 𐫱 ɑ€𐫱", raw: "ɑ € 𐫱 ɑ€𐫱", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/multiline-empty", () => { const input: string = 'empty-1 = """"""\n\n# A newline immediately following the opening delimiter will be trimmed.\nempty-2 = """\n"""\n\n# \\ at the end of line trims newlines as well; note that last \\ is followed by\n# two spaces, which are ignored.\nempty-3 = """\\\n """\nempty-4 = """\\\n \\\n \\ \n """\n\n'; const expected: any = { "empty-1": "", "empty-2": "", "empty-3": "", "empty-4": "" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/multiline-escaped-crlf", () => { const input: string = '# The following line should be an unescaped backslash followed by a Windows\r\n# newline sequence ("\\r\\n")\r\n0="""\\\r\n"""\r\n'; const expected: any = { "0": "" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/multiline-quotes", () => { @@ -1990,8 +1930,8 @@ describe("toml-test/valid", () => { two: '""two quotes""', two_space: ' ""two quotes"" ', }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/multiline", () => { @@ -2008,8 +1948,8 @@ describe("toml-test/valid", () => { "no-space": "ab", "whitespace-after-bs": "The quick brown fox jumps over the lazy dog.", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/nl", () => { @@ -2022,8 +1962,8 @@ describe("toml-test/valid", () => { nl_end: "value\n", nl_mid: "val\nue", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/quoted-unicode", () => { @@ -2036,15 +1976,15 @@ describe("toml-test/valid", () => { not_escaped_string: "\\u0000 \\u0008 \\u000c \\U00000041 \\u007f \\u0080 \\u00ff \\ud7ff \\ue000 \\uffff \\U00010000 \\U0010ffff", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/raw-empty", () => { const input: string = "empty = ''\n"; const expected: any = { empty: "" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/raw-multiline", () => { @@ -2057,8 +1997,8 @@ describe("toml-test/valid", () => { oneline: "This string has a ' quote character.", "this-str-has-apostrophes": "' there's one already\n'' two more\n''", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/raw", () => { @@ -2074,23 +2014,23 @@ describe("toml-test/valid", () => { tab: "This string has a \\t tab character.", unescaped_tab: "This string has an \t unescaped tab character.", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/simple", () => { const input: string = 'answer = "You are not drinking enough whisky."\n'; const expected: any = { answer: "You are not drinking enough whisky." }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/start-mb", () => { const input: string = '# Start first line with a multibyte character.\n#\n# https://github.com/marzer/tomlplusplus/issues/190\ns1 = "§"\ns2 = \'§\'\ns3 = """\\\n§"""\ns4 = """\n§"""\ns5 = """§"""\ns6 = \'\'\'\n§\'\'\'\ns7 = \'\'\'§\'\'\'\n'; const expected: any = { s1: "§", s2: "§", s3: "§", s4: "§", s5: "§", s6: "§", s7: "§" }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/unicode-escape", () => { @@ -2112,8 +2052,8 @@ describe("toml-test/valid", () => { "null-1": "\u0000", "null-2": "\u0000", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/string/with-pound", () => { @@ -2123,36 +2063,36 @@ describe("toml-test/valid", () => { pound: "We see no # comments here.", poundcomment: "But there are # some comments here.", }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/array-empty-name", () => { const input: string = "# Silly thing to do, but valid.\n\n[['']]\na = 1\n[['']]\na = 2\n"; const expected: any = { "": [{ a: 1 }, { a: 2 }] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/array-empty", () => { const input: string = "[[a]]\n"; const expected: any = { a: [{}] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/array-implicit-and-explicit-after", () => { const input: string = "[[a.b]]\nx = 1\n\n[a]\ny = 2\n"; const expected: any = { a: { b: [{ x: 1 }], y: 2 } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/array-implicit", () => { const input: string = '[[albums.songs]]\nname = "Glory Days"\n'; const expected: any = { albums: { songs: [{ name: "Glory Days" }] } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/array-many", () => { @@ -2165,8 +2105,8 @@ describe("toml-test/valid", () => { { first_name: "Bob", last_name: "Seger" }, ], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/array-nest", () => { @@ -2184,58 +2124,58 @@ describe("toml-test/valid", () => { }, ], }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/array-one", () => { const input: string = '[[people]]\nfirst_name = "Bruce"\nlast_name = "Springsteen"\n'; const expected: any = { people: [{ first_name: "Bruce", last_name: "Springsteen" }] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/array-table-array", () => { const input: string = '[[a]]\n [[a.b]]\n [a.b.c]\n d = "val0"\n [[a.b]]\n [a.b.c]\n d = "val1"\n'; const expected: any = { a: [{ b: [{ c: { d: "val0" } }, { c: { d: "val1" } }] }] }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/array-within-dotted", () => { const input: string = '[fruit]\napple.color = "red"\n\n[[fruit.apple.seeds]]\nsize = 2\n'; const expected: any = { fruit: { apple: { color: "red", seeds: [{ size: 2 }] } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/empty-name", () => { const input: string = "['']\nx = 1\n\n[\"\".a]\nx = 2\n\n[a.'']\nx = 3\n"; const expected: any = { "": { x: 1, a: { x: 2 } }, a: { "": { x: 3 } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/empty", () => { const input: string = "[a]\n"; const expected: any = { a: {} }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/keyword-with-values", () => { const input: string = "[true]\nk = 1\n\n[false]\nk = 2\n\n[inf]\nk = 3\n\n[nan]\nk = 4\n"; const expected: any = { false: { k: 2 }, inf: { k: 3 }, nan: { k: 4 }, true: { k: 1 } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/keyword", () => { const input: string = "[true]\n\n[false]\n\n[inf]\n\n[nan]\n\n\n"; const expected: any = { false: {}, inf: {}, nan: {}, true: {} }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/names-with-values", () => { @@ -2253,8 +2193,8 @@ describe("toml-test/valid", () => { j: { "ʞ": { l: { key: 7 } } }, x: { "1": { "2": { key: 8 } } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/names", () => { @@ -2267,97 +2207,97 @@ describe("toml-test/valid", () => { j: { "ʞ": { l: {} } }, x: { "1": { "2": {} } }, }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/no-eol-01", () => { const input: string = "# No newline at end of file.\n[table]"; const expected: any = { table: {} }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/no-eol-02", () => { const input: string = "# No newline at end of file.\n[table]\na=1"; const expected: any = { table: { a: 1 } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/sub-empty", () => { const input: string = "[a]\n[a.b]\n"; const expected: any = { a: { b: {} } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/sub", () => { const input: string = '[a]\nkey = 1\n\n# a.extend is a key inside the "a" table.\n[a.extend]\nkey = 2\n\n[a.extend.more]\nkey = 3\n'; const expected: any = { a: { key: 1, extend: { key: 2, more: { key: 3 } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/whitespace", () => { const input: string = '["valid key"]\n'; const expected: any = { "valid key": {} }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/with-literal-string", () => { const input: string = "['a']\n[a.'\"b\"']\n[a.'\"b\"'.c]\nanswer = 42 \n"; const expected: any = { a: { '"b"': { c: { answer: 42 } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/with-pound", () => { const input: string = '["key#group"]\nanswer = 42\n'; const expected: any = { "key#group": { answer: 42 } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/with-single-quotes", () => { const input: string = "['a']\n[a.'b']\n[a.'b'.c]\nanswer = 42 \n"; const expected: any = { a: { b: { c: { answer: 42 } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/without-super-with-values", () => { const input: string = "# [x] you\n# [x.y] don't\n# [x.y.z] need these\n[x.y.z.w] # for this to work\na = 1\nb = 2\n[x] # defining a super-table afterwards is ok\nc = 3\nd = 4\n"; const expected: any = { x: { c: 3, d: 4, y: { z: { w: { a: 1, b: 2 } } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/table/without-super", () => { const input: string = "# [x] you\n# [x.y] don't\n# [x.y.z] need these\n[x.y.z.w] # for this to work\n[x] # defining a super-table afterwards is ok\n"; const expected: any = { x: { y: { z: { w: {} } } } }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/utf8-bom-01", () => { const input: string = "\ufeff# This file starts with an UTF-8 BOM (\\xEF\\xBB\\xBF), which isn't recommended to use but valid.\na=1\n"; const expected: any = { a: 1 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); test("valid/utf8-bom-02", () => { const input: string = "\ufeffa=1# This file starts with an UTF-8 BOM (\\xEF\\xBB\\xBF), which isn't recommended to use but valid.\n"; const expected: any = { a: 1 }; - expectTomlEqual(TOML.parse(input), expected); - expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + expect(TOML.parse(input)).toEqual(expected); + expect(TOML.parse(TOML.stringify(TOML.parse(input)))).toEqual(expected); }); }); From 627f3e3287cfc87b720efa65a6be90b8d43ac96e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:25:39 +0000 Subject: [PATCH 13/19] Convert the remaining lazy-export unwraps; gate the datetime walk to TOML The six new_lazy_export_ast call sites b0f49a0643 missed get the same Ok(None)-to-ParserError conversion, and parse_data_loader only runs expr_contains_toml_datetime for the TOML loader since no other data parser sets the tag. --- src/bundler/ParseTask.rs | 10 +++++----- src/bundler/bundle_v2.rs | 2 +- src/bundler/transpiler.rs | 5 ++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index 75fb89fcbde5..49d40f0442c8 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -1028,7 +1028,7 @@ pub mod parse_worker { source, b"", )? - .unwrap(), + .ok_or(AnyError::ParserError)?, )); } Loader::Napi => { @@ -1097,7 +1097,7 @@ pub mod parse_worker { source, b"", )? - .unwrap(), + .ok_or(AnyError::ParserError)?, )); } Loader::Html => { @@ -1123,7 +1123,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. @@ -1249,7 +1249,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); @@ -1318,7 +1318,7 @@ pub mod parse_worker { source, b"", )? - .unwrap(), + .ok_or(AnyError::ParserError)?, ); ast.add_url_for_css( bump, diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 741d61bd1eef..360b667d29d6 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -6743,7 +6743,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 { diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 8953ad56ec67..3a3d77ca7a4b 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1910,7 +1910,10 @@ fn parse_data_loader<'a>( }]); } - let contains_datetime = expr_contains_toml_datetime(&expr); + // Only the TOML parser tags date/time strings; skip the walk for the + // other data loaders. + let contains_datetime = + loader == options::Loader::Toml && expr_contains_toml_datetime(&expr); if let Some(obj) = expr.data.e_object_mut() { let properties: &mut [bun_ast::G::Property] = obj.properties.slice_mut(); if !properties.is_empty() { From ca941ea17f838b4e43f05c2b5e296a1ffa5f1de0 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 13 Aug 2026 00:01:33 +0000 Subject: [PATCH 14/19] Gate the datetime lowering walk to TOML lazy exports; downcast pre-classified Temporal cells unchecked --- src/bundler/transpiler.rs | 1 + src/js_parser/parse/parse_entry.rs | 13 +++++++++++-- src/jsc/bindings/bindings.cpp | 10 +++++----- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 11cc13074026..ca998b2dc2ad 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -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; diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 0cdd70af345b..1b3c5044b709 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -104,6 +104,10 @@ pub struct Options<'a> { /// - Wraps last expression in { value: expr } for result capture /// - Wraps code with await in async IIFE pub repl_mode: bool, + + /// The lazy-export expression came from the TOML parser and may contain + /// `toml_datetime`-tagged strings to lower into `Temporal.*.from` calls. + pub lower_toml_datetimes: bool, } impl<'a> Default for Options<'a> { @@ -135,6 +139,7 @@ impl<'a> Default for Options<'a> { lower_import_meta_main_for_node_js: false, framework: None, repl_mode: false, + lower_toml_datetimes: false, } } } @@ -218,6 +223,7 @@ impl<'a> Options<'a> { lower_import_meta_main_for_node_js: self.lower_import_meta_main_for_node_js, framework: self.framework, repl_mode: self.repl_mode, + lower_toml_datetimes: self.lower_toml_datetimes, } } @@ -289,6 +295,7 @@ impl<'a> Options<'a> { lower_import_meta_main_for_node_js: false, framework: None, repl_mode: false, + lower_toml_datetimes: loader == options::Loader::Toml, }; opts.jsx.parse = loader.is_jsx(); opts @@ -562,8 +569,10 @@ impl<'a> Parser<'a> { // 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. - let mut temporal_ref: Option = None; - lower_date_time_literals(p, &mut final_expr, &mut temporal_ref)?; + if p.options.lower_toml_datetimes { + let mut temporal_ref: Option = 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() { diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index da0db6c154a2..7ff91c7b72b9 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -6238,21 +6238,21 @@ extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC: WTF::String string; switch (temporalType) { case 1: - string = JSC::TemporalCore::instantToString(dynamicDowncast(cell)->exactTime(), std::nullopt, autoPrecision); + string = JSC::TemporalCore::instantToString(uncheckedDowncast(cell)->exactTime(), std::nullopt, autoPrecision); break; case 2: { - auto* dateTime = dynamicDowncast(cell); + auto* dateTime = uncheckedDowncast(cell); string = JSC::ISO8601::temporalDateTimeToString(dateTime->plainDate(), dateTime->plainTime(), { JSC::Precision::Auto, 0 }); break; } case 3: - string = JSC::ISO8601::temporalDateToString(dynamicDowncast(cell)->plainDate()); + string = JSC::ISO8601::temporalDateToString(uncheckedDowncast(cell)->plainDate()); break; case 4: - string = JSC::ISO8601::temporalTimeToString(dynamicDowncast(cell)->plainTime(), { JSC::Precision::Auto, 0 }); + string = JSC::ISO8601::temporalTimeToString(uncheckedDowncast(cell)->plainTime(), { JSC::Precision::Auto, 0 }); break; case 5: { - auto* zoned = dynamicDowncast(cell); + auto* zoned = uncheckedDowncast(cell); std::optional offsetNs = zoned->getOffsetNanoseconds(globalObject); RETURN_IF_EXCEPTION(scope, -1); ASSERT(offsetNs); From 25187c81311d535b8ab5c3805fe0a2bbf4d68b48 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 13 Aug 2026 00:14:11 +0000 Subject: [PATCH 15/19] TOML.stringify: spell year-edge instants with an in-range offset instead 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. --- src/jsc/bindings/bindings.cpp | 73 ++++++++++++++++++++++++++++++----- src/runtime/api/TOMLObject.rs | 5 ++- test/js/bun/toml/toml.test.ts | 31 ++++++++++++++- 3 files changed, 97 insertions(+), 12 deletions(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 7ff91c7b72b9..92fa12edb250 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -6223,10 +6223,54 @@ extern "C" [[ZIG_EXPORT(nothrow)]] uint8_t Bun__JSValue__temporalObjectType(JSC: return 0; } +static Int128 ceilToMultiple(Int128 ns, Int128 unit) +{ + Int128 rem = ns % unit; + return rem == 0 ? ns : ns - rem + (ns > 0 ? unit : 0); +} + +static Int128 floorToMultiple(Int128 ns, Int128 unit) +{ + Int128 rem = ns % unit; + return rem == 0 ? ns : ns - rem - (ns < 0 ? unit : 0); +} + +// Every `local±HH:MM` spelling of an instant denotes the same nanosecond +// (`local - offset`); TOML can only carry the spellings whose local year has +// four digits. Returns `preferredNs` if that spelling fits; otherwise the +// offset closest to it whose spelling fits, whole-hour if one does and +// whole-minute (TOML's granularity, within ±23:59) if not; or nullopt if the +// instant is a day or more outside 0000..9999 and so has no TOML spelling. +static std::optional tomlOffsetForInstant(JSC::ISO8601::ExactTime exactTime, int64_t preferredNs) +{ + using JSC::ISO8601::ExactTime; + constexpr Int128 minLocal = Int128 { -62167219200 } * ExactTime::nsPerSecond; // 0000-01-01T00:00:00 + constexpr Int128 maxLocal = Int128 { 253402300800 } * ExactTime::nsPerSecond; // +010000-01-01T00:00:00 + constexpr Int128 maxOffset = ExactTime::nsPerHour * 23 + ExactTime::nsPerMinute * 59; + + Int128 epoch = exactTime.epochNanoseconds(); + // Whole-minute offsets o with minLocal <= epoch + o < maxLocal. + Int128 lo = std::max(ceilToMultiple(minLocal - epoch, ExactTime::nsPerMinute), -maxOffset); + Int128 hi = std::min(floorToMultiple(maxLocal - Int128 { 1 } - epoch, ExactTime::nsPerMinute), maxOffset); + if (lo > hi) + return std::nullopt; + Int128 preferred { preferredNs }; + if (preferred < lo) { + Int128 hour = ceilToMultiple(lo, ExactTime::nsPerHour); + return static_cast(hour <= hi ? hour : lo); + } + if (preferred > hi) { + Int128 hour = floorToMultiple(hi, ExactTime::nsPerHour); + return static_cast(hour >= lo ? hour : hi); + } + return preferredNs; +} + // Formats a Temporal object (`temporalType` 1-5 from the classifier above) -// as a TOML date/time literal into `buf`; returns the length written, or -1 -// if it cannot fit. The ISO fields are formatted directly, dropping the -// `[u-ca=...]` and `[Time/Zone]` annotations TOML cannot carry. +// as a TOML date/time literal into `buf`; returns the length written, -1 if +// it cannot fit, or -2 if the value's year is outside TOML's 0000..9999. The +// ISO fields are formatted directly, dropping the `[u-ca=...]` and +// `[Time/Zone]` annotations TOML cannot carry. extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, uint8_t temporalType, uint8_t* buf, size_t bufLen) { auto& vm = JSC::getVM(globalObject); @@ -6237,9 +6281,16 @@ extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC: WTF::String string; switch (temporalType) { - case 1: - string = JSC::TemporalCore::instantToString(uncheckedDowncast(cell)->exactTime(), std::nullopt, autoPrecision); + case 1: { + auto exactTime = uncheckedDowncast(cell)->exactTime(); + std::optional offsetNs = tomlOffsetForInstant(exactTime, 0); + if (!offsetNs) + return -2; + if (!*offsetNs) + offsetNs = std::nullopt; // `Z` + string = JSC::TemporalCore::instantToString(exactTime, offsetNs, autoPrecision); break; + } case 2: { auto* dateTime = uncheckedDowncast(cell); string = JSC::ISO8601::temporalDateTimeToString(dateTime->plainDate(), dateTime->plainTime(), { JSC::Precision::Auto, 0 }); @@ -6253,12 +6304,16 @@ extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC: break; case 5: { auto* zoned = uncheckedDowncast(cell); - std::optional offsetNs = zoned->getOffsetNanoseconds(globalObject); + std::optional zoneOffsetNs = zoned->getOffsetNanoseconds(globalObject); RETURN_IF_EXCEPTION(scope, -1); - ASSERT(offsetNs); + ASSERT(zoneOffsetNs); // TOML offsets are `HH:MM` only; a historic sub-minute offset falls - // back to the equivalent UTC instant. - if (offsetNs && *offsetNs % 60000000000ll != 0) + // back to the `Z` spelling of the same instant. + bool wholeMinutes = *zoneOffsetNs % 60000000000ll == 0; + std::optional offsetNs = tomlOffsetForInstant(zoned->exactTime(), wholeMinutes ? *zoneOffsetNs : 0); + if (!offsetNs) + return -2; + if (!wholeMinutes && !*offsetNs) offsetNs = std::nullopt; string = JSC::TemporalCore::instantToString(zoned->exactTime(), offsetNs, autoPrecision); break; diff --git a/src/runtime/api/TOMLObject.rs b/src/runtime/api/TOMLObject.rs index 579e5520f70f..f3c9cfeb5507 100644 --- a/src/runtime/api/TOMLObject.rs +++ b/src/runtime/api/TOMLObject.rs @@ -537,8 +537,9 @@ impl Stringifier { buf.len(), ) }?; - // The expanded-year form (leading `+`/`-`) has a 6-digit year, which - // TOML's 4-digit `date-fullyear` cannot carry. + // -2: an instant with no four-digit-year spelling. A leading `+`/`-` + // is the expanded-year form of a `PlainDate`/`PlainDateTime`, which + // TOML's 4-digit `date-fullyear` cannot carry either. if len < 1 || !buf[0].is_ascii_digit() { return Err(global .throw(format_args!( diff --git a/test/js/bun/toml/toml.test.ts b/test/js/bun/toml/toml.test.ts index 83c40aa955c2..80919d3c2bfa 100644 --- a/test/js/bun/toml/toml.test.ts +++ b/test/js/bun/toml/toml.test.ts @@ -820,7 +820,11 @@ describe("TOML.stringify", () => { expect(stringifyError({ d: Temporal.PlainDate.from({ year: -1, month: 12, day: 31 }) }).message).toBe( "TOML.stringify cannot serialize a Temporal.PlainDate outside years 0000-9999", ); - expect(stringifyError({ i: Temporal.Instant.from("+010000-01-01T00:00:00Z") }).message).toBe( + // An instant a day or more outside the range has no `±HH:MM` spelling with a 4-digit year. + expect(stringifyError({ i: Temporal.Instant.from("+010000-01-02T00:00:00Z") }).message).toBe( + "TOML.stringify cannot serialize a Temporal.Instant outside years 0000-9999", + ); + expect(stringifyError({ i: Temporal.Instant.from("-000001-12-31T00:00:00Z") }).message).toBe( "TOML.stringify cannot serialize a Temporal.Instant outside years 0000-9999", ); // The boundary years themselves are fine. @@ -828,6 +832,31 @@ describe("TOML.stringify", () => { expect(TOML.stringify({ d: Temporal.PlainDate.from("9999-12-31") })).toBe("d = 9999-12-31\n"); }); + test("an instant whose UTC year is outside 0000-9999 is spelled with an offset that keeps the year in range", () => { + // These are what `TOML.parse` produces for valid offset date-times at the + // year edges; the same instant must stringify, and to the same nanosecond. + const cases: [string, string][] = [ + ["0000-01-01T00:00:00+01:00", "0000-01-01T00:00:00+01:00"], + ["0000-01-01T00:20:00.5+01:00", "0000-01-01T00:20:00.5+01:00"], + ["0000-01-01T00:00:00+23:59", "0000-01-01T00:00:00+23:59"], + ["0000-01-01T00:00:30+23:59", "0000-01-01T00:00:30+23:59"], + ["9999-12-31T23:30:00-01:00", "9999-12-31T23:30:00-01:00"], + ["9999-12-31T23:59:59.999999999-23:59", "9999-12-31T23:59:59.999999999-23:59"], + // In range as UTC: `Z` is preferred over the written offset. + ["0000-01-01T01:00:00+01:00", "0000-01-01T00:00:00Z"], + ["9999-12-31T23:59:59.999999999+00:00", "9999-12-31T23:59:59.999999999Z"], + ]; + for (const [source, printed] of cases) { + const i = TOML.parse(`i = ${source}`).i as Temporal.Instant; + expect(TOML.stringify({ i })).toBe(`i = ${printed}\n`); + expect((TOML.parse(`i = ${printed}`).i as Temporal.Instant).epochNanoseconds).toBe(i.epochNanoseconds); + } + // A ZonedDateTime keeps its own offset when that spelling fits, else the same rule applies. + const zdt = Temporal.Instant.from("+010000-01-01T00:00:00Z").toZonedDateTimeISO("+02:00"); + expect(TOML.stringify({ zdt })).toBe("zdt = 9999-12-31T23:00:00-01:00\n"); + expect((TOML.parse(TOML.stringify({ zdt })).zdt as Temporal.Instant).epochNanoseconds).toBe(zdt.epochNanoseconds); + }); + test("null values throw with the offending key", () => { expect(stringifyError({ a: { broken: null } }).message).toBe( "TOML cannot represent null (key 'broken'); remove the key or use a sentinel value", From 1dd8b4f482e773007ae157a4130d6b48b38e6803 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 13 Aug 2026 00:18:15 +0000 Subject: [PATCH 16/19] Address review: single out-of-range sentinel from toTOMLDateTime, tighter comments, docs/types wording for truncation, annotations, year bounds --- docs/runtime/toml.mdx | 19 +++++++++-------- packages/bun-types/bun.d.ts | 15 +++++++------- src/js_parser/parse/parse_entry.rs | 3 +-- src/jsc/bindings/bindings.cpp | 33 +++++++++++++++--------------- src/runtime/api/TOMLObject.rs | 5 +---- 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/docs/runtime/toml.mdx b/docs/runtime/toml.mdx index d583924e56b8..c0a85b2ea23d 100644 --- a/docs/runtime/toml.mdx +++ b/docs/runtime/toml.mdx @@ -83,7 +83,7 @@ role = "backend" #### Date/times -Each of TOML's four date/time types maps 1:1 onto a Temporal type, losslessly (Temporal carries nanosecond precision): +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(` @@ -141,13 +141,16 @@ 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`, -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). +`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), and passing one of those as the +top-level value returns `undefined`, as `JSON.stringify` does. --- diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index c89546a76721..0d011bf6c3b5 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -816,13 +816,14 @@ declare module "bun" { * `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). + * 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). * * @category Utilities * diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 1b3c5044b709..1d4a4d40bcf9 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -105,8 +105,7 @@ pub struct Options<'a> { /// - Wraps code with await in async IIFE pub repl_mode: bool, - /// The lazy-export expression came from the TOML parser and may contain - /// `toml_datetime`-tagged strings to lower into `Temporal.*.from` calls. + /// Lower `toml_datetime`-tagged strings in a lazy-export AST to `Temporal.*.from` calls. pub lower_toml_datetimes: bool, } diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 92fa12edb250..f3695c2dbfa9 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -6235,12 +6235,9 @@ static Int128 floorToMultiple(Int128 ns, Int128 unit) return rem == 0 ? ns : ns - rem - (ns < 0 ? unit : 0); } -// Every `local±HH:MM` spelling of an instant denotes the same nanosecond -// (`local - offset`); TOML can only carry the spellings whose local year has -// four digits. Returns `preferredNs` if that spelling fits; otherwise the -// offset closest to it whose spelling fits, whole-hour if one does and -// whole-minute (TOML's granularity, within ±23:59) if not; or nullopt if the -// instant is a day or more outside 0000..9999 and so has no TOML spelling. +// The `±HH:MM` offset to spell `exactTime` with so its local year has TOML's +// four digits: `preferredNs` if that fits, else the closest whole-hour (then +// whole-minute) offset that does; nullopt if none within ±23:59 does. static std::optional tomlOffsetForInstant(JSC::ISO8601::ExactTime exactTime, int64_t preferredNs) { using JSC::ISO8601::ExactTime; @@ -6267,10 +6264,9 @@ static std::optional tomlOffsetForInstant(JSC::ISO8601::ExactTime exact } // Formats a Temporal object (`temporalType` 1-5 from the classifier above) -// as a TOML date/time literal into `buf`; returns the length written, -1 if -// it cannot fit, or -2 if the value's year is outside TOML's 0000..9999. The -// ISO fields are formatted directly, dropping the `[u-ca=...]` and -// `[Time/Zone]` annotations TOML cannot carry. +// as a TOML date/time literal into `buf` and returns the length written, or +// -1 if its year is outside TOML's 0000..9999. The `[u-ca=...]` and +// `[Time/Zone]` annotations, which TOML cannot carry, are dropped. extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, uint8_t temporalType, uint8_t* buf, size_t bufLen) { auto& vm = JSC::getVM(globalObject); @@ -6285,7 +6281,7 @@ extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC: auto exactTime = uncheckedDowncast(cell)->exactTime(); std::optional offsetNs = tomlOffsetForInstant(exactTime, 0); if (!offsetNs) - return -2; + return -1; if (!*offsetNs) offsetNs = std::nullopt; // `Z` string = JSC::TemporalCore::instantToString(exactTime, offsetNs, autoPrecision); @@ -6305,14 +6301,14 @@ extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC: case 5: { auto* zoned = uncheckedDowncast(cell); std::optional zoneOffsetNs = zoned->getOffsetNanoseconds(globalObject); - RETURN_IF_EXCEPTION(scope, -1); + RETURN_IF_EXCEPTION(scope, 0); ASSERT(zoneOffsetNs); - // TOML offsets are `HH:MM` only; a historic sub-minute offset falls - // back to the `Z` spelling of the same instant. + // TOML offsets are `HH:MM`; a historic sub-minute (LMT) offset is + // spelled as `Z` instead. bool wholeMinutes = *zoneOffsetNs % 60000000000ll == 0; std::optional offsetNs = tomlOffsetForInstant(zoned->exactTime(), wholeMinutes ? *zoneOffsetNs : 0); if (!offsetNs) - return -2; + return -1; if (!wholeMinutes && !*offsetNs) offsetNs = std::nullopt; string = JSC::TemporalCore::instantToString(zoned->exactTime(), offsetNs, autoPrecision); @@ -6322,9 +6318,12 @@ extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC: RELEASE_ASSERT_NOT_REACHED(); } - unsigned length = string.length(); - if (length > bufLen) [[unlikely]] + // The expanded-year form of a PlainDate/PlainDateTime (`+010000-…`, `-000001-…`). + if (!isASCIIDigit(string[0])) return -1; + + unsigned length = string.length(); + RELEASE_ASSERT(length <= bufLen); for (unsigned i = 0; i < length; i++) { ASSERT(isASCII(string[i])); buf[i] = static_cast(string[i]); diff --git a/src/runtime/api/TOMLObject.rs b/src/runtime/api/TOMLObject.rs index f3c9cfeb5507..11e37ff1f458 100644 --- a/src/runtime/api/TOMLObject.rs +++ b/src/runtime/api/TOMLObject.rs @@ -537,10 +537,7 @@ impl Stringifier { buf.len(), ) }?; - // -2: an instant with no four-digit-year spelling. A leading `+`/`-` - // is the expanded-year form of a `PlainDate`/`PlainDateTime`, which - // TOML's 4-digit `date-fullyear` cannot carry either. - if len < 1 || !buf[0].is_ascii_digit() { + if len < 0 { return Err(global .throw(format_args!( "TOML.stringify cannot serialize a {} outside years 0000-9999", From c45d52f628500c237bcc889f2c609ab68c114f22 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 13 Aug 2026 00:29:29 +0000 Subject: [PATCH 17/19] Drop the --no-bundle globalThis key mangling; convert the last lazy-export 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. --- src/bundler/ParseTask.rs | 2 +- src/bundler/transpiler.rs | 94 ++++------------------------- src/js_printer/lib.rs | 8 +-- test/bundler/bundler_loader.test.ts | 16 ++--- 4 files changed, 19 insertions(+), 101 deletions(-) diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index d81f479e6bdd..fa6f32fbacba 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -867,7 +867,7 @@ pub mod parse_worker { source, b"", )? - .unwrap(), + .ok_or(AnyError::ParserError)?, )) })(); let _ = temp_log.clone_to_with_recycled(log, true); diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index ca998b2dc2ad..c0b455f7fa54 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1928,40 +1928,10 @@ fn parse_data_loader<'a>( }]); } - // Only the TOML parser tags date/time strings; skip the walk for the - // other data loaders. - let contains_datetime = - loader == options::Loader::Toml && expr_contains_toml_datetime(&expr); if let Some(obj) = expr.data.e_object_mut() { let properties: &mut [bun_ast::G::Property] = obj.properties.slice_mut(); if !properties.is_empty() { let n = properties.len(); - // The var name replacing a `globalThis` key (see the mangle - // below) must not collide with another key, which could - // itself be named `globalThis_`. - let mangled_global_this: &[u8] = if contains_datetime { - let mut candidate: Vec = b"globalThis_".to_vec(); - 'grow: loop { - for property in properties.iter_mut() { - let key: &[u8] = property - .key - .as_mut() - .expect("infallible: prop has key") - .data - .e_string_mut() - .expect("infallible: variant checked") - .slice(arena); - if key == candidate { - candidate.push(b'_'); - continue 'grow; - } - } - break; - } - arena.alloc_slice_copy(&candidate) - } else { - b"globalThis_" - }; // The loop below writes sparsely at index `i` and // `continue`s on `"default"` / duplicate keys, so // some slots are never assigned. In Rust an uninit @@ -2015,29 +1985,18 @@ fn parse_data_loader<'a>( *visited.value_ptr = count as u32; symbols[count] = bun_ast::Symbol { - original_name: { - let valid = match bun_core::MutableString::ensure_valid_identifier(name) - { - Ok(boxed) => boxed, - Err(_) => return None, - }; - // Date/time values print as `globalThis.Temporal.*` - // calls and no renamer runs on this path, so a var - // named `globalThis` would capture them. The named - // export keeps the original alias. - if contains_datetime && &*valid == b"globalThis" { - bun_ast::StoreStr::new(mangled_global_this) - } else { - // The identifier lives in the - // per-parse arena. Arena-copy the - // owned `Box<[u8]>` so it is freed - // with the arena instead of leaking - // (PORTING.md §Forbidden patterns - // bars `heap::alloc` for `&'static`). - // SAFETY: ARENA — `arena` outlives - // the returned `ParseResult.ast`. - bun_ast::StoreStr::new(arena.alloc_slice_copy(&valid)) - } + original_name: match bun_core::MutableString::ensure_valid_identifier(name) + { + // The identifier lives in the + // per-parse arena. Arena-copy the + // owned `Box<[u8]>` so it is freed + // with the arena instead of leaking + // (PORTING.md §Forbidden patterns + // bars `heap::alloc` for `&'static`). + // SAFETY: ARENA — `arena` outlives + // the returned `ParseResult.ast`. + Ok(boxed) => bun_ast::StoreStr::new(arena.alloc_slice_copy(&boxed)), + Err(_) => return None, }, ..Default::default() }; @@ -2132,35 +2091,6 @@ fn parse_data_loader<'a>( }); } -/// Whether any value in a data-format AST is a TOML date/time literal. -/// Iterative: deep dotted TOML headers nest objects far beyond safe -/// recursion depth. -fn expr_contains_toml_datetime(root: &bun_ast::Expr) -> bool { - let mut work: Vec = vec![root.data]; - while let Some(data) = work.pop() { - match data { - bun_ast::ExprData::EString(str) => { - if str.toml_datetime.is_some() { - return true; - } - } - bun_ast::ExprData::EArray(arr) => { - work.extend(arr.items.slice().iter().map(|item| item.data)); - } - bun_ast::ExprData::EObject(obj) => { - work.extend( - obj.properties - .slice() - .iter() - .filter_map(|prop| prop.value.as_ref().map(|value| value.data)), - ); - } - _ => {} - } - } - false -} - #[cold] #[inline(never)] fn parse_text_loader<'a>( diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index ec7e3c1f1d35..34ba9bac3d0c 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -3740,12 +3740,8 @@ 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; this arm serves - // the unrenamed transform paths, where `globalThis.` keeps - // a same-module `var Temporal` (a TOML key of that name) - // from capturing the reference. + // Only reached on the `--no-bundle` data-loader path, which has + // no symbol table; the bundler lowers these to a real call first. if let Some(kind) = e.toml_datetime { let wrap = level.gte(Level::New) || flags.contains(ExprFlag::ForbidCall); if wrap { diff --git a/test/bundler/bundler_loader.test.ts b/test/bundler/bundler_loader.test.ts index 4fdc4590e903..18114450c5b8 100644 --- a/test/bundler/bundler_loader.test.ts +++ b/test/bundler/bundler_loader.test.ts @@ -85,26 +85,18 @@ describe("bundler", async () => { }, run: { stdout: "polyfill true 1979-05-27" }, }); - // The transform path (--no-bundle) has no renamer, and a top-level TOML - // key named Temporal becomes a module-scope var; the printed reference - // goes through globalThis so that var cannot capture it. A key named - // globalThis in turn gets its var mangled (the named export keeps the - // original alias). + // --no-bundle turns each top-level key into a module-scope var, so a key + // named Temporal must not capture the printed reference. itBundled("bun/loader-toml-datetime-no-bundle-temporal-key", { target, bundling: false, entryPoints: ["/config.toml"], files: { - "/config.toml": `Temporal = "x"\nglobalThis = "y"\nglobalThis_ = "z"\nd = 1979-05-27`, + "/config.toml": `Temporal = "x"\nd = 1979-05-27`, }, run: true, onAfterBundle(api) { - const code = api.readFile("/out.js"); - expect(code).toContain('globalThis.Temporal.PlainDate.from("1979-05-27")'); - // The mangled var dodges the real globalThis_ key. - expect(code).toContain('globalThis__ = "y"'); - expect(code).toContain("globalThis__ as globalThis"); - expect(code).toContain('globalThis_ = "z"'); + expect(api.readFile("/out.js")).toContain('globalThis.Temporal.PlainDate.from("1979-05-27")'); }, }); // TOML date/time values bundle as Temporal construction calls; the From 902aef3369fe38d0366aeb07faa5aadd866679d6 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 13 Aug 2026 00:36:30 +0000 Subject: [PATCH 18/19] Print TOML date/times on the --no-bundle path as bare Temporal.*.from calls --- src/js_printer/lib.rs | 6 +++--- test/bundler/bundler_loader.test.ts | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 34ba9bac3d0c..660f78aa0298 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -3740,8 +3740,8 @@ pub(crate) mod __gated_printer { } } ExprData::EString(e) => { - // Only reached on the `--no-bundle` data-loader path, which has - // no symbol table; the bundler lowers these to a real call first. + // The `--no-bundle` data-loader path prints the TOML AST + // as-is; the bundler lowers these to a real call first. if let Some(kind) = e.toml_datetime { let wrap = level.gte(Level::New) || flags.contains(ExprFlag::ForbidCall); if wrap { @@ -3749,7 +3749,7 @@ pub(crate) mod __gated_printer { } self.print_space_before_identifier(); self.add_source_mapping(expr.loc); - self.print(b"globalThis.Temporal."); + self.print(b"Temporal."); self.print(kind.temporal_class()); self.print(b".from(\""); // Always ASCII (validated by the TOML scanner); no escaping. diff --git a/test/bundler/bundler_loader.test.ts b/test/bundler/bundler_loader.test.ts index 18114450c5b8..14e278487608 100644 --- a/test/bundler/bundler_loader.test.ts +++ b/test/bundler/bundler_loader.test.ts @@ -85,18 +85,18 @@ describe("bundler", async () => { }, run: { stdout: "polyfill true 1979-05-27" }, }); - // --no-bundle turns each top-level key into a module-scope var, so a key - // named Temporal must not capture the printed reference. - itBundled("bun/loader-toml-datetime-no-bundle-temporal-key", { + itBundled("bun/loader-toml-datetime-no-bundle", { target, bundling: false, entryPoints: ["/config.toml"], files: { - "/config.toml": `Temporal = "x"\nd = 1979-05-27`, + "/config.toml": `d = 1979-05-27\n[t]\nat = 1979-05-27T00:32:00-07:00`, }, run: true, onAfterBundle(api) { - expect(api.readFile("/out.js")).toContain('globalThis.Temporal.PlainDate.from("1979-05-27")'); + const code = api.readFile("/out.js"); + expect(code).toContain('Temporal.PlainDate.from("1979-05-27")'); + expect(code).toContain('Temporal.Instant.from("1979-05-27T00:32:00-07:00")'); }, }); // TOML date/time values bundle as Temporal construction calls; the From 0a4b01959c3bc5f2fe9571877c905cce1b34f940 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 13 Aug 2026 00:57:24 +0000 Subject: [PATCH 19/19] Classify Temporal values through JSC::temporalType and a shared bun_jsc::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. --- src/codegen/cppbind.ts | 1 + src/jsc/JSValue.rs | 19 +++++++ src/jsc/bindings/bindings.cpp | 53 +++++-------------- src/jsc/lib.rs | 1 + src/runtime/api/TOMLObject.rs | 97 ++++++++++++++--------------------- 5 files changed, 72 insertions(+), 99 deletions(-) diff --git a/src/codegen/cppbind.ts b/src/codegen/cppbind.ts index 4821b9207fc2..2090ca0d1b1e 100644 --- a/src/codegen/cppbind.ts +++ b/src/codegen/cppbind.ts @@ -451,6 +451,7 @@ const rustSharedTypes: Record = { // JSC / Bun "BunString": "bun_core::String", + "JSC::TemporalType": "crate::TemporalType", "JSC::EncodedJSValue": "crate::JSValue", "EncodedJSValue": "crate::JSValue", "JSC::JSGlobalObject": "crate::JSGlobalObject", diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index ed019c91c606..bd5a2fdd5d12 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -1587,6 +1587,10 @@ impl JSValue { JSC__JSValue__jsonStringifyFast(self, global, out) }) } + + pub fn temporal_type(self) -> TemporalType { + crate::cpp::Bun__JSValue__temporalType(self) + } } // ────────────────────────────────────────────────────────────────────────── @@ -2139,6 +2143,21 @@ pub enum ProxyField { Handler = 1, } +/// `JSC::TemporalType` (TemporalObject.h) — result of [`JSValue::temporal_type`]. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TemporalType { + None = 0, + Instant = 1, + PlainDateTime = 2, + PlainDate = 3, + PlainTime = 4, + ZonedDateTime = 5, + PlainYearMonth = 6, + PlainMonthDay = 7, + Duration = 8, +} + /// `JSValue.SerializedFlags`. #[derive(Debug, Default, Clone, Copy)] pub struct SerializedFlags { diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index f3695c2dbfa9..3d5cb3707975 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -109,6 +109,7 @@ #include "JavaScriptCore/TemporalPlainTime.h" #include "JavaScriptCore/TemporalPlainYearMonth.h" #include "JavaScriptCore/TemporalZonedDateTime.h" +#include "JavaScriptCore/TemporalObject.h" #include "JavaScriptCore/TimeZoneICUBridge.h" #include "JavaScriptCore/FunctionPrototype.h" @@ -6190,37 +6191,9 @@ extern "C" [[ZIG_EXPORT(zero_is_throw)]] EncodedJSValue Bun__Temporal__fromDateT return JSValue::encode(result); } -// Classifies a JSValue as one of the Temporal object types, or 0 for -// everything else. Discriminants are shared with `TOMLObject.rs`: 1 Instant, -// 2 PlainDateTime, 3 PlainDate, 4 PlainTime, 5 ZonedDateTime, -// 6 PlainYearMonth, 7 PlainMonthDay, 8 Duration. -extern "C" [[ZIG_EXPORT(nothrow)]] uint8_t Bun__JSValue__temporalObjectType(JSC::EncodedJSValue encodedValue) +extern "C" [[ZIG_EXPORT(nothrow)]] JSC::TemporalType Bun__JSValue__temporalType(JSC::EncodedJSValue encodedValue) { - JSC::JSValue value = JSC::JSValue::decode(encodedValue); - if (!value.isCell()) - return 0; - JSC::JSCell* cell = value.asCell(); - // Every Temporal class is a plain ObjectType cell; anything else - // (JSFinalObject, arrays, dates, functions, …) short-circuits here. - if (cell->type() != JSC::ObjectType) - return 0; - if (cell->inherits()) - return 1; - if (cell->inherits()) - return 2; - if (cell->inherits()) - return 3; - if (cell->inherits()) - return 4; - if (cell->inherits()) - return 5; - if (cell->inherits()) - return 6; - if (cell->inherits()) - return 7; - if (cell->inherits()) - return 8; - return 0; + return JSC::temporalType(JSC::JSValue::decode(encodedValue)); } static Int128 ceilToMultiple(Int128 ns, Int128 unit) @@ -6263,11 +6236,11 @@ static std::optional tomlOffsetForInstant(JSC::ISO8601::ExactTime exact return preferredNs; } -// Formats a Temporal object (`temporalType` 1-5 from the classifier above) -// as a TOML date/time literal into `buf` and returns the length written, or -// -1 if its year is outside TOML's 0000..9999. The `[u-ca=...]` and -// `[Time/Zone]` annotations, which TOML cannot carry, are dropped. -extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, uint8_t temporalType, uint8_t* buf, size_t bufLen) +// Formats a Temporal object as a TOML date/time literal into `buf` and +// returns the length written, or -1 if its year is outside TOML's +// 0000..9999. The `[u-ca=...]` and `[Time/Zone]` annotations, which TOML +// cannot carry, are dropped. +extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, JSC::TemporalType temporalType, uint8_t* buf, size_t bufLen) { auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); @@ -6277,7 +6250,7 @@ extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC: WTF::String string; switch (temporalType) { - case 1: { + case JSC::TemporalType::Instant: { auto exactTime = uncheckedDowncast(cell)->exactTime(); std::optional offsetNs = tomlOffsetForInstant(exactTime, 0); if (!offsetNs) @@ -6287,18 +6260,18 @@ extern "C" [[ZIG_EXPORT(check_slow)]] int32_t Bun__Temporal__toTOMLDateTime(JSC: string = JSC::TemporalCore::instantToString(exactTime, offsetNs, autoPrecision); break; } - case 2: { + case JSC::TemporalType::PlainDateTime: { auto* dateTime = uncheckedDowncast(cell); string = JSC::ISO8601::temporalDateTimeToString(dateTime->plainDate(), dateTime->plainTime(), { JSC::Precision::Auto, 0 }); break; } - case 3: + case JSC::TemporalType::PlainDate: string = JSC::ISO8601::temporalDateToString(uncheckedDowncast(cell)->plainDate()); break; - case 4: + case JSC::TemporalType::PlainTime: string = JSC::ISO8601::temporalTimeToString(uncheckedDowncast(cell)->plainTime(), { JSC::Precision::Auto, 0 }); break; - case 5: { + case JSC::TemporalType::ZonedDateTime: { auto* zoned = uncheckedDowncast(cell); std::optional zoneOffsetNs = zoned->getOffsetNanoseconds(globalObject); RETURN_IF_EXCEPTION(scope, 0); diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 377b657f5ab7..3b4f537beab5 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -180,6 +180,7 @@ pub mod zig_string; pub use self::js_value::{ CoerceTo, ComparisonResult, ForEachCallback, FromAny, FromJsEnum, JSValue, Protected as ProtectedJSValue, ProxyField, SerializedFlags, SerializedScriptValue, + TemporalType, }; // LAYERING (PORTING.md §Dispatch): the task dispatch covers every concrete diff --git a/src/runtime/api/TOMLObject.rs b/src/runtime/api/TOMLObject.rs index 11e37ff1f458..5ae7cb4409e6 100644 --- a/src/runtime/api/TOMLObject.rs +++ b/src/runtime/api/TOMLObject.rs @@ -1,7 +1,9 @@ use bun_collections::HashMap; use bun_core::StackCheck; use bun_core::{OwnedString, String as BunString}; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, wtf}; +use bun_jsc::{ + self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, TemporalType, wtf, +}; use bun_parsers::toml::TOML; pub(crate) fn create(global: &JSGlobalObject) -> JSValue { @@ -122,7 +124,7 @@ enum Layout { Keyval, /// `key = value` whose value is a Temporal object; carries the /// classification so emission does not re-ask. - TemporalKeyval(TemporalObjectType), + TemporalKeyval(TemporalType), /// `[path.key]` section. Table, /// `[[path.key]]` section per element. @@ -306,7 +308,7 @@ impl Stringifier { &mut self, global: &JSGlobalObject, value: JSValue, - known_temporal: Option, + known_temporal: Option, ) -> StringifyResult<()> { if !self.stack_check.is_safe_to_recurse() { return Err(StringifyError::StackOverflow); @@ -516,13 +518,13 @@ impl Stringifier { &mut self, global: &JSGlobalObject, value: JSValue, - temporal_type: TemporalObjectType, + temporal_type: TemporalType, ) -> StringifyResult<()> { - if !temporal_type.has_toml_form() { + if !has_toml_form(temporal_type) { return Err(global .throw(format_args!( "TOML.stringify cannot serialize {} (it has no TOML representation)", - temporal_type.name() + temporal_name(temporal_type) )) .into()); } @@ -532,7 +534,7 @@ impl Stringifier { jsc::cpp::Bun__Temporal__toTOMLDateTime( global, value, - temporal_type as u8, + temporal_type, buf.as_mut_ptr(), buf.len(), ) @@ -541,7 +543,7 @@ impl Stringifier { return Err(global .throw(format_args!( "TOML.stringify cannot serialize a {} outside years 0000-9999", - temporal_type.name() + temporal_name(temporal_type) )) .into()); } @@ -585,62 +587,39 @@ impl Stringifier { } } -/// Mirror of the `Bun__JSValue__temporalObjectType` discriminants (0, not a -/// Temporal object, maps to `None` in `temporal_object_type`). -#[derive(Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -enum TemporalObjectType { - Instant = 1, - PlainDateTime = 2, - PlainDate = 3, - PlainTime = 4, - ZonedDateTime = 5, - PlainYearMonth = 6, - PlainMonthDay = 7, - Duration = 8, -} - -impl TemporalObjectType { - /// Whether TOML has a date/time literal for this type. - fn has_toml_form(self) -> bool { - match self { - TemporalObjectType::Instant - | TemporalObjectType::PlainDateTime - | TemporalObjectType::PlainDate - | TemporalObjectType::PlainTime - | TemporalObjectType::ZonedDateTime => true, - TemporalObjectType::PlainYearMonth - | TemporalObjectType::PlainMonthDay - | TemporalObjectType::Duration => false, - } +fn temporal_object_type(value: JSValue) -> Option { + match value.temporal_type() { + TemporalType::None => None, + t => Some(t), } +} - fn name(self) -> &'static str { - match self { - TemporalObjectType::Instant => "Temporal.Instant", - TemporalObjectType::PlainDateTime => "Temporal.PlainDateTime", - TemporalObjectType::PlainDate => "Temporal.PlainDate", - TemporalObjectType::PlainTime => "Temporal.PlainTime", - TemporalObjectType::ZonedDateTime => "Temporal.ZonedDateTime", - TemporalObjectType::PlainYearMonth => "Temporal.PlainYearMonth", - TemporalObjectType::PlainMonthDay => "Temporal.PlainMonthDay", - TemporalObjectType::Duration => "Temporal.Duration", - } +/// Whether TOML has a date/time literal for this type. +fn has_toml_form(t: TemporalType) -> bool { + match t { + TemporalType::Instant + | TemporalType::PlainDateTime + | TemporalType::PlainDate + | TemporalType::PlainTime + | TemporalType::ZonedDateTime => true, + TemporalType::None + | TemporalType::PlainYearMonth + | TemporalType::PlainMonthDay + | TemporalType::Duration => false, } } -fn temporal_object_type(value: JSValue) -> Option { - match jsc::cpp::Bun__JSValue__temporalObjectType(value) { - 0 => None, - 1 => Some(TemporalObjectType::Instant), - 2 => Some(TemporalObjectType::PlainDateTime), - 3 => Some(TemporalObjectType::PlainDate), - 4 => Some(TemporalObjectType::PlainTime), - 5 => Some(TemporalObjectType::ZonedDateTime), - 6 => Some(TemporalObjectType::PlainYearMonth), - 7 => Some(TemporalObjectType::PlainMonthDay), - 8 => Some(TemporalObjectType::Duration), - _ => unreachable!("Bun__JSValue__temporalObjectType returns 0-8"), +fn temporal_name(t: TemporalType) -> &'static str { + match t { + TemporalType::Instant => "Temporal.Instant", + TemporalType::PlainDateTime => "Temporal.PlainDateTime", + TemporalType::PlainDate => "Temporal.PlainDate", + TemporalType::PlainTime => "Temporal.PlainTime", + TemporalType::ZonedDateTime => "Temporal.ZonedDateTime", + TemporalType::PlainYearMonth => "Temporal.PlainYearMonth", + TemporalType::PlainMonthDay => "Temporal.PlainMonthDay", + TemporalType::Duration => "Temporal.Duration", + TemporalType::None => unreachable!("not a Temporal object"), } }