From 15024474120682b0c1b2073d32a46168ded103d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 08:58:18 +0200 Subject: [PATCH 1/3] =?UTF-8?q?fix(path):=20#7621=20=E2=80=94=20path.*=20a?= =?UTF-8?q?rms=20read=20an=20SSO=20string's=20inline=20bytes=20as=20a=20he?= =?UTF-8?q?ader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `path.resolve("/root", computedShortString)` threw ERR_INVALID_ARG_TYPE where node returns the path. Every `path.*` codegen arm unboxed its operand with `unbox_to_i64` (`bitcast double -> i64; and POINTER_MASK`) and handed the low 48 bits to a runtime entry that dereferences them as `*const StringHeader`. That is the header for a HEAP string and the CHARACTERS for a small-string-optimized one (SHORT_STRING_TAG, <= SHORT_STRING_MAX_LEN = 5 inline bytes) — so a literal worked (interned onto the heap) and a computed short string did not. Bisected by length: 5 bytes throws, 6 works. Twelve arms were affected, not the five the issue named: resolve, resolve's fold step, join, win32.join, normalize, extname, dirname, basename, basename(p, ext), isAbsolute, parse, matchesGlob, plus the win32 sub-namespace equivalents. `parse` and `matchesGlob` were SILENTLY wrong rather than throwing. Single-operand arms now call `js_path_arg_header`, which materializes only the SSO case and reproduces the old mask bit for bit for heap strings AND every non-string — so each entry point keeps its own established non-string behaviour (throw, or `unwrap_or_default`). It is deliberately not `js_get_string_pointer_unified`, which coerces numbers to strings and would turn `path.isAbsolute(5)` from a throw into `false`. Two-operand arms take both operands NaN-boxed and unbox inside one runtime call (`js_path_*_value`). Codegen cannot close that window itself: materializing the first operand allocates, and `rooting::with_operands_rooted` hands the lowering registers rather than slots, so the second operand's register is stale with no re-read to reach for. The runtime entry roots the first operand across the second's materialization via `RuntimeHandle::across_const`. Validated locally: the new gap test is byte-identical to node 26.5.1 and exits 0 (it threw at line 2 before the fix); byte-identical again under PERRY_GC_ZEAL=1 + PERRY_GC_PROTECT_FROMSPACE=1 with 402k copying minors observed; test_parity_path, test_gap_node_path, test_gap_6371_path_lexical_semantics, test_gap_node_fs and test_cli_simulation unchanged. --- .../perry-codegen/src/expr/array_methods.rs | 6 +- crates/perry-codegen/src/expr/arrays_finds.rs | 26 +- .../perry-codegen/src/expr/instance_misc1.rs | 91 ++++-- crates/perry-codegen/src/expr/misc_methods.rs | 3 +- .../src/runtime_decls/strings.rs | 14 + crates/perry-runtime/src/path.rs | 3 + crates/perry-runtime/src/path/value_args.rs | 287 ++++++++++++++++++ test-files/test_gap_7621_path_sso_operands.ts | 106 +++++++ 8 files changed, 497 insertions(+), 39 deletions(-) create mode 100644 crates/perry-runtime/src/path/value_args.rs create mode 100644 test-files/test_gap_7621_path_sso_operands.ts diff --git a/crates/perry-codegen/src/expr/array_methods.rs b/crates/perry-codegen/src/expr/array_methods.rs index 490200a2eb..93638003ea 100644 --- a/crates/perry-codegen/src/expr/array_methods.rs +++ b/crates/perry-codegen/src/expr/array_methods.rs @@ -278,17 +278,19 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Expr::StaticPluginResolve(_) => Ok(double_literal(0.0)), // -------- More cheap stubs -------- + // #7621: `js_path_arg_header`, not `unbox_to_i64` — the plain mask hands + // an SSO string's inline CHARACTERS to a `*StringHeader` consumer. Expr::PathNormalize(p) => { let p_box = lower_expr(ctx, p)?; let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); + let p_handle = blk.call(I64, "js_path_arg_header", &[(DOUBLE, &p_box)]); let result = blk.call(I64, "js_path_normalize", &[(I64, &p_handle)]); Ok(nanbox_string_inline(blk, &result)) } Expr::PathResolve(p) => { let p_box = lower_expr(ctx, p)?; let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); + let p_handle = blk.call(I64, "js_path_arg_header", &[(DOUBLE, &p_box)]); let result = blk.call(I64, "js_path_resolve", &[(I64, &p_handle)]); Ok(nanbox_string_inline(blk, &result)) } diff --git a/crates/perry-codegen/src/expr/arrays_finds.rs b/crates/perry-codegen/src/expr/arrays_finds.rs index 7f48d6482c..6893c1d9da 100644 --- a/crates/perry-codegen/src/expr/arrays_finds.rs +++ b/crates/perry-codegen/src/expr/arrays_finds.rs @@ -556,7 +556,12 @@ pub(crate) fn lower( Expr::PathExtname(p) => { let p_box = lower_expr(ctx, p)?; let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); + // #7621: `js_path_arg_header`, not `unbox_to_i64` — the plain mask + // hands an SSO string's inline CHARACTERS to a `*StringHeader` + // consumer. Single-operand arms need no rooting: the materialising + // call is immediately followed by the consumer, with nothing + // between them that can collect. + let p_handle = blk.call(I64, "js_path_arg_header", &[(DOUBLE, &p_box)]); let result = blk.call(I64, "js_path_extname", &[(I64, &p_handle)]); Ok(nanbox_string_inline(blk, &result)) } @@ -591,26 +596,29 @@ pub(crate) fn lower( let p_box = vals[0].clone(); let pat_box = vals[1].clone(); let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); - let pat_handle = unbox_to_i64(blk, &pat_box); let i32_v = blk.call( I32, - "js_path_matches_glob", - &[(I64, &p_handle), (I64, &pat_handle)], + "js_path_matches_glob_value", + &[(DOUBLE, &p_box), (DOUBLE, &pat_box)], ); Ok(i32_bool_to_nanbox(blk, &i32_v)) }) } + // #7621: both operands go to the runtime NaN-BOXED. Unboxing them here + // would mean two `js_path_arg_header` calls, and the first one + // ALLOCATES for an SSO operand — a collection point with the second + // operand still live in a bare register. This API cannot close that + // window (`with_operands_rooted` yields registers, not slots, and + // `RootedSlot` has no `read` by design), so the pair is unboxed inside + // one runtime call that roots across its own materialisation. Expr::PathResolveJoin(a, b) => rooting::with_operands_rooted(ctx, &[a, b], |ctx, vals| { let a_box = vals[0].clone(); let b_box = vals[1].clone(); let blk = ctx.block(); - let a_handle = unbox_to_i64(blk, &a_box); - let b_handle = unbox_to_i64(blk, &b_box); let result = blk.call( I64, - "js_path_resolve_join", - &[(I64, &a_handle), (I64, &b_handle)], + "js_path_resolve_join_value", + &[(DOUBLE, &a_box), (DOUBLE, &b_box)], ); Ok(nanbox_string_inline(blk, &result)) }), diff --git a/crates/perry-codegen/src/expr/instance_misc1.rs b/crates/perry-codegen/src/expr/instance_misc1.rs index 787b844d18..64c643ceb3 100644 --- a/crates/perry-codegen/src/expr/instance_misc1.rs +++ b/crates/perry-codegen/src/expr/instance_misc1.rs @@ -939,11 +939,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // -------- path.join(a, b) -> string -------- // The HIR variant is binary; multi-arg path.join lowers to // chained PathJoin in the HIR. + // #7621: both operands reach the runtime NaN-BOXED, so this arm emits + // NO unbox at all. Unboxing here would need two `js_path_arg_header` + // calls, and the first ALLOCATES for an SSO operand — a collection + // point with the second operand still in a bare register, i.e. exactly + // the window the #7615 migration exists to remove. Handing both + // NaN-boxed doubles to one runtime entry deletes the window from + // codegen rather than protecting it: `js_path_join_value` roots operand + // 1 across its own materialisation with `RuntimeHandle::across_const`. + // See perry-runtime/src/path/value_args.rs. Expr::PathJoin(a, b) => rooting::with_operands_rooted(ctx, &[a, b], |ctx, vals| { let blk = ctx.block(); - let a_handle = unbox_to_i64(blk, &vals[0]); - let b_handle = unbox_to_i64(blk, &vals[1]); - let result = blk.call(I64, "js_path_join", &[(I64, &a_handle), (I64, &b_handle)]); + let result = blk.call( + I64, + "js_path_join_value", + &[(DOUBLE, &vals[0]), (DOUBLE, &vals[1])], + ); Ok(nanbox_string_inline(blk, &result)) }), @@ -953,12 +964,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // PathWin32Join in the HIR. Expr::PathWin32Join(a, b) => rooting::with_operands_rooted(ctx, &[a, b], |ctx, vals| { let blk = ctx.block(); - let a_handle = unbox_to_i64(blk, &vals[0]); - let b_handle = unbox_to_i64(blk, &vals[1]); + // #7621: NaN-boxed pair, no unbox here — see `Expr::PathJoin`. let result = blk.call( I64, - "js_path_win32_join", - &[(I64, &a_handle), (I64, &b_handle)], + "js_path_win32_join_value", + &[(DOUBLE, &vals[0]), (DOUBLE, &vals[1])], ); Ok(nanbox_string_inline(blk, &result)) }), @@ -998,7 +1008,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { _ => unreachable!(), }; let blk = ctx.block(); - let h = unbox_to_i64(blk, &lowered[0]); + // #7621: `js_path_arg_header`, not `unbox_to_i64` — the + // plain mask hands an SSO string's inline CHARACTERS to + // a `*StringHeader` consumer. It is emitted INSIDE the + // rooted region, and needs no window of its own: the + // materialising call is immediately followed by its + // consumer with nothing between them, and the helper + // only allocates — it cannot re-enter user code or + // enumerate an object, so per #7198 it cannot initiate a + // moving collection and `with_operands_rooted_across_call` + // would be pure cost here. + let h = blk.call(I64, "js_path_arg_header", &[(DOUBLE, &lowered[0])]); let result = blk.call(I64, fn_name, &[(I64, &h)]); Ok(nanbox_string_inline(blk, &result)) } @@ -1016,34 +1036,46 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Ok(nanbox_string_inline(blk, &result)) } PathWin32Method::BasenameExt | PathWin32Method::ResolveJoin => { + // #7621: two-operand methods hand BOTH operands to the + // runtime NaN-boxed, so this arm emits no unbox at all + // — see the `Expr::PathJoin` comment above for why that + // is stronger than protecting the window here. let fn_name = match method { - PathWin32Method::BasenameExt => "js_path_win32_basename_ext", - PathWin32Method::ResolveJoin => "js_path_win32_resolve_join", + PathWin32Method::BasenameExt => "js_path_win32_basename_ext_value", + PathWin32Method::ResolveJoin => "js_path_win32_resolve_join_value", _ => unreachable!(), }; let blk = ctx.block(); - let a = unbox_to_i64(blk, &lowered[0]); - let b = unbox_to_i64(blk, &lowered[1]); - let result = blk.call(I64, fn_name, &[(I64, &a), (I64, &b)]); + let result = blk.call( + I64, + fn_name, + &[(DOUBLE, &lowered[0]), (DOUBLE, &lowered[1])], + ); Ok(nanbox_string_inline(blk, &result)) } PathWin32Method::IsAbsolute => { let blk = ctx.block(); - let h = unbox_to_i64(blk, &lowered[0]); + // #7621: SSO-safe unbox — see the Dirname arm above. + let h = blk.call(I64, "js_path_arg_header", &[(DOUBLE, &lowered[0])]); let i32_v = blk.call(I32, "js_path_win32_is_absolute", &[(I64, &h)]); Ok(i32_bool_to_nanbox(blk, &i32_v)) } PathWin32Method::MatchesGlob => { let blk = ctx.block(); - let p = unbox_to_i64(blk, &lowered[0]); - let pat = unbox_to_i64(blk, &lowered[1]); - let i32_v = - blk.call(I32, "js_path_win32_matches_glob", &[(I64, &p), (I64, &pat)]); + // #7621: NaN-boxed pair, no unbox here. + let i32_v = blk.call( + I32, + "js_path_win32_matches_glob_value", + &[(DOUBLE, &lowered[0]), (DOUBLE, &lowered[1])], + ); Ok(i32_bool_to_nanbox(blk, &i32_v)) } PathWin32Method::Parse => { let blk = ctx.block(); - let h = unbox_to_i64(blk, &lowered[0]); + // #7621: SSO-safe unbox. Before this, + // `path.win32.parse(shortComputed).base` was silently "" + // rather than a throw. + let h = blk.call(I64, "js_path_arg_header", &[(DOUBLE, &lowered[0])]); let result = blk.call(I64, "js_path_win32_parse", &[(I64, &h)]); Ok(nanbox_pointer_inline(blk, &result)) } @@ -1237,7 +1269,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Expr::PathDirname(p) => { let p_box = lower_expr(ctx, p)?; let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); + // #7621: SSO-safe unbox (see path/value_args.rs). + let p_handle = blk.call(I64, "js_path_arg_header", &[(DOUBLE, &p_box)]); let result = blk.call(I64, "js_path_dirname", &[(I64, &p_handle)]); Ok(nanbox_string_inline(blk, &result)) } @@ -1714,21 +1747,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Expr::PathBasename(p) => { let p_box = lower_expr(ctx, p)?; let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); + // #7621: SSO-safe unbox (see path/value_args.rs). + let p_handle = blk.call(I64, "js_path_arg_header", &[(DOUBLE, &p_box)]); let result = blk.call(I64, "js_path_basename", &[(I64, &p_handle)]); Ok(nanbox_string_inline(blk, &result)) } Expr::PathBasenameExt(p, ext) => { // path.basename(path, ext) — strips trailing `ext` suffix. - // Runtime: js_path_basename_ext(path_ptr, ext_ptr) -> *StringHeader. + // #7621: the runtime entry takes both operands NaN-boxed and + // unboxes them under a root, so this arm emits no unbox — see + // `Expr::PathJoin`. rooting::with_operands_rooted(ctx, &[p, ext], |ctx, vals| { let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &vals[0]); - let e_handle = unbox_to_i64(blk, &vals[1]); let result = blk.call( I64, - "js_path_basename_ext", - &[(I64, &p_handle), (I64, &e_handle)], + "js_path_basename_ext_value", + &[(DOUBLE, &vals[0]), (DOUBLE, &vals[1])], ); Ok(nanbox_string_inline(blk, &result)) }) @@ -1737,7 +1771,10 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // path.parse(p) -> object with { dir, base, ext, name, root } let p_box = lower_expr(ctx, p)?; let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); + // #7621: SSO-safe unbox (see path/value_args.rs). Before this, + // `path.parse(shortComputed).base` was silently "" rather than a + // throw, because js_path_parse defaults an unreadable header. + let p_handle = blk.call(I64, "js_path_arg_header", &[(DOUBLE, &p_box)]); let result = blk.call(I64, "js_path_parse", &[(I64, &p_handle)]); Ok(nanbox_pointer_inline(blk, &result)) } diff --git a/crates/perry-codegen/src/expr/misc_methods.rs b/crates/perry-codegen/src/expr/misc_methods.rs index 21fd33f905..2b8ca86cf8 100644 --- a/crates/perry-codegen/src/expr/misc_methods.rs +++ b/crates/perry-codegen/src/expr/misc_methods.rs @@ -207,7 +207,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Expr::PathIsAbsolute(p) => { let p_box = lower_expr(ctx, p)?; let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); + // #7621: SSO-safe unbox — see crates/perry-runtime/src/path/value_args.rs. + let p_handle = blk.call(I64, "js_path_arg_header", &[(DOUBLE, &p_box)]); let i32_res = blk.call(I32, "js_path_is_absolute", &[(I64, &p_handle)]); Ok(i32_bool_to_nanbox(blk, &i32_res)) } diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index 472589e1e5..240d999819 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1528,6 +1528,20 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function("js_path_to_namespaced_path_value", DOUBLE, &[DOUBLE]); module.declare_function("js_path_matches_glob", I32, &[I64, I64]); module.declare_function("js_path_resolve_join", I64, &[I64, I64]); + // #7621 — SSO-safe operand entries. `js_path_arg_header` replaces the + // `unbox_to_i64` mask on every single-operand arm; the `_value` pair + // entries unbox BOTH operands inside the runtime so the first one can be + // rooted across the second's materialisation. See + // perry-runtime/src/path/value_args.rs. + module.declare_function("js_path_arg_header", I64, &[DOUBLE]); + module.declare_function("js_path_join_value", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_path_win32_join_value", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_path_resolve_join_value", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_path_win32_resolve_join_value", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_path_basename_ext_value", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_path_win32_basename_ext_value", I64, &[DOUBLE, DOUBLE]); + module.declare_function("js_path_matches_glob_value", I32, &[DOUBLE, DOUBLE]); + module.declare_function("js_path_win32_matches_glob_value", I32, &[DOUBLE, DOUBLE]); module.declare_function("js_object_from_entries", DOUBLE, &[DOUBLE]); module.declare_function("js_string_match", I64, &[I64, I64]); module.declare_function("js_string_match_all", I64, &[I64, I64]); diff --git a/crates/perry-runtime/src/path.rs b/crates/perry-runtime/src/path.rs index 8c03e41e5e..b85b40a3f3 100644 --- a/crates/perry-runtime/src/path.rs +++ b/crates/perry-runtime/src/path.rs @@ -1862,5 +1862,8 @@ pub extern "C" fn js_path_win32_relative( string_to_js(&parts.join("\\")) } +/// NaN-boxed operand entry points (#7621) — see the child module's docs. +pub mod value_args; + #[cfg(test)] mod tests; diff --git a/crates/perry-runtime/src/path/value_args.rs b/crates/perry-runtime/src/path/value_args.rs new file mode 100644 index 0000000000..f8b9626e8b --- /dev/null +++ b/crates/perry-runtime/src/path/value_args.rs @@ -0,0 +1,287 @@ +//! NaN-boxed operand entry points for the compiled `path.*` fast paths (#7621). +//! +//! # The bug these exist to close +//! +//! Every `path.*` codegen arm used to unbox its operand with `unbox_to_i64` — +//! `bitcast double → i64; and POINTER_MASK` — and hand the low 48 bits to a +//! runtime entry that dereferences them as a `*const StringHeader`. That is the +//! right answer for a HEAP string (STRING_TAG = 0x7FFF, payload = the header), +//! and it is the CHARACTERS for a small-string-optimized one (SHORT_STRING_TAG +//! = 0x7FF9, payload = length + up to `SHORT_STRING_MAX_LEN` = 5 inline bytes). +//! So `path.resolve("/root", "s1")` worked — a literal is interned onto the heap +//! — and `path.resolve("/root", seg(1))` threw `ERR_INVALID_ARG_TYPE`, because a +//! computed short string takes the inline form. The #214 class, bisected by +//! length: 5 bytes throws, 6 bytes works. +//! +//! # Why this is not `js_get_string_pointer_unified` +//! +//! [`js_path_arg_header`] is deliberately NOT that helper. `unified` coerces +//! non-strings — a number arrives back as `"5"` — which would silently turn +//! `path.isAbsolute(5)` from Node's `ERR_INVALID_ARG_TYPE` throw into `false`. +//! This one changes the SSO case and NOTHING else: a heap string and every +//! non-string reproduce the old mask bit for bit, so each entry point keeps its +//! own established non-string behaviour (`js_path_join` throws, +//! `js_path_matches_glob` defaults to `""`) unexamined and unchanged. +//! +//! # Why the two-operand entries exist +//! +//! Materialising an inline operand ALLOCATES, which is a collection point +//! between the two operand unboxes — the #7213 window. Codegen cannot close it: +//! `rooting::with_operands_rooted` hands the lowering registers, not slots, so +//! the second operand's register is stale the moment the first is materialised +//! and there is no re-read to reach for (`RootedSlot` deliberately has no +//! `read`). Doing both unboxes inside ONE runtime call moves the window +//! somewhere it can be closed properly — with a `RuntimeHandleScope` and +//! `RuntimeHandle::across_const`, which never binds the pre-collection address. + +use super::StringHeader; +use crate::gc::RuntimeHandleScope; +use crate::string::js_string_materialize_to_heap; +use crate::value::{JSValue, POINTER_MASK}; + +/// Resolve a NaN-boxed `path.*` operand to the `*const StringHeader` the +/// pointer-ABI entry points expect. +/// +/// SSO operands are materialised onto the heap; everything else — heap strings +/// AND non-strings — reproduces the pre-#7621 `unbox_to_i64` mask exactly. See +/// the module docs for why that asymmetry is the point. +#[no_mangle] +pub extern "C" fn js_path_arg_header(value: f64) -> i64 { + path_arg_header(value) as i64 +} + +/// Keepalive anchor: emitted only from generated code, so the whole-program +/// auto-optimize bitcode pass would otherwise dead-strip it. +#[cfg(feature = "keepalive-anchors")] +#[used] +static KEEP_PATH_ARG_HEADER: extern "C" fn(f64) -> i64 = js_path_arg_header; + +pub(crate) fn path_arg_header(value: f64) -> *const StringHeader { + let bits = value.to_bits(); + if JSValue::from_bits(bits).is_short_string() { + return js_string_materialize_to_heap(value) as *const StringHeader; + } + (bits & POINTER_MASK) as *const StringHeader +} + +/// Materialise both operands and run `f` over the results. +/// +/// The first operand is rooted across the second's materialisation, and its +/// address is re-read through `RuntimeHandle::across_const` rather than bound +/// before the call — the ordering half of the rooting invariant that +/// `docs/src/internals/gc-rooting-invariant.md` says keeps getting dropped. +/// +/// A non-string operand is a masked garbage address (see [`path_arg_header`]), +/// so it is deliberately NOT rooted: handing the collector something to mark +/// that is not an object is a worse bug than the one being fixed. Nothing can +/// move it either, so reading it unrooted is sound by construction. +/// +/// `f` receives raw pointers, and every callee wired up below reads BOTH +/// headers into owned Rust `String`s before it allocates, so neither can be +/// relocated out from under the other inside `f`. The scope stays alive across +/// `f` regardless, so both operands remain reachable for its whole duration. +/// +/// # Honest status of the rooting half +/// +/// This ordering is DEFENSIVE, not sabotage-proven. Under today's collector the +/// window cannot actually fault: the second materialisation allocates, and a +/// collection reached from inside an allocation runs with `GC_FLAG_IN_ALLOC` +/// set, which makes the copying minor ineligible — so nothing MOVES at an +/// allocation point and the pre-bound address stays valid. Reverting this to +/// `let a_ptr = a_ptr0;` was measured against 400k SSO-pair joins under +/// `PERRY_GC_FORCE_EVACUATE=1` + from-space protection and against the #7621 gap +/// test under `PERRY_GC_ZEAL=1` (402k copying minors): zero faults, byte- +/// identical output. It is written this way because it is the shape the +/// invariant asks for and it costs nothing, not because an instrument caught it. +fn with_two_headers( + a: f64, + b: f64, + f: impl FnOnce(*const StringHeader, *const StringHeader) -> R, +) -> R { + let scope = RuntimeHandleScope::new(); + let a_ptr0 = path_arg_header(a); + let a_handle = JSValue::from_bits(a.to_bits()) + .is_any_string() + .then(|| scope.root_string_ptr(a_ptr0)); + let (b_ptr, a_ptr) = match &a_handle { + Some(h) => h.across_const::(|| path_arg_header(b)), + None => (path_arg_header(b), a_ptr0), + }; + f(a_ptr, b_ptr) +} + +macro_rules! two_operand_value_entry { + ($(#[$meta:meta])* $value_fn:ident => $ptr_fn:path, $ret:ty, $keep:ident) => { + $(#[$meta])* + #[no_mangle] + pub extern "C" fn $value_fn(a: f64, b: f64) -> $ret { + with_two_headers(a, b, |x, y| $ptr_fn(x, y)) + } + + /// Keepalive anchor — see [`KEEP_PATH_ARG_HEADER`]. + #[cfg(feature = "keepalive-anchors")] + #[used] + static $keep: extern "C" fn(f64, f64) -> $ret = $value_fn; + }; +} + +two_operand_value_entry!( + /// `path.join(a, b)` over NaN-boxed operands — see the module docs. + js_path_join_value => super::js_path_join, + *mut StringHeader, + KEEP_PATH_JOIN_VALUE +); +two_operand_value_entry!( + /// `path.win32.join(a, b)` over NaN-boxed operands. + js_path_win32_join_value => super::js_path_win32_join, + *mut StringHeader, + KEEP_PATH_WIN32_JOIN_VALUE +); +two_operand_value_entry!( + /// The `path.resolve` fold step over NaN-boxed operands — the issue's + /// headline repro, `path.resolve(base, computedShortString)`. + js_path_resolve_join_value => super::js_path_resolve_join, + *mut StringHeader, + KEEP_PATH_RESOLVE_JOIN_VALUE +); +two_operand_value_entry!( + /// `path.win32.resolve(a, b)` fold step over NaN-boxed operands. + js_path_win32_resolve_join_value => super::js_path_win32_resolve_join, + *mut StringHeader, + KEEP_PATH_WIN32_RESOLVE_JOIN_VALUE +); +two_operand_value_entry!( + /// `path.basename(path, ext)` over NaN-boxed operands. + js_path_basename_ext_value => super::js_path_basename_ext, + *mut StringHeader, + KEEP_PATH_BASENAME_EXT_VALUE +); +two_operand_value_entry!( + /// `path.win32.basename(path, ext)` over NaN-boxed operands. + js_path_win32_basename_ext_value => super::js_path_win32_basename_ext, + *mut StringHeader, + KEEP_PATH_WIN32_BASENAME_EXT_VALUE +); +two_operand_value_entry!( + /// `path.matchesGlob(path, pattern)` over NaN-boxed operands. + js_path_matches_glob_value => super::js_path_matches_glob, + i32, + KEEP_PATH_MATCHES_GLOB_VALUE +); +two_operand_value_entry!( + /// `path.win32.matchesGlob(path, pattern)` over NaN-boxed operands. + js_path_win32_matches_glob_value => super::js_path_win32_matches_glob, + i32, + KEEP_PATH_WIN32_MATCHES_GLOB_VALUE +); + +#[cfg(test)] +mod tests { + use super::*; + + fn heap_string(s: &str) -> f64 { + let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + f64::from_bits(JSValue::string_ptr(ptr).bits()) + } + + fn sso_string(s: &str) -> f64 { + let v = JSValue::try_short_string(s.as_bytes()).expect("fits SHORT_STRING_MAX_LEN"); + assert!(v.is_short_string(), "probe must take the inline form"); + f64::from_bits(v.bits()) + } + + fn read(ptr: *mut StringHeader) -> String { + unsafe { super::super::string_from_header(ptr as *const StringHeader) } + .expect("entry point must return a real StringHeader") + } + + /// The bug: an inline operand's CHARACTERS were dereferenced as a header. + #[test] + fn sso_operand_resolves_to_a_real_header() { + let inline = sso_string("s1"); + let ptr = path_arg_header(inline); + assert_ne!( + ptr as usize, + (inline.to_bits() & POINTER_MASK) as usize, + "an SSO operand must NOT resolve to its own inline payload bits" + ); + assert_eq!(read(ptr as *mut StringHeader), "s1"); + } + + /// ...while a heap operand keeps the old mask, bit for bit. + #[test] + fn heap_operand_keeps_the_old_mask() { + let v = heap_string("segment-that-is-longer-than-sso"); + assert_eq!( + path_arg_header(v) as usize, + (v.to_bits() & POINTER_MASK) as usize + ); + } + + /// ...and so does every non-string, so each entry point keeps its own throw + /// / default behaviour unchanged. + #[test] + fn non_string_operands_keep_the_old_mask() { + for v in [5.0f64, 0.0, f64::from_bits(crate::value::TAG_UNDEFINED)] { + assert_eq!( + path_arg_header(v) as usize, + (v.to_bits() & POINTER_MASK) as usize, + "non-string operand {v:?} must reproduce the pre-#7621 mask" + ); + } + } + + /// The two-operand window: materialising `a` allocates, and `b` must still + /// be read correctly afterwards — in both SSO/heap orders. + #[test] + fn both_operands_survive_the_materialisation_window() { + assert_eq!( + read(js_path_join_value(sso_string("/r"), sso_string("s1"))), + "/r/s1" + ); + assert_eq!( + read(js_path_join_value( + heap_string("/root-that-is-long"), + sso_string("s1") + )), + "/root-that-is-long/s1" + ); + assert_eq!( + read(js_path_join_value( + sso_string("/r"), + heap_string("segment-longer-than-sso") + )), + "/r/segment-longer-than-sso" + ); + assert_eq!( + read(js_path_resolve_join_value( + heap_string("/root"), + sso_string("s1") + )), + "/root/s1" + ); + assert_eq!( + read(js_path_resolve_join_value( + sso_string("/a"), + sso_string("/b") + )), + "/b", + "an absolute later segment still resets the resolve" + ); + assert_eq!( + js_path_matches_glob_value(sso_string("a.ts"), heap_string("*.ts")), + 1 + ); + assert_eq!( + js_path_matches_glob_value(sso_string("a.js"), heap_string("*.ts")), + 0 + ); + assert_eq!( + read(js_path_basename_ext_value( + sso_string("a.ts"), + sso_string(".ts") + )), + "a" + ); + } +} diff --git a/test-files/test_gap_7621_path_sso_operands.ts b/test-files/test_gap_7621_path_sso_operands.ts new file mode 100644 index 0000000000..df76ed9265 --- /dev/null +++ b/test-files/test_gap_7621_path_sso_operands.ts @@ -0,0 +1,106 @@ +// #7621: every `path.*` codegen arm that hands its operand to the runtime as a +// raw `*const StringHeader` read an SSO (small-string-optimized) string's +// INLINE BYTES as a pointer. +// +// A NaN-boxed string is either a heap `StringHeader*` (STRING_TAG = 0x7FFF) or, +// for <= 5 bytes, the characters themselves packed into the payload +// (SHORT_STRING_TAG = 0x7FF9). The arms unboxed with `unbox_to_i64`, which just +// masks the low 48 bits: correct for the heap form, and for the inline form it +// is the CHARACTERS, dereferenced as a header. `path.resolve("/root", seg(1))` +// therefore threw `TypeError [ERR_INVALID_ARG_TYPE]` while the literal form +// worked — literals are interned to the heap, so only a COMPUTED short string +// takes the inline representation. +// +// Everything here is computed at runtime from `n` so no constant folding can +// turn a probe back into a literal, and every operand family is swept ACROSS +// the 5-byte SSO boundary rather than at one length, because that boundary is +// where the two representations swap. + +import * as path from "node:path"; + +// Runtime-computed segments. `n` comes from an array index the compiler cannot +// fold, so `seg(1)` is a real concatenation producing a 2-byte SSO string and +// `seg(20)` a 21-byte heap string. +const ns: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 20]; +function seg(n: number): string { + return "s" + "e".repeat(n); +} +function computed(n: number): string { + return ns[n] === undefined ? "?" : "s" + String(ns[n]); +} + +// ── 1. the issue's exact repro: literal vs computed, same value ── +console.log("1 literal :", path.resolve("/root", "s1")); +console.log("1 computed:", path.resolve("/root", computed(0))); +console.log("1 equal :", path.resolve("/root", "s1") === path.resolve("/root", computed(0))); + +// ── 2. sweep the SSO boundary (inline holds <= 5 bytes) ── +for (let n = 1; n <= 9; n++) { + const s = seg(n); + console.log(`2 len=${s.length}`, path.resolve("/root", s)); +} + +// ── 3. multi-segment resolve, computed in the middle and at the end ── +console.log("3 three:", path.resolve("/a", computed(2), "d")); +console.log("3 four :", path.resolve("/a", "b", computed(3), computed(4))); +console.log("3 reset:", path.resolve("/a", computed(0), "/abs", computed(1))); +console.log("3 dots :", path.resolve("/a/b/c", computed(0), "..", computed(1))); + +// ── 4. relative base — cwd-dependent, so assert the SHAPE, not the bytes ── +const rel = path.resolve("rel", computed(0)); +console.log("4 rel suffix:", rel.endsWith("/rel/s1"), rel.startsWith("/")); +console.log("4 rel equal :", rel === path.resolve(process.cwd(), "rel", computed(0))); +const relEmpty = path.resolve(computed(0)); +console.log("4 bare :", relEmpty.endsWith("/s1"), relEmpty === path.resolve(process.cwd(), "s1")); + +// ── 5. the sibling arms that share the raw-pointer unbox ── +console.log("5 join :", path.join("/root", computed(0)), path.join(computed(0), computed(1))); +console.log("5 normalize:", path.normalize(computed(0)), path.normalize("/a/" + computed(0) + "/../b")); +console.log("5 extname :", JSON.stringify(path.extname(computed(0) + ".ts")), JSON.stringify(path.extname(computed(0)))); +console.log("5 dirname :", path.dirname("/a/" + computed(0)), path.dirname(computed(0))); +console.log("5 basename :", path.basename("/a/" + computed(0)), path.basename(computed(0))); +console.log("5 basenExt :", path.basename(computed(0) + ".ts", ".ts"), path.basename("a.ts", ".ts")); +console.log("5 isAbs :", path.isAbsolute(computed(0)), path.isAbsolute("/" + computed(0))); +console.log("5 relative :", path.relative("/a/" + computed(0), "/a/" + computed(1))); +console.log("5 win32join:", path.win32.join("C:\\r", computed(0))); + +const parsed = path.parse("/a/" + computed(0) + ".ts"); +console.log("5 parse :", parsed.root, parsed.dir, parsed.base, parsed.ext, parsed.name); + +console.log("5 glob :", path.matchesGlob(computed(0) + ".ts", "*.ts"), path.matchesGlob(computed(0), "*.ts")); + +// ── 6. non-string operands must still throw ERR_INVALID_ARG_TYPE ── +try { + // deno-lint-ignore no-explicit-any + path.resolve("/root", ns as any); + console.log("6 resolve non-string: NO THROW"); +} catch (e) { + console.log("6 resolve non-string:", (e as Error).name, (e as { code?: string }).code); +} +try { + // deno-lint-ignore no-explicit-any + path.join("/root", 5 as any); + console.log("6 join non-string: NO THROW"); +} catch (e) { + console.log("6 join non-string:", (e as Error).name, (e as { code?: string }).code); +} + +// ── 7. the SSO operand must survive an allocation between the two unboxes ── +// `path.resolve(a, b)` folds to `PathResolveJoin(a, b)`, and materializing an +// inline `a` onto the heap is itself an allocation — so `b` has to be re-read +// after it, not held in a register across it. Churn the nursery first so a +// collection actually lands inside that window. +function churn(): string { + let last = ""; + for (let i = 0; i < 2000; i++) { + last = { k: "v" + String(i) }.k; + } + return last; +} +churn(); +let acc = ""; +for (let i = 0; i < 200; i++) { + acc = path.resolve("/r" + String(i % 3), computed(i % 5), computed((i + 1) % 5)); + churn(); +} +console.log("7 window:", acc); From 1e8f8433bb50ec39ab046b40dfcd67f3b8561ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 09:00:12 +0200 Subject: [PATCH 2/3] docs(changelog): #7621 changeset fragment for PR #7626 --- changelog.d/7626-path-sso-operands.md | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 changelog.d/7626-path-sso-operands.md diff --git a/changelog.d/7626-path-sso-operands.md b/changelog.d/7626-path-sso-operands.md new file mode 100644 index 0000000000..e4516a3bae --- /dev/null +++ b/changelog.d/7626-path-sso-operands.md @@ -0,0 +1,53 @@ +### Fixed + +- **`path.*` read an SSO string's inline bytes as a `StringHeader` pointer (#7621).** + `path.resolve("/root", computedShortString)` threw + `TypeError [ERR_INVALID_ARG_TYPE]` where node returns the path, while the same + call with a *literal* segment worked. + + Every `path.*` codegen arm unboxed its operand with `unbox_to_i64` — `bitcast + double -> i64; and POINTER_MASK` — and handed the low 48 bits to a runtime + entry that dereferences them as `*const StringHeader`. Those bits are the + header for a **heap** string (`STRING_TAG` = 0x7FFF) and the **characters** for + a small-string-optimized one (`SHORT_STRING_TAG` = 0x7FF9, length + up to + `SHORT_STRING_MAX_LEN` = 5 inline bytes). A literal is interned onto the heap; + a computed short string takes the inline form. The #214 class, bisected by + length — 5 bytes threw, 6 bytes worked. + + Twelve arms were affected, not the five the issue named — `resolve(a, b)`, + `resolve(p)`, `join`, `win32.join`, `normalize`, `extname`, `dirname`, + `basename`, `basename(p, ext)`, `isAbsolute`, `parse` and `matchesGlob`, plus + the `path.win32.*` equivalents. `parse` and `matchesGlob` were **silently + wrong** rather than throwing (`path.parse(short).base` was `""`; + `matchesGlob` matched the pattern against `""`), which is why they had never + been reported. `path.relative`, `path.format` and `path.toNamespacedPath` + already took NaN-boxed values (`js_path_relative_checked`, #2995) and were + unaffected — that is the precedent this change generalises. + + Single-operand arms now call `js_path_arg_header`, which materialises **only** + the SSO case and reproduces the old mask bit for bit for heap strings and for + every non-string, so each entry point keeps its own established non-string + behaviour (throw, or `unwrap_or_default`) unchanged. It is deliberately not + `js_get_string_pointer_unified`, which coerces numbers to strings and would + have turned `path.isAbsolute(5)` from Node's throw into `false`. + + Two-operand arms hand both operands to the runtime NaN-boxed + (`js_path_*_value` in `crates/perry-runtime/src/path/value_args.rs`). Codegen + cannot close that window itself: materialising the first operand allocates, and + `rooting::with_operands_rooted` yields registers rather than slots, so the + second operand's register is stale the instant the first is materialised and + there is no re-read to reach for. The runtime entry roots the first operand in + a `RuntimeHandleScope` and re-reads it through `RuntimeHandle::across_const`. + + That rooting half is defensive rather than instrument-proven, and the module + says so at the site: reverting `across_const` to a pre-bound address produced + zero faults under `PERRY_GC_ZEAL=1` + `PERRY_GC_PROTECT_FROMSPACE=1` (402k + copying minors) and 0/400k mismatches under `PERRY_GC_FORCE_EVACUATE=1`, + because a collection reached from inside an allocation runs with + `GC_FLAG_IN_ALLOC` set and the copying minor is therefore ineligible — nothing + moves at an allocation point today. + + Covered by `test-files/test_gap_7621_path_sso_operands.ts` (both sides of the + SSO boundary, computed and literal operands, absolute and relative bases, + multi-segment resolves, all twelve arms, the non-string throw) plus five + `perry-runtime` unit tests. From d0efa0050b89146ce65af721d65d2a73f9b42dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 11:37:56 +0200 Subject: [PATCH 3/3] chore(version): bump to 0.5.1358 --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 12bda97403..cf8c7dda33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1357 +**Current Version:** 0.5.1358 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 708322f06b..ac4220246f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1357" +version = "0.5.1358" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1357" +version = "0.5.1358" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1357" +version = "0.5.1358" [[package]] name = "perry-ui-tvos" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1357" +version = "0.5.1358" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index bb8a10c0c0..24a4deea2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1357" +version = "0.5.1358" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"