diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index 55fbd574eb..987bd06542 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -210,7 +210,12 @@ perry-diagnostics = { path = "../perry-diagnostics", optional = true } # our code is binding glue per type. `compiled_data` vendors the IANA tz DB # hermetically (needed by ZonedDateTime / Now), `sys-local` adds the # current-system-zone lookup used by Temporal.Now.*ISO() with no argument. -temporal_rs = { version = "0.2.3", default-features = false, features = ["std", "compiled_data", "sys-local"], optional = true } +# NOTE: NO `sys-local` — that feature resolves the system time zone via +# `iana_time_zone`, which links CoreFoundation on macOS and (because Temporal's +# namespace is always registered) forced `-framework CoreFoundation` into every +# output binary. Perry supplies its own CF-free host system in `temporal::now` +# (clock via `SystemTime`, zone via `crate::date::host_time_zone_name`). +temporal_rs = { version = "0.2.3", default-features = false, features = ["std", "compiled_data"], optional = true } serde.workspace = true serde_json.workspace = true diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index 6489a38276..63ab824fec 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -25,8 +25,15 @@ pub(crate) const GC_LAYOUT_ALL_POINTERS: u16 = 0x2000; // canonical raw-f64 / pointer layout is known-valid — and cleared whenever that // descriptor is removed. Every downgrade routes through `layout_set_typed_unknown` // or the `layout_*` remove helpers below, all of which clear it, so the invariant -// intact bit set ⟹ TYPED_LAYOUTS holds this object's canonical descriptor -// holds at all times. The descriptor's raw-f64 mask is exactly the compile-time +// intact bit set ⟹ a canonical typed descriptor exists for this object, +// either per-object in `TYPED_LAYOUTS` OR (the #6893 common +// case) shared by shape in `SHAPE_LAYOUTS`, keyed by the +// object's `keys_array` +// holds at all times. (Before #6893 the descriptor was always the per-object +// `TYPED_LAYOUTS` entry; `shape_install_shared` now sets the bit while routing +// same-shape objects through the shared map, so the bit no longer implies a +// per-object entry — only that *some* descriptor is reachable.) The descriptor's +// raw-f64 mask is exactly the compile-time // canonical mask codegen emits for the class, so combined with a class_id/ // keys_array match the codegen-inlined class-field shape guard can conclude // "slot K is raw-f64" from this single bit — no cross-crate guard call, no @@ -60,7 +67,7 @@ pub(super) fn clear_typed_layout_intact_for_user(user_ptr: usize) { } } -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub(super) enum LayoutSlotMask { Inline(u64), Heap(Vec), @@ -275,7 +282,7 @@ impl LayoutSlotMask { } } -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub(super) struct TypedLayoutDescriptor { pub(super) slot_count: usize, pub(super) raw_f64_mask: LayoutSlotMask, @@ -293,6 +300,139 @@ thread_local! { pub(super) static TRACE_SLOT_READS: Cell = const { Cell::new(0) }; } +// #6893: SHAPE-keyed canonical typed layout. Replaces the per-OBJECT +// TYPED_LAYOUTS + LAYOUT_SLOT_MASKS storage for the common case where an +// object's live layout matches its shape (header `GC_OBJ_TYPED_LAYOUT_INTACT`). +// Keyed by the shared `keys_array` pointer — all same-shape objects share ONE +// canonical keys array ("shared keys_array IS a shape"), so this is O(shapes), +// not O(objects). Measured: object churn stores a per-object descriptor for +// every one of ~2M `{v,w}` objects (all identical) → ~392 MB; keying by the +// (single) shared keys_array collapses that to one entry (churn peak RSS +// 830→262 MB, behaviour-identical). +// +// Value `None` = AMBIGUOUS: two live layouts share the same key NAMES but +// different value TYPES (`{v:1,w:2}` vs `{v:"a",w:"b"}`); those objects fall +// back to the per-object maps. ACCELERATOR ONLY: a miss, a stale entry +// (keys_array relocated/recycled by a moving GC), an ambiguous shape, or a +// field-count mismatch all fall back to the per-object map and then the +// conservative scan — never a wrong descriptor (mirrors the ShapeTable trust +// model). Nothing to prune on object death (entries are per-shape, shared). +thread_local! { + static SHAPE_LAYOUTS: RefCell>> = + RefCell::new(crate::fast_hash::new_ptr_hash_map()); +} + +fn shape_layout_keyed_enabled() -> bool { + use std::sync::OnceLock; + static E: OnceLock = OnceLock::new(); + // Default ON; `PERRY_SHAPE_LAYOUT_KEYED=0` restores the per-object maps + // (A/B validation). + *E.get_or_init(|| { + std::env::var("PERRY_SHAPE_LAYOUT_KEYED") + .map(|v| v != "0") + .unwrap_or(true) + }) +} + +/// keys_array only exists on genuine shaped objects (`ObjectFields`). Arrays, +/// closures, RegExps etc. also flow through `layout_note_slot` / +/// `layout_visit_pointer_slots`, and reading `ObjectHeader::keys_array` off one +/// would interpret unrelated payload bytes as a pointer. Returns 0 for anything +/// that is not an ObjectFields object (⟹ callers skip the shared shape path). +#[inline] +unsafe fn object_keys_array_ptr(user_ptr: usize) -> usize { + if user_ptr < GC_HEADER_SIZE + 0x1000 { + return 0; + } + let header = header_from_user_ptr(user_ptr as *const u8); + if gc_type_layout_slot_kind((*header).obj_type) != GcLayoutSlotKind::ObjectFields { + return 0; + } + (*(user_ptr as *const crate::object::ObjectHeader)).keys_array as usize +} + +/// The shared canonical descriptor for `user_ptr`'s shape, if shape-keying is +/// on, the object carries a keys_array, and the shape is unambiguous (`Some`). +#[inline] +unsafe fn shape_shared_descriptor(user_ptr: usize) -> Option { + if !shape_layout_keyed_enabled() { + return None; + } + let keys = object_keys_array_ptr(user_ptr); + if keys == 0 { + return None; + } + let desc = SHAPE_LAYOUTS.with(|m| m.borrow().get(&keys).and_then(|e| e.clone()))?; + // Defense-in-depth: the descriptor's `slot_count` is pinned to the owning + // object's `field_count` at install (`init_typed_shape_layout` rejects a + // mismatch). A differing current field_count means this object's shape is + // not the one the descriptor describes — e.g. a keys_array address reused by + // a shape with a different field count (moving-GC relocation before the new + // address is re-installed). Fall back (per-object → conservative). + let field_count = (*(user_ptr as *const crate::object::ObjectHeader)).field_count as usize; + if desc.slot_count != field_count { + return None; + } + Some(desc) +} + +/// Trace-path helper: pointer mask for a SIDE_MASK object with no per-object +/// mask entry. Returns the shape's canonical pointer mask iff the object is +/// still INTACT (⟹ it was registered against the shared shape descriptor, not +/// a diverged per-object mask). +#[inline] +unsafe fn shape_shared_pointer_mask( + user_ptr: usize, + header: *const GcHeader, +) -> Option { + if (*header)._reserved & GC_OBJ_TYPED_LAYOUT_INTACT == 0 { + return None; + } + shape_shared_descriptor(user_ptr).map(|d| d.pointer_mask) +} + +/// Install `descriptor` as the canonical layout for `keys` and set the object's +/// header state (INTACT + POINTER_FREE/SIDE_MASK), WITHOUT any per-object map +/// entry. Returns `true` if the object now rides the shared shape descriptor; +/// `false` if the shape is ambiguous (caller falls back to per-object). +unsafe fn shape_install_shared( + keys: usize, + header: *mut GcHeader, + descriptor: &TypedLayoutDescriptor, +) -> bool { + let mut shared_ok = false; + SHAPE_LAYOUTS.with(|m| { + let mut m = m.borrow_mut(); + match m.get(&keys) { + None => { + m.insert(keys, Some(descriptor.clone())); + shared_ok = true; + } + Some(Some(existing)) if existing == descriptor => { + shared_ok = true; + } + Some(Some(_)) => { + // Same keys, different layout ⟹ ambiguous. Poison the entry so + // future lookups (and any still-INTACT siblings) fall back. + m.insert(keys, None); + shared_ok = false; + } + Some(None) => { + shared_ok = false; // already ambiguous + } + } + }); + if shared_ok { + header_set_typed_layout_intact(header); + if descriptor.pointer_mask.is_empty() { + set_layout_state(header, GC_LAYOUT_POINTER_FREE); + } else { + set_layout_state(header, GC_LAYOUT_SIDE_MASK); + } + } + shared_ok +} + pub(super) unsafe fn header_from_user_ptr(user_ptr: *const u8) -> *mut GcHeader { (user_ptr as *mut u8).sub(GC_HEADER_SIZE) as *mut GcHeader } @@ -452,11 +592,12 @@ pub(crate) fn layout_clear_for_ptr(user_ptr: usize) { } /// True when `user_ptr`'s object currently has a canonical `TypedLayoutDescriptor` -/// installed in `TYPED_LAYOUTS`. Reads the O(1) `GC_OBJ_TYPED_LAYOUT_INTACT` -/// header bit instead of probing the thread-local map: the bit is maintained in -/// lock-step with every map insert/remove (intact set ⟺ descriptor present — see -/// the invariant documented on `GC_OBJ_TYPED_LAYOUT_INTACT`), so it answers the -/// same question without a per-call TLS hashmap touch. This is on the dynamic +/// — per-object in `TYPED_LAYOUTS` or (the #6893 common case) shared by shape in +/// `SHAPE_LAYOUTS`. Reads the O(1) `GC_OBJ_TYPED_LAYOUT_INTACT` header bit +/// instead of probing either map: the bit is maintained in lock-step with +/// descriptor install/removal (intact set ⟹ *some* descriptor is reachable — +/// see the invariant on `GC_OBJ_TYPED_LAYOUT_INTACT`), so it answers the same +/// question without a per-call TLS hashmap touch. This is on the dynamic /// object-store hot path via `mark_object_dynamic_shape_unknown` (#5094). pub(crate) fn layout_has_typed_descriptor(user_ptr: usize) -> bool { layout_typed_intact_for_user(user_ptr) @@ -495,16 +636,23 @@ pub(crate) fn layout_note_slot(parent_user: usize, slot_index: usize, value_bits // The canonical typed-shape descriptor probe below is a thread-local // hashmap lookup, paid on every field/element store. Gate it on the // O(1) `GC_OBJ_TYPED_LAYOUT_INTACT` header bit: that bit is set and - // cleared in lock-step with every `TYPED_LAYOUTS` insert/remove (see the - // invariant documented on `GC_OBJ_TYPED_LAYOUT_INTACT`), so a clear bit - // proves the map has no entry for this object — the probe would return - // `None` and fall through to the pointer-mask path below. Skipping it - // removes the per-write TLS touch on the common dynamic-shape / - // pointer-free object and array store path (#5094). The inner `if let` + // cleared in lock-step with descriptor install/removal (per-object in + // `TYPED_LAYOUTS` or, since #6893, shared by shape in `SHAPE_LAYOUTS` — + // see the invariant on `GC_OBJ_TYPED_LAYOUT_INTACT`), so a clear bit + // proves neither map has a descriptor for this object — the probe would + // return `None` and fall through to the pointer-mask path below. + // Skipping it removes the per-write TLS touch on the common dynamic-shape + // / pointer-free object and array store path (#5094). The inner `if let` // still tolerates a `None` defensively, so a transiently desynced bit // can only cost an extra fall-through, never mis-track a slot. if (*header)._reserved & GC_OBJ_TYPED_LAYOUT_INTACT != 0 { - if let Some(typed) = TYPED_LAYOUTS.with(|m| m.borrow().get(&parent_user).cloned()) { + // #6893: per-object descriptor (diverged/ambiguous objects) OR the + // shared shape descriptor (the common INTACT case). Exactly one is + // present for an INTACT object. + let typed = TYPED_LAYOUTS + .with(|m| m.borrow().get(&parent_user).cloned()) + .or_else(|| shape_shared_descriptor(parent_user)); + if let Some(typed) = typed { if slot_index >= typed.slot_count { layout_set_typed_unknown(header, parent_user); return; @@ -683,6 +831,22 @@ unsafe fn init_typed_shape_layout( raw_f64_mask, pointer_mask: pointer_mask.clone(), }; + // #6893: try the O(shapes) shared shape descriptor (keyed by the canonical + // keys_array) before per-object storage. + let keys = if shape_layout_keyed_enabled() { + object_keys_array_ptr(user_ptr) + } else { + 0 + }; + if keys != 0 && shape_install_shared(keys, header, &descriptor) { + TYPED_LAYOUTS.with(|m| { + m.borrow_mut().remove(&user_ptr); + }); + LAYOUT_SLOT_MASKS.with(|m| { + m.borrow_mut().remove(&user_ptr); + }); + return; + } TYPED_LAYOUTS.with(|m| { m.borrow_mut().insert(user_ptr, descriptor); }); @@ -788,6 +952,21 @@ pub extern "C" fn js_gc_init_unboxed_object_layout( raw_f64_mask, pointer_mask: pointer_mask.clone(), }; + // #6893: shared shape descriptor before per-object storage. + let keys = if shape_layout_keyed_enabled() { + object_keys_array_ptr(user_ptr) + } else { + 0 + }; + if keys != 0 && shape_install_shared(keys, header, &descriptor) { + TYPED_LAYOUTS.with(|m| { + m.borrow_mut().remove(&user_ptr); + }); + LAYOUT_SLOT_MASKS.with(|m| { + m.borrow_mut().remove(&user_ptr); + }); + return; + } TYPED_LAYOUTS.with(|m| { m.borrow_mut().insert(user_ptr, descriptor); }); @@ -937,7 +1116,9 @@ pub(super) fn layout_visit_pointer_slots( } return true; } - let mask = LAYOUT_SLOT_MASKS.with(|m| m.borrow().get(&user_ptr).cloned()); + let mask = LAYOUT_SLOT_MASKS + .with(|m| m.borrow().get(&user_ptr).cloned()) + .or_else(|| shape_shared_pointer_mask(user_ptr, header)); let Some(mask) = mask else { set_layout_state(header, GC_LAYOUT_UNKNOWN); return false; @@ -1245,7 +1426,9 @@ pub(super) unsafe fn heap_payload_slot_selection( raw_numeric_recorded: false, }; } - let mask = LAYOUT_SLOT_MASKS.with(|m| m.borrow().get(&user_ptr).cloned()); + let mask = LAYOUT_SLOT_MASKS + .with(|m| m.borrow().get(&user_ptr).cloned()) + .or_else(|| shape_shared_pointer_mask(user_ptr, header)); match mask { Some(mask) => HeapPayloadSlotSelection::Masked { mask, diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 768b6106cb..1b9dcf7315 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -129,6 +129,16 @@ pub(super) fn gc_collect_minor_with_trigger(trigger: GcTriggerSnapshot) -> GcCol restore_minor_in_alloc(prev_in_alloc); return outcome; } + // #6893-followup: major-GC pacing. A non-moving minor can't free array-growth + // forwarding stubs, so reallocation-heavy churn grows the arena unbounded — + // only a full mark-sweep reclaims stubs. Escalate to a full once the arena's + // live bytes exceed K× the last full's live set (belt-and-suspenders for + // callers that reach a minor outside the budgeted pressure path). + if arena_growth_full_escalation_due() { + let outcome = gc_collect_full_mark_sweep_with_trigger(GcTriggerSnapshot::capture(trigger.kind)); + restore_minor_in_alloc(prev_in_alloc); + return outcome; + } let mut trace = GcCycleTrace::new(GcCollectionKind::Minor, trigger); let start = Instant::now(); crate::arena::old_pages_begin_gc_cycle(); diff --git a/crates/perry-runtime/src/gc/policy.rs b/crates/perry-runtime/src/gc/policy.rs index 64fdd03118..961ed576e8 100644 --- a/crates/perry-runtime/src/gc/policy.rs +++ b/crates/perry-runtime/src/gc/policy.rs @@ -533,6 +533,9 @@ thread_local! { const { Cell::new(DeferredGcRequest::None) }; pub(super) static GC_OLD_RECLAIM_PENDING: Cell = const { Cell::new(false) }; pub(super) static GC_LAST_OLD_RECLAIM_IN_USE_BYTES: Cell = const { Cell::new(0) }; + /// Total arena in-use bytes measured right after the last FULL mark-sweep — + /// the baseline for major-GC pacing (`arena_growth_full_escalation_due`). + pub(super) static GC_LAST_FULL_ARENA_IN_USE_BYTES: Cell = const { Cell::new(0) }; /// Re-entrancy guard for the #5476 direct old-gen reclaim driven from /// `gc_check_trigger`: the full collection must not recursively trigger /// another reclaim if a hook it runs allocates. @@ -868,6 +871,10 @@ pub(super) fn finish_full_old_reclaim_baseline() { let old_in_use = crate::arena::old_gen_in_use_bytes().saturating_add(external_side_live_bytes()); GC_LAST_OLD_RECLAIM_IN_USE_BYTES.with(|bytes| bytes.set(old_in_use)); + // Record the TOTAL post-full live set for major-GC pacing (young+old): the + // full sweep is the only collection that frees forwarding stubs, so this is + // the "clean" size the arena returns to and the base for the K× growth gate. + GC_LAST_FULL_ARENA_IN_USE_BYTES.with(|bytes| bytes.set(crate::arena::arena_in_use_bytes())); GC_OLD_RECLAIM_PENDING.with(|pending| pending.set(false)); } @@ -1564,6 +1571,49 @@ pub(super) fn test_start_budgeted_minor_fallback_state_with_trace( cycle.state } +/// #6893-followup: major-GC pacing. A non-moving minor sweep cannot free +/// array-growth forwarding stubs (`Array.prototype.push` reallocations leave a +/// stub per growth), so churn that grows arrays accumulates stubs that pin every +/// arena block → unbounded RSS; only a FULL mark-sweep reclaims them. Escalate a +/// minor to a full once the arena's live bytes exceed K× the clean live set +/// measured after the last full. Gated by an absolute floor so small heaps never +/// pay for a full, and by the K× ratio so a workload with a legitimately large +/// *stable* live set (retain-style) does not over-escalate — its arena hovers +/// near its own baseline, well under K×. +pub(super) fn arena_growth_full_escalation_due() -> bool { + // Config is parsed ONCE — this runs on the minor-GC path, so no per-call + // env lookup / parse / String alloc. The env vars are for tuning and + // measurement (read at process start); defaults chosen so churn oscillates + // ~baseline..2×baseline and stays below node's peak. + use std::sync::OnceLock; + static CONFIG: OnceLock<(usize, usize)> = OnceLock::new(); + let &(floor_bytes, growth_num) = CONFIG.get_or_init(|| { + const DEFAULT_FLOOR_MB: usize = 32; + const DEFAULT_GROWTH_NUM: usize = 2; + let floor_bytes = std::env::var("PERRY_GC_MAJOR_PACING_FLOOR_MB") + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(DEFAULT_FLOOR_MB) + .saturating_mul(1024 * 1024); + let growth_num = std::env::var("PERRY_GC_MAJOR_PACING_GROWTH") + .ok() + .and_then(|s| s.trim().parse::().ok()) + .filter(|&n| n >= 1) + .unwrap_or(DEFAULT_GROWTH_NUM); + (floor_bytes, growth_num) + }); + if floor_bytes == 0 { + return false; // PERRY_GC_MAJOR_PACING_FLOOR_MB=0 disables the pacing + } + let in_use = crate::arena::arena_in_use_bytes(); + if in_use < floor_bytes { + return false; + } + let baseline = GC_LAST_FULL_ARENA_IN_USE_BYTES.with(|bytes| bytes.get()); + // No full yet (baseline 0): bound the initial growth once we clear the floor. + baseline == 0 || in_use > baseline.saturating_mul(growth_num) +} + fn gc_start_budgeted_cycle_for_pressure(progress_kind: GcProgressKind) -> Option { let trigger = gc_budgeted_due_trigger()?; GC_TRIGGER_BUMPED.with(|c| c.set(false)); @@ -1580,7 +1630,10 @@ fn gc_start_budgeted_cycle_for_pressure(progress_kind: GcProgressKind) -> Option let rebaseline = BudgetedGcRebaseline::ArenaBytes { pre_in_use: crate::arena::arena_in_use_bytes(), }; - if gen_gc_enabled() { + // Major-GC pacing: escalate to a full when arena live-bytes grew + // past K× the last full's live set — the non-moving minor can't free + // array-growth forwarding stubs (see `arena_growth_full_escalation_due`). + if gen_gc_enabled() && !arena_growth_full_escalation_due() { gc_start_budgeted_minor_fallback_cycle( GcTriggerKind::ArenaBytes, rebaseline, @@ -1594,7 +1647,8 @@ fn gc_start_budgeted_cycle_for_pressure(progress_kind: GcProgressKind) -> Option let rebaseline = BudgetedGcRebaseline::MallocCount { pre_count: malloc_object_count(), }; - if gen_gc_enabled() { + // Major-GC pacing (malloc-count trigger twin of the ArenaBytes branch). + if gen_gc_enabled() && !arena_growth_full_escalation_due() { gc_start_budgeted_minor_fallback_cycle( GcTriggerKind::MallocCount, rebaseline, diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 395812d10f..bd6dab2b76 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -1943,6 +1943,19 @@ unsafe fn set_object_keys_array(obj: *mut ObjectHeader, keys_array: *mut ArrayHe { (*obj).parent_class_id = 0; } + // #6893: the object's typed-shape layout descriptor is keyed by its + // keys_array (shared per shape via SHAPE_LAYOUTS). A keys_array pointer + // change is a shape change (add/delete key), so the exact typed layout no + // longer applies to THIS object. Pre-#6893 the per-object store-validation + // (`layout_note_slot`, keyed by the object address) caught this implicitly + // during the field shuffle; the shared descriptor lookup now misses on the + // NEW keys_array, so that trigger is lost — invalidate explicitly here. + // Gated: `mark_object_dynamic_shape_unknown` early-returns for objects that + // carry no typed layout, so plain/growing objects and initial construction + // (INTACT not yet set) pay nothing. + if (*obj).keys_array != keys_array { + mark_object_dynamic_shape_unknown(obj); + } // GC_STORE_AUDIT(BARRIERED): keys_array pointer field is followed by an object-slot barrier. (*obj).keys_array = keys_array; crate::gc::runtime_write_barrier_slot( diff --git a/crates/perry-runtime/src/temporal/now.rs b/crates/perry-runtime/src/temporal/now.rs index 42fc18d496..280a8093f8 100644 --- a/crates/perry-runtime/src/temporal/now.rs +++ b/crates/perry-runtime/src/temporal/now.rs @@ -1,12 +1,59 @@ -//! `Temporal.Now` — wraps [`temporal_rs::Temporal::local_now`] (#4689). +//! `Temporal.Now` — a namespace of method thunks reading the host clock/zone //! //! A namespace (not a constructor), like `Math`: a plain object of method -//! thunks. Each call reads the host clock fresh via `Temporal::local_now()` -//! (the `sys-local` feature supplies the system time zone + clock). +//! thunks. Each call reads the host clock fresh via `perry_now()`, backed by +//! [`PerryHostSystem`] — perry's own host system (std `SystemTime` clock, +//! `crate::date::host_time_zone_name` for the zone). We do NOT use temporal_rs's +//! `sys-local` feature, which would pull `iana_time_zone` (CoreFoundation). use super::dispatch::{self, ok_or_throw, raw_arg, string}; use super::{alloc_temporal_cell, TemporalValue}; -use temporal_rs::{Temporal, TimeZone}; +use temporal_rs::host::{HostClock, HostHooks, HostTimeZone}; +use temporal_rs::provider::TimeZoneProvider; +use temporal_rs::unix_time::EpochNanoseconds; +use temporal_rs::now::Now; +use temporal_rs::{TemporalError, TemporalResult, TimeZone}; + +/// Perry's own `Temporal.Now` host system, replacing temporal_rs's +/// `LocalHostSystem` (its `sys-local` feature). `LocalHostSystem` resolved the +/// system zone via `iana_time_zone::get_timezone`, which links CoreFoundation on +/// macOS — and because Temporal's namespace is registered in the always-linked +/// runtime init, that CF dependency was dragged into EVERY output binary, +/// forcing `-framework CoreFoundation` even on otherwise libSystem-only +/// runtime-only programs (they'd fail to link with undefined `_CFRelease` etc.). +/// Perry already resolves the host zone itself — `crate::date::host_time_zone_name` +/// via `TZ` / `/etc/localtime`, no CF — so we drop `sys-local` and feed the zone +/// and clock through temporal_rs's public `HostHooks` traits. Net: no binary +/// links CoreFoundation for time zones; `Temporal.Now.*` is unchanged. +struct PerryHostSystem; + +impl HostClock for PerryHostSystem { + fn get_host_epoch_nanoseconds(&self) -> TemporalResult { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| TemporalError::general("Error fetching system time")) + .map(|d| EpochNanoseconds::from(d.as_nanos() as i128)) + } +} + +impl HostTimeZone for PerryHostSystem { + fn get_host_time_zone( + &self, + provider: &(impl TimeZoneProvider + ?Sized), + ) -> TemporalResult { + TimeZone::try_from_identifier_str_with_provider(crate::date::host_time_zone_name(), provider) + } +} + +impl HostHooks for PerryHostSystem {} + +/// `Temporal.Now` for perry's host system (drop-in for the removed +/// `temporal_rs::Temporal::local_now()`).. +#[inline] +fn perry_now() -> Now { + Now::new(PerryHostSystem) +} /// Resolve an optional time-zone argument (an IANA id string or a /// `Temporal.ZonedDateTime`) to a `TimeZone`, or `None` (absent / `undefined`) @@ -24,39 +71,39 @@ fn tz_arg(v: f64) -> Option { pub fn instant(_args: &[f64]) -> f64 { alloc_temporal_cell(TemporalValue::Instant(ok_or_throw( - Temporal::local_now().instant(), + perry_now().instant(), ))) } pub fn time_zone_id(_args: &[f64]) -> f64 { - let tz = ok_or_throw(Temporal::local_now().time_zone()); + let tz = ok_or_throw(perry_now().time_zone()); string(&ok_or_throw(tz.identifier())) } pub fn plain_date_time_iso(args: &[f64]) -> f64 { let tz = tz_arg(raw_arg(args, 0)); alloc_temporal_cell(TemporalValue::PlainDateTime(ok_or_throw( - Temporal::local_now().plain_date_time_iso(tz), + perry_now().plain_date_time_iso(tz), ))) } pub fn plain_date_iso(args: &[f64]) -> f64 { let tz = tz_arg(raw_arg(args, 0)); alloc_temporal_cell(TemporalValue::PlainDate(ok_or_throw( - Temporal::local_now().plain_date_iso(tz), + perry_now().plain_date_iso(tz), ))) } pub fn plain_time_iso(args: &[f64]) -> f64 { let tz = tz_arg(raw_arg(args, 0)); alloc_temporal_cell(TemporalValue::PlainTime(ok_or_throw( - Temporal::local_now().plain_time_iso(tz), + perry_now().plain_time_iso(tz), ))) } pub fn zoned_date_time_iso(args: &[f64]) -> f64 { let tz = tz_arg(raw_arg(args, 0)); alloc_temporal_cell(TemporalValue::ZonedDateTime(ok_or_throw( - Temporal::local_now().zoned_date_time_iso(tz), + perry_now().zoned_date_time_iso(tz), ))) }