Skip to content

codegen: js_eq's left operand is a stale register across its sibling's allocating lowering (residual #7217 allocation-point witness) #7248

Description

@proggeramlug

Summary

test_gap_gc_assign_string_source_rooting is improved but not green by #7217's fix: bad char 3 count 3bad 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions