Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,17 @@ perry-dispatch = { path = "crates/perry-dispatch" }
# Cargo.toml, and external users never need perry-runtime in their
# Cargo graph (Perry's compiler driver links `libperry_runtime.a`
# into the final binary directly).
perry-runtime = { path = "crates/perry-runtime", version = "0.5.1011" }
# `default-features = false` so dependency EDGES (perry-stdlib, ext crates, the
# perry binary) don't force perry-runtime's heavy default features (`full`,
# `regex-engine`, `temporal`, `url-engine`, `string-normalize`, `intl-segmenter`)
# back on. The auto-optimize build selects perry-runtime with
# `--no-default-features` and re-adds only what a given program needs (see
# optimized_libs.rs), so an edge requesting defaults would defeat that (cargo
# unifies features additively). A plain `cargo build`/`cargo test --workspace`
# still builds perry-runtime as a *selected* package, so its own `default`
# applies — tests and the shipped prebuilt keep every engine. This mirrors why
# `wasm-host` must stay out of `default`, generalized to all heavy features.
perry-runtime = { path = "crates/perry-runtime", version = "0.5.1011", default-features = false }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
perry-ffi = { path = "crates/perry-ffi", version = "0.5.1011" }
perry-ext-dotenv = { path = "crates/perry-ext-dotenv" }
perry-ext-nanoid = { path = "crates/perry-ext-nanoid" }
Expand Down
58 changes: 50 additions & 8 deletions crates/perry-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,49 @@ description = "Runtime library: GC, JSValue, builtins"
crate-type = ["rlib", "staticlib"]

[features]
default = ["full"]
# `default` keeps the shipped prebuilt libperry_runtime.a and plain
# `cargo build`/test full-featured. The auto-optimize path builds with
# `--no-default-features` and re-adds only the features a given program
# actually needs (see optimized_libs.rs), so the heavy subsystems below
# (regex engine, Temporal, URL/IDNA, normalize, segmenter) are *opt-in per
# app* and absent from binaries that never use them.
default = ["full", "regex-engine", "temporal", "url-engine", "string-normalize", "intl-segmenter"]
# The user's regular-expression engine (`regex` + `fancy-regex`, ~1.2 MB of
# DFA/NFA machinery). A program that never evaluates a regex literal, `RegExp`,
# a regex-coercing string method, or a glob API can't produce a RegExp at
# runtime, so the compiler leaves this off and the engine is never linked. The
# RegExp object's identity/display layer (header, `is_regex_pointer`, `toString`)
# stays always compiled, so console-formatting / value-to-string paths keep
# working with no engine present.
regex-engine = ["dep:regex", "dep:fancy-regex"]
# The TC39 `Temporal.*` API (`temporal_rs` + its transitive tz/calendar deps:
# jiff-tzdb, icu_calendar, timezone_provider, calendrical_calculations —
# ~580 KB). Independent of JS `Date` (which has its own `date.rs` impl), so a
# program that uses `Date` but never `Temporal.*` links none of this. The
# compiler enables it on `Temporal` usage; the Temporal cell's identity layer
# (is_temporal_value / GC class-id range) stays compiled so value
# formatting/JSON/instanceof on non-Temporal values keep working.
temporal = ["dep:temporal_rs"]
# WHATWG URL host canonicalization: the `url` crate's full host parser (IPv4
# shorthand / numeric hosts) + `idna` (Unicode domain ↔ punycode), ~195 KB
# incl. the transitive `percent_encoding`. Perry's URL parsing is otherwise
# hand-rolled (url/parse.rs), so the crates are only reached from 4 host/IDNA
# sites behind URL APIs (`new URL`, hostname setter, url.domainTo*, legacy
# url.resolve). A program that never uses a URL API links none of it; each gated
# site has a benign hand-rolled fallback. (`unicode-normalization`/`-segmentation`
# are NOT gated here — they're shared with Intl / String.normalize.)
url-engine = ["dep:url", "dep:idna"]
# `String.prototype.normalize` (NFC/NFD/NFKC/NFKD) via `unicode-normalization`
# (~113 KB of Unicode decomposition/composition tables). Only `string/compare.rs`
# uses it; a program that never calls `.normalize()` links none of it. (This is
# the *old* `unicode-normalization` crate, distinct from `idna`'s `icu_normalizer`,
# so it's independent of `url-engine`.)
string-normalize = ["dep:unicode-normalization"]
# `Intl.Segmenter` (UAX #29 grapheme/word/sentence segmentation) via
# `unicode-segmentation` (~73 KB) — the grapheme path backs string-width@7+/
# wrap-ansi@9+ (and thus ink). Only `intl.rs` uses it; a program that never
# constructs an `Intl.Segmenter` links none of it.
intl-segmenter = ["dep:unicode-segmentation"]
# `full` only opt-ins the small Node-API helpers (os.hostname / os.homedir).
# `postgres`, `redis`, `whoami` were previously listed here but were either
# unimported (postgres, whoami) or only used by a now-deleted `redis_client.rs`
Expand Down Expand Up @@ -50,11 +92,11 @@ thiserror.workspace = true
anyhow.workspace = true
libc = "0.2"
rand = "0.8"
regex = "1"
regex = { version = "1", optional = true }
# Taffy — flexbox / grid layout engine for the perry/tui module
# (#358 Phase 3). Same crate Bevy and Dioxus use; pure Rust, no FFI.
taffy = { version = "0.7", default-features = false, features = ["std", "flexbox", "taffy_tree"] }
fancy-regex = "0.18"
fancy-regex = { version = "0.18", optional = true }
itoa = "1"
ryu = "1"
base64 = "0.22"
Expand All @@ -67,16 +109,16 @@ lazy_static = "1.4"
# 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"] }
temporal_rs = { version = "0.2.3", default-features = false, features = ["std", "compiled_data", "sys-local"], optional = true }

