Found while proving #7301's default path unchanged (object-level branch-vs-merge-base comparison); pre-existing at 9cb31f1b0, unrelated to the in-process backend.
Symptom: the order of adjacent js_register_function_name(ptr @perry_closure_..., ptr @.str.N, ...) registration calls in module init flips between two runs of the same perry binary on the same input. The @.str.N numbering follows first-use, so the string-constant numbering swaps with it. Everything else in the emitted .ll is byte-stable.
Repro (macOS/arm64, perry-dev build, observed on ~1-in-3 runs):
for i in 1 2 3 4; do
rm -rf node_modules # cold object cache each run
mkdir -p ll_$i
PERRY_SAVE_LL=$PWD/ll_$i PERRY_NO_AUTO_OPTIMIZE=1 \
perry test-files/test_gap_1840_class_iterator_for_of_spread.ts -o /dev/null
done
cmp ll_1/*.ll ll_2/*.ll # flips within ~4 runs
Also reproduces on test_gap_yield_star_iterable.ts. Two closures whose registrations swap are always a pair with equal-length names.
Suspected mechanism: registration entries are collected in rayon completion order (per-closure codegen races), not declaration order — consistent with it being timing-dependent yet stable within most consecutive runs. Not a HashMap RandomState effect (that would flip near-every run).
Why it matters:
Suggested fix shape: sort the registration list by closure symbol name (or emit in declaration order) before rendering module init — a one-point sort at the collection site.
Found while proving #7301's default path unchanged (object-level branch-vs-merge-base comparison); pre-existing at
9cb31f1b0, unrelated to the in-process backend.Symptom: the order of adjacent
js_register_function_name(ptr @perry_closure_..., ptr @.str.N, ...)registration calls in module init flips between two runs of the same perry binary on the same input. The@.str.Nnumbering follows first-use, so the string-constant numbering swaps with it. Everything else in the emitted.llis byte-stable.Repro (macOS/arm64, perry-dev build, observed on ~1-in-3 runs):
Also reproduces on
test_gap_yield_star_iterable.ts. Two closures whose registrations swap are always a pair with equal-length names.Suspected mechanism: registration entries are collected in rayon completion order (per-closure codegen races), not declaration order — consistent with it being timing-dependent yet stable within most consecutive runs. Not a
HashMapRandomState effect (that would flip near-every run).Why it matters:
Suggested fix shape: sort the registration list by closure symbol name (or emit in declaration order) before rendering module init — a one-point sort at the collection site.