Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_") {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/lower/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
59 changes: 59 additions & 0 deletions test-parity/node-suite/object/class-static-block.ts
Original file line number Diff line number Diff line change
@@ -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);