Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "ddea71318fec9b923465c7c45ded8fa713ca3251";
export const WEBKIT_VERSION = "78d45d31843463b44445d670601708b3ff141337";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
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",
"JSC::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
12 changes: 12 additions & 0 deletions src/js_parser_jsc/Macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,18 @@ 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),
// No AST representation (same as Date); ClassInfo name is just "Object", so use the label.
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__temporalType(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,
}

/// `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 {
Expand Down
113 changes: 113 additions & 0 deletions src/jsc/bindings/Temporal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// 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/JSCJSValueInlines.h"
#include "JavaScriptCore/TemporalDuration.h"
#include "JavaScriptCore/TemporalInstant.h"
#include "JavaScriptCore/TemporalObject.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"

namespace Bun {

using JSC::TemporalType;

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

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 uncheckedDowncast<JSC::TemporalZonedDateTime>(cell)->toString(globalObject);
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)]] JSC::TemporalType Bun__JSValue__temporalType(JSC::EncodedJSValue encodedValue)
{
return JSC::temporalType(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);
JSC::TemporalType type = JSC::temporalType(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))
{
JSC::TemporalType type = JSC::temporalType(callFrame->argument(0));
if (type == JSC::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);
JSC::TemporalType type = JSC::temporalType(value);
if (type == JSC::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)));
}
8 changes: 8 additions & 0 deletions src/jsc/bindings/Temporal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#pragma once

#include "root.h"

// (value) -> "Temporal.PlainDate" etc., or undefined if not Temporal.
JSC_DECLARE_HOST_FUNCTION(jsFunctionTemporalLabel);
// (value) -> the slot-derived default-options toString() text, or undefined if not Temporal.
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/TemporalPlainTime.h"
#include "JavaScriptCore/TemporalPlainYearMonth.h"
#include "JavaScriptCore/TemporalZonedDateTime.h"
#include "JavaScriptCore/TemporalObject.h"
#include "JavaScriptCore/TimeZoneICUBridge.h"

#include "JavaScriptCore/FunctionPrototype.h"
Expand Down Expand Up @@ -1175,18 +1176,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 @@ -1235,7 +1224,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 (JSC::temporalType(o2) != JSC::TemporalType::None)
return false;
return std::nullopt;
}
Expand Down
1 change: 1 addition & 0 deletions src/jsc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading