Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
3f48ea6
Format Temporal values in console.log, util.inspect, and test pretty-…
robobun Aug 6, 2026
53c171c
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 6, 2026
d20fcc7
Tighten comments
robobun Aug 6, 2026
d3613cf
Fix clippy lints in print_temporal, strip ANSI in failure-message test
robobun Aug 6, 2026
717a951
Cover sub-minute offset rounding and non-ISO YearMonth/MonthDay in tests
robobun Aug 6, 2026
2828881
Temporal inspect: single C++ owner for classify+label+text, safe Rust…
dylan-conway Aug 7, 2026
82e56ae
Merge remote-tracking branch 'origin/main' into farm/8f01c0db/tempora…
dylan-conway Aug 7, 2026
26d5c51
Collapse Temporal helper comments to one line
dylan-conway Aug 7, 2026
90724b6
Temporal: Bun::temporalObjectType(JSValue) with the extern as a thin …
dylan-conway Aug 7, 2026
596e9ae
Temporal: typed TemporalType enum shared between C++ and Rust
dylan-conway Aug 7, 2026
4351a73
Give bundler macros a deliberate Temporal arm
robobun Aug 7, 2026
867820a
Temporal inspect: use JSC::temporalType and TemporalZonedDateTime::to…
dylan-conway Aug 7, 2026
37519f4
Merge remote-tracking branch 'origin/main' into farm/8f01c0db/tempora…
dylan-conway Aug 8, 2026
938dc69
macro Temporal arm: one-line comment; drain stdout in the test
dylan-conway Aug 8, 2026
edf96b1
macro Temporal test: don't assert empty stdout (debug builds print [m…
dylan-conway Aug 8, 2026
6f38c05
Merge remote-tracking branch 'origin/main' into farm/8f01c0db/tempora…
dylan-conway Aug 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/js/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,25 @@ const {
isTypedArray,
} = require("node:util/types");

// Temporal objects are recognized and formatted natively (by ClassInfo and
// internal slots) so inspection never calls user-observable methods.
// getTemporalType returns 0 for non-Temporal values, otherwise an index into
// kTemporalLabels; getTemporalDisplayString returns the default-options
// `toString()` text.
Comment thread
robobun marked this conversation as resolved.
Outdated
const getTemporalType = $newCppFunction("Temporal.cpp", "jsFunctionTemporalObjectType", 1);
const getTemporalDisplayString = $newCppFunction("Temporal.cpp", "jsFunctionTemporalToDisplayString", 1);
const kTemporalLabels = [
"",
"Temporal.Instant",
"Temporal.PlainDateTime",
"Temporal.PlainDate",
"Temporal.PlainTime",
"Temporal.ZonedDateTime",
"Temporal.PlainYearMonth",
"Temporal.PlainMonthDay",
"Temporal.Duration",
];

