From 16753495746a81b83c813be470e5fc5fcb12b022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 08:12:03 +0200 Subject: [PATCH 1/5] perf(class-fields): typed slot stores for pointer-typed class fields (#5094) --- crates/perry-codegen/src/expr/property_set.rs | 206 +++++++++++++++++- .../perry-codegen/src/expr/proxy_reflect.rs | 6 +- .../src/lower_call/field_init.rs | 84 ++++++- crates/perry-codegen/src/typed_shape.rs | 59 +++-- 4 files changed, 307 insertions(+), 48 deletions(-) diff --git a/crates/perry-codegen/src/expr/property_set.rs b/crates/perry-codegen/src/expr/property_set.rs index 2410f5fbd1..c329b53268 100644 --- a/crates/perry-codegen/src/expr/property_set.rs +++ b/crates/perry-codegen/src/expr/property_set.rs @@ -89,11 +89,25 @@ fn class_has_computed_runtime_members(ctx: &FnCtx<'_>, class_name: &str) -> bool /// surrounding `PutValueSet` lowering already uses — instead of the throwing /// by-name setter. /// -/// Scope is deliberately narrow: declared raw-f64 (`number`) fields on a known -/// class, receiver == target. Boxed slots need the layout note and write -/// barrier that the guard-call path emits, so they stay on the unchanged -/// sloppy inline caches. -pub(crate) fn try_lower_sloppy_class_field_raw_store( +/// Scope: a declared field on a known class, receiver == target — raw-f64 +/// (`number`) slots and boxed slots alike. +/// +/// The boxed half is P1 (#5094). #7288 originally took only the raw-f64 slots +/// because "boxed slots need the layout note and write barrier that the +/// guard-call path emits" — but those are emitted by +/// [`emit_jsvalue_slot_store_pointer_tested`], not by the guard, and this arm +/// calls it with the identical value-side predicates the strict arm uses. What +/// the guard call actually contributes is descriptor-aware dispatch and the +/// setter-in-chain walk, and the inline precheck refuses every receiver that +/// needs either. +/// +/// Leaving the boxed slots out was the more expensive half of the omission: +/// a `next: LNode | null` store fell through to the `PutValue` write IC +/// (`expr/proxy_reflect.rs`), whose miss path is `js_put_value_set` → +/// `js_object_set_field_by_name` — by-name dispatch, a `RuntimeHandleScope`, +/// and a per-object side-table touch, for a store whose slot index is a +/// compile-time constant. On `deeplist.ts` that one store was the benchmark. +pub(crate) fn try_lower_sloppy_class_field_store( ctx: &mut FnCtx<'_>, object: &Expr, property: &str, @@ -132,12 +146,20 @@ pub(crate) fn try_lower_sloppy_class_field_raw_store( ) else { return Ok(None); }; - // Raw-f64 slots only — see the doc comment. - if !crate::type_analysis::class_field_declared_type(ctx, &class_name, property) - .as_ref() - .is_some_and(crate::typed_shape::type_is_raw_f64_candidate) - { - return Ok(None); + let requires_raw_f64 = + crate::type_analysis::class_field_declared_type(ctx, &class_name, property) + .as_ref() + .is_some_and(crate::typed_shape::type_is_raw_f64_candidate); + if !requires_raw_f64 { + return try_lower_sloppy_class_field_boxed_store( + ctx, + object, + property, + value, + field_index, + expected_class_id, + &keys_global_name, + ); } // Operand order mirrors the strict class-field arm below verbatim: the @@ -277,6 +299,168 @@ pub(crate) fn try_lower_sloppy_class_field_raw_store( Ok(Some(val_double)) } +/// The boxed-slot half of [`try_lower_sloppy_class_field_store`] — P1 (#5094). +/// +/// Same shape as the raw-f64 half: the #5093 inline precheck decides, a hit +/// stores straight into the packed slot, a miss goes to `js_put_value_set(..., +/// strict = 0)` so a rejected sloppy write stays a silent no-op. +/// +/// # Why the precheck alone licenses a guard-free boxed store +/// +/// `emit_class_field_inline_precheck` is a strict subset of the runtime's +/// `class_field_fast_contract`: on a hit, the guard call would have answered +/// "fast" too. For a SET it additionally proves the receiver is not frozen and +/// carries no per-object descriptors, and the process-global latch it reads +/// first is flipped by any prototype-level descriptor or accessor install. Add +/// the `__set_` refusal the caller already made, and every way a +/// `[[Set]]` could be *rejected* or *diverted* is excluded — which is the only +/// thing sloppy and strict `PutValue` disagree about. The value plays no part: +/// unlike the raw-f64 arm, a boxed slot accepts any `JSValue`, so this arm +/// passes `require_raw_f64 = false` and the plain-finite test is not emitted. +/// +/// # GC obligations +/// +/// All three are discharged by [`emit_jsvalue_slot_store_pointer_tested`], with +/// the same value-side predicates the strict guarded arm computes — the write +/// barrier (`expr_produces_non_pointer_bits_by_construction`), the layout note +/// (`class_field_store_needs_layout_note`) and the string demote +/// (`class_field_store_needs_string_addref`). Whatever survives those static +/// proofs is decided by ONE live test of the stored bits (#7511), so a genuine +/// pointer store still reaches the remembered set. Nothing here is keyed on +/// strictness, so this arm's GC behaviour is byte-identical to the strict one. +#[allow(clippy::too_many_arguments)] +fn try_lower_sloppy_class_field_boxed_store( + ctx: &mut FnCtx<'_>, + object: &Expr, + property: &str, + value: &Expr, + field_index: u32, + expected_class_id: u32, + keys_global_name: &str, +) -> Result> { + // Operand order mirrors the raw-f64 arm and the strict class-field arm + // verbatim: the assignment reference is evaluated before the RHS, and the + // receiver's relocation across an allocating RHS is handled by the same + // statepoint re-read those arms rely on. + let recv_box = lower_expr(ctx, object)?; + let val_double = lower_expr(ctx, value)?; + + // Computed before the block builder is borrowed below. + let barrier_needed = !expr_produces_non_pointer_bits_by_construction(ctx, value); + let layout_note_needed = class_field_store_needs_layout_note(ctx, value); + let string_addref_needed = class_field_store_needs_string_addref(ctx, value); + + let key_idx = ctx.strings.intern(property); + let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let field_idx_str = field_index.to_string(); + let expected_class_id_str = expected_class_id.to_string(); + + let (obj_bits, obj_handle, key_box, val_bits, expected_keys) = { + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(&recv_box); + let obj_handle = blk.and(I64, &obj_bits, POINTER_MASK_I64); + let key_box = blk.load(DOUBLE, &key_handle_global); + let val_bits = blk.bitcast_double_to_i64(&val_double); + let expected_keys = blk.load(I64, &format!("@{}", keys_global_name)); + (obj_bits, obj_handle, key_box, val_bits, expected_keys) + }; + + let fast_idx = ctx.new_block("class_field_sloppy_set.boxed_fast"); + let merge_idx = ctx.new_block("class_field_sloppy_set.boxed_merge"); + let fast_label = ctx.block_label(fast_idx); + let merge_label = ctx.block_label(merge_idx); + + // `set_value_bits` is `Some` so the not-frozen check is emitted; + // `require_raw_f64` is false, so the plain-finite value check is not. + let _miss_label = crate::expr::class_field_inline_guard::emit_class_field_inline_precheck( + ctx, + &obj_bits, + &obj_handle, + &expected_class_id_str, + &expected_keys, + field_index, + false, + Some(&val_bits), + &fast_label, + ); + + { + let blk = ctx.block(); + let _ = blk.call( + DOUBLE, + "js_put_value_set", + &[ + (DOUBLE, &recv_box), + (DOUBLE, &key_box), + (DOUBLE, &val_double), + (DOUBLE, &recv_box), + (I32, "0"), + ], + ); + blk.br(&merge_label); + } + + ctx.current_block = fast_idx; + { + // arm64_32 watchOS: the fields region starts at + // `size_of::()` past the user pointer — same derivation + // as every sibling arm and the runtime setter. + let header_skip = + crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string(); + let (field_ptr, field_addr) = { + let blk = ctx.block(); + let obj_ptr = blk.inttoptr(I64, &obj_handle); + let fields_base = blk.gep(I8, &obj_ptr, &[(I64, &header_skip)]); + let field_ptr = blk.gep(DOUBLE, &fields_base, &[(I64, &field_idx_str)]); + let field_addr = blk.ptrtoint(&field_ptr, I64); + (field_ptr, field_addr) + }; + emit_jsvalue_slot_store_pointer_tested( + ctx, + &field_ptr, + &val_double, + &obj_handle, + &field_idx_str, + string_addref_needed, + layout_note_needed, + &obj_bits, + &field_addr, + barrier_needed, + ); + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + let stored = LoweredValue { + semantic: SemanticKind::JsValue, + rep: NativeRep::JsValue, + llvm_ty: DOUBLE, + value: val_double.clone(), + }; + ctx.record_lowered_value_with_access_mode( + "ClassFieldSet", + None, + "class_field_set.sloppy_boxed_store", + &stored, + Some(BoundsState::Guarded { + guard_id: "class_field_inline_precheck".to_string(), + }), + None, + Some(BufferAccessMode::CheckedNative), + None, + false, + false, + vec![ + format!("field={}", property), + format!("field_index={}", field_idx_str), + "receiver_proof=inline_precheck_exact_class".to_string(), + "field_layout_raw_f64=false".to_string(), + "store_guard_failure=js_put_value_set_sloppy".to_string(), + ], + ); + Ok(Some(val_double)) +} + fn lower_runtime_property_set_by_name( ctx: &mut FnCtx<'_>, object: &Expr, diff --git a/crates/perry-codegen/src/expr/proxy_reflect.rs b/crates/perry-codegen/src/expr/proxy_reflect.rs index f89f2f353f..de7e5d8389 100644 --- a/crates/perry-codegen/src/expr/proxy_reflect.rs +++ b/crates/perry-codegen/src/expr/proxy_reflect.rs @@ -1419,15 +1419,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // FAST arm is mode-independent (its precheck rejects frozen / // descriptor-bearing receivers and non-number values), so emit it // here with a sloppy-correct miss path instead of surrendering the - // whole optimization. See - // `property_set::try_lower_sloppy_class_field_raw_store`. + // whole optimization. #5094/P1 extends it to boxed slots. See + // `property_set::try_lower_sloppy_class_field_store`. if !*strict { if let Expr::String(property) = key.as_ref() { if same_put_value_receiver_expr(target, receiver) && matches!(target.as_ref(), Expr::LocalGet(_) | Expr::This) { if let Some(result) = - super::property_set::try_lower_sloppy_class_field_raw_store( + super::property_set::try_lower_sloppy_class_field_store( ctx, target, property, value, )? { diff --git a/crates/perry-codegen/src/lower_call/field_init.rs b/crates/perry-codegen/src/lower_call/field_init.rs index d2eae38b6a..d6c0e7a328 100644 --- a/crates/perry-codegen/src/lower_call/field_init.rs +++ b/crates/perry-codegen/src/lower_call/field_init.rs @@ -39,9 +39,9 @@ use crate::types::{DOUBLE, I32, I64}; /// overwrite. /// /// The proof obligation is identical for both shapes and is entirely about the -/// *operand* expressions, not the store opcode: `This` and `LocalGet()` cannot throw, allocate, or observe `this`, so the assignment is -/// reached before any other effect of the constructor. +/// *operand* expressions, not the store opcode: neither `This` nor any RHS +/// [`prologue_rhs_cannot_observe_this`] admits can observe `this`, so the +/// assignment is reached before anything that could read the field. /// /// **What the elided write is NOT** (the obvious objection, and it is /// measurably wrong — `test-files/test_class_field_init_proto_setter.ts`): it @@ -66,14 +66,14 @@ fn prologue_assigned_field<'a>( stmt: &'a Stmt, param_ids: &std::collections::HashSet, ) -> Option<&'a str> { - let is_plain_param = |e: &Expr| matches!(e, Expr::LocalGet(id) if param_ids.contains(id)); + let admissible = |e: &Expr| prologue_rhs_cannot_observe_this(e, param_ids); match stmt { // Synthesized (anon-shape ctor, destructuring lowering). Stmt::Expr(Expr::PropertySet { object, property, value, - }) if matches!(object.as_ref(), Expr::This) && is_plain_param(value.as_ref()) => { + }) if matches!(object.as_ref(), Expr::This) && admissible(value.as_ref()) => { Some(property.as_str()) } // User-written `this.f = p;`. @@ -85,7 +85,7 @@ fn prologue_assigned_field<'a>( strict: _, }) if matches!(target.as_ref(), Expr::This) && matches!(receiver.as_ref(), Expr::This) - && is_plain_param(value.as_ref()) => + && admissible(value.as_ref()) => { match key.as_ref() { Expr::String(property) => Some(property.as_str()), @@ -96,6 +96,60 @@ fn prologue_assigned_field<'a>( } } +/// The RHS forms a prologue statement may carry. +/// +/// The whole prologue guarantee is "this statement cannot throw, allocate, or +/// **observe `this`**" — see [`ctor_prologue_param_assigned_fields`]. A plain +/// parameter read was the original (and only) admitted form; #7469's widening +/// adds the two other expression families that satisfy it by construction: +/// +/// * **Literals** (`null`, `undefined`, a number, a string, a bool). `this.next +/// = null` is the single most common opening statement of a linked-structure +/// constructor, and refusing it truncated the prologue at statement 0 — +/// which, since #7510 consults the same set, also denied the class an +/// at-allocation layout declaration and left every store in its constructor +/// on the by-name fallback. +/// * **Pure operator trees over those two** (`s + 1`, `-n`, `a * b + 1`). Every +/// leaf is a parameter read or a literal, and `Binary`/`Unary`/`Compare`/ +/// `Logical` evaluate their operands and combine them — no member access, no +/// call, no `new`, no closure, and (decisively) no `This` anywhere in the +/// tree. `s + 1` can still *allocate* when `s` is a string, and that is fine: +/// the guarantee this predicate underwrites is about observability of `this`, +/// not about the absence of a collection. A GC that scans the +/// still-constructing instance reads the allocator's `undefined` fill through +/// the declared descriptor and rejects it at the tag check. +/// +/// Deliberately NOT admitted: `PropertyGet` (a getter runs user code), +/// `Call`/`New` (arbitrary user code), `Closure` (captures), `Await`/`Yield` +/// (suspension), and anything containing `This`. None of those can reach the +/// half-built instance today — it has not escaped — but each makes the +/// "cannot observe `this`" claim rest on a reachability argument instead of on +/// the expression's own shape, and this predicate is consumed by two callers +/// with different failure modes (a dead-store elision and a GC layout +/// declaration). +fn prologue_rhs_cannot_observe_this( + expr: &Expr, + param_ids: &std::collections::HashSet, +) -> bool { + match expr { + Expr::LocalGet(id) => param_ids.contains(id), + Expr::Undefined + | Expr::Null + | Expr::Bool(_) + | Expr::Number(_) + | Expr::Integer(_) + | Expr::String(_) => true, + Expr::Binary { left, right, .. } + | Expr::Compare { left, right, .. } + | Expr::Logical { left, right, .. } => { + prologue_rhs_cannot_observe_this(left, param_ids) + && prologue_rhs_cannot_observe_this(right, param_ids) + } + Expr::Unary { operand, .. } => prologue_rhs_cannot_observe_this(operand, param_ids), + _ => false, + } +} + /// Field names whose default-`undefined` initializer write is provably dead /// because the class's own constructor unconditionally overwrites them before /// anything can observe `this` (#7469; extended to user-written constructors @@ -141,11 +195,19 @@ fn prologue_assigned_field<'a>( /// `key_expr` none). /// /// The prologue is the maximal leading run of statements that -/// [`prologue_assigned_field`] recognizes as `this. = `. A -/// `LocalGet` of a plain parameter cannot throw, allocate, or observe `this`, -/// so every field it assigns is written before ANY other effect of the -/// constructor — which is exactly the guarantee that makes the earlier -/// `undefined` write dead. +/// [`prologue_assigned_field`] recognizes as `this. = ` — a plain parameter read, a literal, or a pure operator +/// tree over those (see [`prologue_rhs_cannot_observe_this`] for why those +/// three and nothing else). None of them can reach the half-built instance, so +/// every field they assign is written before anything can read it — which is +/// exactly the guarantee that makes the earlier `undefined` write dead. +/// +/// Admitting literals is not a cosmetic widening. `constructor(v) { this.next +/// = null; this.v = v; }` is the canonical linked-structure constructor, and +/// under the param-only rule its prologue truncated at statement 0 and came +/// back EMPTY — so neither field's dead `undefined` write was elided and, since +/// #7510 consults the same set, the class was also refused an at-allocation +/// layout declaration. pub(crate) fn ctor_prologue_param_assigned_fields( class: &perry_hir::Class, ) -> std::collections::HashSet { diff --git a/crates/perry-codegen/src/typed_shape.rs b/crates/perry-codegen/src/typed_shape.rs index 3e21745d48..43bd6c5236 100644 --- a/crates/perry-codegen/src/typed_shape.rs +++ b/crates/perry-codegen/src/typed_shape.rs @@ -108,16 +108,25 @@ pub(crate) fn type_is_raw_f64_candidate(ty: &Type) -> bool { /// require **every** raw-f64 field to be in that set — one field assigned /// later would still be exposed. /// -/// 2. **The collector's view must be true at birth.** We require the pointer -/// mask to be EMPTY, which makes the declared state `GC_LAYOUT_POINTER_FREE` -/// — byte-identical to what `layout_init_pointer_free` already sets on every -/// fresh instance. So the only delta this emits is the intact bit and the -/// shape-shared descriptor install; the collector sees exactly what it saw -/// before. A class with pointer fields would install `SIDE_MASK` and hand the -/// collector slots holding the allocator's fill — sound on the -/// `js_object_alloc_class_inline_keys` path, which pre-fills with -/// `undefined`, but it would rest on that pre-fill rather than on nothing, -/// so it is out of scope here. +/// 2. **The collector's view must be true at birth.** The declared state is +/// `GC_LAYOUT_POINTER_FREE` for an empty pointer mask and `SIDE_MASK` +/// otherwise, and in both cases the collector is handed slots that still +/// hold the allocator's fill. That fill is `TAG_UNDEFINED` on **every** +/// allocation path a `new` site can take — `js_object_alloc_class_inline_keys` +/// writes `max(field_count, INLINE_SLOT_FLOOR)` slots (`object/alloc.rs`, +/// #4717) and codegen's inline bump path writes the same range with the same +/// constant (`lower_call/new_alloc.rs`) — so a pointer-masked slot the tracer +/// visits before its first write yields `undefined`, which +/// `mark_field_into_worklist` rejects at its tag check. A raw-f64-masked slot +/// is not visited at all. Neither can strand a child, because neither holds +/// one yet. +/// +/// This is the one obligation the original #7510 rule discharged by *avoiding* +/// it (pointer mask required empty) rather than by proving it, which cost +/// every pointer-bearing class its at-allocation declaration — and with it +/// every store in its constructor, since the post-constructor install arrives +/// after them all. `tree_wide`'s eight `number` fields were on the by-name +/// fallback for exactly this reason: two `Tree | null` siblings. /// /// Nothing rests on the *values* being numbers. A constructor that stores a /// string into a `number`-declared field is rejected by the store guard @@ -131,26 +140,30 @@ pub(crate) fn class_layout_declarable_at_allocation( if prologue.is_empty() { return false; } - let mut has_raw_f64 = false; + let mut has_slot = false; for field in &class.fields { if field.key_expr.is_some() { continue; } - // An untyped field lands on `Any`/`Unknown`, which - // `type_is_pointer_bearing` answers `true` for — so it is rejected - // here, by the same condition and for the same reason as a declared - // `string`. - if type_is_pointer_bearing(&field.ty) { + has_slot = true; + // Obligation 1 applies to raw-f64 slots ONLY. A pointer-masked slot + // read before its first write yields `undefined`, which is the correct + // answer; a raw-f64 slot read before its first write reinterprets + // `undefined`'s NaN-box bits as a double and yields NaN. So only the + // latter needs the prologue's write-before-anything-else guarantee. + // + // It also covers the field-init phase's own `undefined` write: that + // write lands in a raw-f64-masked slot, fails `layout_raw_f64_bits`, + // and would downgrade the descriptor on the spot — but a + // prologue-assigned field has that write ELIDED + // (`ctor_prologue_param_assigned_fields`, the same set), so it never + // happens. The two consumers of `prologue` have to agree here, and + // they agree because it is literally one set. + if type_is_raw_f64_candidate(&field.ty) && !prologue.contains(&field.name) { return false; } - if type_is_raw_f64_candidate(&field.ty) { - has_raw_f64 = true; - if !prologue.contains(&field.name) { - return false; - } - } } - has_raw_f64 + has_slot } #[derive(Clone, Debug, Default)] From 1dc87fc53861149a4012cd91035af2e157ac0a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 08:29:33 +0200 Subject: [PATCH 2/5] test(class-fields): pin the at-allocation declaration and sloppy boxed store contracts --- crates/perry-codegen/src/typed_shape.rs | 20 +++- .../tests/native_proof_regressions.rs | 102 ++++++++++++++++++ .../typed_shape_declared_at_allocation.rs | 49 ++++++--- 3 files changed, 151 insertions(+), 20 deletions(-) diff --git a/crates/perry-codegen/src/typed_shape.rs b/crates/perry-codegen/src/typed_shape.rs index 43bd6c5236..7ae3e16dfa 100644 --- a/crates/perry-codegen/src/typed_shape.rs +++ b/crates/perry-codegen/src/typed_shape.rs @@ -140,12 +140,19 @@ pub(crate) fn class_layout_declarable_at_allocation( if prologue.is_empty() { return false; } - let mut has_slot = false; + // The declaration costs one call per construction, so it must buy at least + // one mask bit. A class whose every field is `boolean` declares two empty + // masks: the boxed store arm does not read the intact bit at all + // (`require_raw_f64 = false`), and the collector's view is already + // `POINTER_FREE` from `layout_init_pointer_free`. Nothing to unlock. + let mut worth_declaring = false; for field in &class.fields { if field.key_expr.is_some() { continue; } - has_slot = true; + if type_is_pointer_bearing(&field.ty) { + worth_declaring = true; + } // Obligation 1 applies to raw-f64 slots ONLY. A pointer-masked slot // read before its first write yields `undefined`, which is the correct // answer; a raw-f64 slot read before its first write reinterprets @@ -159,11 +166,14 @@ pub(crate) fn class_layout_declarable_at_allocation( // (`ctor_prologue_param_assigned_fields`, the same set), so it never // happens. The two consumers of `prologue` have to agree here, and // they agree because it is literally one set. - if type_is_raw_f64_candidate(&field.ty) && !prologue.contains(&field.name) { - return false; + if type_is_raw_f64_candidate(&field.ty) { + worth_declaring = true; + if !prologue.contains(&field.name) { + return false; + } } } - has_slot + worth_declaring } #[derive(Clone, Debug, Default)] diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 48f4934056..01b7ae8df8 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -14061,6 +14061,108 @@ fn sloppy_class_field_number_store_takes_the_inline_raw_store() { ); } +/// P1 (#5094): the sibling of the test above for a POINTER-typed field. +/// +/// #7288 took only the raw-f64 slots, so `node.next = other` in a sloppy script +/// — the shape of every linked structure — stayed on the `PutValue` write IC +/// whose miss is `js_put_value_set` → `js_object_set_field_by_name`: by-name +/// dispatch and a `RuntimeHandleScope` for a store whose slot index is a +/// compile-time constant. +/// +/// Asserts the codegen decision AND that the GC bookkeeping survived, because +/// that is the half where a mistake is a use-after-free rather than a slowdown: +/// a pointer store into a boxed slot must still reach the write barrier. The +/// value here is an opaque parameter read, so none of the three value-side +/// elisions (`expr_produces_non_pointer_bits_by_construction` and friends) can +/// fire and the emitted bookkeeping block must be present. +#[test] +fn sloppy_class_field_pointer_store_takes_the_inline_boxed_store() { + fn probe_body(ir: &str) -> &str { + let start = ir + .find("define double @perry_fn_sloppy_class_field_ptr_store_ts__probe") + .expect("probe function must be emitted"); + let rest = &ir[start..]; + let end = rest[1..] + .find("\ndefine ") + .map(|offset| offset + 1) + .unwrap_or(rest.len()); + &rest[..end] + } + + fn ir_for(strict: bool) -> String { + let node = class( + 218, + "LNode", + vec![class_field("next", Type::Named("LNode".to_string()))], + ); + let module = module_with_classes_and_params( + "sloppy_class_field_ptr_store.ts", + vec![node], + vec![ + param(1, "node", Type::Named("LNode".to_string())), + param(2, "other", Type::Named("LNode".to_string())), + ], + Type::Number, + vec![ + Stmt::Expr(Expr::PutValueSet { + target: Box::new(local(1)), + key: Box::new(Expr::String("next".to_string())), + value: Box::new(local(2)), + receiver: Box::new(local(1)), + strict, + }), + Stmt::Return(Some(int(0))), + ], + ); + compile_ir_for_module_with_opts(module, empty_opts()).unwrap() + } + + let sloppy_module = ir_for(false); + let sloppy = probe_body(&sloppy_module); + assert!( + sloppy.contains("class_field_sloppy_set.boxed_fast"), + "a sloppy pointer-field store must reach the inline class-field boxed \ + store (#5094 P1):\n{sloppy}" + ); + assert!( + sloppy.contains("class_field_inline.deref"), + "the boxed arm must be fronted by the #5093 shape/flags precheck — it \ + is what rejects the frozen / descriptor-bearing receivers sloppy and \ + strict disagree about:\n{sloppy}" + ); + let miss_call = sloppy + .lines() + .find(|line| line.contains("call double @js_put_value_set(")) + .unwrap_or_else(|| { + panic!("the boxed arm's miss must CALL `js_put_value_set` (#5094):\n{sloppy}") + }); + assert!( + miss_call.trim_end().ends_with("i32 0)"), + "the sloppy miss must pass strict = 0 (#5094):\n {miss_call}" + ); + // The GC half. An opaque parameter can carry a heap pointer, so the store + // must be followed by the pointer-tested bookkeeping block that reaches the + // remembered set. Without this assertion the test would pass just as + // happily on a lowering that dropped the barrier outright. + assert!( + sloppy.contains("class_field_set.gc_bookkeeping"), + "a boxed slot store of a possibly-pointer value must keep the \ + pointer-tested write barrier / layout note (#5094):\n{sloppy}" + ); + assert!( + !sloppy.contains("call void @js_class_field_set_fallback"), + "the sloppy arm must not CALL the throwing strict fallback:\n{sloppy}" + ); + + // Negative control: the strict arm keeps its own (unchanged) lowering. + let strict_module = ir_for(true); + let strict = probe_body(&strict_module); + assert!( + !strict.contains("class_field_sloppy_set"), + "the strict arm must keep its existing lowering:\n{strict}" + ); +} + #[path = "native_proof_regressions/invalidation.rs"] mod invalidation; diff --git a/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs b/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs index d37eb7e519..7b08d84daf 100644 --- a/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs +++ b/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs @@ -341,11 +341,17 @@ fn a_number_field_outside_the_prologue_refuses_the_declaration() { ); } -/// Negative: a pointer field would install `SIDE_MASK` at birth and hand the -/// collector slots holding the allocator's fill. Out of scope — the declared -/// state must stay byte-identical to what `layout_init_pointer_free` sets. +/// P1 (#5094): a pointer field DOES get the declaration, and it carries a real +/// pointer mask. +/// +/// This is the case #7510 deliberately excluded, and the exclusion cost the +/// class every store in its constructor: the post-constructor install arrives +/// after all of them, so each one missed its intact-bit guard and fell to +/// `js_object_set_field_by_name`. Obligation 2 is now discharged rather than +/// avoided — both `new` allocation paths pre-fill every slot with +/// `TAG_UNDEFINED`, which the tracer rejects at its tag check. #[test] -fn a_pointer_field_refuses_the_declaration() { +fn a_pointer_field_gets_the_declaration_with_a_pointer_mask() { let ir = compile_ir(&module_with_new( class( "WithPointer", @@ -363,18 +369,30 @@ fn a_pointer_field_refuses_the_declaration() { ), 2, )); + let line = ir + .lines() + .find(|l| l.contains(DECLARE_CALL)) + .unwrap_or_else(|| panic!("a pointer-bearing class must declare:\n{ir}")); assert!( - !ir.contains(DECLARE_CALL), - "a class with a pointer-bearing field must keep the post-constructor \ - install:\n{ir}" + line.contains("@perry_typed_shape_raw_f64_mask_"), + "the raw-f64 mask must still be passed: {line}" + ); + assert!( + line.contains("@perry_typed_shape_mask_"), + "a pointer-bearing class must pass a non-null pointer mask: {line}" + ); + assert!( + !ir.contains(INIT_CALL), + "the declaration still replaces the post-constructor install:\n{ir}" ); } -/// Negative: an untyped field lands on `Any`, which is pointer-bearing — the -/// same condition, and the reason the synthesized anon-shape classes behind -/// object literals do not qualify (their inferred field types are `Any`). +/// An untyped field lands on `Any`, which is pointer-bearing — so it takes the +/// same route the `string` field above does. This is what puts the synthesized +/// anon-shape classes behind object literals on the at-allocation declaration +/// (their inferred field types are all `Any`). #[test] -fn an_untyped_field_refuses_the_declaration() { +fn an_untyped_field_gets_the_declaration() { let ir = compile_ir(&module_with_new( class( "Untyped", @@ -390,13 +408,14 @@ fn an_untyped_field_refuses_the_declaration() { 2, )); assert!( - !ir.contains(DECLARE_CALL), - "`Any` is pointer-bearing:\n{ir}" + ir.contains(DECLARE_CALL), + "`Any` is pointer-bearing, which is now a reason TO declare:\n{ir}" ); } -/// Negative: with no raw-f64 field there is nothing to unlock, so the extra -/// call would be pure cost. +/// Negative: with neither a raw-f64 nor a pointer-bearing field, both masks are +/// empty — the declaration would install the state `layout_init_pointer_free` +/// already set and unlock nothing, so the extra call would be pure cost. #[test] fn a_class_with_no_number_field_refuses_the_declaration() { let ir = compile_ir(&module_with_new( From 339e6d73f3003c5e6d9f9e173857af22dd92f72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 08:41:48 +0200 Subject: [PATCH 3/5] fix(gc-root-dominance): teach the checker that js_gc_declare_typed_shape_layout cannot collect --- scripts/gc_root_dominance_check.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/gc_root_dominance_check.py b/scripts/gc_root_dominance_check.py index ea13ac04eb..be62f77737 100755 --- a/scripts/gc_root_dominance_check.py +++ b/scripts/gc_root_dominance_check.py @@ -452,7 +452,23 @@ def build_cfg(f): "js_gc_temp_root_push", "js_gc_temp_root_get", "js_gc_temp_root_set", "js_gc_temp_root_truncate", # layout / barrier bookkeeping (no allocation) - "js_gc_init_typed_shape_layout", "js_gc_layout_note_slot", + # + # This block is a second copy of a fact the compiler already states: + # `perry-codegen/src/gc_call_effects.rs` answers `GcCallEffect::CannotCollect` + # for the same helpers. The two lists must agree, and `js_gc_declare_typed_shape_layout` + # is where they drifted -- #7510 added it beside `js_gc_init_typed_shape_layout` + # in the Rust match and not here, which stayed invisible only because the + # corpus then contained no class the #7510 gate admitted. #5094 widened that + # gate to pointer-bearing classes and the omission printed 358 violations, all + # of them `js_object_alloc_class_inline_keys->js_gc_declare_typed_shape_layout` + # and every one spurious. The two entry points share a body + # (`typed_shape_layout_entry` -> `init_typed_shape_layout`) and differ only in + # a `TypedShapeProof` that makes `declare` do strictly LESS: it skips the slot + # validation loop. So `declare` cannot collect for exactly the reason `init` + # cannot -- side-table metadata writes through the system allocator, which + # arms no Perry GC trigger. + "js_gc_init_typed_shape_layout", "js_gc_declare_typed_shape_layout", + "js_gc_layout_note_slot", "js_write_barrier_root_nanbox", "js_write_barrier_slot", "js_runtime_write_barrier_slot", "js_gc_register_global_root", # pure value predicates / bit twiddling From d651da46d99576a0bed498682ee4db0308fb3ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 08:52:32 +0200 Subject: [PATCH 4/5] docs: changelog fragment for the pointer-class-field slot stores --- .../7686-pointer-class-field-slot-stores.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 changelog.d/7686-pointer-class-field-slot-stores.md diff --git a/changelog.d/7686-pointer-class-field-slot-stores.md b/changelog.d/7686-pointer-class-field-slot-stores.md new file mode 100644 index 0000000000..83bddce863 --- /dev/null +++ b/changelog.d/7686-pointer-class-field-slot-stores.md @@ -0,0 +1,90 @@ +### class fields: one pointer field no longer demotes an object's whole store set (#5094) + +A single pointer-typed class field (`peer: Cell | null`, `next: LNode | null`, +`left: Tree | null`) put **every** field store on that object — its `number` +fields included — on `js_object_set_field_by_name`: by-name dispatch, a +`RuntimeHandleScope`, `layout_note_slot`, and a per-object side-table entry, for +stores whose slot index is a compile-time constant. + +Quiet M1 mini, best-of-3 wall, both arms run back-to-back, stdout byte-identical +to the pre-change binary in every row: + +| bench | before | after | scriptc 0.0.22 | node 26.5.1 | +|---|--:|--:|--:|--:| +| `cycles` | 0.79 | **0.24** | 0.31 | 0.07 | +| `deeplist` | 1.14 | **0.33** | 0.22 | 0.09 | +| `tree_wide` | 12.18 | **3.02** | 7.01 | 0.89 | +| `tree` | 5.92 | **4.43** | 4.80 | 0.45 | + +`cycles`, `tree` and `tree_wide` now beat scriptc. Unchanged, as required: +`push_cls` 0.35, `churn` 0.66, `churn_alloc` 0.36, `push_num` 0.13, `retain` +1.32, `retain1` 0.42, `churn_read` 0.35. + +**Three changes, and they are not separable.** + +1. *The sloppy-mode class-field route reaches boxed slots* + (`expr/property_set.rs`). #7288 opened it for raw-f64 slots only — "boxed + slots need the layout note and write barrier that the guard-call path emits" + — but those come from `emit_jsvalue_slot_store_pointer_tested`, not from the + guard, and this arm calls it with the identical value-side predicates the + strict arm uses. What the guard actually contributes is descriptor-aware + dispatch and the setter-in-chain walk, and the #5093 inline precheck refuses + every receiver that needs either. The miss stays + `js_put_value_set(..., strict = 0)`, so a rejected sloppy write is still a + silent no-op. + +2. *A pointer-bearing class declares its layout at allocation* + (`typed_shape.rs`). #7510 required an EMPTY pointer mask, which excluded + exactly these classes — so their descriptor arrived *after* every store in + their constructor and none could pass its intact-bit guard. That is #7512's + defect, still open for this shape: `tree_wide`'s eight `number` fields were on + the by-name fallback because two `Tree | null` siblings existed. Obligation 2 + is now discharged rather than avoided: both `new` allocation paths pre-fill + every slot with `TAG_UNDEFINED` (`object/alloc.rs` #4717 and codegen's inline + bump path), which the tracer rejects at its tag check, so a pointer-masked + slot visited before its first write cannot strand anything. Obligation 1 (no + read may observe a raw-f64 slot before its first write) is unchanged and still + demanded of every `number` field. + +3. *The constructor prologue admits literals and pure operator trees* + (`lower_call/field_init.rs`). `constructor(v) { this.next = null; this.v = v }` + is the canonical linked-structure constructor and its prologue truncated at + statement 0, coming back EMPTY — costing both the dead-`undefined`-store + elision and, through the same set, the declaration in (2). `tree_wide`'s + `this.b = s + 1` needs the operator-tree half. Admitted forms are parameter + reads, literals, and `Binary`/`Unary`/`Compare`/`Logical` trees over those: + no member access, no call, no closure, and no `This` anywhere in the tree. + +**Why they must land together, measured rather than argued.** (1) alone +*regresses* `tree_wide`. Compiling the benchmarks as ESM — which already takes +the class-field route — against the pre-change compiler gives `tree_wide` +12.21 → **14.88** s: routing a constructor's stores to a guard the construction +path has made unsatisfiable is slower than the inline cache it displaces. (2) is +what makes that guard passable. + +**GC.** A pointer store must still reach the remembered set. +`PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1` at depth 800, over binaries +compiled with `PERRY_GC_MOVING_LOOP_POLLS=1`, on pointer-cycle / linked-list / +wide-tree probes: clean, and **not vacuous** — 20005 / 40005 / 5 +`[gc-fromspace-protect]` retirements prove the copying minor ran, and all three +match the pinned `node 26.5.1` oracle including a walk that sums every numeric +slot as well as every pointer edge. `PERRY_GC_VERIFY_EVACUATION`, +`PERRY_GC_VERIFY_MARK` and `PERRY_GC_FROMSPACE_SCAN_ABORT` are clean under zeal. +`PERRY_GC_TRACE` drift: `churn` bit-identical (105 cycles, 0.0039 GB copied, +positive reclamation every cycle); `deeplist`/`tree`/`tree_wide` keep their cycle +counts and kinds exactly; `cycles` copies 10,758 → 14 objects and promotes +4,746 → 2, the side-table bookkeeping this change removes no longer holding dead +objects reachable. + +**A latent gate bug this exposed.** `scripts/gc_root_dominance_check.py`'s +`NONCOLLECTING` set is a second copy of a fact +`perry-codegen/src/gc_call_effects.rs` already states, and the two had drifted: +#7510 added `js_gc_declare_typed_shape_layout` beside +`js_gc_init_typed_shape_layout` in the Rust match and not in the Python set. It +stayed invisible only because the corpus then held no class the #7510 gate +admitted. Widening that gate printed **358 spurious violations**, every one +`js_object_alloc_class_inline_keys->js_gc_declare_typed_shape_layout`. The two +entry points share a body (`typed_shape_layout_entry` → `init_typed_shape_layout`) +and differ only in a `TypedShapeProof` that makes `declare` do strictly *less* +work, so one classification covers both. With the drift fixed the gate reports 0 +violations on both arms, with its 40-seeded-violation control catching 40/40. From 13ba119fdc3db3a712d1ec15bd5b71424bd64c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 09:57:31 +0200 Subject: [PATCH 5/5] chore: bump version to 0.5.1390 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 166a0c0994..5a4522904e 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.1389 +**Current Version:** 0.5.1390 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 61247a05e0..d37c4c9b50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1389" +version = "0.5.1390" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1389" +version = "0.5.1390" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1389" +version = "0.5.1390" [[package]] name = "perry-ui-tvos" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1389" +version = "0.5.1390" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index e39e285533..e2bf66bf27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1389" +version = "0.5.1390" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"