Skip to content

Emitted-IR nondeterminism: js_register_function_name order flips run-to-run (rayon completion order) #7303

Description

@proggeramlug

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.

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