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

// getTemporalLabel(value) is e.g. "Temporal.PlainDate", or undefined for
// non-Temporal values; 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 getTemporalLabel = $newCppFunction("Temporal.cpp", "jsFunctionTemporalLabel", 1);
const getTemporalDisplayString = $newCppFunction("Temporal.cpp", "jsFunctionTemporalToDisplayString", 1);

// 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 +1488,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
let braces;
let extraKeys;
let noIterator = true;
let temporalLabel;
let i = 0;
const filter = ctx.showHidden ? ALL_PROPERTIES : ONLY_ENUMERABLE;

Expand Down Expand Up @@ -1631,6 +1638,16 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
if (keys.length === 0 && protoProps === undefined) {
return base;
}
} else if ((temporalLabel = getTemporalLabel(value)) !== undefined) {
// 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}` === temporalLabel ? temporalLabel : constructor;
const prefix = getPrefix(effectiveConstructor, tag, temporalLabel);
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
36 changes: 36 additions & 0 deletions 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 Down Expand Up @@ -1996,6 +1997,7 @@
Promise,
JSON,
ToJSON,
Temporal,
NativeCode,
JSX,
Event,
Expand Down Expand Up @@ -2042,6 +2044,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 +2090,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,6 +2309,9 @@
T::JSDate => TagPayload::JSON,
T::JSPromise => TagPayload::Promise,

// Temporal cells are plain `ObjectType`; only ClassInfo tells them apart.
T::Object if value.is_temporal() => TagPayload::Temporal,

Check warning on line 2313 in src/jsc/ConsoleObject.rs

View check run for this annotation

Claude / Claude Code Review

New Temporal tag in shared classifier breaks bundler macros returning Temporal values

Adding `TagPayload::Temporal` to the shared `formatter::Tag::get()` classifier changes behavior for another consumer: `src/js_parser_jsc/Macro.rs::run()` (line 589) matches on this tag, has no `T::Temporal` arm, and now hits the `_` wildcard that fails the build with "cannot coerce ... to Bun's AST". Before this PR a Temporal value returned from a bundler macro classified as `T::Object` and produced `{}` — useless, but the build succeeded. Add a deliberate `T::Temporal` arm in `Macro.rs::run()`
Comment thread
robobun marked this conversation as resolved.
Outdated

T::WrapForValidIterator
| T::RegExpStringIterator
| T::JSArrayIterator
Expand Down Expand Up @@ -3415,6 +3422,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 +4315,34 @@
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 (label, str) = value.temporal_display_string(self.global_this)?;
writer.add_for_new_line(label.length() + 1 + str.length());
writer.print(format_args!(
"{} {}{}{}",
label,
pfmt!("<r><magenta>", C),
str,
pfmt!("<r>", C)
));
if writer.failed {
self.failed = true;
}
Ok(())
}

#[inline(never)]
fn print_array<const C: bool>(
&mut self,
Expand Down
25 changes: 25 additions & 0 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,31 @@ impl JSValue {
JSC__JSValue__jsonStringifyFast(self, global, out)
})
}

pub fn is_temporal(self) -> bool {
crate::cpp::Bun__JSValue__temporalObjectType(self) != 0
}

/// Requires `self.is_temporal()`. `(label, text)`, e.g.
/// `("Temporal.PlainDate", "2020-01-02")`: the class label and the value's
/// default-options `toString()`, read from internal slots.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub fn temporal_display_string(
self,
global: &JSGlobalObject,
) -> JsResult<(bun_core::String, bun_core::OwnedString)> {
let mut label = bun_core::String::empty();
let mut text = bun_core::OwnedString::new(bun_core::String::empty());
// SAFETY: both out-pointers are live `BunString`s the callee writes at most once.
unsafe {
crate::cpp::Bun__Temporal__toDisplayString(
global,
self,
&raw mut label,
&raw mut *text,
)?
};
Ok((label, text))
}
}

// ──────────────────────────────────────────────────────────────────────────
Expand Down
178 changes: 178 additions & 0 deletions src/jsc/bindings/Temporal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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 "headers-handwritten.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>

