From 37b11ba497e73bfb3d59b220ccf8aaba1eb4842f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 12:30:07 +0200 Subject: [PATCH 1/4] perf(class-fields): a conforming pointer store skips the layout note (#5094) --- .../src/expr/conforming_layout_note_tests.rs | 347 ++++++++++++++++++ crates/perry-codegen/src/expr/helpers.rs | 72 +++- crates/perry-codegen/src/expr/mod.rs | 14 +- crates/perry-codegen/src/expr/property_set.rs | 23 +- .../perry-codegen/src/expr/write_barrier.rs | 49 ++- crates/perry-codegen/src/typed_shape.rs | 30 ++ 6 files changed, 513 insertions(+), 22 deletions(-) create mode 100644 crates/perry-codegen/src/expr/conforming_layout_note_tests.rs diff --git a/crates/perry-codegen/src/expr/conforming_layout_note_tests.rs b/crates/perry-codegen/src/expr/conforming_layout_note_tests.rs new file mode 100644 index 0000000000..4263c21d0e --- /dev/null +++ b/crates/perry-codegen/src/expr/conforming_layout_note_tests.rs @@ -0,0 +1,347 @@ +//! Phase 4b.2 (#5094): a pointer stored into a slot the class's own pointer +//! mask declares no longer *calls* `js_gc_note_slot_layout` when the receiver +//! carries an intact side-mask descriptor. +//! +//! These are IR-census tests, and both directions matter. +//! +//! The positive one asserts the subject is LIVE — an elision predicate that +//! silently answers `false` everywhere still compiles, still prints the right +//! answer, and shows up in no other test. Only the emitted block label +//! separates "implemented" from "reached" (CLAUDE.md, "a gate must assert its +//! subject was live"). +//! +//! The negatives are the safety half. The elision rests on the emitted header +//! test being paired with a slot the mask really declares a POINTER; if the +//! predicate ever widened to a raw-f64 slot, `layout_note_slot`'s downgrade arm +//! — the one that MUST fire, because a pointer in a raw-f64 slot is a +//! descriptor contradiction — would be skipped, and the collector would keep +//! reading a mask that says "not a pointer" over a live child. That is a silent +//! use-after-free, so it gets a test that fails rather than a comment. +//! +//! The fallback call is asserted PRESENT in the positive case too: the change +//! is "skip the call when the header proves it a no-op", never "elide it +//! outright" — a receiver that reached the store with no descriptor (any path +//! `class_field_store_layout_note_is_conforming`'s reasoning did not enumerate) +//! must still take the real note. + +use crate::{compile_module, AppMetadata, CompileOptions}; +use perry_hir::types::Type; +use perry_hir::{Class, ClassField, Expr, Function, Module, ModuleInitKind, Param, Stmt}; + +/// The block that exists only when the conforming-store elision was emitted. +const NOTE_BLOCK: &str = "class_field_set.layout_note"; +const NOTE_CALL: &str = "call void @js_gc_note_slot_layout("; + +fn ir_opts() -> CompileOptions { + CompileOptions { + target: None, + is_entry_module: true, + non_entry_module_prefixes: Vec::new(), + nextjs_path_init_modules: Vec::new(), + import_function_prefixes: std::collections::HashMap::new(), + import_function_ffi_aliases: std::collections::HashMap::new(), + import_function_origin_names: std::collections::HashMap::new(), + import_function_v8_specifiers: std::collections::HashMap::new(), + import_function_node_submodule: std::collections::HashMap::new(), + namespace_node_submodules: std::collections::HashMap::new(), + namespace_v8_specifiers: std::collections::HashMap::new(), + namespace_member_prefixes: std::collections::HashMap::new(), + namespace_member_origin_names: std::collections::HashMap::new(), + emit_ir_only: true, + verify_native_regions: false, + disable_buffer_fast_path: false, + namespace_imports: Vec::new(), + imported_classes: Vec::new(), + imported_enums: Vec::new(), + imported_async_funcs: std::collections::HashSet::new(), + type_aliases: std::collections::HashMap::new(), + imported_func_param_counts: std::collections::HashMap::new(), + imported_func_has_rest: std::collections::HashSet::new(), + imported_func_synthetic_arguments: std::collections::HashSet::new(), + imported_func_return_types: std::collections::HashMap::new(), + imported_vars: std::collections::HashSet::new(), + output_type: "executable".to_string(), + needs_stdlib: false, + needs_ui: false, + needs_geisterhand: false, + geisterhand_port: 7676, + enabled_features: Vec::new(), + native_module_init_names: Vec::new(), + js_module_specifiers: Vec::new(), + bundled_extensions: Vec::new(), + native_library_functions: Vec::new(), + i18n_table: None, + fast_math: false, + fp_contract_mode: crate::FpContractMode::Off, + app_metadata: AppMetadata::default(), + namespace_entries: Vec::new(), + dynamic_import_path_to_prefix: std::collections::HashMap::new(), + deferred_module_prefixes: std::collections::HashSet::new(), + module_init_deps: Vec::new(), + is_dynamic_import_target: false, + debug_locations: false, + module_source: None, + debug_source_line_offset: 0, + } +} + +fn field(name: &str, ty: Type) -> ClassField { + ClassField { + name: name.to_string(), + key_expr: None, + ty, + init: None, + is_private: false, + is_readonly: false, + decorators: Vec::new(), + } +} + +/// `class Link { next: Link | null; v: number }` — slot 0 pointer-masked, slot +/// 1 raw-f64-masked. The two slots are what make the positive and the negative +/// test differ in exactly one thing. +fn link_class() -> Class { + Class { + id: 303, + name: "Link".to_string(), + type_params: Vec::new(), + extends: None, + extends_name: None, + native_extends: None, + extends_expr: None, + heritage_lexically_shadowed: false, + fields: vec![ + field( + "next", + Type::Union(vec![Type::Named("Link".to_string()), Type::Null]), + ), + field("v", Type::Number), + // Neither pointer-bearing nor a raw-f64 candidate, so slot 2 is in + // NEITHER mask — the case where the note is what SETS the bit. + field("flag", Type::Boolean), + ], + constructor: Some(link_ctor()), + methods: Vec::new(), + getters: Vec::new(), + setters: Vec::new(), + static_accessor_names: Vec::new(), + static_accessor_fn_ids: Vec::new(), + computed_members: Vec::new(), + static_fields: Vec::new(), + static_methods: Vec::new(), + decorators: Vec::new(), + is_exported: false, + aliases: Vec::new(), + is_nested: false, + alloc_width_hint: 0, + specialized_from: None, + } +} + +const A_ID: u32 = 1; +const B_ID: u32 = 2; +const CTOR_V_ID: u32 = 3; +const X_ID: u32 = 4; + +/// `constructor(v: number) { this.next = null; this.v = v }` — the canonical +/// linked-structure prologue, and the shape #7686 taught +/// `ctor_prologue_param_assigned_fields` to admit. Present so the class gets a +/// keys global and an at-allocation layout declaration, exactly as `cycles.ts` +/// does; without it the test would be asking its question of a shape the real +/// compiler never produces. +fn link_ctor() -> Function { + Function { + id: 900, + name: "constructor".to_string(), + type_params: Vec::new(), + params: vec![Param { + id: CTOR_V_ID, + name: "v".to_string(), + ty: Type::Number, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }], + return_type: Type::Void, + body: vec![ + Stmt::Expr(Expr::PropertySet { + object: Box::new(Expr::This), + property: "next".to_string(), + value: Box::new(Expr::Null), + }), + Stmt::Expr(Expr::PropertySet { + object: Box::new(Expr::This), + property: "v".to_string(), + value: Box::new(Expr::LocalGet(CTOR_V_ID)), + }), + ], + is_async: false, + is_generator: false, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + } +} + +/// `function link(a: Link, b: Link) { a. = }`, called once +/// from module init so it is not dead. +/// +/// The store lives in a FUNCTION with declared-`Link` parameters, not in module +/// init, because that is where `receiver_class_name` resolves a monomorphic +/// receiver — the same position `cycles.ts`'s `makeCycle` puts it in. +fn store_module(property: &str, value: Expr) -> Module { + let mut m = Module::new("conforming_layout_note.ts"); + m.classes = vec![link_class()]; + let link = || Type::Named("Link".to_string()); + m.functions = vec![Function { + id: 10, + name: "link".to_string(), + type_params: Vec::new(), + params: vec![ + Param { + id: A_ID, + name: "a".to_string(), + ty: link(), + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }, + Param { + id: B_ID, + name: "b".to_string(), + ty: link(), + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }, + ], + return_type: Type::Void, + body: vec![Stmt::Expr(Expr::PropertySet { + object: Box::new(Expr::LocalGet(A_ID)), + property: property.to_string(), + value: Box::new(value), + })], + is_async: false, + is_generator: false, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + }]; + m.init = vec![ + Stmt::Let { + id: X_ID, + name: "x".to_string(), + ty: link(), + mutable: false, + init: Some(Expr::New { + class_name: "Link".to_string(), + args: vec![Expr::Number(1.0)], + type_args: Vec::new(), + byte_offset: 0, + cap_args_appended: 0, + }), + }, + Stmt::Expr(Expr::Call { + callee: Box::new(Expr::FuncRef(10)), + args: vec![Expr::LocalGet(X_ID), Expr::LocalGet(X_ID)], + type_args: Vec::new(), + byte_offset: 0, + }), + ]; + m.init_kind = ModuleInitKind::Eager; + m +} + +fn emit(m: &Module) -> String { + String::from_utf8(compile_module(m, ir_opts()).unwrap()).expect("LLVM IR should be UTF-8") +} + +/// `a.next = b` — a pointer into the pointer-masked slot 0. The store must +/// reach the header test, and must keep the call on its cold arm. +#[test] +fn a_pointer_into_a_pointer_masked_slot_gates_the_note_on_the_header() { + let ir = emit(&store_module("next", Expr::LocalGet(B_ID))); + assert!( + ir.contains(NOTE_BLOCK), + "the conforming-store elision was not emitted for `a.next = b`; \ + `class_field_store_layout_note_is_conforming` answered false and this \ + optimization is dead:\n{ir}" + ); + // `(GC_LAYOUT_STATE_MASK | GC_OBJ_TYPED_LAYOUT_INTACT)` and the + // `(SIDE_MASK | INTACT)` value it is compared against, as i16 literals. + assert!( + ir.contains("and i16 ") && ir.contains(", -12288") && ir.contains(", -28672"), + "the emitted predicate is not the documented header test:\n{ir}" + ); + assert!( + ir.contains(NOTE_CALL), + "the real note must survive on the cold arm — the elision is \ + 'skip when the header proves it a no-op', never 'never call':\n{ir}" + ); +} + +/// The same store shape into a slot the masks do **not** declare a pointer +/// (`flag: boolean` — neither pointer-bearing nor a raw-f64 candidate). +/// +/// This is the load-bearing negative. There the note is not a no-op that can be +/// skipped: it is the only thing that ever sets the pointer-mask bit the +/// collector reads for that slot, so eliding it would leave a live child in an +/// object the tracer scans zero pointers of. +/// +/// (The raw-f64 slot is not tested by emission: `a.v = b` with a `Link`-typed +/// value never reaches this emitter at all — `requires_raw_f64` routes it to +/// the guarded raw-f64 arm, which side-exits a non-finite value to +/// `js_put_value_set`. Asserting an absent block there would pass for a reason +/// unrelated to the elision. The mask half is covered directly, below.) +#[test] +fn a_store_into_an_undeclared_slot_keeps_an_unconditional_note() { + let ir = emit(&store_module("flag", Expr::LocalGet(B_ID))); + assert!( + ir.contains(NOTE_CALL), + "a store into a slot no mask declares must still note its layout — \ + that note is what sets the collector's pointer bit:\n{ir}" + ); + assert!( + !ir.contains(NOTE_BLOCK), + "a slot outside the pointer mask must NOT take the conforming elision: \ + `layout_note_slot` there is load-bearing, not a no-op:\n{ir}" + ); +} + +/// The mask predicate itself, independent of emission: it answers the pointer +/// mask, refuses a raw-f64 slot, and refuses an out-of-range index. +#[test] +fn layout_declares_pointer_slot_answers_the_pointer_mask_only() { + use crate::typed_shape::{class_typed_layout, layout_declares_pointer_slot}; + let class = link_class(); + let mut classes = std::collections::HashMap::new(); + classes.insert("Link".to_string(), &class); + let layout = class_typed_layout(&classes, "Link"); + + assert_eq!(layout.slot_count, 3); + assert!( + layout_declares_pointer_slot(&layout, 0), + "slot 0 is `Link | null` — pointer-masked" + ); + assert!( + !layout_declares_pointer_slot(&layout, 1), + "slot 1 is `number` — raw-f64-masked, never pointer-masked" + ); + assert!( + !layout_declares_pointer_slot(&layout, 2), + "slot 2 is `boolean` — in neither mask, so not pointer-declared" + ); + assert!( + !layout_declares_pointer_slot(&layout, 3), + "slot 3 is past `slot_count`; a descriptor cannot describe it" + ); +} diff --git a/crates/perry-codegen/src/expr/helpers.rs b/crates/perry-codegen/src/expr/helpers.rs index 166400d138..dc8d8b0ec5 100644 --- a/crates/perry-codegen/src/expr/helpers.rs +++ b/crates/perry-codegen/src/expr/helpers.rs @@ -215,19 +215,73 @@ pub(crate) fn array_store_needs_write_barrier(ctx: &FnCtx<'_>, value: &Expr) -> /// user-address range and are rejected — and the evacuation rewrite path /// routes through the same function. /// -/// **Deliberately NOT elided: a pointer-valued store into a pointer-masked -/// slot.** That would be a no-op under an intact descriptor, but the receiver -/// is not guaranteed to have one — `lower_new_impl` has an exit -/// (`lower_call/new.rs`, the standalone-ctor-symbol branch where -/// `call_local_constructor_symbol` yields `None`) that returns a freshly -/// allocated instance *without* emitting `js_gc_init_typed_shape_layout`. Such -/// an object sits at `GC_LAYOUT_POINTER_FREE`, where the note is the only thing -/// that ever sets the pointer-mask bit the collector reads. Closing that exit -/// (#6921) is the prerequisite for the stronger elision. +/// A pointer-valued store into a pointer-masked slot is handled separately, by +/// [`class_field_store_layout_note_is_conforming`] — see there. pub(crate) fn class_field_store_needs_layout_note(ctx: &FnCtx<'_>, value: &Expr) -> bool { !expr_produces_non_pointer_bits_by_construction(ctx, value) } +/// Phase 4b.2 (#5094, refs #7510): is this class-field store's layout note a +/// *provable no-op whenever the receiver carries an intact side-mask +/// descriptor*? +/// +/// [`class_field_store_needs_layout_note`] above elides the note for a value +/// that is a non-pointer by construction. The complementary case — a **pointer +/// stored into a slot the class's own compile-time pointer mask declares** — +/// was deliberately left un-elided, because "the receiver has a descriptor" was +/// not total: `lower_new_impl`'s standalone-ctor-symbol branch could return a +/// freshly allocated instance with none, sitting at `GC_LAYOUT_POINTER_FREE`, +/// where the note is the only thing that ever sets the pointer-mask bit the +/// collector reads. **#6921 closed that exit** (`lower_call/new.rs` now emits +/// the init there too), so the elision is available — but this returns only +/// *elidable under a header test*, not *elidable outright*, and the emitter +/// pairs it with that test. A descriptor-less receiver from any path this +/// reasoning did not enumerate still takes the full note. +/// +/// The test the emitter pairs this with is +/// `_reserved & (STATE_MASK | TYPED_LAYOUT_INTACT) == SIDE_MASK | INTACT`, and +/// together the two prove `layout_note_slot` would return `Conforms` without +/// touching anything: +/// +/// * The `#5093` inline precheck has already proven, on this path, that the +/// receiver's `keys_array` equals **this class's** keys global and its +/// `field_count` exceeds the slot index. So the descriptor reachable for it +/// was installed from this class's mask globals — shared by shape under that +/// same key (`SHAPE_LAYOUTS`), or per-object if that key was poisoned +/// ambiguous, and in both cases from these same words. +/// * `slot` is in that mask's `pointer_mask` and (checked in +/// [`crate::typed_shape::layout_declares_pointer_slot`]) not in its +/// `raw_f64_mask`, so neither `layout_note_slot` downgrade arm can fire: the +/// raw-f64 arm is not this slot's, and the pointer arm's condition is +/// `!pointer_mask.contains(slot)`. +/// * `INTACT` set is exactly the runtime's own invariant that *some* descriptor +/// is reachable; `SIDE_MASK` is the state a non-empty pointer mask installs. +/// A cleared bit or any other state routes to the real note, which is the +/// pre-change behaviour. +/// +/// Deliberately keyed on the DECLARED field type, not on the value: the value +/// is already known pointer-bearing at this point (the emitter's live +/// `may_carry_heap_pointer` test gates the whole bookkeeping block), and the +/// mask is what the collector will read. +pub(crate) fn class_field_store_layout_note_is_conforming( + ctx: &FnCtx<'_>, + class_name: &str, + field_index: u32, +) -> bool { + // No keys global ⟹ no mask globals are emitted for this class and no + // descriptor is ever installed, so the header test could never pass. Skip + // the extra IR rather than emit a branch that is always taken. + if !ctx.class_keys_globals.contains_key(class_name) { + return false; + } + let layout = ctx + .class_init_chains + .get(class_name) + .map(|chain| crate::typed_shape::class_typed_layout_from_chain(chain)) + .unwrap_or_else(|| crate::typed_shape::class_typed_layout(ctx.classes, class_name)); + crate::typed_shape::layout_declares_pointer_slot(&layout, field_index) +} + /// `js_string_addref_if_heap_string` demotes a uniquely-owned (refcount==1) /// heap string to shared when it becomes aliased from a heap slot, and is a /// no-op for every non-`STRING_TAG` value (`string/alloc.rs`). So it is dead diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 8baf72c996..6939892efd 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -35,6 +35,8 @@ mod array_literal; mod buffer_access; mod buffer_views; mod channel; +#[cfg(test)] +mod conforming_layout_note_tests; mod helpers; mod i32_fast_path; mod index; @@ -72,12 +74,12 @@ pub(crate) use channel::{ }; pub(crate) use helpers::{ array_store_needs_layout_note, array_store_needs_write_barrier, buffer_alias_metadata_suffix, - class_field_store_needs_layout_note, class_field_store_needs_string_addref, - emit_all_pointer_array_declaration, expr_has_numeric_pointer_free_array_layout, - expr_produces_fresh_heap_allocation, expr_produces_non_pointer_bits_by_construction, - is_global_this_builtin_function_name, is_global_this_builtin_name, - lower_expr_with_expected_type, lower_js_args_array, store_needs_string_addref, - unbox_str_handle, unbox_to_i64, + class_field_store_layout_note_is_conforming, class_field_store_needs_layout_note, + class_field_store_needs_string_addref, emit_all_pointer_array_declaration, + expr_has_numeric_pointer_free_array_layout, expr_produces_fresh_heap_allocation, + expr_produces_non_pointer_bits_by_construction, is_global_this_builtin_function_name, + is_global_this_builtin_name, lower_expr_with_expected_type, lower_js_args_array, + store_needs_string_addref, unbox_str_handle, unbox_to_i64, }; pub(crate) use i32_fast_path::{ can_lower_expr_as_i32, can_lower_expr_as_i32_in_current_region, diff --git a/crates/perry-codegen/src/expr/property_set.rs b/crates/perry-codegen/src/expr/property_set.rs index c329b53268..302c6f491e 100644 --- a/crates/perry-codegen/src/expr/property_set.rs +++ b/crates/perry-codegen/src/expr/property_set.rs @@ -36,11 +36,11 @@ use crate::type_analysis::{ use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; use super::{ - class_field_store_needs_layout_note, class_field_store_needs_string_addref, - emit_jsvalue_slot_store_pointer_tested, emit_typed_feedback_register_site, - expr_produces_non_pointer_bits_by_construction, lower_expr, lower_expr_native, - raw_f64_layout_fact, try_lower_pod_field_set, unbox_to_i64, FnCtx, TypedFeedbackContract, - TypedFeedbackKind, + class_field_store_layout_note_is_conforming, class_field_store_needs_layout_note, + class_field_store_needs_string_addref, emit_jsvalue_slot_store_pointer_tested, + emit_typed_feedback_register_site, expr_produces_non_pointer_bits_by_construction, lower_expr, + lower_expr_native, raw_f64_layout_fact, try_lower_pod_field_set, unbox_to_i64, FnCtx, + TypedFeedbackContract, TypedFeedbackKind, }; fn canonicalize_raw_f64_numeric_store_value( @@ -159,6 +159,7 @@ pub(crate) fn try_lower_sloppy_class_field_store( field_index, expected_class_id, &keys_global_name, + &class_name, ); } @@ -337,6 +338,7 @@ fn try_lower_sloppy_class_field_boxed_store( field_index: u32, expected_class_id: u32, keys_global_name: &str, + class_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 @@ -426,6 +428,7 @@ fn try_lower_sloppy_class_field_boxed_store( &obj_bits, &field_addr, barrier_needed, + class_field_store_layout_note_is_conforming(ctx, class_name, field_index), ); ctx.block().br(&merge_label); } @@ -1094,6 +1097,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &obj_bits, &field_addr, field_set_barrier_needed, + class_field_store_layout_note_is_conforming( + ctx, + &class_name, + field_index, + ), ); } let (semantic, rep) = if requires_raw_f64 { @@ -1320,6 +1328,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &obj_bits, &field_addr, field_set_barrier_needed, + class_field_store_layout_note_is_conforming( + ctx, + &class_name, + field_index, + ), ); None }; diff --git a/crates/perry-codegen/src/expr/write_barrier.rs b/crates/perry-codegen/src/expr/write_barrier.rs index 22696bf24f..dcd314231e 100644 --- a/crates/perry-codegen/src/expr/write_barrier.rs +++ b/crates/perry-codegen/src/expr/write_barrier.rs @@ -9,7 +9,14 @@ use super::{lower_expr, FnCtx}; use crate::block::LlBlock; use crate::nanbox::double_literal; use crate::native_value::LoweredValue; -use crate::types::{DOUBLE, I1, I32, I64, I8}; +use crate::types::{DOUBLE, I1, I16, I32, I64, I8}; + +/// `GC_LAYOUT_STATE_MASK | GC_OBJ_TYPED_LAYOUT_INTACT` (`0xD000`) as a signed +/// i16 — the emitted IR is textual, so the constant is written the way LLVM +/// parses an i16 literal (mirrors `class_field_inline_guard`'s convention). +const LAYOUT_STATE_AND_INTACT_MASK_I16: &str = "-12288"; +/// `GC_LAYOUT_SIDE_MASK | GC_OBJ_TYPED_LAYOUT_INTACT` (`0x9000`), same encoding. +const LAYOUT_SIDE_MASK_INTACT_I16: &str = "-28672"; /// Gen-GC Phase C2 helper: emit a write barrier after heap-store sites /// by default. Only explicit `PERRY_WRITE_BARRIERS=0`/`off`/`false` @@ -502,6 +509,7 @@ pub(crate) fn emit_jsvalue_slot_store_pointer_tested( barrier_parent_bits: &str, slot_addr: &str, write_barrier_needed: bool, + layout_note_conforming: bool, ) -> Option { { let blk = ctx.block(); @@ -536,9 +544,46 @@ pub(crate) fn emit_jsvalue_slot_store_pointer_tested( if string_addref_needed { blk.call_void("js_string_addref_if_heap_string", &[(DOUBLE, value_double)]); } - if layout_note_needed { + } + // Phase 4b.2 (#5094): a pointer stored into a slot the class's own pointer + // mask declares is a `Conforms` no-op inside `layout_note_slot` for every + // receiver that carries an intact side-mask descriptor. Test that header + // state inline and skip the cross-crate call — see + // `class_field_store_layout_note_is_conforming` for why the two together + // are a proof, and why the fallback arm is kept rather than eliding + // outright. + if layout_note_needed && layout_note_conforming { + let note_idx = ctx.new_block("class_field_set.layout_note"); + let after_idx = ctx.new_block("class_field_set.layout_note.done"); + let note_label = ctx.block_label(note_idx); + let after_label = ctx.block_label(after_idx); + { + let blk = ctx.block(); + // GcHeader precedes the object by 8 bytes; `_reserved` is the i16 at + // -6 (same derivation as `class_field_inline_guard`). + let obj_ptr = blk.inttoptr(I64, layout_parent_bits); + let res_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-6")]); + let reserved = blk.load(I16, &res_ptr); + // (GC_LAYOUT_STATE_MASK | GC_OBJ_TYPED_LAYOUT_INTACT) == 0xD000, + // and the conforming value (GC_LAYOUT_SIDE_MASK | INTACT) == 0x9000. + // Written signed because the emitted IR is textual i16. + let masked = blk.and(I16, &reserved, LAYOUT_STATE_AND_INTACT_MASK_I16); + let conforming = blk.icmp_eq(I16, &masked, LAYOUT_SIDE_MASK_INTACT_I16); + blk.cond_br(&conforming, &after_label, ¬e_label); + } + ctx.current_block = note_idx; + { + let blk = ctx.block(); emit_layout_note_slot_on_block(blk, layout_parent_bits, slot_index, &value_bits); + blk.br(&after_label); } + ctx.current_block = after_idx; + } else if layout_note_needed { + let blk = ctx.block(); + emit_layout_note_slot_on_block(blk, layout_parent_bits, slot_index, &value_bits); + } + { + let blk = ctx.block(); if write_barrier_emitted { emit_write_barrier_slot_on_block(blk, barrier_parent_bits, slot_addr, &value_bits); } diff --git a/crates/perry-codegen/src/typed_shape.rs b/crates/perry-codegen/src/typed_shape.rs index 7ae3e16dfa..9a92a2333e 100644 --- a/crates/perry-codegen/src/typed_shape.rs +++ b/crates/perry-codegen/src/typed_shape.rs @@ -271,6 +271,36 @@ fn typed_layout_from_fields<'a>( } } +/// Does `layout`'s **pointer** mask declare `slot`? +/// +/// The masks are word-packed exactly as `typed_layout_from_fields` builds them +/// and as `js_gc_{init,declare}_typed_shape_layout` consumes them, so a `true` +/// here is the same bit the runtime's `TypedLayoutDescriptor::pointer_mask` +/// will carry for this shape. +pub(crate) fn layout_declares_pointer_slot(layout: &TypedShapeLayout, slot: u32) -> bool { + let slot = slot as usize; + if slot >= layout.slot_count as usize { + return false; + } + let word = slot / 64; + // A pointer-masked slot may not also be raw-f64-masked. `init_typed_shape_layout` + // rejects an intersecting pair outright (`words_intersect` -> UNKNOWN), so a + // shape that reaches an installed descriptor has disjoint masks — but this + // predicate licenses eliding a store's layout note, so it re-establishes + // disjointness locally rather than importing it. + let raw_f64_here = layout + .raw_f64_mask_words + .get(word) + .is_some_and(|w| w & (1u64 << (slot % 64)) != 0); + if raw_f64_here { + return false; + } + layout + .pointer_mask_words + .get(word) + .is_some_and(|w| w & (1u64 << (slot % 64)) != 0) +} + pub(crate) fn mask_global_name_from_keys_global(keys_global_name: &str) -> String { keys_global_name .strip_prefix("perry_class_keys_") From 41b3a5e70414c9de263e8aa37310cfba5d05060e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 13:01:28 +0200 Subject: [PATCH 2/4] docs(changelog): fragment for the conforming class-field layout note --- ...7698-conforming-class-field-layout-note.md | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 changelog.d/7698-conforming-class-field-layout-note.md diff --git a/changelog.d/7698-conforming-class-field-layout-note.md b/changelog.d/7698-conforming-class-field-layout-note.md new file mode 100644 index 0000000000..518905c38c --- /dev/null +++ b/changelog.d/7698-conforming-class-field-layout-note.md @@ -0,0 +1,73 @@ +### class fields: a conforming pointer store stops calling into the layout machinery (#5094) + +`this.left = left`, `a.peer = b`, `node.next = n` — a pointer written into a +slot the class's **own compile-time pointer mask already declares a pointer** — +called `js_gc_note_slot_layout` on every store, to re-derive a fact codegen knew +when it emitted the mask. Counted with an instrumented runtime, `tree.ts` makes +**20,447,156** such calls and **20,447,154** of them return `Conforms`: a +cross-crate call, a header decode, a TLS touch and a `SHAPE_LAYOUTS` hashmap +lookup, per store, to change nothing. + +`class_field_store_needs_layout_note` already elided the note for a value that is +a non-pointer *by construction*. This is the complementary case, and its doc +comment named both the obstacle and the unblocking condition: + +> **Deliberately NOT elided: a pointer-valued store into a pointer-masked slot.** +> […] Closing that exit (#6921) is the prerequisite for the stronger elision. + +#6921 is closed — `lower_call/new.rs` emits the layout init on the last `new` +exit that used to return an undeclared instance — so the elision is available. +It is taken as a **live header test with the real note kept on the cold arm**, +not as an outright removal: + +```llvm +%res = load i16, ptr (obj - 6) ; GcHeader::_reserved +%m = and i16 %res, -12288 ; STATE_MASK | TYPED_LAYOUT_INTACT +%ok = icmp eq i16 %m, -28672 ; SIDE_MASK | TYPED_LAYOUT_INTACT +br i1 %ok, label %done, label %layout_note +``` + +Two facts make the taken arm a proof rather than a guess. The #5093 inline +precheck has already established, on this path, that the receiver's `keys_array` +is **this class's** keys global and its `field_count` exceeds the slot index — so +whatever descriptor is reachable for it was installed from these very mask +globals, shared by shape or per-object. And the slot is in that mask's +`pointer_mask` and (checked, not assumed) absent from its `raw_f64_mask`, so +neither of `layout_note_slot`'s two downgrade arms can fire. Anything else — a +cleared intact bit, `POINTER_FREE`, `UNKNOWN`, a receiver from a path this +reasoning did not enumerate — falls to the unchanged call. + +**Measured** — quiet M1 mini, best-of-3 wall, arms interleaved back-to-back, +stdout byte-identical in every row, `PERRY_NO_AUTO_OPTIMIZE=1`: + +| bench | before | after | +|---|--:|--:| +| `cycles` | 0.33 | **0.29** | +| `tree_wide` | 7.90 | **7.77** | +| `tree` | 5.17 | **5.13** | + +Unchanged, as required: `churn` 1.21, `push_cls` 0.89, `churn_alloc` 0.89, +`retain` 2.41, `retain1` 0.04, `retain_wide` 3.38, `retain_wide1` 0.06, +`deeplist` 0.03, `cls_mistyped` 0.02. Peak RSS unchanged. + +The `sample` profile is the clearer statement of what happened: on `cycles_big`, +`layout_note_slot` is the **second-heaviest leaf frame in the base arm (155 +samples)** and is **absent from the fix arm's profile entirely**. The wall-clock +share is smaller than that because these workloads are GC-dominated on the +current default pacing (arena walk, barriers, sweep). + +**Demotion is not weakened, and that is tested rather than asserted.** The intact +bit is not made sticky and no downgrade path is touched: `cls_mistyped.ts` — a +`number`-declared field constructed with a heap string per instance, which must +demote or the collector never traces those strings — still prints +`20000 string payload-19999`. `PERRY_GC_VERIFY_MARK=1 PERRY_GC_VERIFY_EVACUATION=1` +is clean over the bench set, and `PERRY_GC_TRACE` cycle counts and copied bytes +are identical between arms (`churn` 13, `tree` 20). + +The three new tests are unit tests (`cargo-test`-visible, per #5960), and each +was checked to be able to fail. The positive one asserts the elision is +**reached** — a predicate that silently answered `false` everywhere would +otherwise be invisible. The negative asserts a slot in *neither* mask +(`flag: boolean`) keeps its unconditional note: there the note is not a no-op to +skip but the only thing that ever sets the pointer bit the collector reads, and +sabotaging the predicate to return `true` makes that test fail. From 61fbae1c7a7dd6a29847df3577cdffe5cc25e402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 16:29:46 +0200 Subject: [PATCH 3/4] test(class-fields): follow the CFG for the guarded bookkeeping region The #5094 layout-note conditional splits the gc_bookkeeping block into three, and LLVM emits gc_bookkeeping.done BETWEEN the entry and layout_note, so the region is not textually contiguous -- neither the old first-block slice nor a wider text slice describes it. Walk the CFG from the guard entry, stopping at the join. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- .../tests/class_field_store_pointer_test.rs | 90 ++++++++++++++++--- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/crates/perry-codegen/tests/class_field_store_pointer_test.rs b/crates/perry-codegen/tests/class_field_store_pointer_test.rs index f25c76f7b4..c4b4627ca1 100644 --- a/crates/perry-codegen/tests/class_field_store_pointer_test.rs +++ b/crates/perry-codegen/tests/class_field_store_pointer_test.rs @@ -229,20 +229,84 @@ fn compile_ir(module: &Module) -> String { /// the name — that is the `br i1 …, label %class_field_set.gc_bookkeeping.N, /// label %class_field_set.gc_bookkeeping.done.M` a line above, and starting /// there yields an empty slice that passes nothing and fails everything. +/// The guarded REGION: every block reachable from the `gc_bookkeeping` entry +/// without passing through `gc_bookkeeping.done`, i.e. exactly the blocks the +/// pointer-bearing guard dominates. +/// +/// This used to be a text slice -- from the `gc_bookkeeping.N:` label to the +/// first `br` -- which is the same thing only while the region is ONE block. +/// #5094 made it three (`gc_bookkeeping` -> `layout_note` / `layout_note.done` +/// -> `gc_bookkeeping.done`), and the slice then returned only the prefix, so +/// `@js_write_barrier_slot` "left the guarded block" without moving at all. +/// +/// A wider text slice would not do either: LLVM emits `gc_bookkeeping.done.5` +/// **between** `gc_bookkeeping.4` and `layout_note.6`, so the region is not +/// textually contiguous and "everything up to the done label" is still just +/// the first block. +/// +/// Following the CFG also makes the assertion strictly stronger than the one +/// it replaces: a call hoisted onto the guard's NOT-taken edge, or past the +/// join into `merge`, is absent from the region and fails -- whereas the old +/// slice only ever proved a call was not in the first block. fn gc_bookkeeping_block(ir: &str) -> Option { - let mut lines = ir.lines().skip_while(|line| { - let label = line.trim_end_matches(':'); - !(line.ends_with(':') - && label.starts_with("class_field_set.gc_bookkeeping.") - && !label.starts_with("class_field_set.gc_bookkeeping.done")) - }); - lines.next()?; - Some( - lines - .take_while(|line| !line.trim_start().starts_with("br ")) - .collect::>() - .join("\n"), - ) + // label -> (body lines, successor labels) + let mut blocks: Vec<(String, Vec<&str>, Vec)> = Vec::new(); + for line in ir.lines() { + let trimmed = line.trim_end(); + if !line.starts_with(char::is_whitespace) && trimmed.ends_with(':') { + blocks.push(( + trimmed.trim_end_matches(':').to_string(), + Vec::new(), + Vec::new(), + )); + continue; + } + let Some((_, body, succs)) = blocks.last_mut() else { + continue; + }; + body.push(line); + if trimmed.trim_start().starts_with("br ") { + for token in trimmed.split("label %").skip(1) { + succs.push( + token + .split(|c: char| c == ',' || c.is_whitespace()) + .next() + .unwrap_or("") + .to_string(), + ); + } + } + } + + let entry = blocks.iter().position(|(label, _, _)| { + label.starts_with("class_field_set.gc_bookkeeping.") + && !label.starts_with("class_field_set.gc_bookkeeping.done") + })?; + + let mut region = String::new(); + let mut stack = vec![entry]; + let mut seen = vec![false; blocks.len()]; + while let Some(idx) = stack.pop() { + if seen[idx] { + continue; + } + seen[idx] = true; + let (_, body, succs) = &blocks[idx]; + for line in body { + region.push_str(line); + region.push('\n'); + } + for succ in succs { + // The join is the region's exit, not part of it. + if succ.starts_with("class_field_set.gc_bookkeeping.done") { + continue; + } + if let Some(next) = blocks.iter().position(|(label, _, _)| label == succ) { + stack.push(next); + } + } + } + Some(region) } /// An `any`-typed field is the case the whole ticket is about: it takes the From e6ddfbf625c171803049007878748aea27887a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 9 Aug 2026 16:30:17 +0200 Subject: [PATCH 4/4] chore: bump version to 0.5.1404 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 01bbdb8397..8ac7ced3ae 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.1403 +**Current Version:** 0.5.1404 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index abe3181c3a..7d89eb61b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1403" +version = "0.5.1404" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1403" +version = "0.5.1404" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1403" +version = "0.5.1404" [[package]] name = "perry-ui-tvos" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1403" +version = "0.5.1404" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index c239494386..226aeab86e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1403" +version = "0.5.1404" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"