From ed8e47facc4c2de0d16d66100834d0b514e04c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 06:15:31 +0200 Subject: [PATCH 1/2] fix(codegen): sort closure-source emission so the same input compiles 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. --- crates/perry-codegen/src/codegen/artifacts.rs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index 015fadd153..865230483c 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1858,10 +1858,23 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { } } } - for (func_id, src) in &hir.closure_source_text { - if registered_fn_ids.contains(func_id) || !materialized_closure_ids.contains(func_id) { - continue; - } + // Sorted, NOT raw `HashMap` iteration (#7038). The loop above walks + // `hir.functions` (a `Vec`) and is already deterministic; this one keyed off + // the map's iteration order, so the `@.str.N` numbering of the emitted + // string constants was a per-process permutation. Same input, different + // `.ll` on every run — which silently invalidates any A/B that compares raw + // IR, a technique several representation and GC investigations relied on. + // Emission order is the only thing that changes; sorting by `FuncId` makes + // it stable without altering what is emitted. + let mut materialized_closure_sources: Vec<(&perry_hir::types::FuncId, &String)> = hir + .closure_source_text + .iter() + .filter(|(func_id, _)| { + !registered_fn_ids.contains(*func_id) && materialized_closure_ids.contains(*func_id) + }) + .collect(); + materialized_closure_sources.sort_by_key(|(func_id, _)| **func_id); + for (func_id, src) in materialized_closure_sources { let sym = format!("perry_closure_{}__{}", module_prefix, func_id); user_fn_source.push((sym, src.clone())); } From 8afa08a6ae6932b50938a547e3f6a1a7ae77c899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 06:15:58 +0200 Subject: [PATCH 2/2] changelog: fragment for #7039 --- changelog.d/7039-codegen-determinism.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 changelog.d/7039-codegen-determinism.md diff --git a/changelog.d/7039-codegen-determinism.md b/changelog.d/7039-codegen-determinism.md new file mode 100644 index 0000000000..2f6281e227 --- /dev/null +++ b/changelog.d/7039-codegen-determinism.md @@ -0,0 +1,13 @@ +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. + +This was not cosmetic. It silently invalidates any A/B comparison of raw LLVM IR, +a technique several representation-selection and GC investigations relied on for +evidence. PR #7037 hit it directly: its first proof that `--opt-report` was +observational passed on nondeterministic output and had to be redone with four +runs per arm and a normalizer. + +Measured over 6 compiles of one file (md5 of the concatenated per-module `.ll`): +5 distinct hashes before, 1 after. Emission order is the only thing that changes.