From 6e82edc5c1cf507ef1a795597e6d891d1f7b2953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 08:02:35 +0200 Subject: [PATCH 1/2] fix(codegen): a non-numeric key on a Uint8Array/Buffer local is a property read, not a byte (#7700) --- changelog.d/7746-uint8array-nonnumeric-key.md | 13 ++ .../src/collectors/byte_read_key.rs | 195 +++++++++++++++++ .../src/collectors/byte_read_key_tests.rs | 185 +++++++++++++++++ .../perry-codegen/src/collectors/hir_facts.rs | 16 ++ .../src/collectors/i32_locals.rs | 46 +++- .../src/collectors/int_valued_ta_locals.rs | 196 ++++++++++++++---- .../src/collectors/integer_locals.rs | 42 +++- crates/perry-codegen/src/collectors/mod.rs | 4 + .../src/collectors/not_bigint_locals.rs | 36 +++- .../src/stmt/masked_window_region.rs | 6 + .../src/type_analysis/numeric.rs | 26 ++- ...test_gap_uint8array_nonnumeric_key_7700.ts | 68 ++++++ 12 files changed, 772 insertions(+), 61 deletions(-) create mode 100644 changelog.d/7746-uint8array-nonnumeric-key.md create mode 100644 crates/perry-codegen/src/collectors/byte_read_key.rs create mode 100644 crates/perry-codegen/src/collectors/byte_read_key_tests.rs create mode 100644 test-files/test_gap_uint8array_nonnumeric_key_7700.ts diff --git a/changelog.d/7746-uint8array-nonnumeric-key.md b/changelog.d/7746-uint8array-nonnumeric-key.md new file mode 100644 index 0000000000..8755692171 --- /dev/null +++ b/changelog.d/7746-uint8array-nonnumeric-key.md @@ -0,0 +1,13 @@ +### Fixed + +- **codegen: a non-numeric key on a `Uint8Array`/`Buffer`-typed local no longer reads a byte (#7700).** `const it = u8[Symbol.iterator]` reported `typeof it === "number"`; `const k: any = "byteLength"; const n = u8[k]` read `0`; an own expando read `0`. Node returns the iterator function, `4`, and the object. + + `lower/expr_member/member_tail.rs` folds every non-STRING key on such a local onto `Expr::Uint8ArrayGet`, and six codegen collectors read that node as "a byte, hence a number" **with no regard for the key kind** — so the destination local was classified integer-valued, took an i32 slot, and `i32_from_indexed_get_lowered` applied `ToInt32(ToNumber(v))` to a function pointer. Codegen's own byte-path gate was already correct: `arrays_finds::lower_uint8array_get_i32` routes an unproven key to `js_object_get_index_polymorphic` and hands back a boxed JS value. The representation decision one level up is what discarded it, which is why only the *stored* form was wrong — `typeof u8[Symbol.iterator]` consumed directly was always right. + + The key-kind condition now lives in one place, `collectors/byte_read_key.rs`, and is an **allowlist**: the key must be provably a number. The "not a string" blocklist it replaces is precisely what shipped this bug, by putting every key kind nobody enumerated — symbols first — on the byte-read side. Applied at all six sites (`integer_locals` ×2, `i32_locals`, `int_valued_ta_locals` ×3, `not_bigint_locals`) plus the three `type_analysis/numeric.rs` predicates that also mean "a raw double" (`is_numeric_expr`, `is_provably_not_bigint`, `integer_magnitude_bits`). + + **The hot path is unchanged, and that is measured, not asserted.** A `for (let i = …) sum += buf[i]` counter is a body `let`, and the `binding_types` map the collectors are handed covers only params and module globals — gating on that map alone demotes the loop from `js_uint8array_get`/i32-slot to `js_uint8array_index_get_value`/double-slot. So `collect_numeric_typed_locals` walks the body for declared numeric types (including `for`-init counters) and that set is the evidence. With it, the emitted LLVM IR for a four-loop buffer fixture (FNV-1a, byte sum, masked mix, `Buffer`→`Buffer` copy) is **byte-identical** to the pre-fix compiler. + + Also fixed by the same condition: `u8.n = 1n; const k: any = "n"; const b = u8[k]` was classified non-BigInt. + + Not changed: the fold itself. Refusing to fold an unproven key and letting it fall through to `Expr::IndexGet` was tried and regressed two shapes the polymorphic escape gets right (`buf["1"]` → `undefined`, and an `any`-typed index in an accumulator loop → `NaN`). diff --git a/crates/perry-codegen/src/collectors/byte_read_key.rs b/crates/perry-codegen/src/collectors/byte_read_key.rs new file mode 100644 index 0000000000..25cf83bb59 --- /dev/null +++ b/crates/perry-codegen/src/collectors/byte_read_key.rs @@ -0,0 +1,195 @@ +//! #7700: ONE answer to "is this `Uint8ArrayGet` a byte read?". +//! +//! `lower/expr_member/member_tail.rs` folds every non-STRING key on a +//! `Uint8Array`/`Buffer`-typed local onto `Expr::Uint8ArrayGet`. A symbol key +//! is not a string, and neither is a `LocalGet` of an `any`-typed local that +//! happens to hold a string at runtime — so the node is NOT a byte read by +//! construction, and its value is whatever the property lookup finds: +//! +//! ```ts +//! const u8 = new Uint8Array([1, 2, 3, 4]); +//! const it = u8[Symbol.iterator]; // a function +//! const k: any = "byteLength"; +//! const n = u8[k]; // 4 +//! ``` +//! +//! Codegen already knows this. `arrays_finds::lower_uint8array_get_i32` routes +//! an unproven key to `js_object_get_index_polymorphic`, which dispatches +//! numeric keys to the byte read and everything else to the property path, and +//! hands back a boxed JS value. What broke is one level up: the collectors that +//! decide a LOCAL's representation answered "a `Uint8ArrayGet` is a number" +//! unconditionally, so `const it = u8[Symbol.iterator]` got an i32 slot, and +//! `i32_from_indexed_get_lowered` then applied `ToInt32(ToNumber(fn))` to the +//! polymorphic result. `typeof it` was `number`. +//! +//! The correctness condition is about the RUNTIME key, not about which codegen +//! path is taken: when the key really is a number the polymorphic helper +//! returns the byte, and coercing a byte to i32 is exact. So this predicate +//! asks only "is this key a number at runtime?" and is free to admit a local +//! the caller has proven integer-valued even though `is_numeric_expr` would +//! not — the byte read that follows is a number either way. +//! +//! It is an ALLOWLIST on purpose. The blocklist it replaces ("not a string +//! literal") is what shipped this bug: every key kind nobody thought of — +//! symbols first — landed on the wrong side of it. +//! +//! `collectors/pointer_locals.rs` asks a strictly harder question (a local with +//! no shadow slot must not be able to hold a heap value at all), so it passes +//! no local evidence and gets the purely structural answer. + +use std::collections::{HashMap, HashSet}; + +use perry_hir::types::Type as HirType; +use perry_hir::{BinaryOp, Expr, Param, Stmt, UnaryOp}; + +/// Every local whose DECLARED type says it holds a number: params, module +/// bindings, and body `let`s at any nesting depth (including a `for`-init +/// counter, which is the shape the byte-read fast path lives in). +/// +/// The collectors run before any `FnCtx` exists, and the `binding_types` map +/// they are handed covers only params and module globals — so without this +/// walk a `for (let i = 0; …) sum += buf[i]` counter is untypeable, the key +/// reads as unproven, and the loop loses its i32 representation. That is a +/// measured deoptimization (`js_uint8array_get` → `js_uint8array_index_get_value`, +/// i32 slot → double slot), not a theoretical one. +/// +/// Declared types are evidence here in a way they are NOT generally: Perry +/// does not enforce annotations, but a mis-annotated key is a pre-existing +/// wrong-code hazard on the byte path, identical to the one before #7700. +pub(crate) fn collect_numeric_typed_locals( + stmts: &[Stmt], + params: &[Param], + binding_types: &HashMap, +) -> HashSet { + fn is_numeric(ty: &HirType) -> bool { + matches!(ty, HirType::Number | HirType::Int32) + } + let mut out: HashSet = binding_types + .iter() + .filter(|(_, ty)| is_numeric(ty)) + .map(|(id, _)| *id) + .collect(); + for p in params { + if is_numeric(&p.ty) { + out.insert(p.id); + } + } + walk(stmts, &mut out); + return out; + + fn walk(stmts: &[Stmt], out: &mut HashSet) { + for s in stmts { + match s { + Stmt::Let { id, ty, .. } => { + if is_numeric(ty) { + out.insert(*id); + } + } + Stmt::If { + then_branch, + else_branch, + .. + } => { + walk(then_branch, out); + if let Some(eb) = else_branch { + walk(eb, out); + } + } + Stmt::For { init, body, .. } => { + if let Some(init_stmt) = init { + walk(std::slice::from_ref(init_stmt), out); + } + walk(body, out); + } + Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => walk(body, out), + Stmt::Try { + body, + catch, + finally, + } => { + walk(body, out); + if let Some(c) = catch { + walk(&c.body, out); + } + if let Some(f) = finally { + walk(f, out); + } + } + Stmt::Switch { cases, .. } => { + for case in cases { + walk(&case.body, out); + } + } + Stmt::Labeled { body, .. } => walk(std::slice::from_ref(body.as_ref()), out), + _ => {} + } + } + } +} + +/// Is `index` a number at runtime, so `u8[index]` provably reads a byte? +/// +/// `is_numeric_local` supplies the caller's evidence for a bare local — a +/// declared-type lookup, an integer-candidate set, or `|_| false` for a caller +/// that has none. Every other arm is structural, so the answer cannot drift +/// between the collectors that share it. +pub(crate) fn uint8array_get_reads_a_byte( + index: &Expr, + is_numeric_local: &mut dyn FnMut(u32) -> bool, +) -> bool { + match index { + Expr::Integer(_) | Expr::Number(_) => true, + // A byte read / a typed-array length is itself a number, so it is a + // number-valued key. + Expr::Uint8ArrayGet { index, .. } => uint8array_get_reads_a_byte(index, is_numeric_local), + Expr::BufferIndexGet { .. } | Expr::Uint8ArrayLength(_) | Expr::BufferLength(_) => true, + // Explicit ToNumber. + Expr::NumberCoerce(_) => true, + // `Math.*` coerce their operands internally (ToNumber — BigInt and + // Symbol throw) and return a raw double. Only the arms that plausibly + // appear in an index position are listed; an omission costs a + // representation, never correctness. + Expr::MathFloor(..) + | Expr::MathCeil(..) + | Expr::MathRound(..) + | Expr::MathTrunc(..) + | Expr::MathAbs(..) + | Expr::MathMin(..) + | Expr::MathMax(..) + | Expr::MathImul(..) => true, + // `-x` / `+x` / `~x` are ToNumber/ToInt32 — except on a BigInt, which + // stays a BigInt. Requiring a numeric operand excludes that case; it + // is stricter than `is_numeric_expr`'s `!is_bigint_expr` test, which + // is the safe direction here. + Expr::Unary { op, operand } => { + matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::BitNot) + && uint8array_get_reads_a_byte(operand, is_numeric_local) + } + Expr::Binary { op, left, right } => match op { + // ToInt32/ToUint32 — a number regardless of operand shape. + BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Shl + | BinaryOp::Shr + | BinaryOp::UShr => true, + // Arithmetic is ToNumeric on both sides: a number unless an + // operand is a BigInt, in which case the result is a BigInt. + BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod | BinaryOp::Pow => { + uint8array_get_reads_a_byte(left, is_numeric_local) + && uint8array_get_reads_a_byte(right, is_numeric_local) + } + // `+` may be string concatenation. + BinaryOp::Add => { + uint8array_get_reads_a_byte(left, is_numeric_local) + && uint8array_get_reads_a_byte(right, is_numeric_local) + } + }, + // `x++` / `--x` evaluate to `ToNumeric(x) ± 1` — a number unless `x` + // is a BigInt, which the caller's evidence has to rule out. + Expr::LocalGet(id) | Expr::Update { id, .. } => is_numeric_local(*id), + // Notably NOT here: `SymbolFor` (`u8[Symbol.iterator]`), `String`, + // template literals, calls, and property reads. + _ => false, + } +} diff --git a/crates/perry-codegen/src/collectors/byte_read_key_tests.rs b/crates/perry-codegen/src/collectors/byte_read_key_tests.rs new file mode 100644 index 0000000000..3e6776de07 --- /dev/null +++ b/crates/perry-codegen/src/collectors/byte_read_key_tests.rs @@ -0,0 +1,185 @@ +//! #7700: the key-kind condition on `Expr::Uint8ArrayGet`. +//! +//! These are unit tests on purpose. The acceptance case is +//! `test-files/test_gap_uint8array_nonnumeric_key_7700.ts`, and the gap suite +//! is TAG-gated — a regression there would sit red for days (#5960). The +//! predicate is `--lib`-visible, so this file runs on every PR. + +use super::byte_read_key::uint8array_get_reads_a_byte; +use perry_hir::{BinaryOp, Expr, UnaryOp}; + +/// No per-local evidence — the purely structural answer. +fn structural(index: &Expr) -> bool { + uint8array_get_reads_a_byte(index, &mut |_| false) +} + +/// Local `7` is proven numeric; nothing else is. +fn with_numeric_local_7(index: &Expr) -> bool { + uint8array_get_reads_a_byte(index, &mut |id| id == 7) +} + +fn sym(key: &str) -> Expr { + Expr::SymbolFor(Box::new(Expr::String(key.to_string()))) +} + +fn bin(op: BinaryOp, l: Expr, r: Expr) -> Expr { + Expr::Binary { + op, + left: Box::new(l), + right: Box::new(r), + } +} + +#[test] +fn numeric_literals_read_a_byte() { + assert!(structural(&Expr::Integer(3))); + assert!(structural(&Expr::Number(3.0))); +} + +/// The regression itself: `const it = u8[Symbol.iterator]` lowers to a +/// `Uint8ArrayGet` with a `SymbolFor` key. Answering "byte read" here gave the +/// destination local an i32 slot, and `ToInt32(ToNumber(fn))` made `typeof it` +/// report `number` instead of `function`. +#[test] +fn a_symbol_key_does_not_read_a_byte() { + assert!(!structural(&sym("@@__perry_wk_iterator"))); + assert!(!with_numeric_local_7(&sym("@@__perry_wk_iterator"))); +} + +/// `const k: any = "byteLength"; u8[k]` — the key is a `LocalGet` the caller +/// cannot prove numeric, so this is a property read, not a byte read. +#[test] +fn an_unproven_local_key_does_not_read_a_byte() { + assert!(!structural(&Expr::LocalGet(0))); + assert!(!with_numeric_local_7(&Expr::LocalGet(0))); +} + +/// …and a string key never did. +#[test] +fn a_string_key_does_not_read_a_byte() { + assert!(!structural(&Expr::String("subarray".to_string()))); +} + +/// The hot shape must survive: `for (let i = …) sum += buf[i]`. A local the +/// caller has proven integer-valued IS numeric-key evidence — the read is a +/// byte at runtime whichever codegen path it takes — so the loop keeps its i32 +/// representation. +#[test] +fn a_proven_numeric_local_key_reads_a_byte() { + assert!(with_numeric_local_7(&Expr::LocalGet(7))); + assert!(with_numeric_local_7(&Expr::Update { + id: 7, + op: perry_hir::UpdateOp::Increment, + prefix: false, + })); + // `buf[i + 1]`, `buf[i & 7]`. + assert!(with_numeric_local_7(&bin( + BinaryOp::Add, + Expr::LocalGet(7), + Expr::Integer(1) + ))); + assert!(with_numeric_local_7(&bin( + BinaryOp::BitAnd, + Expr::LocalGet(7), + Expr::Integer(7) + ))); +} + +/// ToInt32/ToUint32 producers are numeric whatever the operands are, so an +/// unproven local under a mask still reads a byte — `buf[k & 0xff]` is the +/// idiom this must not deoptimize. +#[test] +fn a_masked_unproven_key_still_reads_a_byte() { + assert!(structural(&bin( + BinaryOp::BitAnd, + Expr::LocalGet(0), + Expr::Integer(255) + ))); + assert!(structural(&bin( + BinaryOp::Shr, + Expr::LocalGet(0), + Expr::Integer(2) + ))); +} + +/// `+` may be string concatenation, so BOTH sides must be numeric — otherwise +/// `u8["by" + "teLength"]` would be admitted as a byte read. +#[test] +fn add_needs_both_sides_numeric() { + assert!(!structural(&bin( + BinaryOp::Add, + Expr::LocalGet(0), + Expr::Integer(1) + ))); + assert!(structural(&bin( + BinaryOp::Add, + Expr::Integer(2), + Expr::Integer(1) + ))); + assert!(!structural(&bin( + BinaryOp::Add, + Expr::String("by".to_string()), + Expr::String("teLength".to_string()) + ))); +} + +/// `-x` is ToNumber — except on a BigInt, which stays a BigInt. The operand +/// has to be numeric. +#[test] +fn unary_needs_a_numeric_operand() { + assert!(structural(&Expr::Unary { + op: UnaryOp::Neg, + operand: Box::new(Expr::Integer(1)), + })); + assert!(!structural(&Expr::Unary { + op: UnaryOp::Neg, + operand: Box::new(Expr::LocalGet(0)), + })); + // `!x` is a boolean, not a number. + assert!(!structural(&Expr::Unary { + op: UnaryOp::Not, + operand: Box::new(Expr::Integer(1)), + })); +} + +/// A nested byte read is a number, so it is a numeric key — but only if ITS +/// own key is numeric. +#[test] +fn a_nested_byte_read_is_a_numeric_key() { + let inner_numeric = Expr::Uint8ArrayGet { + array: Box::new(Expr::LocalGet(1)), + index: Box::new(Expr::Integer(0)), + }; + assert!(structural(&inner_numeric)); + + let inner_symbol = Expr::Uint8ArrayGet { + array: Box::new(Expr::LocalGet(1)), + index: Box::new(sym("@@__perry_wk_iterator")), + }; + assert!(!structural(&inner_symbol)); +} + +/// `u8.length` / `Math.floor(x)` are numbers. +#[test] +fn lengths_and_math_are_numeric_keys() { + assert!(structural(&Expr::Uint8ArrayLength(Box::new( + Expr::LocalGet(1) + )))); + assert!(structural(&Expr::MathFloor(Box::new(Expr::LocalGet(0))))); +} + +/// The allowlist's default: an expression kind nobody enumerated is NOT a +/// numeric key. This is the property the blocklist it replaced did not have — +/// "not a string literal" put every unconsidered key kind, symbols first, on +/// the wrong side. +#[test] +fn an_unenumerated_key_kind_is_rejected() { + assert!(!structural(&Expr::PropertyGet { + byte_offset: 0, + object: Box::new(Expr::LocalGet(0)), + property: "k".to_string(), + })); + assert!(!structural(&Expr::Undefined)); + assert!(!structural(&Expr::Null)); + assert!(!structural(&Expr::Bool(true))); +} diff --git a/crates/perry-codegen/src/collectors/hir_facts.rs b/crates/perry-codegen/src/collectors/hir_facts.rs index 16d51b46be..5e84255835 100644 --- a/crates/perry-codegen/src/collectors/hir_facts.rs +++ b/crates/perry-codegen/src/collectors/hir_facts.rs @@ -410,11 +410,18 @@ pub(crate) fn collect_type_facts( module_dispatch: &super::ModuleDispatchFacts, spec_ta_lens: &HashMap, ) -> TypeFacts { + // #7700: which locals hold a NUMBER, so a `u8[k]` keyed on one is a byte + // read rather than a property read. Computed once here because + // `binding_types` covers only params and module globals — the body `let`s, + // above all the counter in `for (let i = …) sum += buf[i]`, have to be + // walked for or the hottest buffer shape loses its i32 representation. + let numeric_locals = super::collect_numeric_typed_locals(stmts, params, binding_types); let mut integer_locals = super::integer_locals::collect_integer_locals( stmts, flat_const_ids, clamp_fn_ids, arg_dependent_clamp_fn_ids, + &numeric_locals, ); // Native-i32 residency for integer-valued locals whose init/writes include a // possibly-out-of-bounds INT typed-array element read (bcryptjs `_encipher` @@ -503,6 +510,7 @@ pub(crate) fn collect_type_facts( flat_const_ids, clamp_fn_ids, strict_int_ta_views, + &numeric_locals, ); // #7128: the profitability half of canonical-i32 selection. Every term // above answers "may we?"; this one answers "should we?", and it is @@ -2298,6 +2306,7 @@ mod tests { &HashSet::new(), &HashSet::new(), &HashSet::new(), + &HashSet::new(), ); assert!( @@ -2336,6 +2345,7 @@ mod tests { &HashSet::new(), &HashSet::new(), &HashSet::new(), + &HashSet::new(), ); assert!(ints.contains(&1), "live |0 accumulator must stay integer"); @@ -2375,6 +2385,7 @@ mod tests { &HashSet::new(), &HashSet::new(), &HashSet::new(), + &HashSet::new(), ); assert!( @@ -2428,6 +2439,7 @@ mod tests { &HashSet::new(), &clamp_ids, &clamp_ids, + &HashSet::new(), ); assert!(!ints.contains(&1), "non-int-written seed must be pruned"); assert!( @@ -2450,6 +2462,7 @@ mod tests { &HashSet::new(), &clamp_ids, &clamp_ids, + &HashSet::new(), ); assert!(ints.contains(&2), "int-arg clamp3 result must stay integer"); assert!(ints.contains(&3), "copy of live clamp3 result must stay"); @@ -2462,6 +2475,7 @@ mod tests { &HashSet::new(), &clamp_ids, &HashSet::new(), + &HashSet::new(), ); assert!( ints.contains(&2), @@ -2497,6 +2511,7 @@ mod tests { &HashSet::new(), &HashSet::new(), &HashSet::new(), + &HashSet::new(), ); assert!( !ints.contains(&2), @@ -2534,6 +2549,7 @@ mod tests { &HashSet::new(), &HashSet::new(), &HashSet::new(), + &HashSet::new(), ); assert!( !ints.contains(&2), diff --git a/crates/perry-codegen/src/collectors/i32_locals.rs b/crates/perry-codegen/src/collectors/i32_locals.rs index 43285d5d78..d0147aeef8 100644 --- a/crates/perry-codegen/src/collectors/i32_locals.rs +++ b/crates/perry-codegen/src/collectors/i32_locals.rs @@ -48,6 +48,9 @@ pub fn is_strictly_i32_bounded_expr( flat_row_alias_ids: &HashSet, clamp_fn_ids: &HashSet, int_ta_views: &HashMap, + // #7700: locals whose declared type says they hold a number — the + // evidence that `u8[k]` is a byte read rather than a property read. + numeric_locals: &HashSet, on_dep: &mut dyn FnMut(u32), ) -> bool { use perry_hir::{BinaryOp, Expr}; @@ -117,7 +120,11 @@ pub fn is_strictly_i32_bounded_expr( } ok } - Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => true, + // #7700: a byte read, hence i32-ranged, only with a numeric key. + Expr::Uint8ArrayGet { index, .. } => { + super::uint8array_get_reads_a_byte(index, &mut |id| numeric_locals.contains(&id)) + } + Expr::BufferIndexGet { .. } => true, Expr::MathImul(_, _) => true, // Repsel Phase 1 widening (gated by the caller passing a non-empty // view map, itself behind `PERRY_CANONICAL_I32_LOCALS`): a proven @@ -163,6 +170,7 @@ pub struct StrictWriteFacts { } /// Judge one write to `id` against the oracle and fold the verdict into `out`. +#[allow(clippy::too_many_arguments)] fn record_strict_write( id: u32, value: &perry_hir::Expr, @@ -170,6 +178,7 @@ fn record_strict_write( flat_const_ids: &HashSet, flat_row_alias_ids: &HashSet, clamp_fn_ids: &HashSet, + numeric_locals: &HashSet, out: &mut StrictWriteFacts, ) { let mut deps: Vec = Vec::new(); @@ -180,6 +189,7 @@ fn record_strict_write( flat_row_alias_ids, clamp_fn_ids, &out.int_ta_views, + numeric_locals, &mut |d| deps.push(d), ); out.saw_any.insert(id); @@ -242,6 +252,8 @@ pub fn collect_strictly_i32_bounded_locals( flat_const_ids: &HashSet, clamp_fn_ids: &HashSet, int_ta_views: HashMap, + // #7700: see `is_strictly_i32_bounded_expr`. + numeric_locals: &HashSet, ) -> HashSet { let mut flat_row_alias_ids: HashSet = HashSet::new(); collect_flat_row_aliases(stmts, flat_const_ids, &mut flat_row_alias_ids); @@ -259,6 +271,7 @@ pub fn collect_strictly_i32_bounded_locals( flat_const_ids, &flat_row_alias_ids, clamp_fn_ids, + numeric_locals, &mut out, ); @@ -434,6 +447,7 @@ pub fn walk_writes_for_strict( flat_const_ids: &HashSet, flat_row_alias_ids: &HashSet, clamp_fn_ids: &HashSet, + numeric_locals: &HashSet, out: &mut StrictWriteFacts, ) { use perry_hir::Stmt; @@ -451,6 +465,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); walk_writes_in_expr_for_strict( @@ -459,6 +474,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -470,6 +486,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -481,6 +498,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -496,6 +514,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); walk_writes_for_strict( @@ -504,6 +523,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); if let Some(eb) = else_branch { @@ -513,6 +533,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -524,6 +545,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); walk_writes_for_strict( @@ -532,6 +554,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -548,6 +571,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -558,6 +582,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -568,6 +593,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -577,6 +603,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -591,6 +618,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); if let Some(c) = catch { @@ -600,6 +628,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -610,6 +639,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -624,6 +654,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); for c in cases { @@ -634,6 +665,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -643,6 +675,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -654,6 +687,7 @@ pub fn walk_writes_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -668,6 +702,7 @@ pub fn walk_writes_in_expr_for_strict( flat_const_ids: &HashSet, flat_row_alias_ids: &HashSet, clamp_fn_ids: &HashSet, + numeric_locals: &HashSet, out: &mut StrictWriteFacts, ) { use perry_hir::Expr; @@ -680,6 +715,7 @@ pub fn walk_writes_in_expr_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); walk_writes_in_expr_for_strict( @@ -688,6 +724,7 @@ pub fn walk_writes_in_expr_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); } @@ -719,6 +756,7 @@ pub fn walk_writes_in_expr_for_strict( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, out, ); }); @@ -1211,6 +1249,11 @@ pub fn collect_localset_ids_in_expr_filtered( clamp_fn_ids, ); }; + // Every caller of this walker passes `filter: None` (the only entry point + // is `collect_localset_ids_in_stmts`), so the `Some` arm below never runs. + // It keeps its shape rather than being deleted; an empty numeric-local set + // is the conservative answer if it is ever revived (#7700). + let no_numeric_locals: HashSet = HashSet::new(); match e { Expr::LocalSet(id, value) => { match filter { @@ -1221,6 +1264,7 @@ pub fn collect_localset_ids_in_expr_filtered( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + &no_numeric_locals, ) => {} _ => { out.insert(*id); diff --git a/crates/perry-codegen/src/collectors/int_valued_ta_locals.rs b/crates/perry-codegen/src/collectors/int_valued_ta_locals.rs index 9f6dce0c25..8ff00d9371 100644 --- a/crates/perry-codegen/src/collectors/int_valued_ta_locals.rs +++ b/crates/perry-codegen/src/collectors/int_valued_ta_locals.rs @@ -124,6 +124,13 @@ fn is_int_kind_ta_read(e: &Expr, types: &HashMap) -> bool { matches!(e, Expr::IndexGet { object, .. } if receiver_is_int_kind_ta(object, types)) } +/// #7700: `u8[k]` is a BYTE read — hence integer-valued — only when `k` is a +/// number at runtime. With a symbol or string key it reads a PROPERTY, and the +/// value is whatever that property holds (a method, a length, an expando). +fn uint8array_get_is_byte_read(index: &Expr, numeric_locals: &HashSet) -> bool { + super::uint8array_get_reads_a_byte(index, &mut |id| numeric_locals.contains(&id)) +} + fn is_bitwise_binop(op: BinaryOp) -> bool { matches!( op, @@ -140,7 +147,11 @@ fn is_bitwise_binop(op: BinaryOp) -> bool { /// possibly-OOB int typed-array read (integer in-bounds, `undefined` OOB — made /// observationally equivalent to `0` by rule (2)). Rejects additive / `*` / /// `/` / `%` (i32 overflow / non-integer), copies, calls, and everything else. -fn write_is_i32_producing_safe(e: &Expr, types: &HashMap) -> bool { +fn write_is_i32_producing_safe( + e: &Expr, + types: &HashMap, + numeric_locals: &HashSet, +) -> bool { match e { Expr::Integer(n) => super::i32_locals::integer_literal_fits_i32(*n), // Hoisted-`var` seed (`var n, l = lr[off]` lowers as `Let{l, @@ -149,8 +160,9 @@ fn write_is_i32_producing_safe(e: &Expr, types: &HashMap) -> bool // already guarantees every observation is ToInt32-coercing — so an // `undefined` write is indistinguishable from the 0 it becomes. Expr::Undefined => true, - // Byte reads: `0` OOB, always integer. - Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => true, + // Byte reads: `0` OOB, always integer — #7700: with a numeric key. + Expr::Uint8ArrayGet { index, .. } => uint8array_get_is_byte_read(index, numeric_locals), + Expr::BufferIndexGet { .. } => true, // Int-kind typed-array element read (possibly OOB → `undefined`). Expr::IndexGet { object, .. } => receiver_is_int_kind_ta(object, types), // Bitwise ops coerce both operands to int32 and yield int32 regardless @@ -213,11 +225,14 @@ fn additive_write_admissible( types: &HashMap, ta_lens: &HashMap, pool: &HashSet, + numeric_locals: &HashSet, ) -> bool { match e { Expr::Integer(n) => super::i32_locals::integer_literal_fits_i32(*n), Expr::LocalGet(id) => pool.contains(id), - Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => true, + // #7700: a byte read only with a numeric key. + Expr::Uint8ArrayGet { index, .. } => uint8array_get_is_byte_read(index, numeric_locals), + Expr::BufferIndexGet { .. } => true, // In-bounds-proven int-kind typed-array read: never `undefined`. Expr::IndexGet { object, index } => { receiver_is_int_kind_ta(object, types) @@ -235,8 +250,8 @@ fn additive_write_admissible( } => true, Expr::MathImul(_, _) => true, Expr::Binary { op, left, right } if matches!(op, BinaryOp::Add | BinaryOp::Sub) => { - additive_write_admissible(left, types, ta_lens, pool) - && additive_write_admissible(right, types, ta_lens, pool) + additive_write_admissible(left, types, ta_lens, pool, numeric_locals) + && additive_write_admissible(right, types, ta_lens, pool, numeric_locals) } _ => false, } @@ -261,11 +276,14 @@ fn write_establishes_number( types: &HashMap, ta_lens: &HashMap, numberish: &HashSet, + numeric_locals: &HashSet, ) -> bool { match e { Expr::Integer(_) | Expr::Number(_) => true, - // Byte reads return `0` OOB — always a number. - Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => true, + // Byte reads return `0` OOB — always a number, with a numeric key + // (#7700). + Expr::Uint8ArrayGet { index, .. } => uint8array_get_is_byte_read(index, numeric_locals), + Expr::BufferIndexGet { .. } => true, // Bitwise / `~` / `Math.imul` are number-or-throw (a throw means the // write never completes). Expr::Binary { op, .. } if is_bitwise_binop(*op) => true, @@ -285,8 +303,8 @@ fn write_establishes_number( } Expr::LocalGet(id) => numberish.contains(id), Expr::Binary { op, left, right } if matches!(op, BinaryOp::Add | BinaryOp::Sub) => { - write_establishes_number(left, types, ta_lens, numberish) - && write_establishes_number(right, types, ta_lens, numberish) + write_establishes_number(left, types, ta_lens, numberish, numeric_locals) + && write_establishes_number(right, types, ta_lens, numberish, numeric_locals) } _ => false, } @@ -320,10 +338,18 @@ fn additive_flow_invalid_targets( stmts: &[Stmt], types: &HashMap, ta_lens: &HashMap, + numeric_locals: &HashSet, ) -> HashSet { let mut invalid = HashSet::new(); let mut numberish = HashSet::new(); - additive_flow_stmts(stmts, types, ta_lens, &mut numberish, &mut invalid); + additive_flow_stmts( + stmts, + types, + ta_lens, + numeric_locals, + &mut numberish, + &mut invalid, + ); invalid } @@ -331,11 +357,12 @@ fn additive_flow_stmts( stmts: &[Stmt], types: &HashMap, ta_lens: &HashMap, + numeric_locals: &HashSet, numberish: &mut HashSet, invalid: &mut HashSet, ) { for s in stmts { - additive_flow_stmt(s, types, ta_lens, numberish, invalid); + additive_flow_stmt(s, types, ta_lens, numeric_locals, numberish, invalid); } } @@ -343,30 +370,51 @@ fn additive_flow_stmt( s: &Stmt, types: &HashMap, ta_lens: &HashMap, + numeric_locals: &HashSet, numberish: &mut HashSet, invalid: &mut HashSet, ) { match s { Stmt::Let { id, init, .. } => match init { - Some(e) => additive_flow_expr_write(*id, e, types, ta_lens, numberish, invalid), + Some(e) => { + additive_flow_expr_write(*id, e, types, ta_lens, numeric_locals, numberish, invalid) + } // `let x;` — undefined. None => { numberish.remove(id); } }, - Stmt::Expr(e) | Stmt::Throw(e) => additive_flow_expr(e, types, ta_lens, numberish, invalid), - Stmt::Return(Some(e)) => additive_flow_expr(e, types, ta_lens, numberish, invalid), + Stmt::Expr(e) | Stmt::Throw(e) => { + additive_flow_expr(e, types, ta_lens, numeric_locals, numberish, invalid) + } + Stmt::Return(Some(e)) => { + additive_flow_expr(e, types, ta_lens, numeric_locals, numberish, invalid) + } Stmt::If { condition, then_branch, else_branch, } => { - additive_flow_expr(condition, types, ta_lens, numberish, invalid); + additive_flow_expr( + condition, + types, + ta_lens, + numeric_locals, + numberish, + invalid, + ); let mut then_set = numberish.clone(); - additive_flow_stmts(then_branch, types, ta_lens, &mut then_set, invalid); + additive_flow_stmts( + then_branch, + types, + ta_lens, + numeric_locals, + &mut then_set, + invalid, + ); let mut else_set = numberish.clone(); if let Some(eb) = else_branch { - additive_flow_stmts(eb, types, ta_lens, &mut else_set, invalid); + additive_flow_stmts(eb, types, ta_lens, numeric_locals, &mut else_set, invalid); } *numberish = then_set.intersection(&else_set).copied().collect(); } @@ -376,8 +424,15 @@ fn additive_flow_stmt( // conservative for later iterations too, since the walk can only // remove entries the body would remove on any iteration. let mut body_set = numberish.clone(); - additive_flow_expr(condition, types, ta_lens, &mut body_set, invalid); - additive_flow_stmts(body, types, ta_lens, &mut body_set, invalid); + additive_flow_expr( + condition, + types, + ta_lens, + numeric_locals, + &mut body_set, + invalid, + ); + additive_flow_stmts(body, types, ta_lens, numeric_locals, &mut body_set, invalid); numberish.retain(|id| body_set.contains(id)); } Stmt::For { @@ -387,19 +442,21 @@ fn additive_flow_stmt( body, } => { if let Some(i) = init { - additive_flow_stmt(i, types, ta_lens, numberish, invalid); + additive_flow_stmt(i, types, ta_lens, numeric_locals, numberish, invalid); } let mut body_set = numberish.clone(); if let Some(c) = condition { - additive_flow_expr(c, types, ta_lens, &mut body_set, invalid); + additive_flow_expr(c, types, ta_lens, numeric_locals, &mut body_set, invalid); } if let Some(u) = update { - additive_flow_expr(u, types, ta_lens, &mut body_set, invalid); + additive_flow_expr(u, types, ta_lens, numeric_locals, &mut body_set, invalid); } - additive_flow_stmts(body, types, ta_lens, &mut body_set, invalid); + additive_flow_stmts(body, types, ta_lens, numeric_locals, &mut body_set, invalid); numberish.retain(|id| body_set.contains(id)); } - Stmt::Labeled { body, .. } => additive_flow_stmt(body, types, ta_lens, numberish, invalid), + Stmt::Labeled { body, .. } => { + additive_flow_stmt(body, types, ta_lens, numeric_locals, numberish, invalid) + } Stmt::Try { body, catch, @@ -408,16 +465,23 @@ fn additive_flow_stmt( // The try body may partially execute; the catch entry state is // unknown. Meet everything. let mut body_set = numberish.clone(); - additive_flow_stmts(body, types, ta_lens, &mut body_set, invalid); + additive_flow_stmts(body, types, ta_lens, numeric_locals, &mut body_set, invalid); numberish.retain(|id| body_set.contains(id)); if let Some(c) = catch { let mut catch_set = numberish.clone(); - additive_flow_stmts(&c.body, types, ta_lens, &mut catch_set, invalid); + additive_flow_stmts( + &c.body, + types, + ta_lens, + numeric_locals, + &mut catch_set, + invalid, + ); numberish.retain(|id| catch_set.contains(id)); } if let Some(f) = finally { let mut fin_set = numberish.clone(); - additive_flow_stmts(f, types, ta_lens, &mut fin_set, invalid); + additive_flow_stmts(f, types, ta_lens, numeric_locals, &mut fin_set, invalid); numberish.retain(|id| fin_set.contains(id)); } } @@ -425,14 +489,28 @@ fn additive_flow_stmt( discriminant, cases, } => { - additive_flow_expr(discriminant, types, ta_lens, numberish, invalid); + additive_flow_expr( + discriminant, + types, + ta_lens, + numeric_locals, + numberish, + invalid, + ); let pre = numberish.clone(); for case in cases { if let Some(t) = &case.test { - additive_flow_expr(t, types, ta_lens, numberish, invalid); + additive_flow_expr(t, types, ta_lens, numeric_locals, numberish, invalid); } let mut case_set = pre.clone(); - additive_flow_stmts(&case.body, types, ta_lens, &mut case_set, invalid); + additive_flow_stmts( + &case.body, + types, + ta_lens, + numeric_locals, + &mut case_set, + invalid, + ); numberish.retain(|id| case_set.contains(id)); } } @@ -454,18 +532,19 @@ fn additive_flow_expr_write( rhs: &Expr, types: &HashMap, ta_lens: &HashMap, + numeric_locals: &HashSet, numberish: &mut HashSet, invalid: &mut HashSet, ) { // Walk nested writes inside the RHS first (their effects precede the // outer store; any imprecision here only removes numberish entries). - additive_flow_expr(rhs, types, ta_lens, numberish, invalid); + additive_flow_expr(rhs, types, ta_lens, numeric_locals, numberish, invalid); if matches!(rhs, Expr::Binary { op, .. } if matches!(op, BinaryOp::Add | BinaryOp::Sub)) && !additive_spine_locals_numberish(rhs, numberish) { invalid.insert(target); } - if write_establishes_number(rhs, types, ta_lens, numberish) { + if write_establishes_number(rhs, types, ta_lens, numberish, numeric_locals) { numberish.insert(target); } else { numberish.remove(&target); @@ -476,12 +555,13 @@ fn additive_flow_expr( e: &Expr, types: &HashMap, ta_lens: &HashMap, + numeric_locals: &HashSet, numberish: &mut HashSet, invalid: &mut HashSet, ) { match e { Expr::LocalSet(id, rhs) => { - additive_flow_expr_write(*id, rhs, types, ta_lens, numberish, invalid); + additive_flow_expr_write(*id, rhs, types, ta_lens, numeric_locals, numberish, invalid); } // `x++` may produce a BigInt (ToNumeric preserves kind) — drop. Expr::Update { id, .. } => { @@ -497,7 +577,7 @@ fn additive_flow_expr( } _ => { perry_hir::walker::walk_expr_children(e, &mut |c| { - additive_flow_expr(c, types, ta_lens, numberish, invalid) + additive_flow_expr(c, types, ta_lens, numeric_locals, numberish, invalid) }); } } @@ -518,6 +598,11 @@ pub fn collect_int_valued_ta_locals( // Constant typed-array lengths for the wrap-i32 in-bounds proof: in-body // literal-length const views plus caller-supplied lengths (spec-ABI // `TaPtr` params carry theirs from the call-site pre-pass). + // #7700: which locals hold a number, so `u8[k]` keyed on one is a byte + // read. `binding_types` covers params and module globals only, so the body + // `let`s — above all the counter in `for (let i = …) sum += buf[i]` — have + // to be walked for, or the hottest buffer shape loses its i32 slot. + let numeric_locals = super::collect_numeric_typed_locals(stmts, params, binding_types); let mut ta_lens = super::integer_locals::collect_const_int_ta_views(stmts); for (id, len) in extra_ta_lens { ta_lens.entry(*id).or_insert(*len); @@ -529,7 +614,7 @@ pub fn collect_int_valued_ta_locals( // writes whose spine operands were not provably NUMBERS at the write site // (an `undefined`-able operand breaks `image == ToInt32(true)` through a // float add — `undefined + 1` is NaN→0, the image path would say 1). - let additive_invalid = additive_flow_invalid_targets(stmts, &types, &ta_lens); + let additive_invalid = additive_flow_invalid_targets(stmts, &types, &ta_lens, &numeric_locals); // Rule (1) admission. A candidate is a `let`-declared local with ≥1 // int-TA-read write, whose EVERY write is i32-producing-safe (or, in the @@ -554,10 +639,16 @@ pub fn collect_int_valued_ta_locals( let snapshot = pool.clone(); pool.retain(|id| { facts.writes[id].iter().all(|(w, in_loop)| { - write_is_i32_producing_safe(w, &types) + write_is_i32_producing_safe(w, &types, &numeric_locals) || (!in_loop && !additive_invalid.contains(id) - && additive_write_admissible(w, &types, &ta_lens, &snapshot)) + && additive_write_admissible( + w, + &types, + &ta_lens, + &snapshot, + &numeric_locals, + )) }) }); if pool.len() == before { @@ -579,6 +670,7 @@ pub fn collect_int_valued_ta_locals( let additive_ctx = AdditiveCtx { ta_lens: &ta_lens, pool: &candidates, + numeric_locals: &numeric_locals, }; observe_stmts(stmts, &candidates, &types, &additive_ctx, &mut disqualified); if disqualified.is_empty() { @@ -593,10 +685,16 @@ pub fn collect_int_valued_ta_locals( let snapshot = candidates.clone(); candidates.retain(|id| { facts.writes[id].iter().all(|(w, in_loop)| { - write_is_i32_producing_safe(w, &types) + write_is_i32_producing_safe(w, &types, &numeric_locals) || (!in_loop && !additive_invalid.contains(id) - && additive_write_admissible(w, &types, &ta_lens, &snapshot)) + && additive_write_admissible( + w, + &types, + &ta_lens, + &snapshot, + &numeric_locals, + )) }) }); changed = candidates.len() != before; @@ -612,6 +710,8 @@ pub fn collect_int_valued_ta_locals( struct AdditiveCtx<'a> { ta_lens: &'a HashMap, pool: &'a HashSet, + /// #7700: locals whose declared type says they hold a number. + numeric_locals: &'a HashSet, } // --------------------------------------------------------------------------- @@ -894,7 +994,13 @@ fn observe_stmts( // Same additive blessing as the `LocalSet` arm — a // candidate's Let-init may be an admissible additive tree. if cands.contains(id) - && additive_write_admissible(e, types, additive.ta_lens, additive.pool) + && additive_write_admissible( + e, + types, + additive.ta_lens, + additive.pool, + additive.numeric_locals, + ) { observe_additive_rhs(e, cands, types, additive, disq); } else { @@ -1089,7 +1195,13 @@ fn observe( // at its Add/Sub-operand positions. Expr::LocalSet(target, value) => { if cands.contains(target) - && additive_write_admissible(value, types, additive.ta_lens, additive.pool) + && additive_write_admissible( + value, + types, + additive.ta_lens, + additive.pool, + additive.numeric_locals, + ) { observe_additive_rhs(value, cands, types, additive, disq); } else { diff --git a/crates/perry-codegen/src/collectors/integer_locals.rs b/crates/perry-codegen/src/collectors/integer_locals.rs index 8fcb338c8f..d4aa2ab953 100644 --- a/crates/perry-codegen/src/collectors/integer_locals.rs +++ b/crates/perry-codegen/src/collectors/integer_locals.rs @@ -374,6 +374,10 @@ pub fn collect_integer_locals( flat_const_ids: &HashSet, clamp_fn_ids: &HashSet, arg_dependent_clamp_fn_ids: &HashSet, + // #7700: locals whose declared type says they hold a number. A + // `Uint8ArrayGet` is a byte read — hence integer-valued — only when its + // key is one of those, or is numeric by construction. + numeric_locals: &HashSet, ) -> HashSet { let mut candidates: HashSet = HashSet::new(); @@ -423,6 +427,7 @@ pub fn collect_integer_locals( flat_const_ids, &flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); if candidates.len() == before { break; @@ -447,6 +452,7 @@ pub fn collect_integer_locals( flat_row_alias_ids: &flat_row_alias_ids, clamp_fn_ids, arg_dependent_clamp_fn_ids, + numeric_locals, int_ta_views: &int_ta_views, dependents: HashMap::new(), disqualified: HashSet::new(), @@ -496,6 +502,9 @@ struct ProvenanceJudge<'a> { flat_row_alias_ids: &'a HashSet, clamp_fn_ids: &'a HashSet, arg_dependent_clamp_fn_ids: &'a HashSet, + /// #7700: locals whose declared type says they hold a number, so a + /// `u8[k]` keyed on one is a byte read. + numeric_locals: &'a HashSet, /// Const int-typed-array views (`id → length`) whose in-window element /// loads are integers by construction — obligations whose rhs is such a /// load pass without deps. @@ -524,6 +533,7 @@ impl ProvenanceJudge<'_> { self.flat_row_alias_ids, self.clamp_fn_ids, self.arg_dependent_clamp_fn_ids, + self.numeric_locals, &mut deps, ) { for dep in deps { @@ -673,6 +683,7 @@ fn int32_producing_deps( flat_row_alias_ids: &HashSet, clamp_fn_ids: &HashSet, arg_dependent_clamp_fn_ids: &HashSet, + numeric_locals: &HashSet, deps: &mut HashSet, ) -> bool { let recurse = |sub: &Expr, deps: &mut HashSet| { @@ -683,6 +694,7 @@ fn int32_producing_deps( flat_row_alias_ids, clamp_fn_ids, arg_dependent_clamp_fn_ids, + numeric_locals, deps, ) }; @@ -733,7 +745,13 @@ fn int32_producing_deps( deps.insert(*id); true } - Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => true, + // #7700: a byte read, hence integer-valued, only when the KEY is a + // number. `const it = u8[Symbol.iterator]` is a function; answering + // `true` here gave it an i32 slot and `typeof it` reported `number`. + Expr::Uint8ArrayGet { index, .. } => { + super::uint8array_get_reads_a_byte(index, &mut |id| numeric_locals.contains(&id)) + } + Expr::BufferIndexGet { .. } => true, Expr::MathImul(_, _) => true, // Issue #50 bridge: element access on a flat-const 2D int array // produces i32. The flat-const facts are immutable within this @@ -763,6 +781,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids: &HashSet, flat_row_alias_ids: &HashSet, clamp_fn_ids: &HashSet, + numeric_locals: &HashSet, ) { use perry_hir::Stmt; for s in stmts { @@ -783,6 +802,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ) => { out.insert(*id); @@ -798,6 +818,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); if let Some(eb) = else_branch { collect_extra_integer_let_ids( @@ -806,6 +827,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); } } @@ -817,6 +839,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); } collect_extra_integer_let_ids( @@ -825,6 +848,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); } Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => { @@ -834,6 +858,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); } Stmt::Try { @@ -847,6 +872,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); if let Some(c) = catch { collect_extra_integer_let_ids( @@ -855,6 +881,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); } if let Some(f) = finally { @@ -864,6 +891,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); } } @@ -875,6 +903,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); } } @@ -885,6 +914,7 @@ pub fn collect_extra_integer_let_ids( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ); } _ => {} @@ -997,6 +1027,8 @@ pub fn is_int32_producing_expr( flat_const_ids: &HashSet, flat_row_alias_ids: &HashSet, clamp_fn_ids: &HashSet, + // #7700: see `collect_integer_locals`. + numeric_locals: &HashSet, ) -> bool { use perry_hir::{BinaryOp, Expr}; match e { @@ -1020,12 +1052,14 @@ pub fn is_int32_producing_expr( flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ) && is_int32_producing_expr( right, known_int_locals, flat_const_ids, flat_row_alias_ids, clamp_fn_ids, + numeric_locals, ) } Expr::Call { callee, .. } => { @@ -1045,7 +1079,11 @@ pub fn is_int32_producing_expr( | BinaryOp::UShr ), Expr::LocalGet(id) => known_int_locals.contains(id), - Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => true, + // #7700: a byte read, hence integer-valued, only with a numeric key. + Expr::Uint8ArrayGet { index, .. } => { + super::uint8array_get_reads_a_byte(index, &mut |id| numeric_locals.contains(&id)) + } + Expr::BufferIndexGet { .. } => true, Expr::MathImul(_, _) => true, // Math.imul always returns i32 // Issue #50 bridge: element access on a flat-const 2D int array // produces i32. Two shapes: diff --git a/crates/perry-codegen/src/collectors/mod.rs b/crates/perry-codegen/src/collectors/mod.rs index c2826e3a65..a370c03c50 100644 --- a/crates/perry-codegen/src/collectors/mod.rs +++ b/crates/perry-codegen/src/collectors/mod.rs @@ -6,6 +6,9 @@ //! hub — public-API shape (`crate::collectors::*`) is preserved. mod all_pointer_arrays; +mod byte_read_key; +#[cfg(test)] +mod byte_read_key_tests; mod cjs_scaffolding; mod clamp_detect; mod class_accessors; @@ -50,6 +53,7 @@ pub use clamp_detect::{detect_clamp3, detect_clamp_u8, returns_i32_identity_arg, // Internal-to-crate re-exports — explicit names because globs don't // transitively expose through `pub(crate) use crate::collectors::*`. +pub(crate) use byte_read_key::{collect_numeric_typed_locals, uint8array_get_reads_a_byte}; pub(crate) use class_accessors::{is_class_getter, is_class_setter}; pub(crate) use closures::collect_closures_in_stmts; pub(crate) use escape_arrays::{const_index, MAX_SCALAR_OBJECT_FIELDS}; diff --git a/crates/perry-codegen/src/collectors/not_bigint_locals.rs b/crates/perry-codegen/src/collectors/not_bigint_locals.rs index 57dea8ebfe..e6e056a29b 100644 --- a/crates/perry-codegen/src/collectors/not_bigint_locals.rs +++ b/crates/perry-codegen/src/collectors/not_bigint_locals.rs @@ -43,6 +43,9 @@ pub fn collect_not_bigint_locals( for p in params { types.entry(p.id).or_insert_with(|| p.ty.clone()); } + // #7700: locals holding a number, so `u8[k]` keyed on one is a byte read + // (which is never a BigInt) rather than a property read (which can be). + let numeric_locals = super::collect_numeric_typed_locals(stmts, params, binding_types); // Every write (Let init + `LocalSet` rhs) per candidate local. Descends // into closure bodies so a `LocalSet` to an ENCLOSING local inside a @@ -68,7 +71,7 @@ pub fn collect_not_bigint_locals( // non-BigInt — so no writes means the local stays. .map(|ws| { ws.iter() - .all(|rhs| expr_not_bigint(rhs, &types, ¬_bigint)) + .all(|rhs| expr_not_bigint(rhs, &types, ¬_bigint, &numeric_locals)) }) .unwrap_or(true); if !all_ok { @@ -89,7 +92,12 @@ pub fn collect_not_bigint_locals( /// `type_analysis::is_provably_not_bigint`: can this expression's value never be /// a BigInt? `LocalGet` leaves resolve against `set` (optimistic membership) or /// a concrete non-BigInt declared type. -fn expr_not_bigint(e: &Expr, types: &HashMap, set: &HashSet) -> bool { +fn expr_not_bigint( + e: &Expr, + types: &HashMap, + set: &HashSet, + numeric_locals: &HashSet, +) -> bool { match e { // Non-BigInt literals. Expr::Undefined @@ -121,15 +129,20 @@ fn expr_not_bigint(e: &Expr, types: &HashMap, set: &HashSet) | Expr::DateNow => true, // Typed-array / numeric-array element reads yield Number | undefined, - // never a BigInt. - Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => true, + // never a BigInt. #7700: only a NUMERIC key reads an element — with a + // symbol or string key this is a PROPERTY read, and an expando holds + // anything (`u8.n = 1n; const k: any = "n"; u8[k]` is a BigInt). + Expr::Uint8ArrayGet { index, .. } => { + super::uint8array_get_reads_a_byte(index, &mut |id| numeric_locals.contains(&id)) + } + Expr::BufferIndexGet { .. } => true, Expr::IndexGet { object, .. } => index_receiver_is_numeric(object, types), // `!x` → boolean; `+x` → Number-or-throw (never a BigInt VALUE). // `-x` / `~x` preserve BigInt. Expr::Unary { op, operand } => match op { UnaryOp::Not | UnaryOp::Pos => true, - UnaryOp::Neg | UnaryOp::BitNot => expr_not_bigint(operand, types, set), + UnaryOp::Neg | UnaryOp::BitNot => expr_not_bigint(operand, types, set, numeric_locals), }, // Arithmetic / bitwise binary ops yield a BigInt only when BOTH @@ -137,7 +150,8 @@ fn expr_not_bigint(e: &Expr, types: &HashMap, set: &HashSet) // as EITHER operand is. (`BinaryOp` has only arithmetic/bitwise // variants.) Expr::Binary { left, right, .. } => { - expr_not_bigint(left, types, set) || expr_not_bigint(right, types, set) + expr_not_bigint(left, types, set, numeric_locals) + || expr_not_bigint(right, types, set, numeric_locals) } // Selection: non-BigInt when every branch that can become the value is. @@ -145,13 +159,17 @@ fn expr_not_bigint(e: &Expr, types: &HashMap, set: &HashSet) then_expr, else_expr, .. - } => expr_not_bigint(then_expr, types, set) && expr_not_bigint(else_expr, types, set), + } => { + expr_not_bigint(then_expr, types, set, numeric_locals) + && expr_not_bigint(else_expr, types, set, numeric_locals) + } Expr::Logical { left, right, .. } => { - expr_not_bigint(left, types, set) && expr_not_bigint(right, types, set) + expr_not_bigint(left, types, set, numeric_locals) + && expr_not_bigint(right, types, set, numeric_locals) } // A `LocalSet` used as an expression evaluates to the assigned value. - Expr::LocalSet(_, rhs) => expr_not_bigint(rhs, types, set), + Expr::LocalSet(_, rhs) => expr_not_bigint(rhs, types, set, numeric_locals), // Leaf locals: proven by the running assumption or a concrete // non-BigInt declared type. `Update` (`i++`) yields `ToNumeric(i) ± 1`, diff --git a/crates/perry-codegen/src/stmt/masked_window_region.rs b/crates/perry-codegen/src/stmt/masked_window_region.rs index 557bc0e35f..8b07904a92 100644 --- a/crates/perry-codegen/src/stmt/masked_window_region.rs +++ b/crates/perry-codegen/src/stmt/masked_window_region.rs @@ -272,6 +272,12 @@ fn region_i32_bounded_write_locals(stmts: &[Stmt]) -> std::collections::HashSet< &empty, &empty, &empty_views, + // #7700: this region walker deliberately consults none of + // the function-wide oracles (see the doc comment), so it + // has no numeric-local evidence either — a `u8[k]` write + // source keyed on a bare local drops out, exactly like the + // copy-shaped writes it already drops. + &empty, &mut |_| {}, ); if !strict { diff --git a/crates/perry-codegen/src/type_analysis/numeric.rs b/crates/perry-codegen/src/type_analysis/numeric.rs index 20ce26445a..411a77967a 100644 --- a/crates/perry-codegen/src/type_analysis/numeric.rs +++ b/crates/perry-codegen/src/type_analysis/numeric.rs @@ -131,10 +131,15 @@ pub(crate) fn is_numeric_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { | Expr::PodLayoutSizeOf { .. } | Expr::PodLayoutAlignOf { .. } | Expr::PodLayoutOffsetOf { .. } => true, - Expr::Uint8ArrayGet { .. } - | Expr::BufferIndexGet { .. } - | Expr::Uint8ArrayLength(_) - | Expr::BufferLength(_) => true, + // #7700: a `Uint8ArrayGet` is a BYTE read only when its key is numeric. + // This is the very test `arrays_finds::lower_uint8array_get_i32` applies + // to choose between the byte accessor and + // `js_object_get_index_polymorphic`, so the two cannot disagree about + // whether `u8[Symbol.iterator]` is a number — which matters wherever a + // `true` here means "a raw double": `fcmp`-based truthiness, `fadd` + // operands, the non-BigInt bitwise fast path. + Expr::Uint8ArrayGet { index, .. } => is_numeric_expr(ctx, index), + Expr::BufferIndexGet { .. } | Expr::Uint8ArrayLength(_) | Expr::BufferLength(_) => true, Expr::LocalGet(id) => matches!( ctx.local_types.get(id), Some(HirType::Number) | Some(HirType::Int32) @@ -614,7 +619,11 @@ pub(crate) fn is_provably_not_bigint(ctx: &FnCtx<'_>, e: &Expr) -> bool { // BigInt. `Uint8ArrayGet` / `BufferIndexGet` are byte reads; a general // `IndexGet` qualifies only when the receiver is a numeric typed array // (a plain object / array element could hold a BigInt). - Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => true, + // #7700: a byte read only with a numeric key. With any other key this + // is a property read, and an expando holds anything — `u8.n = 1n; + // const k: any = "n"; u8[k]` IS a BigInt. + Expr::Uint8ArrayGet { index, .. } => is_numeric_expr(ctx, index), + Expr::BufferIndexGet { .. } => true, Expr::IndexGet { object, .. } => receiver_class_name(ctx, object) .as_deref() .is_some_and(is_numeric_typed_array_class), @@ -722,8 +731,11 @@ fn integer_magnitude_bits_inner(ctx: &FnCtx<'_>, e: &Expr, allow_i64_locals: boo let recurse = |sub: &Expr| integer_magnitude_bits_inner(ctx, sub, allow_i64_locals); match e { Expr::Integer(v) => Some(crate::collectors::ceil_log2_abs(*v)), - // A byte value. - Expr::Uint8ArrayGet { .. } | Expr::BufferIndexGet { .. } => Some(8), + // A byte value — #7700: only with a numeric key. A property read has no + // magnitude bound at all. + Expr::Uint8ArrayGet { index, .. } if is_numeric_expr(ctx, index) => Some(8), + Expr::Uint8ArrayGet { .. } => None, + Expr::BufferIndexGet { .. } => Some(8), Expr::LocalGet(id) | Expr::Update { id, .. } => { if ctx.integer_locals.contains(id) { Some(31) diff --git a/test-files/test_gap_uint8array_nonnumeric_key_7700.ts b/test-files/test_gap_uint8array_nonnumeric_key_7700.ts new file mode 100644 index 0000000000..71231ea679 --- /dev/null +++ b/test-files/test_gap_uint8array_nonnumeric_key_7700.ts @@ -0,0 +1,68 @@ +// #7700: a non-numeric key on a Uint8Array/Buffer-typed local must read a +// PROPERTY, not a byte. `lower/expr_member/member_tail.rs` folds every +// non-STRING key on such a local onto `Expr::Uint8ArrayGet`, and the codegen +// collectors then classified the destination local as integer-valued no matter +// what the key was — so `const it = u8[Symbol.iterator]` took an i32 slot and +// `ToInt32(ToNumber(fn))` reported `typeof it === "number"`. +// +// Every case below stores the read in a LOCAL: that is what selects the +// representation, and the direct-consumption form (`typeof u8[Symbol.iterator]`) +// was already correct. + +const u8 = new Uint8Array([1, 2, 3, 4]); + +// A symbol key reads the iterator method. +const it = u8[Symbol.iterator]; +console.log("iterator:", typeof it); + +// An `any`-typed key holding a method name reads the method. +const methodKey: any = "subarray"; +const method = u8[methodKey]; +console.log("subarray:", typeof method); + +// …a length accessor reads the length. +const lenKey: any = "byteLength"; +const byteLength = u8[lenKey]; +console.log("byteLength:", byteLength); + +// …and an own expando reads the expando. +const anyU8: any = u8; +anyU8.tag = { kind: "buffer" }; +const tagKey: any = "tag"; +const tag = u8[tagKey]; +console.log("tag:", JSON.stringify(tag)); + +// A BigInt expando must not be classified non-BigInt either. +anyU8.big = 7n; +const bigKey: any = "big"; +const big = u8[bigKey]; +console.log("big:", typeof big, String(big)); + +// Buffer is a Uint8Array subclass and folds the same way. +const buf = Buffer.from([9, 8, 7]); +const bufIt = buf[Symbol.iterator]; +console.log("buffer iterator:", typeof bufIt); +const bufWrite: any = "writeUInt8"; +const bufMethod = buf[bufWrite]; +console.log("buffer method:", typeof bufMethod); + +// The numeric-key byte read is unchanged — including the loop shape whose i32 +// representation this fix must not cost. +const i = 2; +const b = u8[i]; +console.log("byte:", b); + +let sum = 0; +for (let k = 0; k < u8.length; k++) { + sum += u8[k]; +} +console.log("sum:", sum); + +let masked = 0; +for (let k = 0; k < 8; k++) { + masked = (masked + u8[k & 3]) | 0; +} +console.log("masked:", masked); + +// Iteration still works through the real iterator. +console.log("spread:", JSON.stringify([...u8])); From fe1ba7d783f15a85c1b6a8c876c493fe0d59744e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 08:40:58 +0200 Subject: [PATCH 2/2] chore: bump version to 0.5.1436 Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- 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 dea59726be..a4dffcf3b2 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.1435 +**Current Version:** 0.5.1436 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 43293cd591..47c268417d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1435" +version = "0.5.1436" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1435" +version = "0.5.1436" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1435" +version = "0.5.1436" [[package]] name = "perry-ui-tvos" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1435" +version = "0.5.1436" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index d093e6d901..55062c67e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1435" +version = "0.5.1436" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"