serde_json = "1"
unicode-normalization = "0.1"
unicode-normalization = { version = "0.1", optional = true }
# #4877: extended grapheme-cluster / word / sentence segmentation backing
# Intl.Segmenter (the grapheme path is what string-width@7+/wrap-ansi@9+ use,
# so it gates ink). Pure-Rust UAX #29 implementation, already in our lock graph.
unicode-segmentation = "1"
idna = "1"
url = "2"
unicode-segmentation = { version = "1", optional = true }
idna = { version = "1", optional = true }
url = { version = "2", optional = true }
# #4911: real node:dns resolve*/reverse. hickory-proto provides DNS wire-format
# encode/decode + record types; we drive it synchronously over a blocking
# std::net::UdpSocket (TCP fallback on truncation), so — unlike hickory-resolver
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/builtins/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ unsafe fn rel_to_primitive(value: f64) -> f64 {
// `TypeError` for every `Temporal.*` value (the spec bans relational ordering
// of Temporal values: `plainDate < plainDate` throws). Without this the cell
// fell through to the `DefaultString` arm and compared ISO strings silently.
#[cfg(feature = "temporal")]
if crate::temporal::is_temporal_value(value) {
return crate::temporal::dispatch::call_method(value, "valueOf", &[]);
}
Expand Down
32 changes: 26 additions & 6 deletions crates/perry-runtime/src/builtins/formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,28 @@ unsafe fn date_inspect_string(value: f64) -> String {
.to_string()
}

/// `util.inspect` arm for a Temporal cell: `Temporal.X <iso>` (or
/// `[object Object]` if the cell can't be read). Returns `None` when `addr` is
/// not a Temporal cell, so the caller's `else if let Some(..)` chain falls
/// through. Cfg-paired: with the Temporal engine gated off no cell can exist, so
/// the off twin is a constant `None` (and doesn't reference the gated module).
#[cfg(feature = "temporal")]
fn temporal_inspect_arm(addr: usize, value: f64) -> Option<String> {
if crate::temporal::is_temporal_cell_addr(addr) {
Some(
crate::temporal::temporal_inspect_string(value)
.unwrap_or_else(|| "[object Object]".to_string()),
)
} else {
None
}
}

#[cfg(not(feature = "temporal"))]
fn temporal_inspect_arm(_addr: usize, _value: f64) -> Option<String> {
None
}

/// Print multiple values from an array (console.log with spread support)
/// Takes a pointer to an ArrayHeader containing f64 values
/// Helper function to format a JSValue as a string (for spread arrays)
Expand Down Expand Up @@ -819,12 +841,11 @@ pub(crate) fn format_jsvalue(value: f64, depth: usize) -> String {
// `Invalid Date`). Handle before the GC-header object dispatch
// below, which would deref the 8-byte cell as an ObjectHeader.
date_inspect_string(value)
} else if crate::temporal::is_temporal_cell_addr(ptr as usize) {
} else if let Some(s) = temporal_inspect_arm(ptr as usize, value) {
// Temporal (#4686): `util.inspect` prints `Temporal.Duration
// <P1Y…>`. Handle before the GC-header object dispatch (the cell
// is smaller than an ObjectHeader).
crate::temporal::temporal_inspect_string(value)
.unwrap_or_else(|| "[object Object]".to_string())
s
} else if crate::value::addr_class::is_handle_band(ptr as usize) {
// Refs #421: Web Fetch (and other) handles are NaN-boxed
// POINTER_TAG values whose payload is a small registry id, NOT
Expand Down Expand Up @@ -1560,10 +1581,9 @@ fn format_jsvalue_for_json(value: f64, depth: usize) -> String {
// unquoted (or `Invalid Date`), not the 8-byte cell deref'd
// as an object.
date_inspect_string(value)
} else if crate::temporal::is_temporal_cell_addr(ptr as usize) {
} else if let Some(s) = temporal_inspect_arm(ptr as usize, value) {
// Temporal value inside an inspected object → `Temporal.X <iso>`.
crate::temporal::temporal_inspect_string(value)
.unwrap_or_else(|| "[object Object]".to_string())
s
} else if crate::value::addr_class::is_handle_band(ptr as usize) {
"[object Object]".to_string()
} else if crate::symbol::is_registered_symbol(ptr as usize)
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/builtins/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@ fn js_structured_clone_inner(value: f64) -> f64 {
// arena slot with GC_TYPE_OBJECT but tracked in
// REGEX_POINTERS). Clone by reading source/flags and
// building a fresh one via js_regexp_new.
#[cfg(feature = "regex-engine")]
if crate::regex::is_regex_pointer(ptr as *const u8) {
let re_ptr = ptr as *const crate::regex::RegExpHeader;
let src = crate::regex::js_regexp_get_source(re_ptr);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,7 @@ pub extern "C" fn js_date_value_of(timestamp: f64) -> f64 {
// hard `TypeError` (the spec bans implicit numeric coercion / ordering), so
// route a Temporal receiver to its brand dispatch, which throws — rather
// than returning the opaque cell as a pseudo-Date timestamp.
#[cfg(feature = "temporal")]
if crate::temporal::is_temporal_value(timestamp) {
return crate::temporal::dispatch::call_method(timestamp, "valueOf", &[]);
}
Expand Down
Loading