Summary
test_gap_gc_assign_string_source_rooting is improved but not green by #7217's fix: bad char 3 count 3 → bad char 1 count 1 on the allocation-point arm, both 10/10 deterministic on the same build lineage. Two of the three bad iterations were the lazy globalThis bootstrap #7217 closed. The residual one is a codegen-side stale operand in the test's own assertion, not in Object.assign at all.
So the ten assign_string_source entries in test-parity/gc_repsel_triage.txt are retargeted here, not deleted — and their stated cause ("allocation-point relocation inside js_object_assign_one / object_assign_string_source") is now known to be wrong.
Reproduction
export PERRY_RUNTIME_DIR=<build>/perry-dev PERRY_NO_AUTO_OPTIMIZE=1
perry test-files/test_gap_gc_assign_string_source_rooting.ts -o /tmp/asrc # no compile-time env
PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off /tmp/asrc
# bad char 1 count 1 (oracle: bad char 0 count 0)
| build |
result, 10/10 |
merge-base 8b024958f |
bad char 3 count 3 |
| with #7217's no-move bootstrap window |
bad char 1 count 1 |
The site, named by #7196's quarantine
[gc-fromspace-protect] FAULT: signal 10 at 0x5484173ffac
block=0x54841640000 +1048492 retired_bytes=1048512 retired_by_minor=#2
last-known object: user_ptr=0x5484173ffa8 obj_type=3 size=32
2 js_jsvalue_equals + 332
3 js_eq + 20
4 main + 1028
obj_type=3 is a string; retired_by_minor=#2 (not #0), which is why it survived the bootstrap fix.
Why
The failing line is the test's own comparison:
if (got !== ALPHA[i % 26]) { badChar++; break; }
got (a copied property read) is lowered first; ALPHA[i % 26] is a string index read, which allocates a fresh one-character string. So the left operand sits in a register across its sibling's allocating lowering and js_eq hands js_jsvalue_equals a from-space StringHeader*, which it then dereferences to compare bytes.
The emitted IR says it without inference (--trace llvm, no compile-time GC env):
%r136 = load double, ptr %r97 ; `got` -- loaded ABOVE
...
%r149 = call double @js_string_index_get_boxed(double %r137, double %r148) ; ALLOCATES
%r150 = bitcast double %r136 to i64 ; the STALE register
%r151 = bitcast double %r149 to i64
%r152 = call i64 @js_eq(i64 %r150, i64 %r151)
%r97 may well be a shadow-bound alloca that the collector rewrites -- but %r136 is a register taken before the collection point and it is never re-read. That is the third property a root must buy (#7207's words: liveness, a rewritten location, and the value the call actually observed), and only the first two are present.
That is exactly the family #7206 (a call receiver and a computed-read base across their sibling operands) and #7214 (the callee, this and every argument of js_closure_callN) closed for other call shapes. js_eq / js_strict_eq / the relational operators take two lowered operands and were apparently not swept in either pass: guard_store_operand_across / RootedOperands::reread exist for precisely this and are not applied here.
Why it matters beyond the witness
a !== b where either side allocates is ordinary JS. The reason this is only visible on the allocation-point route is that on the safepoint route the sibling's allocation is not a collection point — which is the #7217 route argument, restated for a codegen site rather than a runtime one.
Suggested shape
Sweep the binary-operator lowerings (js_eq, js_strict_eq, js_lt/le/gt/ge, js_add where it can be a string concat) with the same guard_store_operand_across treatment #7206 gave the call receiver: root the first-lowered operand whenever the second's lowering collects, and re-read it below. A codegen regression test in the #7206/#7214 style (assert the emitted IR re-reads the left operand below the sibling's allocating call) is the gate.
Refs #7217, #7216, #7206, #7214, #7196.
Summary
test_gap_gc_assign_string_source_rootingis improved but not green by #7217's fix:bad char 3 count 3→bad char 1 count 1on the allocation-point arm, both 10/10 deterministic on the same build lineage. Two of the three bad iterations were the lazyglobalThisbootstrap #7217 closed. The residual one is a codegen-side stale operand in the test's own assertion, not inObject.assignat all.So the ten
assign_string_sourceentries intest-parity/gc_repsel_triage.txtare retargeted here, not deleted — and their stated cause ("allocation-point relocation insidejs_object_assign_one/object_assign_string_source") is now known to be wrong.Reproduction
8b024958fbad char 3 count 3bad char 1 count 1The site, named by #7196's quarantine
obj_type=3is a string;retired_by_minor=#2(not #0), which is why it survived the bootstrap fix.Why
The failing line is the test's own comparison:
got(a copied property read) is lowered first;ALPHA[i % 26]is a string index read, which allocates a fresh one-character string. So the left operand sits in a register across its sibling's allocating lowering andjs_eqhandsjs_jsvalue_equalsa from-spaceStringHeader*, which it then dereferences to compare bytes.The emitted IR says it without inference (
--trace llvm, no compile-time GC env):%r97may well be a shadow-bound alloca that the collector rewrites -- but%r136is a register taken before the collection point and it is never re-read. That is the third property a root must buy (#7207's words: liveness, a rewritten location, and the value the call actually observed), and only the first two are present.That is exactly the family #7206 (
a call receiver and a computed-read base across their sibling operands) and #7214 (the callee, this and every argument of js_closure_callN) closed for other call shapes.js_eq/js_strict_eq/ the relational operators take two lowered operands and were apparently not swept in either pass:guard_store_operand_across/RootedOperands::rereadexist for precisely this and are not applied here.Why it matters beyond the witness
a !== bwhere either side allocates is ordinary JS. The reason this is only visible on the allocation-point route is that on the safepoint route the sibling's allocation is not a collection point — which is the #7217 route argument, restated for a codegen site rather than a runtime one.Suggested shape
Sweep the binary-operator lowerings (
js_eq,js_strict_eq,js_lt/le/gt/ge,js_addwhere it can be a string concat) with the sameguard_store_operand_acrosstreatment #7206 gave the call receiver: root the first-lowered operand whenever the second's loweringcollects, and re-read it below. A codegen regression test in the #7206/#7214 style (assert the emitted IR re-reads the left operand below the sibling's allocating call) is the gate.Refs #7217, #7216, #7206, #7214, #7196.