Skip to content

fix(codegen): sort closure-source emission so the same input compiles to the same IR (#7038) - #7039

Merged
proggeramlug merged 2 commits into
mainfrom
fix/7038-codegen-nondeterminism
Jul 30, 2026
Merged

fix(codegen): sort closure-source emission so the same input compiles to the same IR (#7038)#7039
proggeramlug merged 2 commits into
mainfrom
fix/7038-codegen-nondeterminism

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #7038.

What

emit_artifacts (codegen/artifacts.rs) iterated hir.closure_source_text — a HashMap — when emitting retained closure-source constants. The @.str.N numbering of those constants was therefore a per-process permutation: the same input compiled to different LLVM IR on every run.

The loop immediately above it walks hir.functions (a Vec) and was already deterministic; only the materialized-closure loop keyed off map iteration order. The fix collects, filters, sorts by FuncId, then emits. Emission order is the only thing that changes — the set of emitted constants and every other instruction is untouched.

Why it is not cosmetic

It silently invalidates any A/B that compares raw LLVM IR — a technique several representation-selection and GC investigations have relied on for evidence.

PR #7037 hit it head-on. Its first proof that --opt-report was observational was the obvious one: compile with the flag off, compile with it on, diff the .ll. That passed, and was not evidence — two runs can match by luck or differ for reasons unrelated to the change. It had to be redone with four runs per arm plus a content-canonicalising normalizer.

It also matters for what comes next: the planned codegen-perturbation measurement (how many hot loops lose native regions to rooting calls) works by comparing emitted IR. That measurement is unsound while codegen is nondeterministic.

Evidence

6 compiles of one file (arrow functions in map/filter/reduce/sort, so several materialized closures), md5 of the concatenated per-module .ll, PERRY_NO_AUTO_OPTIMIZE=1, same binary and same source in each arm:

arm distinct hashes across 6 runs
pristine main 5
this fix 1

Falsifiable in both directions: if the diagnosis were wrong, the fixed arm would still show multiple hashes; if the file exercised no materialized closures, the unfixed arm would show one.

Scope

One function, one loop. No behaviour change to emitted code — only the order in which two string constants are written.

Note for reviewers: lint on this branch

cargo fmt --all -- --check fails on four files this PR does not touch, all pre-existing on main at b5dbf3f63:

crates/perry-codegen/src/lower_call/native/mod.rs:409
crates/perry-runtime/src/object/global_this/install_static.rs:899
crates/perry/src/commands/check.rs:783
crates/perry/src/commands/deps.rs:819

They are deliberately not fixed here — that is a separate janitorial change and folding it in would obscure a one-loop diff. The file this PR touches is rustfmt --check clean.

lint is in any case already red repo-wide on the stale public benchmark baseline, which fails before the fmt step.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed inconsistent generated LLVM IR across repeated compilations.
    • Stabilized string-constant numbering to ensure identical inputs produce reproducible output.
    • Improved reliability of raw IR comparisons and optimization reports.

Ralph Küpper added 2 commits July 30, 2026 06:15
… to the same IR

emit_artifacts iterated hir.closure_source_text -- a HashMap -- when emitting
retained closure-source constants, so the @.str.N numbering of those constants
was a per-process permutation. Same input, different .ll on every run.

The loop immediately above walks hir.functions (a Vec) and was already
deterministic; only the materialized-closure loop keyed off map order. Collect,
filter, sort by FuncId, then emit. Emission order is the only thing that changes.

Why it matters beyond tidiness: it silently invalidates any A/B that compares
raw LLVM IR. That technique underpins the planned codegen-perturbation
measurement, and #7037 hit it directly -- its first observational-equivalence
proof passed on nondeterministic output and had to be redone with a normalizer
and four runs per arm.

Evidence -- 6 compiles of one file, md5 of the concatenated per-module .ll:

  pristine main   5 distinct hashes across 6 runs
  this fix        1 hash across 6 runs

Reported by the #7037 agent while proving --opt-report observational.
Closes #7038.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

LLVM artifact generation now sorts retained closure-source entries by FuncId before emission, stabilizing string-constant numbering and registration order. A changelog entry documents the nondeterminism fix and observed hash results.

Changes

Codegen determinism

Layer / File(s) Summary
Sort closure sources before emission
crates/perry-codegen/src/codegen/artifacts.rs, changelog.d/7039-codegen-determinism.md
Closure source entries are filtered, sorted by FuncId, and emitted deterministically; the changelog records the resulting LLVM IR stability.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: andrewtdiz, thehypnoo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not follow the required Summary/Changes/Related issue/Test plan/Checklist template. Rewrite the PR description using the repository template and fill in Summary, Changes, Related issue, Test plan, and Checklist sections.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: making closure-source emission deterministic for identical IR output.
Linked Issues check ✅ Passed The code sorts closure-source entries by FuncId before emission, directly fixing the HashMap iteration nondeterminism in #7038.
Out of Scope Changes check ✅ Passed The changes stay focused on deterministic codegen plus a matching changelog note, with no obvious unrelated edits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7038-codegen-nondeterminism

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/perry-codegen/src/codegen/artifacts.rs (1)

1861-1877: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a separate-process determinism regression test.

The sorting fix is correct, but the PR objective calls for an automated check that invokes separate compiler processes and compares byte-identical output. The six-run measurement in the changelog is not an ongoing regression guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/codegen/artifacts.rs` around lines 1861 - 1877, Add
a regression test covering the materialized closure emission path around
materialized_closure_sources that invokes the compiler in separate processes
multiple times with identical input, captures the generated output, and asserts
the outputs are byte-for-byte identical. Keep the test focused on cross-process
determinism so it guards the FuncId-based sorting fix rather than only repeated
execution within one process.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changelog.d/7039-codegen-determinism.md`:
- Around line 1-4: Update the changelog entry’s nondeterminism statement to use
probabilistic wording: replace the claim that the same input produces different
IR on every run with wording that it can produce different IR across runs.
Preserve the explanation about HashMap iteration and @.str.N numbering.

---

Nitpick comments:
In `@crates/perry-codegen/src/codegen/artifacts.rs`:
- Around line 1861-1877: Add a regression test covering the materialized closure
emission path around materialized_closure_sources that invokes the compiler in
separate processes multiple times with identical input, captures the generated
output, and asserts the outputs are byte-for-byte identical. Keep the test
focused on cross-process determinism so it guards the FuncId-based sorting fix
rather than only repeated execution within one process.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc809ffb-63ae-4a48-9873-0a859694731e

📥 Commits

Reviewing files that changed from the base of the PR and between b5dbf3f and 8afa08a.

📒 Files selected for processing (2)
  • changelog.d/7039-codegen-determinism.md
  • crates/perry-codegen/src/codegen/artifacts.rs

Comment on lines +1 to +4
Fixed non-deterministic LLVM IR: `emit_artifacts` iterated `hir.closure_source_text`
(a `HashMap`) when emitting retained closure-source constants, so the `@.str.N`
numbering of those constants was a per-process permutation — the same input
compiled to different IR on every run.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use probabilistic wording for the nondeterminism claim.

HashMap iteration can vary across processes, but independent runs may still produce the same order. Replace “different IR on every run” with “can produce different IR across runs.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.d/7039-codegen-determinism.md` around lines 1 - 4, Update the
changelog entry’s nondeterminism statement to use probabilistic wording: replace
the claim that the same input produces different IR on every run with wording
that it can produce different IR across runs. Preserve the explanation about
HashMap iteration and @.str.N numbering.

@proggeramlug
proggeramlug merged commit 3a130dd into main Jul 30, 2026
27 of 33 checks passed
@proggeramlug
proggeramlug deleted the fix/7038-codegen-nondeterminism branch July 30, 2026 04:23
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.

codegen: same input compiles to different LLVM IR run-to-run — HashMap iteration order in closure-source registration breaks reproducible builds

1 participant