Skip to content

fix(hir): class static blocks run at class-decl source position#2283

Merged
proggeramlug merged 1 commit into
mainfrom
fix-static-block-source-order
May 28, 2026
Merged

fix(hir): class static blocks run at class-decl source position#2283
proggeramlug merged 1 commit into
mainfrom
fix-static-block-source-order

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

A class static { ... } block runs as part of class evaluation in source order per ES spec — right after that class's static-field initializers, BEFORE any code that follows the class declaration. Perry hoists classes, so the codegen-side init_static_fields_late loop ran every block at the end of module init, after all user top-level statements had executed.

So:

class A { static { A.flag = true; } }
console.log(A.flag);    // node: true   perry: false (block ran LAST)

Fix: in lower/stmt.rs when walking a class decl in the module body, push a StaticMethodCall for each synthetic __perry_static_init_N method right after that class's static-field-init stmts. The block now runs at the spec-correct point in module.init. The codegen fallback in init_static_fields_late is kept (skipping blocks already invoked via the init stream) so class expressions that bypass the stmt-decl path still get their blocks invoked.

Repros (after fix, byte-identical with Node)

$ ./perry compile static.ts -o /tmp/s && /tmp/s
(1) initialized: true
(2) A.n: 10
(3) D.x: 1 D.y: 2
(4) C.both: 30

Where static.ts is the new test-files/test_gap_class_static_blocks.ts covering: declared-only static field set in a block, source-order ordering against static x = 0, multiple blocks in one class, cross-class read.

Scope

  • crates/perry-hir/src/lower/stmt.rs: inline StaticMethodCall per __perry_static_init_* after the class's static-field-inits.
  • crates/perry-codegen/src/codegen/helpers.rs: in init_static_fields_late, skip blocks already invoked via the init stream (detected by a top-level Expr(StaticMethodCall { class, method, .. }) matching the synthetic name).
  • test-files/test_gap_class_static_blocks.ts: new parity test (above).
  • test-parity/known_failures.json: drop test_gap_class_advanced — its static block initialized: true diff was the only divergence and is now byte-identical with Node.

Test plan

  • cargo fmt --all -- --check
  • cargo test --release -p perry-codegen -p perry-hir
  • diff <(node --experimental-strip-types test_gap_class_static_blocks.ts) <(./perry compile ... && /tmp/out) — empty
  • diff <(node --experimental-strip-types test_gap_class_advanced.ts) <(./perry compile ... && /tmp/out) — empty (was the static block initialized line before fix)

Per ES spec, a `class { static { ... } }` block runs as part of class
evaluation in source order — right after that class's static-field
initializers and BEFORE any code that follows the class declaration.

Perry hoists class declarations to a separate `module.classes` list,
so the codegen-side machinery only knew the class set, not where each
class appeared in source. The old `init_static_fields_late` loop ran
every static block at the *end* of module init, after all user
top-level statements had executed. So:

  class A { static { A.flag = true; } }
  console.log(A.flag);    // node: true   perry: false (block runs LAST)

The fix lifts static-block invocation into HIR: when `lower_stmt` walks
a `class` decl in the module body, it pushes a `StaticMethodCall` for
each synthetic `__perry_static_init_N` method right after that class's
static-field-init stmts. The block now runs at the spec-correct point
in `module.init`. The codegen fallback loop in
`init_static_fields_late` is kept (skip when the init stream already
calls the block) so class expressions that bypass the stmt-decl path
still get their blocks invoked.

- `crates/perry-hir/src/lower/stmt.rs`: inline `StaticMethodCall` for
    each `__perry_static_init_*` after the class's static-field-inits.
- `crates/perry-codegen/src/codegen/helpers.rs`: in
    `init_static_fields_late`, skip blocks already invoked via the
    init stream (detected by a top-level `Expr(StaticMethodCall { ... })`
    matching the synthetic name).
- `test-files/test_gap_class_static_blocks.ts`: new parity test —
    declared-only static field set in a block, source-order ordering
    against `static x = 0`, multiple blocks in one class, cross-class
    read.
- `test-parity/known_failures.json`: drop `test_gap_class_advanced`;
    its `static block initialized: true` diff was the only divergence
    and is now byte-identical with Node.
@proggeramlug proggeramlug force-pushed the fix-static-block-source-order branch from 8b6a9bf to 8fdef9e Compare May 28, 2026 18:47
@proggeramlug
Copy link
Copy Markdown
Contributor Author

Note: rebased against main, which now pins this exact bug as #2278 (the static-block sub-case bisected out of #1635 by #2279). This PR closes #2278.

@proggeramlug proggeramlug merged commit a7eb211 into main May 28, 2026
11 checks passed
@proggeramlug proggeramlug deleted the fix-static-block-source-order branch May 28, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant