Summary
{ ...src, k: v } where src carries an accessor SIGSEGVs (exit=139) under PERRY_GC_MOVING_LOOP_POLLS=1. Clean under the shipped default (polls off since #7161). A lighter variant of the same program does not crash but silently loses the copied accessor value 10 times in 400.
This is one of the residual sites of the #7154 class that #7184 and #7192 did not close, and it is 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: "z" });
return a.length;
}
const seen: any[] = [];
for (let r = 0; r < 400; r++) {
const src: any = { plain: 5 };
Object.defineProperty(src, "hot", { enumerable: true, get: function () { return churn(); } });
const out: any = { ...src, tail: 7 };
if (out.hot !== 600) seen.push(r + ":" + String(out.hot) + ":" + typeof out.hot);
}
console.log("bad", seen.length, seen.slice(0, 6).join(" "));
node --experimental-strip-types → bad 0
perry, default → bad 0
perry, PERRY_GC_MOVING_LOOP_POLLS=1 (compile AND run) → exit=139, no output
Without the string building in the loop (console.log("plain", bp, "hot", bh, "tail", bt) counting each field separately) it does not crash, and reports plain 0 hot 10 tail 0 — so the destination object is intact and it is specifically the accessor's copied value that is lost.
Reproduced identically at 73a9084ea (before #7184 and #7192), so it is inherited, not caused by either.
Where it is
Not Expr::ObjectSpread — that arm is reached only from a JSX spread attribute (crates/perry-hir/src/jsx.rs:67, its sole construction site). Since #809 an object literal containing a spread lowers to a source-ordered IIFE built on js_object_assign_one (crates/perry-hir/src/lower/expr_object.rs:844), and that is the path here.
The accumulator in Expr::ObjectAssign's lowering (crates/perry-codegen/src/expr/logical_collections.rs) is threaded through a bare SSA register across each lower_expr(ctx, src) and across each js_object_assign_one call:
let mut acc = ctx.block().call(DOUBLE, "js_object_assign_validate_target", &[(DOUBLE, &target_box)]);
for src in sources {
let src_box = lower_expr(ctx, src)?;
acc = ctx.block().call(DOUBLE, "js_object_assign_one", &[(DOUBLE, &acc), (DOUBLE, &src_box)]);
}
js_object_assign_one reads every own key of the source, so an accessor there runs arbitrary user code inside the helper, which under polls can reach a back-edge poll and an evacuating minor with the caller's frame live. That is the same shape #7192 fixed for Expr::Object/ObjectSpread/ClassExprFresh, applied to the arm the syntax actually takes.
The evidence that the object is fine but the accessor's value is not points one layer further in — at the runtime helper's own handling of the value the getter returned — so the fix may be on both sides. js_object_assign_one's RuntimeHandleScope usage is the place to look.
Why it matters
{ ...props } is ubiquitous. Until this is closed, PERRY_GC_MOVING_LOOP_POLLS=1 cannot become the default again, i.e. #7161 cannot be reverted and #7019's minor-GC RSS win stays reverted.
Refs #7154, #7161, #7192, #809.
Summary
{ ...src, k: v }wheresrccarries an accessor SIGSEGVs (exit=139) underPERRY_GC_MOVING_LOOP_POLLS=1. Clean under the shipped default (polls off since #7161). A lighter variant of the same program does not crash but silently loses the copied accessor value 10 times in 400.This is one of the residual sites of the #7154 class that #7184 and #7192 did not close, and it is a hard blocker for reverting #7161.
Reproducer
Without the string building in the loop (
console.log("plain", bp, "hot", bh, "tail", bt)counting each field separately) it does not crash, and reportsplain 0 hot 10 tail 0— so the destination object is intact and it is specifically the accessor's copied value that is lost.Reproduced identically at
73a9084ea(before #7184 and #7192), so it is inherited, not caused by either.Where it is
Not
Expr::ObjectSpread— that arm is reached only from a JSX spread attribute (crates/perry-hir/src/jsx.rs:67, its sole construction site). Since #809 an object literal containing a spread lowers to a source-ordered IIFE built onjs_object_assign_one(crates/perry-hir/src/lower/expr_object.rs:844), and that is the path here.The accumulator in
Expr::ObjectAssign's lowering (crates/perry-codegen/src/expr/logical_collections.rs) is threaded through a bare SSA register across eachlower_expr(ctx, src)and across eachjs_object_assign_onecall:js_object_assign_onereads every own key of the source, so an accessor there runs arbitrary user code inside the helper, which under polls can reach a back-edge poll and an evacuating minor with the caller's frame live. That is the same shape #7192 fixed forExpr::Object/ObjectSpread/ClassExprFresh, applied to the arm the syntax actually takes.The evidence that the object is fine but the accessor's value is not points one layer further in — at the runtime helper's own handling of the value the getter returned — so the fix may be on both sides.
js_object_assign_one'sRuntimeHandleScopeusage is the place to look.Why it matters
{ ...props }is ubiquitous. Until this is closed,PERRY_GC_MOVING_LOOP_POLLS=1cannot become the default again, i.e. #7161 cannot be reverted and #7019's minor-GC RSS win stays reverted.Refs #7154, #7161, #7192, #809.