diff options
| author | Cliff Click <cliffc@acm.org> | 2026-09-20 22:38:58 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-09-20 22:38:58 -0700 |
| commit | 40b33cb6f8b8dc4d8551c3b522e7c5682a44540a (patch) | |
| tree | e44d0380390109e3597bc5a1b1744e715cc30b62 /docs/chapter-backports.md | |
| parent | 1e527b16b28f31b08cbb36b7b1bf2d689487a989 (diff) | |
| download | Simple-40b33cb6f8b8dc4d8551c3b522e7c5682a44540a.tar.gz Simple-40b33cb6f8b8dc4d8551c3b522e7c5682a44540a.zip | |
Final reg-alloc across chapters cleanup
Diffstat (limited to 'docs/chapter-backports.md')
| -rw-r--r-- | docs/chapter-backports.md | 1124 |
1 files changed, 179 insertions, 945 deletions
diff --git a/docs/chapter-backports.md b/docs/chapter-backports.md index 7096156f..150afe20 100644 --- a/docs/chapter-backports.md +++ b/docs/chapter-backports.md @@ -1,75 +1,80 @@ # Chapter backport review -This is a queue of pending work. Remove completed corrections from the queue. Independent -corrections should start in the earliest affected chapter and propagate through -every later affected snapshot. A regression in Chapter 25's `Chapter21Test` -does not test the compiler in the `chapter21` directory. - -For now, keep building SSA with incomplete types in Chapter 25. Moving that -architecture earlier, or splitting Chapter 25, is deferred while small changes -establish the review workflow. No renumbering is committed. - -Keep each correction's reduced failure, smallest patch, and test results together -for review. - -## Follow-ups exposed by the Chapter 22 allocator review - -- **Narrow-store register masks:** fixed locally in 22-24, with - `Chapter21Test.testNarrowStores`. Bring the x86 restriction to 20-21, - and the RISC-V restriction to 21; 19's masks and 20's RISC-V mask are - already general-register-only, and 25 already has both fixes. Byte/short stores - cannot consume XMM/FPR values. The old x86 `_sz >= 2` compares a character to - an integer and always accepts XMM; the intended threshold is `'4'`. RISC-V - previously accepted its full memory mask for every width. With Chapter 21's - bias in 22, `testSextSuccess` emitted an invalid FP byte store on RISC-V and - asserted in `StoreX86.encVal` on x86. Preserve cohort data when backporting: - correcting a mask can substantially change spill counts even without tuning. -- **FunPtrNode backport: defer until register allocation work is complete.** The - minimal reproducer is `return {->42;};`; `return sys.io.p;` fails for the same - reason. Both fail on x86, RISC-V, and ARM at the default seed 123. The first - iterative Opto pass preserves the target. The subsequent pruning loop in - `CodeGen.opto` equates no linked calls with no callers for anonymous and `sys.` - functions, clears their unknown-caller Start edge, and deletes their bodies. - Their function-address ConstantNodes have no edge to keep the bodies alive. - The returned pointer survives while its Return disappears from Stop. - - Instruction selection resets NIDs and never selects the deleted function, so - the linker still maps its pointer to an ideal FunNode. In the reduced anonymous - case that node is 256; the x86 machine graph has UID 19. `Encoding.relo` accepts - the stale target and `patchLocalRelocations` indexes `_opStart[256]`. Enlarging - the array would merely hide a missing function body and produce a wrong address. - - `val f = {->42;}; return f;` retains the named function and encodes on all three - targets. An isolated build that excludes anonymous functions from pruning also - encodes both anonymous reproductions, confirming the cause; it does not fix - escaped `sys.` addresses. Cliff's chosen direction is to investigate backporting - Chapter 25's `FunPtrNode`, whose edge to the function Return keeps the function - body alive. This supersedes the proposed address-reference scan. Leave both - examples as documented known failures while finishing the allocator chapters; - do not fold this reachability change into that work. - - After register allocation, find the earliest applicable chapter and carry the - function-pointer representation and its retention rules together: pointer - creation, Opto's unknown-caller pruning, instruction selection, global constant - cloning, and relocation must agree on the retained function. Adapt these to - each chapter without importing unrelated Chapter 25 module/escape machinery. - Validate a returned pointer by calling it, and cover library pointers and - genuinely unused helpers. Chapter 21 lacks this pruning pass and also treats a constant-only - default main differently, so its old encoding-only coverage was not equivalent. - Diagnostic logs are in `chapter22/build/funptr-review`; the reduced sources and - causal trace above are the durable reproduction. -- **String without an explicit return in 22:** use the unchanged source in - `Chapter20Test.testString`, with the same driver and seeds 0-29. All three CPUs - fail before allocation, in loop-tree construction or GCM (missing loop owner, - dead CallEnd, or a FunNode cast to LoopNode, depending on seed). The original - snapshot reproduces the same 90 failures; seed 123 folds the work away. Reduce - this around Chapter 22's default-return/dead-call handling before changing a - central reachability invariant. Do not count a different seed or added return - as fixing this input. +This is a queue of pending work. Completed corrections leave the queue; the +[validation summary](#validation-record) records their scope. Detailed old logs +are disposable build artifacts. Reusable rules live in +[the AI notes](../skills/chapter25-codex-notes.md). + +Introduce independent fixes in the earliest applicable chapter and propagate +them through every affected snapshot. Testing Chapter 25's inherited tests does +not validate the compilers in the earlier chapter directories. Keep building +SSA with incomplete types in Chapter 25 for now; moving that architecture or +renumbering chapters is deferred. + +## Pending corrections + +- **B10: preserve integer widening in `nonZero`, Chapter 24.** Still missing: + `[0,255]` and `[-255,0]` with widening levels 1-3 become level zero. Chapter 25 + passes `_widen` to `make`; 24 does not. Widening first appears in 24, so 15-23's + older `nonZero` needs no patch. The previous audit established the type-level + discrepancy, not a loop-failure reproducer. Source: `51f1f8f4`. +- **FunPtrNode lifetime backport, now next after the allocator review.** + `return {->42;};` and `return sys.io.p;` fail during relocation in Chapter 22 + on all three targets at seed 123. Opto's name-based pruning equates no linked + calls with no callers and deletes anonymous/library function bodies. A plain + function-address ConstantNode keeps the pointer, but has no edge to keep the + Return alive. Instruction selection resets NIDs; the linker retains an ideal + target and `patchLocalRelocations` indexes outside the machine graph. + + Backport Chapter 25's FunPtrNode/Return retention rules together with pointer + creation, unknown-caller pruning, instruction selection, constant cloning, and + relocation. Determine the earliest applicable home; 21 lacks this pruning + pass. `val f={->42;}; return f;` surviving is not a fix for anonymous/library + addresses. Do not enlarge relocation arrays or add an interim address scan. + Validate returned pointers by calling them, including library pointers, while + still deleting genuinely unused helpers. Keep module/escape machinery in 25. +- **Chapter 22 String without an explicit return.** The unchanged source in + `Chapter20Test.testString` fails before allocation in loop-tree construction + or GCM at the historically tested seeds 0-29, on all targets. The original + snapshot has the same failures; seed 123 folds the work away. Reduce default + return/dead-call handling. A new return or a different seed is not a fix; + this historical sweep is not a reason to sweep routine backend tests. +- **Chapter 25 null-dereference diagnostic.** This setup was accepted despite + dereferencing null on the taken arm; investigate separately from B13: + + ```java + struct Point { int x; new Point={int v -> x=v;}; }; + Point?[] !points=new Point?[2]; points[arg]=new Point(42); + Point? p=points[1]; + if(p==null) return p.x; return -1; + ``` + +- **Chapter 18 fuzzer seed `973358943756616234`.** Remains in + `OPEN_FAILING_SEEDS`. The reduction below exposes nullable field lookup with + peepholes disabled and, historically, a mixed-return error with them enabled. + B11 fixes the latter reduced diagnostic; neither it nor Chapter 19's change + from an off/on oracle to worklist-seed comparison proves the full seed fixed. + + ```java + struct s0 { u8 v1; }; + s0? !UmPOLQK=null; + if(0) while(0&UmPOLQK.v1) {} + if(UmPOLQK.v1) {} + return new s0; + while(0) {} + ``` + +- **Chapter 25 ARM extern data.** `i32 errno="C"; return 0;`, compiled through + Encoding with ARM/SystemV, reaches `arm.load_str_imm` with register -1 for the + extern-data value stored by `<clinit>`. Both the saved original and final + compiler reproduce it. This is an extern-data lowering/encoding issue, not + the repaired float-store allocation conflict. The historical Bubble Sort + replay allocates successfully but also exposes this failure if ARM encoding + is requested. Do not describe its allocation statistics as runtime coverage. ## AOT class initialization: larger independent work -Proposed on 2026-09-19; deferred behind the smaller BXX cleanup above. This is +Proposed on 2026-09-19; deferred behind the smaller queue above. This is a substantial, self-contained Chapter 25 change, not an entangled backport. Earlier chapter applicability has not been established. @@ -124,896 +129,125 @@ Chapter 25 suite, including `make -j 4 tests`. | Group | Eventual home | Why not in the small queue yet | |---|---|---| -| Register allocation and spilling | 20 onward | Shared corrections/support now carried through 25; staged allocator/cohort/README reviews are complete through 24, with 25 next. | | Conditional Store and array Load control | Memory/arrays chapters | Reproduce under the earlier alias model before extracting fixes from the new memory implementation. | | SCCP dependencies, function revival, reachability | 24; some foundations may fit 18 | Separate old-IR corrections from new Guard/Escape/BulkMemPhi and external-caller machinery. | | TypeScalar, numeric modes, guards, symbolic fields, open/forward types | Revisit earlier homes later | A connected incomplete-types architecture, including phase ordering and errors. | | BulkMemPhi/MemPhi, private constructor memory, allocation helpers | Revisit memory / constructors / methods | Move invariants and regressions together. | | Serialization, global identity remapping, module escape summaries | Separate compilation | Remain with modules. | -## Register allocation: correctness first, staged improvements +## Register allocation: completed chapter progression -Review on 2026-09-20. The staged plan follows; Chapters 20-24 -have since been implemented and validated as recorded below. Shared corrections -and test support have also been carried through 24-25; their quality/cohort/README -reviews remain pending. Compared RegAlloc, BuildLRG, IFG, -LRG, Coalesce, RegMask, and split support across 20-25, with the original fix -commits. Allocation starts in 20; 19 has instruction selection/register masks -but no coloring allocator. Most changes are in 20->21 and 24->25. The 22->23 -allocator changes are imports/API cleanup; 23->24 has no allocator changes. +The allocator review is complete through Chapter 25. Each README ends with the +introduced technique, measured program cohorts, and commentary. `make spill-stats` +reports actual retained moves, per target and cohort, with assertions enabled. -Cliff's direction: correctness fixes belong at the earliest applicable chapter; -quality improvements should accumulate gradually. Debugging/printing/support -can start in 20. Judge spill changes across a fixed suite, allowing a local -increase when compensated elsewhere. Do not copy the entire Chapter 25 allocator -into 20, and do not classify a whole historical commit by its title. - -### Correctness work to extract - -| Work | Current evidence / source | Earliest intended home | +| Chapter | Additional technique | Final cohort sizes | |---|---|---| -| Register-mask and LRG bookkeeping | Original 20: `RegMask(int)` assigns `bit=64` instead of subtracting 64, and empty `firstReg()` returns 128. `LRG._union` replaces `_machUse` without its `_uidx`, and can retain null from an immutable mask intersection. Fixed in 21; see `7e1deb4c`, `8d6de0e1`. | 20 | -| BuildLRG/IFG instruction contracts | 21 checks null use masks, avoids revisiting CFG projections, processes kills even without an output LRG, and distinguishes an instruction's fixed output from a range narrowed elsewhere. The 25 commutative-input fallback uses `mach.outregmap()` rather than a possibly nonexistent LRG. | 20, after reduced graph/source regressions | -| Self-conflicts and effective splits | 21 visits extending uses before rewriting definitions, permits needed backedge splits, checks intervening clobbers/self-conflicts before reusing splits, and avoids immediately folding away capacity splits. `35057d50`, `cc32b0cf`, and `880329e6` explicitly address failed progress. | 20; preserve Phi parallel-assignment semantics | -| Legal rematerialization | 22 checks a clone's output mask against the use mask before choosing a clone over a move. Otherwise repeated cloning can preserve the original hard conflict. The 25 kill handling chooses the live cloneable value to split rather than the killing instruction (`c05d7df4`). | 20; isolate correctness from ranking cloneable spill candidates | -| Empty-mask split bookkeeping | 25 saves the original def use-count before inserting a split, checks for a missing sample use, and skips null traversal roots. | 20 where the affected helper/path exists | -| Persistent multi-def/fixed-register conflicts | 25 directly splits single-register uses and sometimes splits the deep side of a loop (`c05d7df4`). Its `_ns._len > 5` and related thresholds mix progress with tuning. | Reduce a no-progress case in 20, then extract the smallest sufficient rule; do not transplant thresholds as a proven invariant | -| Encoding and stack ABI support | Correct frame sizing, incoming/outgoing stack arguments, and stack-to-stack move encoding are required once native emission exists (`7e1deb4c`). | 21, where encoding is introduced; retain CPU/ABI distinctions | - -Each correctness packet needs a failure demonstrated against the destination's -old implementation, then allocation completion and legal register use. From -21, also execute native/emulated results. An eight-round cutoff failure is a -correctness failure, not permission to increase the cutoff. Some entries above -are established historical fixes; applicability of the larger 25 changes to -older graphs still needs reproduction. - -### Proposed quality progression - -| Chapter | Additional quality technique | -|---|---| -| 20 | Keep the existing basic coloring, biased coloring, rematerialization, and loop-boundary splitting, corrected for legality/progress. | -| 21 | Conservative copy coalescing, already introduced here. Keep its mask/adjacency correctness fixes with it. | -| 22 | Stronger color preferences: follow loop backedges for bias, improve copy-chain searches, and refine cheap-spill ordering for callee saves/cloneables. | -| 23 | Group popular single-def values by their uses' required register classes instead of splitting each use. Include safe use-list mutation and call-crossing restrictions when introduced. | -| 24 | Try cold splits first for loop-Phi self-conflicts, with a remembered one-attempt limit and aggressive fallback. The delay is optional; its fallback is mandatory. | -| 25 | Rank recovered live-range area against loop-scaled split cost (`a03bc567`), plus any further measured tuning. | - -This progression would move some heuristics currently bundled into 21 later; -it is not just a forward copy. Keep each introduced technique in subsequent -chapters and update the chapter prose with it. Chapter 20's README already -discusses popular-value grouping and coalescing beyond its current code, so -that prose needs realignment too. The one-attempt loop-Phi delay from -`a03bc567` is a quality feature with its own progress safeguard, not a reason -to add persistent deferral state to the first allocator chapter. - -### Support and measurement - -- Start deterministic split ordering, null-safe split printing, useful LRG/mask - diagnostics, and an optional function-local-edge verifier in 20. Preserve the - existing side-effect-free `_` printer accessors. Actual stack offsets require - the frame layout introduced in 21; module-specific exceptions stay in 25. -- Record actual `_spills` and `_spillScaled` for each compilation, keyed by test, - CPU, ABI, and fixed seed. Both count retained SplitNodes; `_spillScaled` weights - each by `8^loopDepth`. These are move/split metrics, not solely memory traffic. -- Use one fixed optimizer seed for routine backend validation. Seeds shuffle - IterPeeps/Opto worklists, whose output should normalize modulo NIDs and equivalent - operand orderings; similar post-Opto graphs give little additional allocation, - scheduling, or encoding coverage. Reserve seed sweeps for optimizer/worklist - investigations or demonstrated order-sensitive failures. Inspect post-Opto - differences before multiplying backend runs. Prefer varied programs, register - constraints, targets/ABIs, and execution checks for allocator coverage. -- Compare total scaled counts as the existing quality measure, and report raw - totals plus the largest local changes. Report per CPU/ABI as well as the whole - suite so a regression on one target is visible. Include compilations without - a current golden spill assertion; do not exclude failures or disable checks - to obtain an aggregate. Freeze test membership and seeds for each comparison. -- Compare before/after within each chapter. For tutorial progression, also use - the common program/target subset: totals from different suites or changed IR - are not a controlled allocator comparison. -- Existing helpers assert individual spill goldens (often with tolerance). - Review those local changes against the measured aggregate before updating - expectations. Correctness tests must assert completion/results independently - of the heuristic's exact spill count. -- `splitBypass` originally scanned from `j-1` with `idx++` throughout 20-25, - reaching its own destination and rejecting nonadjacent bypasses. Corrected - through 25 with intervening kill-mask checks and a regression. - Pre-color copy reuse also checks fixed-register definitions before they have - an assigned `_reg`. Chapter 25 is not a complete correctness reference. - -Suggested execution order: support/measurement, small mask/LRG correctness -fixes, constrained-register and self-conflict regressions/fixes, then one -quality technique at a time. Run each affected snapshot's full suite at each -accepted boundary. Chapters 20-24 have now been implemented; stop for Cliff's review before 25. -Review each chapter's README along the way. Chapter 21's encoding discussion -has been shortened, with the bit-level notes retained as a separate reference. End each README with its -RegAlloc improvement, measured cohort table, and commentary; the Chapter 25 -table must have rows for cohorts 20-25 using the Chapter 25 compiler. +| 20 | Basic coloring, rematerialization and loop splitting, with shared legality/progress fixes | 39 | +| 21 | Conservative copy coalescing | 39, 52 | +| 22 | Stronger copy-chain/backedge bias and cheap-spill ordering | 39, 52, 24 | +| 23 | Group popular single-def uses by compatible register classes | 39, 52, 24, 30 | +| 24 | One cold-only attempt for loop-Phi self-conflicts, then mandatory fallback | 39, 52, 24, 30, 67 | +| 25 | Existing area/cost ranking and later conflict strategies | 39, 52, 24, 30, 67, 10 | + +Shared corrections include RegMask/LRG bookkeeping, null use masks, kills without +an output LRG, self-conflict splitting, compatible rematerialization, clobber-aware +copy reuse/bypass, deterministic ordering, and side-effect-free diagnostics. The +final audit adds the missing narrow x86 masks in 20-21 and RISC-V mask in 21; +fixed-neighbor color bias in 20-25; and ARM memory register-bank selection, +float opcodes, and matching emulator fixes in 21-25. + +Chapter 25's additional fixed-use/deep-side splitting thresholds remain there. +The def-use-count snapshot and null-use guard support its extra split branch; +20-24 do not have that branch. Earlier kill handling subtracts killed registers; +25 can choose to split cloneables earlier. No independent earlier-chapter +correctness failure was established for these strategy differences. They are +not uncompleted promises to copy the complete allocator backwards. Compilation- +unit ownership and void/metadata handling also stay with the later representation. + +The final ARM failure was substantive: the original Newton float programs reached +the allocation round limit because a float store demanded a GPR; the deeper loop +split kept cutting the wrong portion of the range. Permitting the supported FP +registers removes that conflict. Encoding must then use the allocated register +bank, with five-bit register numbers, for both immediate and indexed loads/stores. +The emulator must preserve floating bits and scale double offsets by eight. + +Chapter 25 freezes the 212 earlier compilation entries in +[documented source fixtures](../chapter25/src/test/java/com/seaofnodes/simple/spill/README.md). +Constructor/library adaptations change IR, so this is a program-cohort comparison, +not identical machine graphs. Those rows replay allocation and legality checks; +current Chapter25Test native checks and a fresh system-library encoding contribute +ten more allocations. Total: **2,597 moves / 5,579 loop-weighted moves** over 222 +compilations. Chapter 25's area/cost implementation is retained. Substituting the +earlier ranking saves five moves on the clients but fails a fresh `sys` allocation +at the eight-round limit; a failed library cannot be omitted from the comparison. + +The earlier tables are refreshed where the final mask fixes changed them: +Chapter 20 is 238 / 357; Chapter 21 is 840 / 1,456. Coalescing on/off now saves +230 / 433 moves in Chapter 21 with identical legality fixes. Chapters 22-24's +aggregate values are unchanged. Full details and comparison limits belong in +the individual READMEs rather than a second evolving set of tables here. ## Review and test protocol -1. Run the destination's unmodified `make tests`. Record baseline failures and - resolve them before accepting a compiler backport into that chapter. -2. Add a deterministic regression proving the old failure. Prefer tiny source - programs; use encoding checks when register placement is the property at issue. -3. Apply the smallest correction; run the focused regression and full chapter - `make tests`. Stop for Cliff's review before the next trial fix. -4. Propagate the accepted fix and regression through every affected later - snapshot. Run each snapshot's full `make tests`, not just 25's inherited tests. -5. Run TypeTest for lattice changes and rebuild system objects when compiler - changes invalidate their IR/native code. Force a full Java rebuild when shared - APIs or constants change: older Makefiles do not track all Java dependencies. - Do not weaken checks or drop tests. -6. Report chapters, commands, and results for review; remove completed items from - this queue. Keep unrelated dirty changes; - do not push without explicit approval. - -The top-level runner supports all 25 chapters: - -```sh -make lib # dependencies, before tests on a fresh checkout -make tests -make -k tests # visit all chapters; failures still fail the command -make tests CHAPTERS="chapter21 chapter22" -make tags release CHAPTERS="chapter01 chapter24 chapter25" -``` - -Runs are sequential, and child failures propagate. `make tag` aliases `tags`. -Java, GNU make, bash, ctags (for tags), and the existing native test tools must -be on PATH. Early release targets package classes into jars; they do not imply -standalone executable compiler drivers. Older Makefiles discover jars when make -starts, so run `make lib` separately before `make tests`. +1. Establish the destination baseline. Reproduce a real failure with a small + source or constrained machine graph before classifying a change as a bug fix. +2. Apply the smallest correction, propagate it to all affected snapshots, and + run each snapshot's focused checks and full `make tests`. Respect the current + user-authorized review boundary; do not push without explicit instructions. +3. Force Java rebuilds when older Makefiles miss dependencies. Rebuild `sys.o` + after compiler changes; include a fresh library compilation in allocator + measurements rather than relying on an up-to-date object. +4. Keep legality/progress/runtime checks separate from spill expectations. + Eight-round exhaustion is a failure; neither increasing the cutoff nor + changing a golden proves it fixed. Preserve test membership, targets/ABIs, + and fixed seeds. Do not sweep optimizer seeds for routine backend validation. +5. Report aggregate and local changes. Use same-compiler heuristic comparisons; + changed frontend/lowering graphs are not allocator-only measurements. +6. Keep edited text LF-only, preserve unrelated changes, and run `git diff --check`. + +The top-level runner accepts explicit chapter lists, e.g. +`make -k tests CHAPTERS="chapter20 chapter21"`. Tests in 25 alone are insufficient. ## Validation record -Historical results below are dated evidence, not a substitute for a fresh -baseline. Logs live in ignored build directories and may no longer exist. -Reusable implementation lessons are in `skills/chapter25-codex-notes.md`. - -### Chapter 24 allocator: ready for review, 2026-09-20 - -Introduced Chapter 25's cold-first loop-Phi self-conflict splitting in 24. -Remember each deferred Phi across rebuilt live ranges; a second conflict uses -ordinary aggressive splitting. The graph regression covers both a Phi conflict -and a backedge-only conflict where the first attempt makes no graph change. -It fails with the saved allocator and with a deliberately disabled fallback, -and passes in 24 and 25 (where this strategy was already present). - -Kept cohorts 20-23 at 39 / 52 / 24 / 30 allocations, including the earlier -64-bit Person, guarded String, and smaller emulator argument list. Chapter 24's -revised Jig, BubbleSort, Newton, and stack-argument cases move to Chapter24AllocTest; -its own existing tests add the rest of cohort 24. Native/emulator helpers retain -24's inline-source APIs. Restored the ARM BrainFuck check's own result pointer in 24-25. - -The frozen Chapter20Test.testString exposes a pre-existing CallEndNode.idealize -NPE on all three targets, before allocation. Chapter 24's new dominator loop -lost 23's null termination check. Restored that check and avoid adding a null -dependency when no enclosing function is found; 25 uses a different inliner. -The unchanged String test fails against the original snapshot and passes after -this local correction. Both sides of the allocator comparison include the fix. - -| Cohort, using Chapter 24 | Allocations | Raw splits | Weighted splits | -|---|---:|---:|---:| -| 20 | 39 | 331 | 457 | -| 21 | 52 | 436 | 975 | -| 22 | 24 | 67 | 67 | -| 23 | 30 | 66 | 115 | -| 24 | 67 | 432 | 1,342 | -| **Total** | **212** | **1,332** | **2,956** | - -With only cold-first splitting absent, totals are 1,338 / 2,962: six fewer moves -and six fewer weighted moves. Per-target raw/weighted totals are ARM 430 / 710, -RISC-V 451 / 745, x86 SystemV 152 / 285, and x86 Win64 299 / 1,216. The respective -savings are 2 / 2, 1 / 1, 1 / 1, and 2 / 2. RISC-V cohort-20 MergeSort adds one; -other MergeSort cases, Sieve, and ARM testFlags2 offset it. Native MergeSort's -weighted expectation becomes 34 (observed 35 without this heuristic), replacing -the older loose 40 expectation. No allocation or execution failure was hidden. - -On cohorts 20-23 alone, 23's 933 / 1,745 becomes 900 / 1,614. Of that change, -only 6 / 6 is the new allocator rule; other compiler changes account for 27 / 125. -Reviewed the README, labelled its SSA examples as pseudocode, corrected which -Phi is available at loop exit, and avoided implying a constant value proves a -loop removable. It ends with the cold-first explanation, cohort table, and -controlled comparison. Area/cost ranking and Chapter 25's cohort review remain. - -Validation: the original 24 suite passed 456 + 1 fuzzer. Final `make -j 4 tests` -passes 470 + 1; `make spill-stats` passes 106 tests / 212 allocations, including -runtime, register/ownership, and quality checks. The forwarded regression passes -25's full 465 tests across six groups. Snapshots, comparisons, and negative -controls are in `chapter24/build/regalloc-review`; 25's full log is in its -`build/regalloc-review/full.log`. Edited files use LF endings. - -### ARM calls and stack frames: corrected in 21-25, 2026-09-20 - -The Chapter 23 README vector example exposed ordinary BL corrupting an argument -register in the emulator and RetARM subtracting eight times the frame size. -FunARM also emitted ADD with a wrapped negative immediate, masked by the -emulator's signed decoding; RET omitted its X30 operand. Corrected the encoder -and emulator together in 21-25, the first chapters with actual ARM encoding. -Frames use SUB/ADD with byte counts and 16-byte alignment, including leaf spills. -Negative arithmetic immediates switch ADD/SUB rather than wrapping. BL writes -only X30; RET/BLR use their encoded register (read before overwriting X30). -Chapter 21's older decoder already avoided BL writeback, but did not execute RET. - -Instruction regressions and recursive calls check actual return values, emitted -frame/RET words, preserved registers, stack restoration, and bounded completion. -The vector-growth regression starts at 23, where struct methods arrive, and uses -25's explicit constructor syntax there. All regressions fail against the saved -pre-fix compiler/emulator. The original README example now returns 511 on ARM, -matching RISC-V, and restores SP. Full `make -j 4 tests` passes in 21 (404 + 1 -fuzzer), 22 (419 + 1), 23 (441 + 1), 24 (456 + 1), and 25 (464 across six groups). -Spill reports retain the same allocation counts and raw/weighted totals: -21: 91 allocations, 884 / 1,794; 22: 115, 838 / 1,496; 23: 145, 933 / 1,745. -Logs and saved pre-fix classes are under each chapter's `build/arm-call-review`. - -### Chapter 23 allocator: ready for review, 2026-09-20 - -The prior batch already supplied shared correctness/support through 25. This -pass keeps Chapter 22's color bias/cheap-spill ordering and introduces popular-use -grouping: a single-definition empty-mask range with more than two fixed-register -uses may share copies by compatible register class. Existing same-depth split -fallback remains; cold-first splitting and area/cost ranking remain for 24/25. - -Simplified grouping to one pass. Intersecting a class preserves compatibility -with all earlier users of that class; new classes are disjoint, so rescanning -is unnecessary. Snapshot distinct users before rewiring, skip null register -masks, and count each call once even if it uses the value in several slots. -Values used by multiple calls still fall back to ordinary splitting. The old -code throws a null-mask NPE in the new reduced graph regression. Its corrected -version checks shared copies, overlapping masks, repeated input edges, preserved -scheduling dependencies, and one-call/two-call handling. Carried this same fix -and regression through 24-25 to avoid repeating its review in later chapters. - -Added `make spill-stats` with frozen cohorts 20-23. Keep `person21`, and preserve -21's guarded String file as `stringHash21`; 23's simplified String and two newly -enabled Jig encoding tests live in Chapter23AllocTest. Cohort 23 also includes -its two executed RISC-V short-circuit regressions. The reporter defers quality -failures while still executing remaining targets/results, and exits nonzero. -Restored the inherited ARM BrainFuck check to use the ARM result pointer. - -| Cohort, using Chapter 23 | Allocations | Raw splits | Weighted splits | -|---|---:|---:|---:| -| 20 | 39 | 332 | 458 | -| 21 | 52 | 450 | 989 | -| 22 | 24 | 67 | 67 | -| 23 | 30 | 84 | 231 | -| **Total** | **145** | **933** | **1,745** | - -The saved original compiler, corrected grouping, and a controlled ablation -which disables only the grouping dispatch all produce these same totals. -There is **no demonstrated spill improvement** from grouping on this suite. -By target the totals are ARM 303 raw / 464 weighted, RISC-V 324 / 499, -x86 SystemV 153 / 286, and x86 Win64 153 / 496. The new machine-graph regression -covers behavior that the program suite does not distinguish; it is excluded -from the original-compiler measurement and contributes no allocation statistics. - -Across the earlier cohorts, Chapter 22's 838 / 1,496 becomes 849 / 1,514 here. -These are different compilers, not an allocator-only comparison: the frozen -Chapter 20 String source survives optimization in 23 (9 + 4 + 3 weighted moves) -where 22 folded it away. Its goldens are updated accordingly. Array type printing -also changes `[u8]` to `[]u8`; only its rendered expectation changes. - -The original Chapter 23 full suite passed 428 + 1 fuzzer. Final `make -j 4 tests` -passes 437 + 1; `make spill-stats` passes 72 tests / 145 allocations, including -quality, ownership, register, native, and emulator checks. The forwarded grouping -fix passes full 24 (452 + 1) and 25 (460 across six groups) suites. Original -snapshots, controlled ablation, and detailed logs are under -`chapter23/build/regalloc-review`; later chapter logs are in `build/grouping-review`. - -Reviewed the README's type discussion and vector example, corrected buffer -installation/mutability/initialization and object-versus-array storage, and ended -it with the grouping explanation, cohort table, and the unchanged ablation -result. Its ARM execution failure is corrected in the call/frame record above. -FunPtrNode reachability work remains deferred until the allocator reviews finish. - -### Shared allocator fixes/support: forwarded through 25, 2026-09-20 - -At Cliff's request, carried the common Chapter 22 corrections into 23-25 in -this review batch, avoiding repeated chapter-by-chapter review of the same -changes. Preserved each later allocator's existing grouping, split-delay, and -area/cost choices; the staged quality/cohort/README reviews still resume at 23. - -The shared packet includes bounded register-mask iteration and nonempty -single-bit tests; commutative BuildLRG output fallback/null masks; resultless -kill handling; fixed-register clone progress; null sample/traversal guards; -and backward copy bypass with kill masks and uncolored fixed definitions. -Chapters 23-24 also receive the corrected clone adjacency/multiple-use test, -narrow-store masks, and native process exit-status assertions. Chapter 25 -already has the store/exit-status fixes and retains its module-aware verifier. - -Forwarded all eight RegAllocTestSupport diagnostic groups and Chapter20Test/ -Chapter21Test hooks. Generic allocation, encoding, native, and emulator helpers -use CheckedCodeGen, whose regAlloc override checks function ownership, register -masks, two-address constraints, and Phis immediately after allocation. Encoding -adds untyped branches and rewrites tail calls, so running these checks afterward -is too late: even Call.regmap may consult CFG.fun. This test-only hook preserves -25's driver serialization/import-unlink ordering. Chapter 22 uses the same hook. - -Unmodified destination baselines passed. Against their saved main classes, -five diagnostic groups fail in 23/24 (masks, resultless kills, clone progress, -commutative Phi fallback, copy clobbers), and three fail in 25 (masks, clone -progress, copy clobbers). All eight pass in all three corrected destinations. -Final `make -j 4 tests` passes 23: 428 + 1 fuzzer, 24: 451 + 1 fuzzer, -and 25: 459 tests across all six groups (8 + 386 + 34 + 18 + 1 + 12). -Chapter 22 still passes 416 + 1 fuzzer; its separate `make spill-stats` passes -55 tests / 115 allocations with the unchanged 838 raw / 1,496 weighted total. - -For this common-fix comparison, froze each destination's existing local program -suite and default seed. Included its Chapter20-24 allocator/native cases as -applicable, BrainFuck, and MergeSort; excluded diagnostic graphs/new regressions. -These are within-chapter before/after comparisons, not the historical cohort -rows needed for the later README reviews. In particular, 25's module suite is -covered by the full tests, not included in these spill sums. - -| Compiler / same local suite | Allocations | Raw before | Raw after | Weighted before | Weighted after | -|---|---:|---:|---:|---:|---:| -| 23 | 119 | 629 | 614 | 1,511 | 1,279 | -| 24 | 174 | 968 | 953 | 2,620 | 2,381 | -| 25 | 146 | 660 | 656 | 1,255 | 1,244 | - -| Compiler | ARM weighted | RISC-V weighted | x86 SystemV weighted | x86 Win64 weighted | -|---|---:|---:|---:|---:| -| 23 | 413 -> 343 | 417 -> 345 | 95 -> 95 | 586 -> 496 | -| 24 | 643 -> 573 | 647 -> 575 | 95 -> 95 | 1,235 -> 1,138 | -| 25 | 348 -> 347 | 346 -> 343 | 123 -> 114 | 438 -> 440 | - -Changed goldens only after measuring the aggregate: in 23/24, NewtonInteger -Win64 55 -> 48, Sieve Win64 257 -> 186 / RISC-V 160 -> 92 / ARM 160 -> 94, -and arg_count Win64 42 -> 32. In 25, NewtonFloat SystemV 48 -> 40 and arg_count -Win64 31 -> 35. That local increase is accepted against the overall reduction. -All measured cases now complete their legality, quality, native/emulator checks: -53 tests in 23, 77 in 24, and 67 in 25. Logs, original classes, and disposable -measurement sources are under each chapter's `build/forward-regalloc`. -The separate FunPtrNode reachability backport remains deferred as requested. - -### Inlined return typing and ARM emulator: corrected, 2026-09-20 - -The ARM seed-9 String failure was an optimizer bug: inlining can delete a FunNode -while its Return still carries live control, memory, and data. ReturnNode.compute -mistook the deleted entry for an unreachable return and supplied Top to its -caller. Chapter 20 introduced direct use of the linked Return's type, exposing -this stale guard. Removed it in 20-21, matching the existing 22-25 implementation. -Chapter 19 does not read that linked return type and passes the reduced probe. - -Reduced source: `struct S { int x; }; val f={ S s -> s.x=g(); }; -val g={ -> 123; }; S !s=new S; return f(s);`. The old 20 compiler returns null -instead of 123 at seed 58; 21 fails at seed 8. The regression checks interpreted -results across seeds 0-63 in Chapter20Test in every snapshot 20-25. - -Execution then exposed missing register-register SUB decoding in EvalArm64. -Added the unshifted SUB form already emitted by Simple in 21-25. The matching -Chapter21Test checks positive/negative results, 64-bit overflow, and preserved -flags; it traps with the old emulator. The original String source at seed 9 now -encodes and executes on both ARM and RISC-V, producing the independently computed -hash `-2449306563677080489`. All 1,170 encoding/register checks pass over the frozen -13-program cohort, three targets, and seeds 0-29. - -Fresh baselines passed before edits. Full suites after correction: 20: 376+1; -21: 401+1; 22: 399+1; 23: 418+1; 24: 441+1; 25's six groups: -8, 378, 34, 18, 1, 12. Spill totals are unchanged: Chapter 20 remains 236 splits / -355 weighted; Chapter 21's two cohorts remain 401/653 and 483/1,141. These regressions -are excluded from spill measurements. Logs/reducers are in each affected chapter's -ignored `build/arm-top`; the earlier chapter-by-chapter allocator review gate remains. - -### Chapter 22 allocator: ready for review, 2026-09-20 - -Carried forward the corrected Chapter 21 allocator and its diagnostic checks, -retaining native frame/call support (`CallEndMach` in 22). Five inherited -regressions fail against the original 22 classes: mask iteration/empty masks, -resultless register kills, fixed-register clone progress, commutative Phi fallback, -and copy bypass across clobbers. All eight diagnostic groups pass now. - -Chapter 22 keeps the stronger copy-chain and loop-backedge color bias, and cheap -cloneable/callee-save spill ordering. Cloneable candidates must have samples; -coalescing can leave either absent. The adjacent-use test now compares -`defIndex+1 < useIndex`, not `defIndex < useIndex+1`, and treats multiple uses -explicitly. Popular-use grouping is removed for introduction in 23; cold loop -split deferral and area/cost ranking remain for 24 and 25. - -The native test helper had lost its exit-status assertion. Restored checking of -its full integer exit code; `testNativeExitStatus` proves an empty-output program -returning 7 fails. `testNarrowStores` first failed against the old RISC-V mask, -then against the old x86 mask; both now reject FP registers for byte/short stores, -and signed/unsigned RISC-V byte/short writes execute with the expected bytes. -These restrictions already exist in 25; remaining destinations are queued above. - -Preserved cohorts 20 and 21 rather than silently accepting changed inputs: -`person21` holds the original 64-bit age example, and `testInfiniteReturn` belongs -to 22 while the old loop remains in 21. Cohort 22 includes its new allocation -programs and repeated encoding/emulator compilations; diagnostic tests stay out. - -| Cohort, all using Chapter 22 | Compilations | Original splits | Current splits | Original weighted | Current weighted | -|---|---:|---:|---:|---:|---:| -| Chapter 20 | 39 | 322 | 324 | 448 | 443 | -| Chapter 21 | 52 | 465 | 447 | 1,221 | 986 | -| Chapter 22 | 24 | 67 | 67 | 67 | 67 | -| **Total** | **115** | **854** | **838** | **1,736** | **1,496** | - -The original full suite passed 399 tests plus the fuzzer. Its frozen-cohort run -completed all 115 allocations and execution checks, with 15 differences from -Chapter 21's spill expectations. The final `make -j 4 tests` passes 416 tests plus -the fuzzer; `make spill-stats` passes 55 tests, 115 allocations, and all quality, -register-legality, function-ownership, native, and emulator checks. Sources and -expectations were force-recompiled before checking. The README ends with the -cohort table and explains the metrics; its FFI examples now match `sys.smp`. - -A controlled ablation substitutes only Chapter 21's IFG bias/ordering, keeping -all other code and the new narrow-store restrictions fixed. It completes all -115 allocations and runtime checks: 885 splits / 1,543 weighted, versus 838 / -1,496 with the new preferences, a 47 weighted move (3.0%) improvement. The scratch -ablation expects 42 instead of 41 bytes for the x86 narrow-store example because -its chosen registers need an extra REX byte; runtime and mask checks remain on. -Quality differences are reported with nonzero exit status, not suppressed. - -| Target/ABI, all cohorts | Compilations | Chapter 21 preferences, weighted | Chapter 22 preferences, weighted | -|---|---:|---:|---:| -| x86 SystemV | 22 | 267 | 268 | -| x86 Win64 | 15 | 414 | 415 | -| RISC-V SystemV | 39 | 437 | 407 | -| ARM SystemV | 39 | 425 | 406 | - -Against the controlled ablation, BrainFuck saves 14 on RISC-V and 6 on ARM in -both cohorts; ARM `testAlloc2` saves 4. Several MergeSort variants and RISC-V Sieve -increase by one. Against the original mixed snapshot, Sieve saves 72 weighted -moves on each target and Win64 argCount saves 10. This shows why individual spill -goldens are reviewed against the aggregate rather than requiring every case to -improve. Chapter totals also reflect lowering: the frozen String example has no -explicit return and folds away at seed 123 in 22. - -The supplementary 13-program, three-target, 30-seed encoding sweep completes -990 of 1,170 compilations with register checks. The remaining 180 fail on String -(before allocation) and FltArg (relocation); the original classes reproduce all -180 and additionally fail five allocator diagnostic groups per seed. No new -program failures were introduced by the allocator work. The two source-level -failures are queued above, not hidden by changing inputs, seeds, or expectations. -Cliff clarified after this run that optimizer seed variation is not productive -routine allocator coverage: normalized post-Opto graphs should produce similar -backend work. Retain these historical reproductions, but use fixed-seed backend -validation going forward; seed variation belongs to optimizer investigations. -Stop for Cliff's review before starting Chapter 23. - -### Chapter 21 allocator: ready for review, 2026-09-20 - -Carries the Chapter 20 legality/progress/support fixes into 21 while retaining -native frame/ABI handling and conservative coalescing. Stronger color bias, -clone/callee-save ranking, and popular-use grouping are deferred to the planned -later chapters. The existing loop-entry-tail treatment remains; removing it -increased weighted counts in a controlled trial. Coalescing's mask, interference, -capacity rollback, and adjacency-remapping checks pass. Five inherited regression -cases fail against the original compiler and pass now: mask iteration/empty masks, -resultless kills, incompatible clone/use classes (round limit), commutative Phi -uses, and copy cleanup across clobbers. The other two inherited cases already pass. - -`make -j 4 tests`: original 385 + 1; corrected 399 + 1 at the allocator boundary (401 + 1 after the return/emulator -corrections above), after forced rebuilds, -including native x86/Win64 and RISC-V/ARM emulator checks. All 1,170 allocation -and register-constraint checks pass for the 13 frozen Chapter 20 inputs, three -CPUs, and seeds 0-29. The subsequent encoding sweep also passes after the return-typing correction -recorded above; the original ARM seed-9 failure is resolved. -Native harnesses now retain full process exit codes, and ARM BrainFuck checks its -own result pointer. `make spill-stats` passes all 45 selected tests (43 at the allocator boundary) and records -91 compilations; spill-golden mismatches are deferred during reporting so they do -not suppress later targets/runtime checks, but still make the command fail. - -| Cohort, all using Chapter 21 | Compilations | Original splits | Current splits | Original weighted | Current weighted | -|---|---:|---:|---:|---:|---:| -| Chapter 20, three SystemV targets | 39 | 340 | 401 | 445 | 653 | -| Chapter 21, fixed Windows native/emulator suite | 52 | 454 | 483 | 1,175 | 1,141 | -| Total | 91 | 794 | 884 | 1,620 | 1,794 | - -Same corrected compiler without coalescing: 1,074 splits / 1,921 weighted; with -coalescing: 884 / 1,794, saving 190 / 127. Relative to the original combined -heuristics, staging costs 174 weighted moves (+10.7%). RISC-V BrainFuck is the -largest regression; sieve improves on all three targets. Do not claim an overall -quality improvement over the original snapshot. Chapter-to-chapter raw totals -also reflect changed lowering and native ABI obligations, not just coalescing. - -Frozen Chapter 20 sources (including original BrainFuck/MergeSort) now reside in -Chapter20Test. The four revised source/ABI cases formerly there are preserved in -Chapter21AllocTest and counted in 21; its native BrainFuck/MergeSort variants also -belong to 21. No diagnostic graphs count toward quality totals. The README now -summarizes encoding/ELF, links the retained encoding reference, and ends with -coalescing, both cohort rows, and the controlled comparison. Logs and isolated -baseline/ablation sources are under `chapter21/build/regalloc-review` (ignored). -The allocator changes remain confined to Chapter 21; the subsequent return/emulator -correction is recorded separately above. Chapter 22 allocator work is recorded below the review heading. - -### Chapter 20 allocator: ready for review, 2026-09-20 - -Implemented only in 20, per Cliff's chapter-by-chapter review gate. Corrected -high-word/empty register masks and boundary iteration, LRG mask merging/use-slot -bookkeeping, null register-mask dependencies, commutative outputs used by Phis, -CFG projection visitation, and kills from instructions without output LRGs. -Splits now preserve progress through self-conflicts/backedges and intervening -clobbers. Rematerialization must satisfy the use mask; fixed-register clones -with flexible uses go through use-side splitting instead of a no-op simple -split. Copy cleanup scans backward and respects both fixed definitions and kill -masks. Removed the unused `splitEmptyMask` path and its `_killed` bookkeeping. - -Support includes deterministic failed-range ordering, null-safe split printing, -a function-local-edge diagnostic, and `make spill-stats`. The program tests also -check assigned register masks, two-address constraints, and Phi agreement. -Seven small machine-graph/mask regressions fail against the saved original -implementation and pass after correction, including a round-limit failure for -incompatible clone/use register classes. These are separate from the quality -corpus; they do not supply extra zero-spill examples to improve a total. - -The complete program cohort includes Chapter20Test's 11 examples plus BrainFuck -and MergeSort on all three SystemV targets: 39 compilations, default seed 123. -The fresh original baseline passes 368 tests plus the fuzzer regression. After -forced rebuilds, `make -j 4 tests` passes 375 plus 1. The 11 ordinary examples -also pass 990 compilations across seeds 0-29 and three CPUs, with the current -register/ownership checks enabled. Native execution begins in Chapter 21. - -| SystemV target | Compilations | Original splits | Corrected splits | Original weighted | Corrected weighted | -|---|---:|---:|---:|---:|---:| -| x86-64 | 13 | 80 | 82 | 150 | 152 | -| RISC-V | 13 | 82 | 80 | 103 | 101 | -| ARM | 13 | 73 | 74 | 101 | 102 | -| Total | 39 | 235 | 236 | 354 | 355 | - -This is a one-move correctness cost, not a quality win. Adjusted three local -weighted goldens after reviewing the full aggregate: x86 array 3->5, x86 String -18->21, ARM String 3->5 (the original actual ARM count was 2 within tolerance). -All assertions remain active. The stats runner emits per-compilation data and -fails on any JUnit failure; the Chapter20Test collector allows all three target -checks to run before reporting golden differences. The README preserves Cliff's -new introduction, removes premature coalescing/popular-use-grouping claims, -and ends with the measured baseline and discussion of statistical comparison. -No advanced coalescing, color-bias, grouping, cold-first, or area/cost heuristic -was imported. Logs and the original-code snapshots are under -`chapter20/build/regalloc-review/`. Chapters 21-25 are unchanged. - -### TypeFunPtr normalization: complete locally, 2026-09-20 - -Backported the trailing-default normalization from `7f3b8856` to 23-24, where -open/closed function argument tails first appear. Their complete `make` factory -trims trailing BOTTOM arguments for open signatures and TOP arguments for closed -signatures before interning; the existing meet uses that factory too. Raw cyclic -allocation stays unchanged. Earlier chapters use fixed TypeTuple signatures. -Chapter 25 already normalizes with scalar BOT/TOP and retains its implementation. - -Added only `gather` cases in 23-25: open/closed signatures, repeated trailing -defaults, all-default signatures, and defaults before a real argument. No new -test methods or files. The expanded existing lattice-law tests also passed -before the 23-24 fix; these cases extend coverage rather than prove an old law -failure. The other changes in `7f3b8856` concern Chapter 25's TypeMem final flags -and XInt sets, already fixed there; neither representation exists earlier. - -Fresh baselines and forced-rebuild validation passed `make -j 4 tests` in -23 (416 + 1), 24 (439 + 1), and 25 (449 total), with assertions enabled. -Focused TypeTest runs also passed all six tests in each chapter. Edited files -are LF-only and `git diff --check` passes. Logs: -`chapter25/build/tfp-normalization/{baseline,repro,validate}.log`. - -### Scheduling without isPinned: complete locally, 2026-09-20 - -Removed `isPinned()` and all overrides in 11-25. Early scheduling walks inputs, -then assigns a block only when input 0 is null. Existing control and Phi/Proj -bindings stay intact. For ordinary values, that control is an earliest-placement -bound, not a prohibition on sinking during late scheduling. - -Chapters 11-14 also used the predicate for late placement; GCM now preserves -those cases explicitly (Proj, New, Parser.ZERO, and Cast from 13), alongside its -existing CFG/Phi handling. From 15, late scheduling already handles its fixed -CFG/Phi/Proj cases structurally. - -The preceding GCM suites supplied the passing baseline. After forced Java -rebuilds, every full snapshot suite in 11-25 passed `make -j 4 tests`, with -assertions enabled and unchanged expectations/counts (449 total in 25). Existing -constant-chain, guard, loop, allocation, native, and emulator tests all pass. -No remaining `isPinned` declaration or call exists in the chapter sources. -Edited files retain LF endings; `git diff --check` passes. -Logs: `chapter25/build/pinned-review/{chapter25,backports}.log`. - -### GCM and global constant cloning: unified locally, 2026-09-20 - -GCM starts in 11; functions make constant ownership relevant in 18. Chapters -11-25 now share the early definitions-first / late uses-first worklist strategy, -including Region/Loop Phi discovery, waiting-load wakeups, and LCA over every -matching Phi input. Anti-dependence marks moved from CFGNode into a temporary -GCM array. Removed the obsolete fixed-register placement heuristic in 21-24. - -Chapters 18-25 clone entire global constant-building graphs with one identity -map per function, reusing shared subgraphs within that function. Originals stay -intact until all rewrites finish. `Node.copyEmpty()` supplies exact-class clones -with fresh IDs and empty edges, avoiding incompatible machine `copy()` contracts. -Removed 25's redundant instruction-selection pinning to the old ideal Start. - -Necessary chapter differences remain explicit: 18-19's linked Parm inputs -belong to their callers; from 20, unlinked Parms belong to the callee. In 11-20, -the evaluator independently reschedules nodes, so anti-dependencies retain the -full store placement range. Trying the later single-block rule fails the existing -`SchedulerTest.testStoreInIf2`. Alias/tuple memory representations stay local to -their chapters; 25 retains its compilation-unit ownership check. - -The new Chapter18Test regression in every snapshot 18-25 exercises two surviving -recursive functions, repeated uses, registered edges, and function-local chains: -stacked Cast/Constant nodes in 18-20 and RISC LUI/AddI expansions in 21-25. It -fails against isolated original GCM classes in 18 and 20, and passes afterward. -Original 21 and 25 already pass the machine-chain case; those changes simplify -and unify the implementation rather than correcting that particular case. - -Changed ordering exposed 21's existing empty-block layout bug: entering a nested -loop was mistaken for a backedge, producing a bad native Sieve branch. Backported -22's same-loop-tree check; native and emulated Sieve pass. Updated 21's tighter -scaled spill expectations: stringHash RISC 5->3 and ARM 6->3; BrainFuck RISC -44->28. BrainFuck executes on both emulators; a separate direct-entry RISC -hashCode probe verifies both initial hashing and the cached result. - -Validation: unmodified full baselines passed in 11-25. After forced Java rebuilds, -all full chapter targets pass with assertions and `make -j 4 tests`: ordinary -counts in 11-24 are 166, 169, 184, 204, 215, 233, 285, 316, 358, 368, 385, 397, -416, and 439, plus the separately invoked fuzzer wrappers where applicable. -Chapter 25 passes 449 tests across its groups. All edited files use LF endings. -Logs: `chapter25/build/gcm-review/{baseline,before-test,focus-all,final,final25}.log`; -`final.log` covers successful 11-24 runs and `final25.log` the final 25 rebuild/run. - -Independent follow-up: a direct-entry ARM emulator probe of 21's exported -`hashCode` traps with code 3 under both original and updated GCM. It uses the -existing stringHash.smp library, a String containing the u8 array "test", and a -zero cached hash; the cause is not diagnosed. Do not claim the green suite or -lower spill count validates this extra case. Scratch reproducer: `StringProof.java` -in the same review directory; the RISC equivalent returns/caches 3556498. - -### Dominator caches: unified locally, 2026-09-19 - -Real dominator searches begin in 6. The original Region pointer cache in 6-9 -returned the old, still-live dominator after its predecessors were rewired. -Regions now recompute through the shared depth-based `domLCA` walk in 6-25, -including one, two, or more predecessors. Existing dependency direction and -24-25's dead-predecessor filtering are preserved. - -Per review, 6-17 use a char depth with no version. Chapters 18-25 use separate -char depth/version fields, checked before depth narrowing and global version -increments. Added the invalidation hook to 18-20's inlining; 21-25 already had -it. Region, Loop and Stop all validate their caches, and folding functions use -their caller-side depth. CFG copy constructors preserve both fields, as do -ordinary clones. Packed versions in 21-24 failed after 100 invalidations; -25's packed depth overflowed at 2148. Both limits are now 65535, with explicit -overflow assertions. - -Development checks covered rewiring, depth overflow, actual inlining with warmed -caches, repeated invalidation, copy preservation, and version overflow. At -Cliff's request, the dedicated dominator helpers/tests and their Makefile -accommodations were subsequently removed as unnecessary for this bookkeeping. -Existing printer regressions still check that both cache fields stay unchanged. - -Baseline full targets passed in 6-24. After full Java rebuilds, all affected -snapshot suites passed with assertions enabled. Chapter 25 initially passed -`make tests` and `make -j 4 tests` in a scratch copy with a pre-existing stray -`v` removed from Node.java. After Cliff authorized removing that typo from the -live tree, its Java sources and system object rebuilt and `make -j 4 tests` -passed there too (451 tests, exit status zero). - -Logs: `chapter25/build/dominator-review/{baseline,fixed,later,final18-24,chapter25-scratch,chapter25-final,chapter25-live}.log`. -Reusable cache rules are in the skills notes. - -### B11: optimized return-type checking backported locally, 2026-09-19 - -The false mixed-return rejection begins in Chapter 18, where function returns -first merge through one Return/return-value Phi. `ReturnNode.err()` now checks -`expr()._type`, as in Chapter 19. The parse-time `mt` meet could retain a type -from an unreachable return forever. Removed that aggregate from 18-24; it was -already unused in 19-24 and absent in 25. Parsed kind flags remain only for -formatting diagnostics. Chapter 24's additional TOP check is preserved. - -Earlier snapshots were checked with reduced syntax. Struct/reference returns -appear in 10; floating-point returns in 12. Chapters 10-17 already accept the -dead reference/int examples, and 12-17 accept the dead float/int examples. -They retain separate Return nodes and do not require one common type across -reachable returns. The B11 false rejection therefore has no earlier compiler -patch; introducing common-return-type checking there would be separate work. - -`Chapter10Test.testDeadReferenceReturn` starts in 10 and is copied through 25; -`Chapter12Test.testDeadNumericReturns` starts in 12 and is copied through 25. -They cover both constant If arms and unreachable returns after an unconditional -return; numeric cases also verify execution results. Both tests failed in 18 -before the correction with `No common type amongst ...`. -`Chapter18Test.testReachableMixedReturns` in 18-25 still rejects reachable -int/float and int/reference alternatives, using each chapter's diagnostic. - -Unmodified baseline targets passed in 9-25. Final full `make tests` targets -passed with assertions enabled in every changed snapshot, 10-25; Chapter 25 -passed 448 tests. Its reference test selects the entry function explicitly, -because constructors add other Returns to the same compilation unit. -Logs: `chapter25/build/b11-review/{probe,probe-later,baseline,red,fixed,chapter25-fixed}.log`. - -### B12: immediate multiply destination corrected locally, 2026-09-19 - -The x86 backend has distinct integer register (`MulX86`, 0F AF), integer -immediate (`MulIX86`, 69/6B), and floating-point (`MulFX86`, MULSD) encoders. -Chapter 21 incorrectly routed immediate multiply through `ImmX86`, which -encodes an opcode extension in ModRM.reg. IMUL needs the destination there, -including its high bit in REX.R. Chapter 20 has no executable encoder yet; -22-25 already have a dedicated immediate multiply encoder. - -Chapter 21 now uses that dedicated encoder while preserving its -`twoAddress() == 1` allocation contract. Other `ImmX86` subclasses are unchanged. -For `rcx *= 11`, the regression failed with ModRM C1 instead of C9; the fix emits -`48 6b c9 0b`. For `r9 *= 11`, it emits `4d 6b c9 0b`. - -`Chapter21Test.testX86MultiplySameRegister` is carried through 21-25. It forces -rax/rcx/r9/r15 and positive/negative imm8/imm32 values, independently of allocator -choices, and checks register fields, opcode, immediate, length, and each -chapter's allocation contract. Existing distinct-register checks remain in -22-24. Unmodified baselines and final full `make tests` targets passed in all -five snapshots, with assertions enabled; Chapter 25 passed 446 tests. -Logs: `chapter25/build/b12-review/{baseline,red,fixed}.log`. - -### Issue #251: dead-node printing corrected locally, 2026-09-19 - -[Issue #251](https://github.com/SeaOfNodes/Simple/issues/251) identifies dead-node -checks against a null `_inputs` array. Constructors and clones allocate the -array; `kill()` empties it without setting it to null. Line printers now use -`isDead()` throughout 7-25, including both columnar and LLVM formats in 10-17. -Chapter 25's whole-program printer uses the same predicate to omit dead -functions and nodes. Its bounds-safe input accessor simply checks the input -count. Printing does not prune the linker. - -`Chapter07Test.testDeadNodePrinting` in every snapshot 7-25 checks retained input -storage, the `DEAD` marker, and live inputless nodes. Chapter 25 also tests -omitting a dead function while preserving its linker entry. The regressions -fail against isolated original printers in 7, 10, 18, and 25. Full `make tests` -passed with assertions enabled in every affected snapshot, including 447 tests -in 25. The initial Chapter 17 test assumed an inputless Start; its setup was -corrected for that chapter's representation and its full target rerun. -Logs: `chapter25/build/issue251-review/{original,fixed,chapter17-fixed}.log`. - -### Issue #247: emulator 64-bit stores corrected locally, 2026-09-19 - -[Issue #247](https://github.com/SeaOfNodes/Simple/issues/247) identifies `st8` -writing the upper half with `st2`. The identical bug exists in EvalRisc5 and -EvalArm64 in every snapshot 21-25, starting with their introduction. Both now -use `st4` for the upper half, writing all eight bytes rather than leaving the -upper two unchanged. - -`Chapter21Test.testRisc64BitStore` and `testArm64BitStore` are carried through -21-25. They check individual little-endian bytes independently of the load -helper, round trips, positive/negative/extreme values, overwriting with zero, -and untouched neighboring memory. All ten cases failed before the correction -at the first unwritten byte (0xA5 instead of 0x23). - -Full `make tests` passed with assertions enabled in each snapshot 21-25, -including 445 tests in Chapter 25. Log: -`chapter25/build/issue247-review/fixed.log`. - -### B10 audit: still pending in Chapter 24, 2026-09-19 - -The fix is present in Chapter 25 (introduced with commit `51f1f8f4`), but has -not reached Chapter 24. Integer `nonZero` appears in 15; `_widen` first appears -in 24, so earlier snapshots do not need this backport. - -An assertions-enabled probe of the current compiled snapshots tested `[0,255]` -and `[-255,0]` at widening levels 0-3. Chapter 24 resets all six nonzero widening -levels to zero; Chapter 25 preserves all of them. Both pass refinement (`isa`), -idempotence, double-dual, singleton normalization, and zero-input checks. This -establishes the missing type-level backport; it is not a loop-failure reproducer. -Probe: `chapter25/build/b10-review/B10Probe.java`. No compiler correction was -made during this audit; B10 remains in the pending queue above. - -### B09: complete locally, 2026-09-19 - -Audited `Node.p(depth)`, recursive printing, labels, scope/graph viewers, and -assembly helpers. Corrections follow the first affected representation: - -- 11-24: scheduled IR display uses local `_idepth` state; 11-14 also use - identity maps to avoid setting Node hashes/GVN locks. -- 18-24: memory display reads raw lazy aliases instead of creating Phis; - scope display uses `Var._type` without resolving forward references. - 25's stale alias-based memory display reads its actual bulk-memory input. -- 19-23: printers use ordinary `link` and tolerate missing targets. Per Cliff's - review, type interning is allowed: the proposed non-interning linker scan and - `sameTarget` helper were removed. 24-25 use `CodeGen._link` to preserve dead - entries, including through 25's `funcName`; optimizing `link` still cleans up. -- 20-25: register display uses `_lrg` without compressing chains or rewriting - node mappings. Function predicates use the existing leaf `_isConstant` from - 23; integer size/value accessors also avoid entering shared recursion state. -- 21-25: pool display uses identity bookkeeping and recorded alignment, plus - recorded struct size from 22 and section choice in 25. Printers do not ask - Types for layouts, even when an answer might already be cached. Layout is - separate from type identity and may eventually move out of TypeStruct. - -Regressions cover graph caches/edges, lazy Phis, unresolved declarations, stale -linker entries, register chains, leaf type accessors, and forbidden layout queries. -Isolated original Chapter 18 classes fail the lazy-memory and forward-reference -tests; original printers also reproduce cache mutation, register compression, -shared-scratch assertions, and dead-linker pruning. Tests permit type interning. - -Every affected compiler snapshot (11-25) passed its full `make tests` target with -assertions enabled after the review adjustment, including 443 tests in 25. -The constant-pool regression rejects size/alignment queries even if they would -return cached answers. Log: `chapter25/build/b09-review/revised.log`. -Durable rules are in the skills notes. - -### B13 / issue #246: complete, 2026-09-19 - -Cliff reports the correction pushed. Nested negation in `p != null` / `!!p` -now discovers the underlying pointer guard in every snapshot 10-25, retaining -each chapter's guard representation. Chapters 23-24 also decompose short-circuit -Phis with availability checks; Eval2 in 18-24 now handles pointer/null Not. - -`Chapter10Test.testNullGuards` failed before the backport in 10-24 and passes -afterward; `testNullGuardErrors` checks rejected uses. Both tests live in every -snapshot 10-25. `testShortCircuitGuardScheduling` covers RHS-only call results -in 23-25; Chapter 25 also rotates optimizer seeds. Full `make tests` passed in -each directory 10-25. Ordinary test counts were 149, 164, 166, 181, 201, 212, -230, 282, 309, 350, 359, 374, 386, 403, 425 in 10-24; Chapter 25 passed 428 -total. No assertions or existing expectations were weakened. -Logs: `chapter25/build/issue246-review/`, particularly -`chapters18-24-fixed.log` and `chapters23-25-final.log`. - -Independent findings retained for follow-up: - -- B14's cyclic-equality failure remained reproducible despite green B13 - suites; it is corrected separately below. -- A pre-existing Chapter 25 diagnostic concern remains outside B13: with the - issue's nullable array setup, `if (p == null) return p.x; return -1;` was - accepted despite dereferencing null on the taken arm. Original setup: - `struct Point { int x; new Point = { int v -> x = v; }; };` - `Point?[] !points = new Point?[2]; points[arg] = new Point(42);` - `Point? p = points[1];`. This needs separate investigation. - -### B14: complete locally, 2026-09-19 - -Added the existing Chapter 25 leaf type-kind check to `Type.cycle_eq` in 23-24. -Cyclic structural equality first appears in 23. Inspection of 9-22 found the -older interned-child identity representation with kind checks in `Type.equals`; -the equivalent BOTTOM/float-struct meet probe passes in 22 without a correction. -No earlier compiler change is needed for B14. - -`TypeTest.testCyclicLeafKinds` in 23-25 interns same-named structs with `x` -typed BOTTOM, 1.0, and 2.0, then meets the latter two. Before the correction it -throws ClassCastException through `TypeFloat.eq` in 23 and 24; 25 passes. -Afterward all three pass, checking F32 field type, canonical interning, -commutativity, absorption by the BOTTOM-field struct, and the dual/join result. - -Validation with assertions enabled (Windows/Cygwin, JDK 21.0.4): - -- Unmodified baseline: `make -k tests CHAPTERS="chapter22 chapter23 chapter24 chapter25"` - passed each full target. -- After a full Java rebuild of 23-24, the focused regression and full TypeTest - passed in 23-25 (6 TypeTest cases in each). -- `make -k tests CHAPTERS="chapter23 chapter24 chapter25"` passed: 404 and 426 - ordinary tests plus their fuzzer wrappers in 23 and 24; 429 total in 25 - (365 raw0, 32 raw1, 8 standalone, 16 system, 1 fuzzer, 7 remaining). - -Logs: `chapter25/build/b14-review/{baseline,before-fix,fixed}.log`. - -### Build and fuzzer baseline, 2026-09-17 - -Windows/Cygwin, OpenJDK 22.0.2: all 25 chapters passed `tags` and `release`. -Build setup supplied missing early targets and corrected argument-list writes -in 22-24; no compiler behavior was backported. Chapter 22 initially failed -TypeTest with a TypeConAry/TypeRPC cast error, then passed a forced rebuild -(369 ordinary tests plus fuzzer), consistent with stale classes. - -The initial all-chapter test baseline failed Chapter 18's random fuzzer on -seed `973358943756616234`. At Cliff's request, wrappers in 8-24 now follow -25's explicit seed-list structure, and 8-15 invoke their wrappers from -`make tests`. Exploratory fuzzing is opt-in. All 25 full chapter targets -passed after these harness changes; 8-15 passed again after wrapper invocation -was added. Logs: `build/review/seed-lists-all-tests.log` and -`seed-lists-08-15-tests.log`. - -**The Chapter 18 compiler failure remains open.** Its seed is in -`OPEN_FAILING_SEEDS`, and explicitly running it still failed. Regression lists -started empty, so green default wrappers added no fuzz regression coverage. -Reduced input: - -```java -struct s0 { u8 v1; }; -s0? !UmPOLQK=null; -if(0) while(0&UmPOLQK.v1) {} -if(UmPOLQK.v1) {} -return new s0; -while(0) {} -``` - -This exposes two distinct concerns: nullable field lookup can fail during -parsing with peepholes disabled; with peepholes enabled it reaches a -mixed-return error. B11 isolates the latter further to -`struct S { u8 x; }; return new S; return 0;`, rejected in 18 and accepted in -19. Chapter 19 also changes the fuzzer comparison from peepholes off/on to -two optimizer worklist seeds. Neither that harness change nor B11 alone proves -the original seed fixed. Keep it open until the full seed passes. -Log: `build/review/chapter18-open-seed.log` (intentional failure). +This is a condensed completion record, not a list of current suite counts for +old revisions. Earlier detailed traces are in Git history; durable invariants +have been consolidated into the [AI notes](../skills/chapter25-codex-notes.md). +Unresolved reproductions have been promoted to the pending queue above. + +| Completed work | Scope and evidence retained | +|---|---| +| B14 cyclic leaf-kind equality | 23-24 backport; 25 already correct. Cyclic equality starts in 23. `TypeTest.testCyclicLeafKinds` fails before the fix; earlier interned-child equality already checks kinds. Full affected suites passed. | +| B13 / #246 null guards | Nested Not/null guard discovery from 10; short-circuit Phi availability in 23-24. Regression and full suites in 10-25 passed. Separate null-dereference diagnostic remains queued. | +| B09 and #251 debug printing | Side-effect-free print/accessor paths from their first applicable chapters; `_` diagnostic naming, type interning allowed, layouts forbidden. Dead nodes use `isDead()`, not null `_inputs`, from 7. Original-printer negative checks and full affected suites passed. | +| #247 emulator stores | Full eight-byte writes in ARM/RISC-V emulators, 21-25. Independent byte-pattern/overwrite regressions failed before the fix; all affected suites passed. | +| B12 x86 immediate multiply | Dedicated destination-register encoder in 21; 22-25 already had it. Low/extended-register encoding regressions and full 21-25 suites passed. | +| B11 optimized return checking | Remove parse-time meet in 18-24; 18 checks the optimized result type. Earlier separate Returns already accept dead mixed-type exits. Regressions start in 10/12; full 10-25 suites passed. | +| Dominator caches | Shared searches from 6; char depth in 6-17, separate char depth/version from 18, checked overflow and preserved clone fields. Inlining invalidation in 18-20. Full affected suites passed; dedicated bookkeeping helpers/tests were removed at Cliff's request. | +| GCM, constant cloning, isPinned removal | Shared scheduling from 11, per-function cloning of stacked constants from 18; preserve chapter-specific anti-dependencies and linked Parm ownership. Full 11-25 suites passed. Keep clone-based copyEmpty and non-final graph fields per review. | +| TypeFunPtr normalization | Trailing-default normalization in 23-24; 25 already correct. Only gather cases added; lattice laws and full 23-25 suites passed. Other `7f3b8856` representations remain in 25. | +| Inlining Return types and ARM execution | Live return expressions outlast a deleted inline entry (20-25); ARM register SUB, call instructions, frame byte counts/alignment, and vector growth (21-25 as applicable). Reduced negative cases and full affected suites passed. The earlier standalone ARM String/hash failure is resolved. | +| Native test reliability | Full process exit status, stdout plus successful execution, and distinct concurrent HelloWorld artifact names. Crashes no longer pass merely because output matches or an exit status narrows to zero. | +| Allocator progression 20-25 | Fixed cohorts, legality checks, native/emulated execution from 21, and same-compiler comparisons. Final results below. | +| Build/fuzzer harness baseline | All 25 chapter targets passed after build setup and explicit seed lists. Empty default lists were not exploratory fuzz coverage; the Chapter 18 open seed remains queued. | + +Final allocator audit, 2026-09-20, Windows/Cygwin with assertions: + +| Snapshot | Full suite | Spill report | +|---|---|---| +| 20 | 378 tests + fuzzer | 39 compilations, 238 / 357 | +| 21 | 408 tests + fuzzer | 91 compilations, 840 / 1,456 | +| 22 | 422 tests + fuzzer | 115 compilations, 838 / 1,496 | +| 23 | 444 tests + fuzzer | 145 compilations, 933 / 1,745 | +| 24 | 473 tests + fuzzer | 212 compilations, 1,332 / 2,956 | +| 25 | 468 tests across parallel groups, including fuzzer | 222 compilations, 2,597 / 5,579 | + +All full suites and final spill reports passed. The Chapter 25 run rebuilt +`sys.o`. Negative checks against saved original classes reproduce fixed-neighbor +bias, narrow-store masks, and ARM floating-memory encoding failures. The original +Newton ARM allocation also failed before the fix. The Chapter 21 coalescing-off +comparison passes all allocation/runtime checks but reports nine changed spill +goldens, intentionally exiting nonzero. The Chapter 25 earlier-ranking ablation +fails fresh library allocation, so its client-only totals are explicitly partial. +Logs and saved original classes: `chapter{20..25}/build/regalloc-final/`. |