extern "C" [[ZIG_EXPORT(nothrow)]] uint8_t Bun__JSValue__temporalObjectType(JSC::EncodedJSValue encodedValue)
{
JSC::JSValue value = JSC::JSValue::decode(encodedValue);
// Every Temporal class is a plain ObjectType cell; anything else short-circuits.
if (!value.isCell() || value.asCell()->type() != JSC::ObjectType)
return 0;
JSC::JSCell* cell = value.asCell();
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;
}

namespace Bun {

static ASCIILiteral temporalLabel(uint8_t temporalType)
{
switch (temporalType) {
case 1:
return "Temporal.Instant"_s;
case 2:
return "Temporal.PlainDateTime"_s;
case 3:
return "Temporal.PlainDate"_s;
case 4:
return "Temporal.PlainTime"_s;
case 5:
return "Temporal.ZonedDateTime"_s;
case 6:
return "Temporal.PlainYearMonth"_s;
case 7:
return "Temporal.PlainMonthDay"_s;
case 8:
return "Temporal.Duration"_s;
default:
RELEASE_ASSERT_NOT_REACHED();
}
}

// 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, {});

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;
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();
}

// `temporalType` is the non-zero `Bun__JSValue__temporalObjectType(cell)`.
static 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

// `encodedValue` must be a Temporal value (`Bun__JSValue__temporalObjectType` != 0).
// Writes e.g. `Temporal.PlainDate` to `label` and `2020-01-02` to `text`.
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, BunString* label, BunString* text)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
uint8_t temporalType = Bun__JSValue__temporalObjectType(encodedValue);
WTF::String string = Bun::temporalDisplayString(globalObject, JSC::JSValue::decode(encodedValue).asCell(), temporalType);
RETURN_IF_EXCEPTION(scope, );
*label = Bun::toStringView(Bun::temporalLabel(temporalType));
*text = Bun::toStringRef(string);
}

JSC_DEFINE_HOST_FUNCTION(jsFunctionTemporalLabel, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
uint8_t temporalType = Bun__JSValue__temporalObjectType(JSC::JSValue::encode(callFrame->argument(0)));
if (!temporalType)
return JSC::JSValue::encode(JSC::jsUndefined());
return JSC::JSValue::encode(JSC::jsNontrivialString(JSC::getVM(globalObject), Bun::temporalLabel(temporalType)));
}

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)));
}
14 changes: 14 additions & 0 deletions src/jsc/bindings/Temporal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include "root.h"

// 0 for non-Temporal values, otherwise 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" uint8_t Bun__JSValue__temporalObjectType(JSC::EncodedJSValue);

// `jsFunctionTemporalLabel(value)` -> e.g. "Temporal.PlainDate", or undefined
// if not Temporal.
Comment thread
robobun marked this conversation as resolved.
Outdated
JSC_DECLARE_HOST_FUNCTION(jsFunctionTemporalLabel);
// `jsFunctionTemporalToDisplayString(value)` -> the value's default-options
// `toString()` text, or undefined if not Temporal.
Comment thread
robobun marked this conversation as resolved.
Outdated
JSC_DECLARE_HOST_FUNCTION(jsFunctionTemporalToDisplayString);
15 changes: 2 additions & 13 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 @@ -1067,18 +1068,6 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return true;
}

static bool isTemporalObject(JSC::JSObject* object)
{
return object->inherits<JSC::TemporalInstant>()
|| object->inherits<JSC::TemporalPlainDate>()
|| object->inherits<JSC::TemporalPlainDateTime>()
|| object->inherits<JSC::TemporalPlainTime>()
|| object->inherits<JSC::TemporalZonedDateTime>()
|| object->inherits<JSC::TemporalPlainYearMonth>()
|| object->inherits<JSC::TemporalPlainMonthDay>()
|| object->inherits<JSC::TemporalDuration>();
}

// Temporal objects keep their state in internal slots and have no own
// properties, so the generic own-property walk would call any two instances
// of a class equal. Compare the internal fields instead, the way JSDateType
Expand Down Expand Up @@ -1127,7 +1116,7 @@ static std::optional<bool> temporalObjectsDequal(JSC::JSObject* o1, JSC::JSObjec
}
// `o1` is not a Temporal object; a Temporal `o2` can then never be equal
// (and must not reach the own-property walk).
if (isTemporalObject(o2))
if (Bun__JSValue__temporalObjectType(JSValue::encode(o2)))
return false;
return std::nullopt;
}
Expand Down
Loading