fix(hir): class static blocks run at class-decl source position#2283
Merged
Conversation
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.
8b6a9bf to
8fdef9e
Compare
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-sideinit_static_fields_lateloop ran every block at the end of module init, after all user top-level statements had executed.So:
Fix: in
lower/stmt.rswhen walking aclassdecl in the module body, push aStaticMethodCallfor each synthetic__perry_static_init_Nmethod right after that class's static-field-init stmts. The block now runs at the spec-correct point inmodule.init. The codegen fallback ininit_static_fields_lateis 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)
Where
static.tsis the newtest-files/test_gap_class_static_blocks.tscovering: declared-only static field set in a block, source-order ordering againststatic x = 0, multiple blocks in one class, cross-class read.Scope
crates/perry-hir/src/lower/stmt.rs: inlineStaticMethodCallper__perry_static_init_*after the class's static-field-inits.crates/perry-codegen/src/codegen/helpers.rs: ininit_static_fields_late, skip blocks already invoked via the init stream (detected by a top-levelExpr(StaticMethodCall { class, method, .. })matching the synthetic name).test-files/test_gap_class_static_blocks.ts: new parity test (above).test-parity/known_failures.json: droptest_gap_class_advanced— itsstatic block initialized: truediff was the only divergence and is now byte-identical with Node.Test plan
cargo fmt --all -- --checkcargo test --release -p perry-codegen -p perry-hirdiff <(node --experimental-strip-types test_gap_class_static_blocks.ts) <(./perry compile ... && /tmp/out)— emptydiff <(node --experimental-strip-types test_gap_class_advanced.ts) <(./perry compile ... && /tmp/out)— empty (was thestatic block initializedline before fix)