Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
1 change: 1 addition & 0 deletions src/codegen/cppbind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ const rustSharedTypes: Record<string, string> = {

// JSC / Bun
"BunString": "bun_core::String",
"Bun::TemporalType": "crate::TemporalType",
"JSC::EncodedJSValue": "crate::JSValue",
"EncodedJSValue": "crate::JSValue",
"JSC::JSGlobalObject": "crate::JSGlobalObject",
Expand Down
15 changes: 15 additions & 0 deletions src/js/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ const {
isTypedArray,
} = require("node:util/types");

// "Temporal.PlainDate" / undefined, and the slot-derived default toString() text.
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 @@ -1478,6 +1482,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 @@ -1622,6 +1627,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
15 changes: 15 additions & 0 deletions src/js_parser_jsc/Macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,21 @@ impl<'a> Run<'a> {
T::Double => self.coerce(T::Double, value),
T::String => self.coerce(T::String, value),
T::Promise => self.coerce(T::Promise, value),
// Like Date (whose JSON coercion also fails), Temporal values have
// no AST representation; erroring beats emitting `{}`. Their
// ClassInfo names are the generic "Object", so name the type via
// its label.
Comment thread
dylan-conway marked this conversation as resolved.
Outdated
T::Temporal => {
let (label, _text) = value.temporal_display_string(self.global)?;
self.log.add_error_fmt(
Some(self.source),
self.caller.loc,
format_args!(
"cannot coerce {label} to Bun's AST. Please return a simpler type"
),
);
Err(MacroError::MacroFailed)
}
_ => {
let name = value.get_class_info_name().unwrap_or(b"unknown");

Expand Down
38 changes: 38 additions & 0 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1925,6 +1925,7 @@ pub mod formatter {

JSON,
ToJSON,
Temporal,
NativeCode,

JSX,
Expand Down Expand Up @@ -1996,6 +1997,7 @@ pub mod formatter {
Promise,
JSON,
ToJSON,
Temporal,
NativeCode,
JSX,
Event,
Expand Down Expand Up @@ -2042,6 +2044,7 @@ pub mod formatter {
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 @@ pub mod formatter {
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,11 @@ pub mod formatter {
T::JSDate => TagPayload::JSON,
T::JSPromise => TagPayload::Promise,

// Temporal cells are plain `ObjectType`; only ClassInfo tells them apart.
T::Object if value.temporal_type() != crate::TemporalType::None => {
TagPayload::Temporal
}

T::WrapForValidIterator
| T::RegExpStringIterator
| T::JSArrayIterator
Expand Down Expand Up @@ -3415,6 +3424,7 @@ pub mod formatter {
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 +4317,34 @@ pub mod formatter {
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
38 changes: 38 additions & 0 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,29 @@ impl JSValue {
JSC__JSValue__jsonStringifyFast(self, global, out)
})
}

pub fn temporal_type(self) -> TemporalType {
crate::cpp::Bun__JSValue__temporalObjectType(self)
}

/// Requires `self.temporal_type() != TemporalType::None`; e.g. `("Temporal.PlainDate", "2020-01-02")`.
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 Expand Up @@ -2138,6 +2161,21 @@ pub enum ProxyField {
Handler = 1,
}

/// `Bun::TemporalType` (Temporal.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 {
Expand Down
181 changes: 181 additions & 0 deletions src/jsc/bindings/Temporal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// 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>

namespace Bun {

TemporalType temporalObjectType(JSC::JSValue value)
{
// Every Temporal class is a plain ObjectType cell; anything else short-circuits.
if (!value.isCell() || value.asCell()->type() != JSC::ObjectType)
return TemporalType::None;
JSC::JSCell* cell = value.asCell();
if (cell->inherits<JSC::TemporalInstant>())
return TemporalType::Instant;
if (cell->inherits<JSC::TemporalPlainDateTime>())
return TemporalType::PlainDateTime;
if (cell->inherits<JSC::TemporalPlainDate>())
return TemporalType::PlainDate;
if (cell->inherits<JSC::TemporalPlainTime>())
return TemporalType::PlainTime;
if (cell->inherits<JSC::TemporalZonedDateTime>())
return TemporalType::ZonedDateTime;
if (cell->inherits<JSC::TemporalPlainYearMonth>())
return TemporalType::PlainYearMonth;
if (cell->inherits<JSC::TemporalPlainMonthDay>())
return TemporalType::PlainMonthDay;
if (cell->inherits<JSC::TemporalDuration>())
return TemporalType::Duration;
return TemporalType::None;
}

static ASCIILiteral temporalLabel(TemporalType type)
{
switch (type) {
case TemporalType::Instant:
return "Temporal.Instant"_s;
case TemporalType::PlainDateTime:
return "Temporal.PlainDateTime"_s;
case TemporalType::PlainDate:
return "Temporal.PlainDate"_s;
case TemporalType::PlainTime:
return "Temporal.PlainTime"_s;
case TemporalType::ZonedDateTime:
return "Temporal.ZonedDateTime"_s;
case TemporalType::PlainYearMonth:
return "Temporal.PlainYearMonth"_s;
case TemporalType::PlainMonthDay:
return "Temporal.PlainMonthDay"_s;
case TemporalType::Duration:
return "Temporal.Duration"_s;
case TemporalType::None:
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();
}

static WTF::String temporalDisplayString(JSC::JSGlobalObject* globalObject, JSC::JSCell* cell, TemporalType type)
{
switch (type) {
case TemporalType::Instant:
return uncheckedDowncast<JSC::TemporalInstant>(cell)->toString();
case TemporalType::PlainDateTime:
return uncheckedDowncast<JSC::TemporalPlainDateTime>(cell)->toString();
case TemporalType::PlainDate:
return uncheckedDowncast<JSC::TemporalPlainDate>(cell)->toString();
case TemporalType::PlainTime:
return uncheckedDowncast<JSC::TemporalPlainTime>(cell)->toString();
case TemporalType::ZonedDateTime:
return zonedDateTimeDisplayString(globalObject, uncheckedDowncast<JSC::TemporalZonedDateTime>(cell));
case TemporalType::PlainYearMonth:
return uncheckedDowncast<JSC::TemporalPlainYearMonth>(cell)->toString();
case TemporalType::PlainMonthDay:
return uncheckedDowncast<JSC::TemporalPlainMonthDay>(cell)->toString();
case TemporalType::Duration:
return uncheckedDowncast<JSC::TemporalDuration>(cell)->toString(globalObject);
case TemporalType::None:
RELEASE_ASSERT_NOT_REACHED();
}
}

} // namespace Bun

extern "C" [[ZIG_EXPORT(nothrow)]] Bun::TemporalType Bun__JSValue__temporalObjectType(JSC::EncodedJSValue encodedValue)
{
return Bun::temporalObjectType(JSC::JSValue::decode(encodedValue));
}

// Precondition: value is Temporal. Writes e.g. ("Temporal.PlainDate", "2020-01-02").
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);
JSC::JSValue value = JSC::JSValue::decode(encodedValue);
Bun::TemporalType type = Bun::temporalObjectType(value);
WTF::String string = Bun::temporalDisplayString(globalObject, value.asCell(), type);
RETURN_IF_EXCEPTION(scope, );
*label = Bun::toStringView(Bun::temporalLabel(type));
*text = Bun::toStringRef(string);
}

JSC_DEFINE_HOST_FUNCTION(jsFunctionTemporalLabel, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
Bun::TemporalType type = Bun::temporalObjectType(callFrame->argument(0));
if (type == Bun::TemporalType::None)
return JSC::JSValue::encode(JSC::jsUndefined());
return JSC::JSValue::encode(JSC::jsNontrivialString(JSC::getVM(globalObject), Bun::temporalLabel(type)));
}

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);
Bun::TemporalType type = Bun::temporalObjectType(value);
if (type == Bun::TemporalType::None)
return JSC::JSValue::encode(JSC::jsUndefined());

WTF::String result = Bun::temporalDisplayString(globalObject, value.asCell(), type);
RETURN_IF_EXCEPTION(scope, {});
return JSC::JSValue::encode(JSC::jsString(vm, WTF::move(result)));
}
Loading