Conversation
An imported program is compiled as a unit of its own, and the importer embeds the hash of what comes out as program_hash. The names the compiler makes up along the way (letbinding_$_N, lambda_$_N, cse_$_N, renamed variables) were numbered from a counter shared by the whole process and never reset, so a compile started wherever earlier compiles had left it. The CSE pass orders the bindings it hoists by the hash of the renamed expressions, so a file with two or more repeated subexpressions compiled to different bytes depending on what preceded it: its program_hash as seen from one importer did not match the hash seen from another, or the hash of the file compiled on its own. The counter now lives on the thread, and compile_pre_forms, where every unit passes, top-level, library or imported, numbers it from zero and restores the enclosing count when done, so what a file compiles to depends on its own source alone. A file that imports programs compiles differently once as a result, since it no longer inherits the count its imports consumed. The new test turns the optimizer on the way the command line does for cl23; the module test harness left it off, so nothing in the suite had CSE and the numbering never showed.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What goes wrong
Importing a program compiles it as a unit of its own, and the importer embeds the hash of what comes out as
program_hash. That hash is not a function of the imported file alone: it also depends on what the importer compiled before the import.resources/tests/module/programs/gensym-order.clsp(a defun with three repeated subexpressions) imported on its own:gives
0x2a893a5d…. Imported after a one-letprogram:gives
0xc032511d…. Both are honest — each is the tree hash of the bytes that import actually produced — and both differ from a top-level compile of the same file. The programs are the same size and differ in the environment positions of two hoisted bindings.This is what a game built on chia-gaming runs into: each validator names its successor by
program_hash, the factory registers the copies it imported, and when the two imports were numbered differently the session stops at that transition with "validator transition returned unregistered program hash". Which links break moves around with unrelated edits anywhere in the package.Why
gensymnumbers the names the compiler makes up (letbinding_$_N,lambda_$_N,cse_$_N, renamed variables) fromARGNAME_CTR, alazy_staticAtomicUsizeshared by the whole process and never reset.Preprocessor::import_programcompiles the imported file with a fresh allocator, symbol table and context, but the counter carries over, so the nested unit's names start wherever the importer and its earlier imports left it — later imports never matter, earlier ones always do.Names then decide layout in one place: the CSE pass groups the subexpressions it finds in a
BTreeMapkeyed by the hash of the renamed expression ("Group them by hash since we've renamed variables"), so the order in which it hoists them — and the order of the resulting bindings in the environment — follows the digits inp_$_437versusp_$_471. Two or more repeated subexpressions in one function are enough.Change
gensym.rs: the counter is athread_local!Cell<usize>, so compiles on different threads no longer interleave, andGensymUnit(crate-private) restarts it for a compilation unit and restores the enclosing count when dropped — the same shape asNewStyleIntConversioninclvm.rs.compiler.rs:compile_pre_formsholds aGensymUnit. Every unit passes through it — a top-level file, a program compiled through the library, an import — so each is numbered from zero, and a file compiles to the same bytes whether it is imported, compiled on its own, or compiled again later in the same process.main(the two hashes above) and passes here. It setsoptimizethe way the command line does for cl23; the module test harness left it off, which is why nothing in the existing suite noticed — without the Strategy23 optimizer there is no CSE and the names never matter.ARGNAME_CTRwaspubbut nothing outsidegensym.rsused it; it is private now.Why it surfaced now
Until #531 the on-disk module cache from #345 returned the first compile's bytes for every later import of a module with the same fingerprint, which would have hidden this whenever the cache was warm; 0.5.0 shipped four days after the cache came out. Inferred from the history rather than measured on a pre-#531 build.
What changes for existing builds
A file that imports programs compiles to different bytes once after this, because it no longer inherits the count its imports consumed. After that, every context agrees: a program's bytes depend on its own source alone, and its
program_hashis the same wherever it is imported from and equal to its own compile.Full
cargo test --lib,cargo fmtandcargo clippypass.Note
Medium Risk
Changes compiled output for multi-import builds and affects
program_hashidentity used by module/gaming flows, though behavior becomes correct and deterministic.Overview
Fixes non-deterministic
program_hashfor imported modules when earlier compiles in the same process had already advanced the globalgensymcounter. Compiler-generated names (letbinding_$_N, CSE hoists, etc.) now restart at zero per compilation unit via thread-local counting andGensymUnitincompile_pre_forms, so an imported file’s bytes match a standalone compile regardless of import order or prior edits in the importer.Existing builds that import programs will get different hex once (importers no longer inherit consumed counter state); after that, hashes are stable and source-only. Adds CL23 optimizer fixtures and a regression test that compares standalone vs repeated imports (alone and after a prefix import).
Reviewed by Cursor Bugbot for commit 061951b. Bugbot is set up for automated code reviews on this repo. Configure here.