diff --git a/CLAUDE.md b/CLAUDE.md
index e4b44b0f90..e6bfb35c8a 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.1282
+**Current Version:** 0.5.1283
## TypeScript Parity Status
diff --git a/Cargo.lock b/Cargo.lock
index 8274e1cfc7..d20004fdaa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "perry"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"base64",
@@ -5607,14 +5607,14 @@ dependencies = [
[[package]]
name = "perry-api-manifest"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"serde",
]
[[package]]
name = "perry-audio-miniaudio"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"cc",
"libc",
@@ -5622,7 +5622,7 @@ dependencies = [
[[package]]
name = "perry-codegen"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"inkwell",
@@ -5638,7 +5638,7 @@ dependencies = [
[[package]]
name = "perry-codegen-arkts"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"perry-hir",
@@ -5646,7 +5646,7 @@ dependencies = [
[[package]]
name = "perry-codegen-glance"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"perry-hir",
@@ -5654,7 +5654,7 @@ dependencies = [
[[package]]
name = "perry-codegen-js"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"perry-dispatch",
@@ -5663,7 +5663,7 @@ dependencies = [
[[package]]
name = "perry-codegen-swiftui"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"perry-hir",
@@ -5671,7 +5671,7 @@ dependencies = [
[[package]]
name = "perry-codegen-wasm"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"base64",
@@ -5683,7 +5683,7 @@ dependencies = [
[[package]]
name = "perry-codegen-wear-tiles"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"perry-hir",
@@ -5691,7 +5691,7 @@ dependencies = [
[[package]]
name = "perry-container-compose"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"async-trait",
@@ -5720,14 +5720,14 @@ dependencies = [
[[package]]
name = "perry-container-e2e"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
]
[[package]]
name = "perry-diagnostics"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"serde",
"serde_json",
@@ -5735,7 +5735,7 @@ dependencies = [
[[package]]
name = "perry-dispatch"
-version = "0.5.1282"
+version = "0.5.1283"
[[package]]
name = "perry-doc-fixture-my-bindings"
@@ -5746,7 +5746,7 @@ dependencies = [
[[package]]
name = "perry-doc-tests"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"clap",
@@ -5761,7 +5761,7 @@ dependencies = [
[[package]]
name = "perry-ext-ads"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"block2",
"objc2",
@@ -5771,7 +5771,7 @@ dependencies = [
[[package]]
name = "perry-ext-argon2"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"argon2",
"perry-ffi",
@@ -5779,7 +5779,7 @@ dependencies = [
[[package]]
name = "perry-ext-axios"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"reqwest",
@@ -5788,7 +5788,7 @@ dependencies = [
[[package]]
name = "perry-ext-bcrypt"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"bcrypt",
"perry-ffi",
@@ -5796,7 +5796,7 @@ dependencies = [
[[package]]
name = "perry-ext-better-sqlite3"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"rusqlite",
@@ -5804,7 +5804,7 @@ dependencies = [
[[package]]
name = "perry-ext-cheerio"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"scraper",
@@ -5812,7 +5812,7 @@ dependencies = [
[[package]]
name = "perry-ext-commander"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"perry-runtime",
@@ -5820,7 +5820,7 @@ dependencies = [
[[package]]
name = "perry-ext-cron"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"chrono",
"cron",
@@ -5830,7 +5830,7 @@ dependencies = [
[[package]]
name = "perry-ext-dayjs"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"chrono",
"perry-ffi",
@@ -5838,7 +5838,7 @@ dependencies = [
[[package]]
name = "perry-ext-decimal"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"rust_decimal",
@@ -5846,7 +5846,7 @@ dependencies = [
[[package]]
name = "perry-ext-dotenv"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"serde_json",
@@ -5854,7 +5854,7 @@ dependencies = [
[[package]]
name = "perry-ext-ethers"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"rand 0.10.1",
@@ -5862,7 +5862,7 @@ dependencies = [
[[package]]
name = "perry-ext-events"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"perry-runtime",
@@ -5870,14 +5870,14 @@ dependencies = [
[[package]]
name = "perry-ext-exponential-backoff"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
]
[[package]]
name = "perry-ext-fastify"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"bytes",
"http-body-util",
@@ -5895,7 +5895,7 @@ dependencies = [
[[package]]
name = "perry-ext-fetch"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"bytes",
"lazy_static",
@@ -5908,7 +5908,7 @@ dependencies = [
[[package]]
name = "perry-ext-http"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"bytes",
"h2",
@@ -5932,7 +5932,7 @@ dependencies = [
[[package]]
name = "perry-ext-ioredis"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"lazy_static",
"perry-ffi",
@@ -5942,7 +5942,7 @@ dependencies = [
[[package]]
name = "perry-ext-jsonwebtoken"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"base64",
"jsonwebtoken",
@@ -5953,7 +5953,7 @@ dependencies = [
[[package]]
name = "perry-ext-lru-cache"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"lru",
"perry-ffi",
@@ -5962,7 +5962,7 @@ dependencies = [
[[package]]
name = "perry-ext-moment"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"chrono",
"perry-ffi",
@@ -5970,7 +5970,7 @@ dependencies = [
[[package]]
name = "perry-ext-mongodb"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"bson",
"futures-util",
@@ -5982,7 +5982,7 @@ dependencies = [
[[package]]
name = "perry-ext-mysql2"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"chrono",
"perry-ffi",
@@ -5992,7 +5992,7 @@ dependencies = [
[[package]]
name = "perry-ext-nanoid"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"nanoid",
"perry-ffi",
@@ -6001,7 +6001,7 @@ dependencies = [
[[package]]
name = "perry-ext-net"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"bytes",
"perry-ffi",
@@ -6014,7 +6014,7 @@ dependencies = [
[[package]]
name = "perry-ext-node-forge"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"const-oid 0.9.6",
"der 0.7.10",
@@ -6033,7 +6033,7 @@ dependencies = [
[[package]]
name = "perry-ext-nodemailer"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"lettre",
"perry-ffi",
@@ -6043,7 +6043,7 @@ dependencies = [
[[package]]
name = "perry-ext-pdf"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"printpdf",
@@ -6051,7 +6051,7 @@ dependencies = [
[[package]]
name = "perry-ext-pg"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"sqlx",
@@ -6060,7 +6060,7 @@ dependencies = [
[[package]]
name = "perry-ext-ratelimit"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"governor",
"perry-ffi",
@@ -6068,7 +6068,7 @@ dependencies = [
[[package]]
name = "perry-ext-sharp"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"fast_image_resize",
"image",
@@ -6078,14 +6078,14 @@ dependencies = [
[[package]]
name = "perry-ext-slugify"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
]
[[package]]
name = "perry-ext-streams"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"lazy_static",
"perry-ffi",
@@ -6094,7 +6094,7 @@ dependencies = [
[[package]]
name = "perry-ext-undici"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"perry-runtime",
@@ -6103,7 +6103,7 @@ dependencies = [
[[package]]
name = "perry-ext-uuid"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"uuid",
@@ -6111,7 +6111,7 @@ dependencies = [
[[package]]
name = "perry-ext-validator"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ffi",
"regex",
@@ -6121,7 +6121,7 @@ dependencies = [
[[package]]
name = "perry-ext-ws"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"futures-util",
"lazy_static",
@@ -6134,7 +6134,7 @@ dependencies = [
[[package]]
name = "perry-ext-zlib"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"brotli",
"flate2",
@@ -6144,7 +6144,7 @@ dependencies = [
[[package]]
name = "perry-ffi"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"dashmap",
"once_cell",
@@ -6153,7 +6153,7 @@ dependencies = [
[[package]]
name = "perry-hir"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"perry-api-manifest",
@@ -6171,7 +6171,7 @@ dependencies = [
[[package]]
name = "perry-parser"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"perry-diagnostics",
@@ -6183,7 +6183,7 @@ dependencies = [
[[package]]
name = "perry-runtime"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"base64",
@@ -6225,14 +6225,14 @@ dependencies = [
[[package]]
name = "perry-runtime-static"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-runtime",
]
[[package]]
name = "perry-stdlib"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"aes 0.8.4",
"aes 0.9.1",
@@ -6327,14 +6327,14 @@ dependencies = [
[[package]]
name = "perry-stdlib-static"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-stdlib",
]
[[package]]
name = "perry-transform"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"perry-hir",
@@ -6343,14 +6343,14 @@ dependencies = [
[[package]]
name = "perry-ui"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ui-model",
]
[[package]]
name = "perry-ui-android"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"base64",
"itoa",
@@ -6367,7 +6367,7 @@ dependencies = [
[[package]]
name = "perry-ui-geisterhand"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"rand 0.10.1",
"serde",
@@ -6377,7 +6377,7 @@ dependencies = [
[[package]]
name = "perry-ui-gtk4"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"base64",
"cairo-rs 0.22.0",
@@ -6400,7 +6400,7 @@ dependencies = [
[[package]]
name = "perry-ui-ios"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"base64",
"block2",
@@ -6416,7 +6416,7 @@ dependencies = [
[[package]]
name = "perry-ui-macos"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"base64",
"block2",
@@ -6431,7 +6431,7 @@ dependencies = [
[[package]]
name = "perry-ui-model"
-version = "0.5.1282"
+version = "0.5.1283"
[[package]]
name = "perry-ui-test"
@@ -6442,11 +6442,11 @@ dependencies = [
[[package]]
name = "perry-ui-testkit"
-version = "0.5.1282"
+version = "0.5.1283"
[[package]]
name = "perry-ui-tvos"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"base64",
"block2",
@@ -6462,7 +6462,7 @@ dependencies = [
[[package]]
name = "perry-ui-visionos"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"base64",
"block2",
@@ -6478,7 +6478,7 @@ dependencies = [
[[package]]
name = "perry-ui-watchos"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"block2",
"libc",
@@ -6491,7 +6491,7 @@ dependencies = [
[[package]]
name = "perry-ui-windows"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"base64",
"libc",
@@ -6508,14 +6508,14 @@ dependencies = [
[[package]]
name = "perry-ui-windows-winui"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"perry-ui-windows",
]
[[package]]
name = "perry-updater"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"anyhow",
"base64",
@@ -6531,7 +6531,7 @@ dependencies = [
[[package]]
name = "perry-wasm-host"
-version = "0.5.1282"
+version = "0.5.1283"
dependencies = [
"wasmi",
]
diff --git a/Cargo.toml b/Cargo.toml
index 19dc9d726e..4a4c562db6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -315,7 +315,7 @@ codegen-units = 16
codegen-units = 16
[workspace.package]
-version = "0.5.1282"
+version = "0.5.1283"
edition = "2021"
license = "MIT"
repository = "https://github.com/PerryTS/perry"
diff --git a/changelog.d/7486-dead-store-elision.md b/changelog.d/7486-dead-store-elision.md
new file mode 100644
index 0000000000..d0f78d0d62
--- /dev/null
+++ b/changelog.d/7486-dead-store-elision.md
@@ -0,0 +1,40 @@
+### Codegen: provably-dead per-store bookkeeping elided on class-field stores (#7469)
+
+Profiling `churn.ts` after the hot-TLS work showed the synthesized anon-shape
+constructor — executed once per object literal — emitting **four** guarded
+field-store sequences for a two-field literal: two real parameter stores, and
+two field-default initializations storing a compile-time-constant `undefined`
+that the very next statements overwrite. Each dead pair cost a
+`js_typed_feedback_class_field_set_guard` FFI call, a
+`js_string_addref_if_heap_string` on a constant, and a `js_gc_note_slot_layout`,
+20 million times.
+
+Two elisions, both riding existing machinery:
+
+- **Dead default-`undefined` field inits.** `apply_field_initializers_recursive`
+ writes `undefined` into every `init: None` field (#486 — `new C().x` must read
+ `undefined`, not allocator bytes). When the class's own constructor opens with
+ an unbroken run of `this.f = ` statements — every synthesized
+ anon-shape ctor, and plain user ctors like `constructor(a, b) { this.a = a;
+ this.b = b }` — those writes are dead stores: nothing can observe `this.f`
+ before the prologue overwrites it. `ctor_prologue_param_assigned_fields`
+ proves eligibility (class extends nothing, all fields bare and undecorated,
+ all ctor params plain, no setter shadows a prologue field, prologue statements
+ are throw-free `LocalGet` assigns) and the init loop skips exactly those
+ fields. The `js_object_alloc_class_inline_keys` allocation path already
+ pre-fills slots with `undefined` (#4717), making the writes doubly dead — but
+ the elision rests only on the allocator-independent prologue guarantee.
+- **Value-side elision on the guarded store path.** The guarded class-field
+ store already elided the write barrier for values that are non-pointers by
+ construction (#5334 lever D) but hardcoded the string-addref and layout-note
+ on. The Phase 4b.1 predicates are value-side-only proofs — safe in every
+ receiver layout state per their own documentation — so they now gate all
+ three calls. `this.count = 0` on the guarded path emits guard + raw store and
+ nothing else.
+
+Semantics probed against Node byte-for-byte across the edge cases (unassigned
+fields still read `undefined`, param defaults and pre-assignment side effects
+refuse the elision, setter-shadowed fields refuse it, `Object.keys`/JSON shape
+unchanged). GC ratchet vs `main` agrees on 107 of 108 deterministic metrics
+(the one difference is a −288-byte `heap_used_bytes` allocation-boundary
+wobble); collector accounting is identical on all 12 probes.
diff --git a/crates/perry-codegen/src/expr/property_set.rs b/crates/perry-codegen/src/expr/property_set.rs
index 2630a23ed7..3b729ee15f 100644
--- a/crates/perry-codegen/src/expr/property_set.rs
+++ b/crates/perry-codegen/src/expr/property_set.rs
@@ -20,10 +20,10 @@ 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_on_block, emit_jsvalue_slot_store_with_flags_on_block,
- 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,
+ emit_jsvalue_slot_store_with_flags_on_block, 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(
@@ -1037,6 +1037,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result {
// as the array-store barrier elision.
let field_set_barrier_needed =
!expr_produces_non_pointer_bits_by_construction(ctx, value);
+ // #7469: value-side elision of the addref and layout
+ // note on the guarded arm — computed here because the
+ // predicates take `&FnCtx` and the block builder is
+ // borrowed below.
+ let guarded_note_needed = class_field_store_needs_layout_note(ctx, value);
+ let guarded_addref_needed =
+ class_field_store_needs_string_addref(ctx, value);
let raw_stored_value = {
// arm64_32 watchOS: the object fields region begins at
// `size_of::()` past the user pointer — 24 on
@@ -1065,15 +1072,26 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result {
Some(numeric_value)
} else {
// #5334 lever D: skip the barrier when the value
- // is a non-pointer by construction.
+ // is a non-pointer by construction. #7469 extends
+ // the same value-expression gating to the addref
+ // and layout note — the Phase 4b.1 predicates are
+ // value-side-only proofs (see their docs: safe in
+ // every layout state the receiver can be in), so
+ // they apply on this guarded arm exactly as on
+ // the ptr-shape-proven arm above. The guard
+ // passing does not change what the VALUE can be;
+ // `requires_raw_f64` is false here, which is the
+ // precondition `class_field_store_needs_layout_note`
+ // documents.
let field_addr = blk.ptrtoint(&field_ptr, I64);
- emit_jsvalue_slot_store_on_block(
+ emit_jsvalue_slot_store_with_flags_on_block(
blk,
&field_ptr,
&val_double,
&obj_handle,
&field_idx_str,
- true,
+ guarded_addref_needed,
+ guarded_note_needed,
&obj_bits,
&field_addr,
field_set_barrier_needed,
diff --git a/crates/perry-codegen/src/lower_call/field_init.rs b/crates/perry-codegen/src/lower_call/field_init.rs
index b618628937..92bbeef0dc 100644
--- a/crates/perry-codegen/src/lower_call/field_init.rs
+++ b/crates/perry-codegen/src/lower_call/field_init.rs
@@ -7,12 +7,116 @@
//! `this` per the requested mode.
use anyhow::Result;
-use perry_hir::Expr;
+use perry_hir::{Expr, Stmt};
use crate::expr::{lower_expr, FnCtx};
use crate::nanbox::{double_literal, POINTER_MASK_I64};
use crate::types::{DOUBLE, I32, I64};
+/// 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).
+///
+/// A field declared without an initializer must normally be written as
+/// `undefined` in the init phase (#486: `new C().x === undefined` is spec, not
+/// zero-bytes-from-the-allocator). But the most common constructor shape —
+/// including every synthesized anon-shape literal ctor
+/// (`lower/context.rs::mint_anon_shape_class`) — opens with a run of plain
+/// `this.f = ` statements. For those fields the `undefined` write is a
+/// dead store: it is overwritten before any code that could read `this.f`
+/// runs. On `churn.ts` that dead store was 2 of the 4 guarded field-store
+/// sequences per object literal — a set-guard FFI call, a string addref, and a
+/// layout note per field, 20M times, all storing a compile-time constant that
+/// the very next statements overwrite. (On the `js_object_alloc_class_inline_keys`
+/// allocation path the slots are ALREADY `undefined`-prefilled (#4717), making
+/// the write doubly dead — but this elision does not rely on that: the
+/// prologue overwrite guarantee is allocator-independent.)
+///
+/// Returns the empty set — i.e. elides nothing — unless every condition holds:
+///
+/// - **The class extends nothing** (`extends`/`extends_name`/`native_extends`/
+/// `extends_expr` all `None`). A base class is still fine to *be* extended:
+/// its field-init phase and ctor body may then be separated by a derived
+/// ctor's pre-`super()` statements, but those cannot touch `this` (TDZ), so
+/// the skipped slot stays unobservable. What this condition excludes is the
+/// class having its OWN super machinery in between.
+/// - **No field anywhere on the class carries an initializer expression or a
+/// computed key, and the class and its fields are undecorated.** Initializer
+/// and key expressions run during the init phase and may legally read
+/// `this.` of an earlier field — eliding f's `undefined` write would let
+/// them observe whatever the allocator left in the slot. All-`init: None`
+/// fields mean the init phase contains no user expression at all.
+/// - **Every constructor parameter is plain**: no default (a default expression
+/// evaluates before the prologue and, in the general lowering, could observe
+/// `this`), no rest, no decorators, no `arguments` materialization.
+/// - **No setter shares a name with a prologue-assigned field** — the
+/// PropertySet would dispatch to the setter instead of writing the slot, and
+/// the elided `undefined` write was the only slot write.
+/// - The field itself is public and non-computed (`is_private` false,
+/// `key_expr` none).
+///
+/// The prologue is the maximal leading run of
+/// `Stmt::Expr(PropertySet { object: This, property, value: LocalGet() })`
+/// statements. 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.
+fn ctor_prologue_param_assigned_fields(
+ class: &perry_hir::Class,
+) -> std::collections::HashSet {
+ let empty = std::collections::HashSet::new();
+ if class.extends.is_some()
+ || class.extends_name.is_some()
+ || class.native_extends.is_some()
+ || class.extends_expr.is_some()
+ || !class.decorators.is_empty()
+ {
+ return empty;
+ }
+ let Some(ctor) = class.constructor.as_ref() else {
+ return empty;
+ };
+ let all_fields_bare = class.fields.iter().all(|f| {
+ f.init.is_none() && f.key_expr.is_none() && f.decorators.is_empty() && !f.is_private
+ });
+ if !all_fields_bare {
+ return empty;
+ }
+ let params_plain = ctor.params.iter().all(|p| {
+ p.default.is_none() && !p.is_rest && p.decorators.is_empty() && p.arguments_object.is_none()
+ });
+ if !params_plain {
+ return empty;
+ }
+ let param_ids: std::collections::HashSet<_> = ctor.params.iter().map(|p| p.id).collect();
+ let mut assigned = std::collections::HashSet::new();
+ for stmt in &ctor.body {
+ match stmt {
+ Stmt::Expr(Expr::PropertySet {
+ object,
+ property,
+ value,
+ }) if matches!(object.as_ref(), Expr::This)
+ && matches!(value.as_ref(), Expr::LocalGet(id) if param_ids.contains(id)) =>
+ {
+ assigned.insert(property.clone());
+ }
+ _ => break,
+ }
+ }
+ if assigned.is_empty() {
+ return empty;
+ }
+ if class
+ .setters
+ .iter()
+ .any(|(name, _)| assigned.contains(name))
+ {
+ return empty;
+ }
+ assigned
+}
+
/// Walk the inheritance chain from the root down and apply each class's
/// field initializers to `this`. Call this inside `lower_new` after the
/// `this` slot is pushed but before the constructor body is inlined.
@@ -193,6 +297,17 @@ pub(crate) fn apply_field_initializers_recursive(
// in hono's Context. Lower the missing-init case to
// `Expr::Undefined` so the constructor writes the spec-correct
// value into the field slot. Refs #486.
+ // #7469: default-`undefined` writes that the class's own ctor prologue
+ // provably overwrites are dead — see the function doc for the proof
+ // obligations. Computed from the leaf-authoritative `ctx.classes` entry
+ // (an ancestor resolved only through `chain_field_override` has no
+ // visible ctor here and gets the conservative empty set).
+ let prologue_assigned = ctx
+ .classes
+ .get(&class_name_in_chain)
+ .copied()
+ .map(ctor_prologue_param_assigned_fields)
+ .unwrap_or_default();
let mut init_pairs: Vec<(String, Expr)> = Vec::new();
let mut init_pairs_computed: Vec<(Expr, Expr)> = Vec::new();
for field in &class_fields {
@@ -212,6 +327,17 @@ pub(crate) fn apply_field_initializers_recursive(
if field.key_expr.is_none() && field.name.starts_with("__perry_cap_") {
continue;
}
+ // #7469: skip the dead default write for prologue-overwritten
+ // fields. `ctor_prologue_param_assigned_fields` returns non-empty
+ // only when EVERY field on the class is bare (`init: None`, named
+ // key, undecorated, public), so this arm can only ever drop
+ // `Expr::Undefined` writes — never a real initializer.
+ if field.init.is_none()
+ && field.key_expr.is_none()
+ && prologue_assigned.contains(&field.name)
+ {
+ continue;
+ }
let init = match &field.init {
Some(e) => e.clone(),
None => Expr::Undefined,