Summary
A class expression carrying a static { … } block SIGSEGVs (exit=139) under PERRY_GC_MOVING_LOOP_POLLS=1 when the block's body allocates. Clean under the shipped default. A variant that captures an outer value does not crash but computes the wrong static value 4 times in 400.
This is a residual site of the #7154 class that #7192 did not close, and a hard blocker for reverting #7161.
Reproducer
function churn(): number {
const a: any[] = [];
for (let i = 0; i < 600; i++) a.push({ i: i, s: "q" });
return a.length;
}
function make(): any {
return class {
static k: number = 1;
static {
(this as any).viaBlock = churn();
}
};
}
let bad = 0;
for (let r = 0; r < 400; r++) {
const C: any = make();
if (C.k !== 1) bad++;
if (C.viaBlock !== 600) bad++;
}
console.log("bad", bad);
node --experimental-strip-types → bad 0
perry, default → bad 0
perry, PERRY_GC_MOVING_LOOP_POLLS=1 (compile AND run) → exit=139, no output
Reproduced identically at 73a9084ea, so inherited, not caused by #7184/#7192.
A capturing variant (make(tag) with churn() + tag) does not crash and reports viaBlock 4 wrong out of 400 — that one already pushed a root before #7198 via the !captured_args.is_empty() clause, which is the evidence that the class object is not the stale thing here.
Where it is
Not the class object. #7198 adds !block_fns.is_empty() to Expr::ClassExprFresh's protection predicate, and the emitted IR confirms the object is now temp-rooted at allocation and re-read before every use:
%r18 = call i64 @js_object_alloc(i32 3, i32 1)
call void @js_object_mark_class(i64 %r18)
call void @js_class_object_pin_parent(i64 %r18, i32 3)
%r19 = call i32 @js_gc_temp_root_push(i64 %r18)
...
%r33 = call i64 @js_gc_temp_root_get(i32 %r19) ; re-read before arming
%r35 = <nanbox %r33>
call void @js_static_this_arm_value(double %r35)
%r36 = call double @perry_static_..._init_0() ; the block body — user code
%r37 = call i64 @js_gc_temp_root_get(i32 %r19) ; re-read after
The crash survives that. What is still stale is the value js_static_this_arm_value parks in the runtime's one-shot static-this cell: the compiled block body reads it back through js_static_this_resolve, and if the object relocates between the arm and the resolve — or between the resolve and the body's this.x = … stores — the cell hands out a from-space address. That cell needs to be a registered, rewritten root (mark and rewrite, like RuntimeHandleScope at gc/roots/runtime_handles.rs:258-275), not a plain thread-local word.
crates/perry-runtime/src/object/... js_static_this_arm_value / js_static_this_resolve are the entry points.
Why it matters
Until this is closed, PERRY_GC_MOVING_LOOP_POLLS=1 cannot become the default again — #7161 cannot be reverted and #7019's minor-GC RSS win stays reverted.
Refs #7154, #7161, #7192, #685.
Summary
A class expression carrying a
static { … }block SIGSEGVs (exit=139) underPERRY_GC_MOVING_LOOP_POLLS=1when the block's body allocates. Clean under the shipped default. A variant that captures an outer value does not crash but computes the wrongstaticvalue 4 times in 400.This is a residual site of the #7154 class that #7192 did not close, and a hard blocker for reverting #7161.
Reproducer
Reproduced identically at
73a9084ea, so inherited, not caused by #7184/#7192.A capturing variant (
make(tag)withchurn() + tag) does not crash and reportsviaBlock 4wrong out of 400 — that one already pushed a root before #7198 via the!captured_args.is_empty()clause, which is the evidence that the class object is not the stale thing here.Where it is
Not the class object. #7198 adds
!block_fns.is_empty()toExpr::ClassExprFresh's protection predicate, and the emitted IR confirms the object is now temp-rooted at allocation and re-read before every use:The crash survives that. What is still stale is the value
js_static_this_arm_valueparks in the runtime's one-shot static-thiscell: the compiled block body reads it back throughjs_static_this_resolve, and if the object relocates between the arm and the resolve — or between the resolve and the body'sthis.x = …stores — the cell hands out a from-space address. That cell needs to be a registered, rewritten root (mark and rewrite, likeRuntimeHandleScopeatgc/roots/runtime_handles.rs:258-275), not a plain thread-local word.crates/perry-runtime/src/object/...js_static_this_arm_value/js_static_this_resolveare the entry points.Why it matters
Until this is closed,
PERRY_GC_MOVING_LOOP_POLLS=1cannot become the default again — #7161 cannot be reverted and #7019's minor-GC RSS win stays reverted.Refs #7154, #7161, #7192, #685.