Skip to content

fix(codegen): per-process temp object paths, and make emission determinism a hard gate (#7131) - #7140

Open
proggeramlug wants to merge 8 commits into
mainfrom
fix/7131-linux-determinism-gate
Open

fix(codegen): per-process temp object paths, and make emission determinism a hard gate (#7131)#7140
proggeramlug wants to merge 8 commits into
mainfrom
fix/7131-linux-determinism-gate

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes the loop on #7131. #7135 fixed the cause but its Linux verification box
was never ticked, and the workaround the defect forced on the census gate was
still in place. Verifying it on the Pi found a second defect that #7135 shipped.

1. What was embedded where

Measured, not assumed (aarch64 Debian clang 19.1.7, ELF, no -g):

name recorded in the .o?
.ll basename yesSTT_FILE in .symtab
.ll directory, process CWD no (needs DWARF, i.e. -g)
-o output path no
ld -r input / output paths no

So the .ll name must be a pure function of the IR — that is #7131 — and
uniquifiers are free on every output name. The table is now a comment on
TEMP_NONCE_COUNTER, because both halves have now been got wrong once.

On Mach-O the .ll basename is not recorded at all (verified directly, not
inferred): that is why macOS looked clean while carrying the same defect, and
why a macOS-only reviewer could not tell a fixed compiler from a broken one.

2. A regression #7135 shipped, found by verifying it

#7135 content-addressed both temp names. The .o did not need it and lost the
pid it used to carry. TEMP_NONCE_COUNTER is per-process state that every
process starts at 0, so two perry processes compiling identical IR both chose
perry_llvm_<hash>_0.o — and compile_ll_to_object deletes the object once it
has read it.

Failed to read clang output at /tmp/perry_llvm_eee31bbdd9dc24a5_0.o:
No such file or directory (os error 2)

Four concurrent compiles of one census fixture:

host a3b31c0d8 (main) this branch
macOS arm64 8 / 12 failed 0 / 12
Raspberry Pi 5, aarch64 Linux 3 / 16 failed 0 / 16

This is #509 again, one scope out. It breaks any parallel build of the same
input — including every A/B harness in scripts/compiler_output_harness/. Fixed
by putting the pid back on the .o and on the atomic-write staging .tmp,
which had the same defect. The .ll stays content-addressed.

It was found because the new check runs its repeats concurrently: identical
IR now shares one .ll, so racing it is part of the subject. A serial check
never opens the window — and indeed the Pi's serial arms were all green.

3. Red then green

Three arms on a Raspberry Pi 5, built sequentially from one target dir (same
toolchain by construction), three distinct binary hashes:

arm census-determinism, 26 workloads
22367565f (before #7135) exit 1 — 26/26 nondeterministic
a3b31c0d8 (#7135 as merged) exit 0 — 26/26 identical
this branch exit 0 — 26/26 identical (also --repeat 3 --jobs 3)

suite_01_startup twice, serially, with the mechanism visible:

pre : objects DIFFER in 12 bytes
      STT_FILE  perry_llvm_217502_1785528949373123236_0.ll
      STT_FILE  perry_llvm_217533_1785528951945773193_0.ll
post: objects IDENTICAL
      STT_FILE  perry_llvm_2791e842224ea99c.ll   (both runs)

macOS: 26/26 workloads x 3 compiles -> one hash each. The #7039
closure-iteration-order fix has not regressed.

4. No behavioural change

Stronger than an oracle diff, and available because of the very instrument this
PR is about: across all 26 census workloads the objects emitted by a3b31c0d8
and by this branch are byte-identical, 0/26 differences. This change renames
temp files and nothing else, and the emitted bytes say so.

Why that is the better instrument here, not a substitute for one: an oracle
diff can only observe behaviour the corpus happens to exercise, whereas
identical object bytes leave no room for a behavioural difference at all. The
property is also true by construction — this change alters only the names of
files in the temp directory, and section 1 measures that output names are
recorded nowhere in the object.

And the oracle diff, on the Pi against the pinned Node 26.5.1:

scripts/gc_repsel_matrix.sh --arms all --pressure 8
summary: PASS=426 UNVER=119 XFAIL=1 FAIL=0    (546 rows, 21 arms x 26 tests, 1567 s)

FAIL=0 — the acceptance criterion (the script exits non-zero iff FAIL != 0).
The 119 UNVER are arms that were inert on that row: the corpus did not make
them collect or move anything, which the matrix reports as not-green by design
(#6942/#6946/#6950) rather than counting as a pass. That ratio is a property of
the corpus, not of this change. The Pi needed node 26.5.1 installed
user-locally, because the script refuses an unpinned oracle — correctly.

5. The payoff: the workaround is gone

census-knob-isolation detected a nondeterministic host and skipped its
emission half — so the half that caught the PERRY_CANONICAL_STR_LOCALS leak
could not run on Linux at all, the host where object-hash A/B is most useful (it
is the one with an unprivileged instruction-retired counter). Removed:

  • a determinism disagreement is now a hard failure, on every host, before any
    knob is judged;
  • --require-emission is deleted — it selected between a real check and a skip,
    and only one of those is still a decision worth having;
  • census-determinism (new) is the standalone instrument: establish that
    object-hash evidence is valid on the host you are measuring on before trusting
    a day of A/B. The whole corpus twice is 7 s on an M1 with an optimized
    compiler, so CI runs the full 26 rather than a subset — cheap enough that
    there is no reason to weaken it;
  • both run on the ubuntu-latest repsel-census job — the ELF host that can
    actually observe a relapse — alongside both verdict self-tests.

The gate's subject is live on the CI host's architecture, not just on the Pi:
compiling one .ll under two different basenames produces different objects for
--target x86_64-unknown-linux-gnu as well as aarch64, with the basename
visible in strings. The embedding is a property of the ELF writer, not of the
target arch — so ubuntu-latest (x86_64) can see the defect if it returns.

Full census-knob-isolation re-run on macOS with these changes: OK, emission
half exercised on every knob.

6. Sabotage-verified, both directions

Every check here was made to fail on purpose before being believed:

sabotage result
drop the pid from the .o name (the #7135 shape) object_temp_name... RED
drop the pid from the staging .tmp name object_temp_name... RED
put the pid into the .ll name (re-break #7131) both .ll tests RED
restore the Linux skip (nondeterminism -> exit 0) knob-isolation self-test RED
compare only the first two observations determinism self-test RED
accept a single observation as proof determinism self-test RED

7. What I could not verify, and other findings

  • The matrix ran on the Pi, not the Mac. The Mac fell to 8.8 GB free, under
    the floor I was given, and a disk-full sweep corrupts every other agent's run
    on that box. The Pi needed node 26.5.1 installed user-locally
    (~/node-26.5.1, PATH-scoped) because its system node is v22 — the matrix
    refuses to run against an unpinned oracle, correctly.
  • main is red independently of this PR, and has been for five nights.
    The nightly Tests workflow fails on compiler-output-regression,
    api-docs-drift, cargo-test, lint and both Warnings jobs. lint's
    reported failure is unclassified workspace crates: perry-ext-node-forge,
    unrelated to anything here. Separately and verifiably, origin/main's
    crates/perry-codegen/src/linker.rs does not pass rustfmt --checkfix(codegen): content-address LLVM temp IR names (#7131) #7135
    landed it unformatted — and scripts/check_file_size.sh lists nine over-cap
    files, none of them touched here. I fixed the formatting of the file I am
    editing; the rest is not mine to smuggle into this PR. Judge this PR's CI
    against main's, not against green.
  • A one-version stale-cache window exists and closes itself. fix(codegen): content-address LLVM temp IR names (#7131) #7135 changed
    the content of emitted objects without a version bump (both 22367565f and
    a3b31c0d8 are 0.5.1265), and the build cache keys on perry_version, so a
    cache populated by a pre-fix compiler still validates against a post-fix one at
    the same version. The census never sees it (--no-cache plus
    PERRY_NO_AUTO_OPTIMIZE=1), and the maintainer's bump at merge invalidates it.
    No cache-key change here: nothing in this PR alters cache identity in either
    direction.
  • Filed codegen: content-addressed temp .ll files are never deleted (~1 GB on a dev box) #7144: content-addressed .ll files are never deleted. fix(codegen): content-address LLVM temp IR names (#7131) #7135 stopped
    unlinking them (a sibling worker may share the path), so the temp dir now
    accumulates one file per distinct IR ever compiled — measured 1627 files /
    951.8 MB on one dev box after a day. Not a correctness bug, and it deserves its
    own change rather than riding on a determinism fix.
  • Not re-run: the full gap/parity suite. This change cannot reach it — the
    byte-identical-objects result in section 4 is the direct evidence.
  • Method deviation, stated: the three Pi arms were built sequentially from
    one
    CARGO_TARGET_DIR, not one per arm — the pattern the repo's own
    build_arms.sh uses, which makes the rustc/LLVM toolchain identical across
    arms by construction. The failure mode that costs (cargo not rebuilding, so
    two "arms" are the same binary) is caught directly: all three perry hashes
    differ, and each arm's artifacts were copied out with mtimes after its own
    build started. Package set was -p perry -p perry-runtime-static -p perry-stdlib-static on every hop.
  • Arms were built with --profile perry-dev, not --release. The property under
    test is the compiler's output, which the compiler's own optimization level
    does not reach; --release on a contended 4-core Pi shared with another agent
    was not a good trade.

Ralph Küpper added 2 commits July 31, 2026 21:47
)

The knob-isolation gate detected a nondeterministic host and SKIPPED its
emission half rather than reporting phantom diffs. That workaround existed
because objects were nondeterministic on ELF (#7131); #7135 fixed the cause,
so the skip now hides a regression instead of a known defect.

- nondeterminism is a hard failure, on every host, before any knob is judged
- --require-emission removed: it selected between a real check and a skip,
  and only one of those is still a decision worth having
- new census-determinism subcommand + self-test: the standalone instrument
  the issue asks for, so any agent can establish object-hash validity on
  the host it is measuring on
…x caveat (#7131)

- linker.rs: a measured table of which paths clang/ld actually record in an
  ELF object (only the .ll BASENAME does), so a future temp-path change can
  be reviewed without re-deriving it, plus the PERRY_DEBUG_SYMBOLS caveat
- census README: replace 'it is not deterministic on aarch64 Linux' with the
  check that establishes whether it is
- CI: run the determinism check on the ubuntu-latest repsel-census job — the
  ELF host that can actually observe a relapse — and both verdict self-tests
- correct the two unreleased changelog fragments that still describe the skip
  and the removed --require-emission flag
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@proggeramlug, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7db4ea9d-4c83-4f35-9d10-080d426e29b6

📥 Commits

Reviewing files that changed from the base of the PR and between a3b31c0 and 1692963.

📒 Files selected for processing (9)
  • .github/workflows/test.yml
  • benchmarks/repsel_census/README.md
  • changelog.d/7133-repsel-knob-isolation.md
  • changelog.d/7135-deterministic-llvm-temp-names.md
  • changelog.d/7140-linux-emission-determinism.md
  • crates/perry-codegen/src/linker.rs
  • scripts/compiler_output_harness/cli.py
  • scripts/compiler_output_harness/repsel_determinism.py
  • scripts/compiler_output_harness/repsel_knob_isolation.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Ralph Küpper added 5 commits July 31, 2026 21:58
…path (#7131)

#7135 content-addressed both temp names. The .ll had to become a pure
function of the IR — clang records a unit's source basename into the ELF
object, which was #7131. The .o did not, and lost the pid it used to carry.

TEMP_NONCE_COUNTER is per-process state and every process starts it at 0, so
two 'perry' processes compiling identical IR both chose perry_llvm_<hash>_0.o.
compile_ll_to_object deletes the object once it has read it, so they deleted
it out from under each other. Measured on macOS at a3b31c0: 8 of 12
concurrent same-source compiles failed with

  Failed to read clang output at .../perry_llvm_eee31bbdd9dc24a5_0.o

This is #509 again, one scope out — the pid that used to prevent it was
removed as collateral.

The output path is recorded nowhere (measured: clang records only the .ll
BASENAME, as an STT_FILE symbol; not the directory, not the CWD, not -o, and
ld -r records neither its inputs nor its output), so uniquifiers are free on
the .o and mandatory. Same for the atomic-write staging .tmp, which two
processes also reached with the same hash and the same counter.

cargo fmt: linker.rs was left unformatted by #7135, so lint is red on main.
…surements disproved

- knob-isolation and the determinism check had identical private _digest
  implementations; share one so 'the objects are the same' cannot drift
- the module docstring said output names 'may keep their uniquifying counter'.
  The counter alone is not enough — it is per-process state every process
  starts at 0 — which is the collision this check then found
- Mach-O does not keep the .ll basename in the debug map, it does not record
  it at all (verified directly); the byte delta is ~10, measured 12
The 'debug map' explanation came from the issue and is not what I observed:
Mach-O does not record the .ll basename in the .o at all (no -g, no debug
map). Also the real byte delta and the concurrency note.
@proggeramlug
proggeramlug marked this pull request as ready for review July 31, 2026 20:30
@proggeramlug proggeramlug changed the title test(repsel): verify #7131 on Linux and make emission determinism a hard gate fix(codegen): per-process temp object paths, and make emission determinism a hard gate (#7131) Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant