Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
28 changes: 28 additions & 0 deletions src/js/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,23 @@ const {
isTypedArray,
} = require("node:util/types");

// getTemporalType(value) is 0 for non-Temporal values, otherwise an index into
// kTemporalLabels; getTemporalDisplayString is the value's default-options
// toString() text, computed without calling user-observable methods.
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 +1499,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 +1649,16 @@ 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 names the constructors `PlainDate` where V8 uses `Temporal.PlainDate`;
// map direct instances onto the label so the prefix matches Node's output.
Comment thread
robobun marked this conversation as resolved.
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
68 changes: 67 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,21 @@
RevokedProxy,
}

/// Label for a non-zero `Bun__JSValue__temporalObjectType` discriminant.
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 +2012,7 @@
Promise,
JSON,
ToJSON,
Temporal,
NativeCode,
JSX,
Event,
Expand Down Expand Up @@ -2042,6 +2059,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 +2105,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 +2324,20 @@
T::JSDate => TagPayload::JSON,
T::JSPromise => TagPayload::Promise,

// Temporal cells are plain `ObjectType`; only ClassInfo tells them apart.
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 +3442,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 +4335,44 @@
Ok(())
}

/// `Temporal.PlainDate 2020-01-02` — label uncolored, `toString()` text
/// in Date's magenta; own properties and subclasses ignored like `Date`.
Comment thread
robobun marked this conversation as resolved.
#[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 4354 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 4359 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 4367 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
111 changes: 111 additions & 0 deletions src/jsc/bindings/Temporal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Temporal value formatting shared by console.log/Bun.inspect, the test
// runner's pretty-format, and util.inspect: each type's default-options spec
// toString() text, built from internal slots, never from user-reachable code.
Comment thread
robobun marked this conversation as resolved.

#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 every option ~auto~; JSC's own implementation is file-static in
// TemporalZonedDateTimePrototype.cpp, so the recipe is replicated here.
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)));
}
21 changes: 21 additions & 0 deletions src/jsc/bindings/Temporal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include "root.h"

// 0 for non-Temporal values, otherwise 1-8 per the discriminant table at the
// definition in bindings.cpp (shared with the Rust and JS callers).
Comment thread
robobun marked this conversation as resolved.
Outdated
extern "C" uint8_t Bun__JSValue__temporalObjectType(JSC::EncodedJSValue);

namespace Bun {

// The default-options `toString()` text for a Temporal `cell` whose non-zero
// classifier result is `temporalType`, built from internal slots. May throw
// (ZonedDateTime offset lookups, Duration integer formatting).
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, undefined if not Temporal.
JSC_DECLARE_HOST_FUNCTION(jsFunctionTemporalToDisplayString);
43 changes: 43 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,48 @@ extern "C" [[ZIG_EXPORT(nothrow)]] double Bun__gregorianDateTimeToMSInZone(JSC::
return static_cast<double>(r->epochMilliseconds());
}

// Temporal type discriminant of a JSValue: 0 not Temporal, 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 short-circuits.
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 (see
// Bun::temporalDisplayString); `temporalType` is a non-zero classifier result.
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