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. 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())); }