From f5aac4dc4a67558acf9a854e1da5a47c8c0cbcd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 28 May 2026 20:51:45 +0200 Subject: [PATCH] fix(hir): run static class blocks at declaration source position (#2278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `static { ... }` blocks now emit a synthetic `Class.__perry_static_init_N()` call at the class declaration's source position in `lower/stmt.rs::Decl::Class`, mirroring how static field initializers are already emitted there. Pre-fix the only emitter was `init_static_fields_late`, which runs AFTER every module-level stmt — so a `console.log(C.staticField)` on the line after a class declaration read the field's zero default instead of the value the block had assigned. The late pass keeps calling static blocks for any class that *didn't* get an inline call emitted (inner classes hoisted into `hir.classes` from a function body still hit this fallback). A new `collect_classes_with_inline_static_block_calls` helper scans `hir.init` for the synthetic calls and dedupes them out of the late pass so we don't double-execute. Closes #2278; clears the residual gap entry in `test_gap_class_advanced.ts`. --- crates/perry-codegen/src/codegen/helpers.rs | 6 +- crates/perry-hir/src/lower/stmt.rs | 2 +- .../node-suite/object/class-static-block.ts | 59 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 test-parity/node-suite/object/class-static-block.ts 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);