diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index bad890eaf..2b8d43066 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -568,7 +568,7 @@ pub(super) fn init_static_fields_late( // class whose static_methods include a block not yet hooked via // init (e.g. class expressions that bypass the stmt-decl path); // calling it here keeps the legacy behavior of "always run, just - // late" for those. + // late" for those. (#2278) for c in &hir.classes { for sm in &c.static_methods { if !sm.name.starts_with("__perry_static_init_") { @@ -599,7 +599,9 @@ pub(super) fn init_static_fields_late( /// Returns true if `stmt` is a top-level `Expr(StaticMethodCall)` /// invoking the (`class_name`, `method_name`) pair — the shape HIR /// lowering emits at the class-decl position for each -/// `__perry_static_init_*` synthetic method. +/// `__perry_static_init_*` synthetic method. Used by +/// `init_static_fields_late` to skip per-(class, block) pairs that +/// have already been invoked inline. (#2278) fn init_calls_static_block(stmt: &perry_hir::Stmt, class_name: &str, method_name: &str) -> bool { if let perry_hir::Stmt::Expr(perry_hir::Expr::StaticMethodCall { class_name: c, diff --git a/crates/perry-hir/src/lower/stmt.rs b/crates/perry-hir/src/lower/stmt.rs index 9357d9781..298c7e5b3 100644 --- a/crates/perry-hir/src/lower/stmt.rs +++ b/crates/perry-hir/src/lower/stmt.rs @@ -874,7 +874,7 @@ pub(crate) fn lower_stmt( // is kept for class expressions that bypass this // declaration path; it skips blocks already invoked // via this inline call. Closes the `test_gap_class_advanced` - // "static block initialized" diff. + // "static block initialized" diff (#2278). for sm in &class.static_methods { if sm.name.starts_with("__perry_static_init_") { module.init.push(Stmt::Expr(Expr::StaticMethodCall { diff --git a/test-parity/node-suite/object/class-static-block.ts b/test-parity/node-suite/object/class-static-block.ts new file mode 100644 index 000000000..ae594740d --- /dev/null +++ b/test-parity/node-suite/object/class-static-block.ts @@ -0,0 +1,59 @@ +// Issue #2278 — `static { ... }` blocks on a class declaration must +// run at the class declaration's source position (per ES spec), not +// at the end of module init. Pre-fix every read after the class +// declaration saw the field's zero default because Perry deferred the +// static-block call to `init_static_fields_late` (after every +// module-level stmt). Surfaced by bisecting `test_gap_class_advanced.ts` +// for #1635 — the single residual failure. + +// Basic case from the issue: declared-typed boolean field, assigned in +// a static block, read in the next module-level stmt. +class WithStaticBlock { + static initialized: boolean; + static { + WithStaticBlock.initialized = true; + } +} +console.log("initialized:", WithStaticBlock.initialized); + +// Static block sees the field's declared initializer and overrides it. +class WithFieldAndBlock { + static value: number = 1; + static { + WithFieldAndBlock.value = 42; + } +} +console.log("value:", WithFieldAndBlock.value); + +// Multiple assignments inside a single block. +class MultiAssign { + static a: number; + static b: string; + static { + MultiAssign.a = 7; + MultiAssign.b = "ok"; + } +} +console.log("a:", MultiAssign.a, "b:", MultiAssign.b); + +// Two static blocks on the same class run in declaration order. +class TwoBlocks { + static counter: number; + static { + TwoBlocks.counter = 0; + } + static { + TwoBlocks.counter = TwoBlocks.counter + 10; + } +} +console.log("counter:", TwoBlocks.counter); + +// The class is visible by name inside its own static block (this is +// the `WithStaticBlock.initialized = true` shape from the issue). +class SelfRef { + static label: string; + static { + SelfRef.label = "self-ref ok"; + } +} +console.log("label:", SelfRef.label);