// We need this duplicate here to avoid a circular dependency between node:assert and node:util.
class AssertionError extends Error {
constructor(message, isForced = false) {
Expand Down Expand Up @@ -1482,6 +1501,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
let braces;
let extraKeys;
let noIterator = true;
let temporalType = 0;
let i = 0;
const filter = ctx.showHidden ? ALL_PROPERTIES : ONLY_ENUMERABLE;

Expand Down Expand Up @@ -1631,6 +1651,17 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
if (keys.length === 0 && protoProps === undefined) {
return base;
}
} else if ((temporalType = getTemporalType(value)) !== 0) {
const label = kTemporalLabels[temporalType];
// JSC's Temporal constructors are named `PlainDate`, not the
// `Temporal.PlainDate` V8 uses; map direct instances onto the label so
// the prefix comes out the same as Node's.
Comment thread
robobun marked this conversation as resolved.
Outdated
const effectiveConstructor = constructor !== null && `Temporal.${constructor}` === label ? label : constructor;
const prefix = getPrefix(effectiveConstructor, tag, label);
base = `${prefix}${getTemporalDisplayString(value)}`;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (keys.length === 0 && protoProps === undefined) {
return ctx.stylize(base, "date");
}
} else {
if (keys.length === 0 && protoProps === undefined) {
if (isExternal(value)) {
Expand Down
72 changes: 71 additions & 1 deletion src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1925,6 +1925,7 @@

JSON,
ToJSON,
Temporal,
NativeCode,

JSX,
Expand All @@ -1937,6 +1938,22 @@
RevokedProxy,
}

/// Class label (the `@@toStringTag` spelling) for a non-zero Temporal type
/// discriminant from `Bun__JSValue__temporalObjectType`.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub fn temporal_class_label(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",
8 => "Temporal.Duration",
_ => unreachable!("not a Temporal type discriminant"),
}
}

impl Tag {
pub(crate) fn is_primitive(self) -> bool {
matches!(
Expand Down Expand Up @@ -1996,6 +2013,7 @@
Promise,
JSON,
ToJSON,
Temporal,
NativeCode,
JSX,
Event,
Expand Down Expand Up @@ -2042,6 +2060,7 @@
TagPayload::Promise => Tag::Promise,
TagPayload::JSON => Tag::JSON,
TagPayload::ToJSON => Tag::ToJSON,
TagPayload::Temporal => Tag::Temporal,
TagPayload::NativeCode => Tag::NativeCode,
TagPayload::JSX => Tag::JSX,
TagPayload::Event => Tag::Event,
Expand Down Expand Up @@ -2087,6 +2106,7 @@
Tag::Promise => TagPayload::Promise,
Tag::JSON => TagPayload::JSON,
Tag::ToJSON => TagPayload::ToJSON,
Tag::Temporal => TagPayload::Temporal,
Tag::NativeCode => TagPayload::NativeCode,
Tag::JSX => TagPayload::JSX,
Tag::Event => TagPayload::Event,
Expand Down Expand Up @@ -2305,12 +2325,21 @@
T::JSDate => TagPayload::JSON,
T::JSPromise => TagPayload::Promise,

// Temporal cells are plain `ObjectType`; only ClassInfo can
// tell them apart from other host objects.
Comment thread
robobun marked this conversation as resolved.
Outdated
T::Object => {
if crate::cpp::Bun__JSValue__temporalObjectType(value) != 0 {
TagPayload::Temporal
} else {
TagPayload::Object
}
}

T::WrapForValidIterator
| T::RegExpStringIterator
| T::JSArrayIterator
| T::Iterator
| T::IteratorHelper
| T::Object
| T::FinalObject
| T::ModuleNamespaceObject => TagPayload::Object,

Expand Down Expand Up @@ -3415,6 +3444,7 @@
Tag::Set => self.print_set::<ENABLE_ANSI_COLORS>(writer_, value),
Tag::ToJSON => self.print_to_json::<ENABLE_ANSI_COLORS>(writer_, value),
Tag::JSON => self.print_json::<ENABLE_ANSI_COLORS>(writer_, value, js_type),
Tag::Temporal => self.print_temporal::<ENABLE_ANSI_COLORS>(writer_, value),
Tag::Event => self.print_event::<ENABLE_ANSI_COLORS>(
writer_,
value,
Expand Down Expand Up @@ -4307,6 +4337,46 @@
Ok(())
}

/// `Temporal.PlainDate 2020-01-02` — the class label uncolored, the
/// default-options `toString()` text in Date's magenta. A single
/// atomic token like `Date`: own properties and subclass names are
/// ignored.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline(never)]
fn print_temporal<const C: bool>(
&mut self,
writer_: &mut dyn bun_io::Write,
value: JSValue,
) -> JsResult<()> {
let mut writer = WrappedWriter {
ctx: writer_,
failed: false,
estimated_line_length: &mut self.estimated_line_length,
};
let temporal_type = crate::cpp::Bun__JSValue__temporalObjectType(value);
let label = temporal_class_label(temporal_type);
let mut str = OwnedString::new(BunString::empty());
unsafe {

Check failure on line 4358 in src/jsc/ConsoleObject.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

unsafe block missing a safety comment
crate::cpp::Bun__Temporal__toDisplayString(
self.global_this,
value,
temporal_type,
&mut *str,

Check failure on line 4363 in src/jsc/ConsoleObject.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

implicit borrow as raw pointer
)?;
}
writer.add_for_new_line(label.len() + 1 + str.length());
writer.print(format_args!(
"{} {}{}{}",
label,
pfmt!("<r><magenta>", C),
&*str,

Check failure on line 4371 in src/jsc/ConsoleObject.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

redundant reference in `format_args!` argument
pfmt!("<r>", C)
));
if writer.failed {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
self.failed = true;
}
Ok(())
}

#[inline(never)]
fn print_array<const C: bool>(
&mut self,
Expand Down
115 changes: 115 additions & 0 deletions src/jsc/bindings/Temporal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Shared formatting for Temporal values in console.log/Bun.inspect
// (ConsoleObject.rs), the test runner's pretty-format (pretty_format.rs), and
// util.inspect (internal/util/inspect.js, via the host functions at the
// bottom). The text matches each type's spec `toString()` with default
// options, built from internal slots so tampered prototypes can't change or
// observe inspection.
Comment thread
robobun marked this conversation as resolved.
Outdated

#include "root.h"
#include "Temporal.h"

#include "JavaScriptCore/ISO8601.h"
#include "JavaScriptCore/JSCast.h"
#include "JavaScriptCore/JSCJSValueInlines.h"
#include "JavaScriptCore/JSGlobalObjectInlines.h"
#include "JavaScriptCore/ObjectConstructor.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 <wtf/text/StringBuilder.h>

namespace Bun {

// https://tc39.es/proposal-temporal/#sec-temporal-temporalzoneddatetimetostring
// with precision/showOffset/showTimeZone/showCalendar all ~auto~, mirroring
// `temporalZonedDateTimeToString` (file-static in
// TemporalZonedDateTimePrototype.cpp, so it cannot be called directly).
Comment thread
robobun marked this conversation as resolved.
Outdated
static WTF::String zonedDateTimeDisplayString(JSC::JSGlobalObject* globalObject, JSC::TemporalZonedDateTime* zonedDateTime)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

auto [date, time] = zonedDateTime->getLocalDateTime(globalObject);
RETURN_IF_EXCEPTION(scope, {});

std::optional<int64_t> offsetOpt = zonedDateTime->getOffsetNanoseconds(globalObject);
RETURN_IF_EXCEPTION(scope, {});
ASSERT(offsetOpt);

WTF::StringBuilder builder;
builder.append(JSC::ISO8601::temporalDateTimeToString(date, time, { JSC::Precision::Auto, 0 }));

// FormatDateTimeUTCOffsetRounded: round the offset to the nearest minute.
int64_t offsetNs = offsetOpt.value_or(0);
int64_t offsetMinutes = offsetNs / 60'000'000'000;
int64_t remainder = offsetNs % 60'000'000'000;
if (remainder > 30'000'000'000 || (remainder == 30'000'000'000 && offsetNs > 0))
offsetMinutes++;
else if (remainder < -30'000'000'000 || (remainder == -30'000'000'000 && offsetNs < 0))
offsetMinutes--;
builder.append(JSC::ISO8601::formatTimeZoneOffsetString(offsetMinutes * 60'000'000'000));

builder.append('[');
builder.append(zonedDateTime->timeZoneId());
builder.append(']');

if (!JSC::TemporalCore::calendarIsISO(zonedDateTime->calendarID())) {
builder.append("[u-ca="_s);
builder.append(zonedDateTime->calendarId());
builder.append(']');
}
return builder.toString();
}

WTF::String temporalDisplayString(JSC::JSGlobalObject* globalObject, JSC::JSCell* cell, uint8_t temporalType)
{
switch (temporalType) {
case 1:
return uncheckedDowncast<JSC::TemporalInstant>(cell)->toString();
case 2:
return uncheckedDowncast<JSC::TemporalPlainDateTime>(cell)->toString();
case 3:
return uncheckedDowncast<JSC::TemporalPlainDate>(cell)->toString();
case 4:
return uncheckedDowncast<JSC::TemporalPlainTime>(cell)->toString();
case 5:
return zonedDateTimeDisplayString(globalObject, uncheckedDowncast<JSC::TemporalZonedDateTime>(cell));
case 6:
return uncheckedDowncast<JSC::TemporalPlainYearMonth>(cell)->toString();
case 7:
return uncheckedDowncast<JSC::TemporalPlainMonthDay>(cell)->toString();
case 8:
return uncheckedDowncast<JSC::TemporalDuration>(cell)->toString(globalObject);
default:
RELEASE_ASSERT_NOT_REACHED();
}
}

} // namespace Bun

JSC_DEFINE_HOST_FUNCTION(jsFunctionTemporalObjectType, (JSC::JSGlobalObject*, JSC::CallFrame* callFrame))
{
return JSC::JSValue::encode(JSC::jsNumber(Bun__JSValue__temporalObjectType(JSC::JSValue::encode(callFrame->argument(0)))));
}

JSC_DEFINE_HOST_FUNCTION(jsFunctionTemporalToDisplayString, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

JSC::JSValue value = callFrame->argument(0);
uint8_t temporalType = Bun__JSValue__temporalObjectType(JSC::JSValue::encode(value));
if (!temporalType)
return JSC::JSValue::encode(JSC::jsUndefined());

WTF::String result = Bun::temporalDisplayString(globalObject, value.asCell(), temporalType);
RETURN_IF_EXCEPTION(scope, {});
return JSC::JSValue::encode(JSC::jsString(vm, WTF::move(result)));
}
28 changes: 28 additions & 0 deletions src/jsc/bindings/Temporal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#include "root.h"

// Classifies a JSValue as one of the Temporal object types, or 0 for
// everything else. Discriminants are shared with the Rust callers
// (`ConsoleObject.rs`, `pretty_format.rs`): 1 Instant, 2 PlainDateTime,
// 3 PlainDate, 4 PlainTime, 5 ZonedDateTime, 6 PlainYearMonth,
// 7 PlainMonthDay, 8 Duration. Defined in bindings.cpp.
Comment thread
robobun marked this conversation as resolved.
Outdated
extern "C" uint8_t Bun__JSValue__temporalObjectType(JSC::EncodedJSValue);

namespace Bun {

// The text `cell.toString()` would produce with default options (auto
// precision, `[TimeZone]`/`[u-ca=...]` annotations included), computed from
// the internal slots without calling any user-observable method.
// `temporalType` is a non-zero `Bun__JSValue__temporalObjectType` result for
// `cell`. May throw (ZonedDateTime offset lookups, Duration integer
// formatting); callers check the exception scope.
Comment thread
robobun marked this conversation as resolved.
Outdated
WTF::String temporalDisplayString(JSC::JSGlobalObject*, JSC::JSCell*, uint8_t temporalType);

} // namespace Bun

// `jsFunctionTemporalObjectType(value)` -> number 0-8 (the classifier above).
JSC_DECLARE_HOST_FUNCTION(jsFunctionTemporalObjectType);
// `jsFunctionTemporalToDisplayString(value)` -> string, or undefined when
// `value` is not a Temporal object.
Comment thread
robobun marked this conversation as resolved.
Outdated
JSC_DECLARE_HOST_FUNCTION(jsFunctionTemporalToDisplayString);
46 changes: 46 additions & 0 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
#include "JavaScriptCore/TemporalPlainYearMonth.h"
#include "JavaScriptCore/TemporalZonedDateTime.h"
#include "JavaScriptCore/TimeZoneICUBridge.h"
#include "Temporal.h"

#include "JavaScriptCore/FunctionPrototype.h"
#include "JSFetchHeaders.h"
Expand Down Expand Up @@ -5911,6 +5912,51 @@ extern "C" [[ZIG_EXPORT(nothrow)]] double Bun__gregorianDateTimeToMSInZone(JSC::
return static_cast<double>(r->epochMilliseconds());
}

// Classifies a JSValue as one of the Temporal object types, or 0 for
// everything else. Discriminants: 1 Instant, 2 PlainDateTime, 3 PlainDate,
// 4 PlainTime, 5 ZonedDateTime, 6 PlainYearMonth, 7 PlainMonthDay,
// 8 Duration.
Comment thread
robobun marked this conversation as resolved.
Outdated
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.
Comment thread
robobun marked this conversation as resolved.
Outdated
if (cell->type() != JSC::ObjectType)
return 0;
if (cell->inherits<JSC::TemporalInstant>())
return 1;
if (cell->inherits<JSC::TemporalPlainDateTime>())
return 2;
if (cell->inherits<JSC::TemporalPlainDate>())
return 3;
if (cell->inherits<JSC::TemporalPlainTime>())
return 4;
if (cell->inherits<JSC::TemporalZonedDateTime>())
return 5;
if (cell->inherits<JSC::TemporalPlainYearMonth>())
return 6;
if (cell->inherits<JSC::TemporalPlainMonthDay>())
return 7;
if (cell->inherits<JSC::TemporalDuration>())
return 8;
return 0;
}

// Default-options `toString()` text for a Temporal object, for inspection
// (see Bun::temporalDisplayString in Temporal.cpp). `temporalType` is a
// non-zero result of the classifier above for `encodedValue`.
Comment thread
robobun marked this conversation as resolved.
Outdated
extern "C" [[ZIG_EXPORT(check_slow)]] void Bun__Temporal__toDisplayString(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, uint8_t temporalType, BunString* out)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
WTF::String string = Bun::temporalDisplayString(globalObject, JSC::JSValue::decode(encodedValue).asCell(), temporalType);
RETURN_IF_EXCEPTION(scope, );
*out = Bun::toStringRef(string);
}

extern "C" EncodedJSValue JSC__JSValue__dateInstanceFromNumber(JSC::JSGlobalObject* globalObject, double unixTimestamp)
{
auto& vm = JSC::getVM(globalObject);
Expand Down
1 change: 1 addition & 0 deletions src/jsc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ pub use self::console_object::Formatter;
/// Request.rs / S3Client.rs). Same enum; the split is naming drift only.
pub use self::console_object::formatter::Tag as FormatTag;
pub use self::console_object::formatter::Tag as FormatAs;
pub use self::console_object::formatter::temporal_class_label;
pub use self::js_array_iterator::JSArrayIterator;
pub use self::js_promise::JSPromise;
/// `JSInternalPromise` was removed upstream; the module loader uses `JSPromise`
Expand Down
Loading
Loading