summaryrefslogtreecommitdiff
path: root/skills/chapter25-codex-notes.md
diff options
context:
space:
mode:
authorDibyendu Majumdar <mobile@majumdar.org.uk>2023-08-19 08:52:57 -0700
committerDibyendu Majumdar <mobile@majumdar.org.uk>2026-10-24 15:56:44 -0700
commita6dc6bea9072b5b632c4d698f2b8f876dbdeed8a (patch)
tree3e70a2eb5dde4d0a8f1a2200d280c66cc9243ed9 /skills/chapter25-codex-notes.md
downloadSimple-a6dc6bea9072b5b632c4d698f2b8f876dbdeed8a.tar.gz
Simple-a6dc6bea9072b5b632c4d698f2b8f876dbdeed8a.zip
Chapter 1: Introduction
Co-authored-by: Cliff Click <cliffc@acm.org> Co-authored-by: Thalia Archibald <thalia@archibald.dev> Co-authored-by: hadha <helsgithub15@gmail.com> Co-authored-by: XmiliaH <45106915+XmiliaH@users.noreply.github.com> Co-authored-by: Tamás Hadházy <85063808+Hels15@users.noreply.github.com> Co-authored-by: Jun Kawasaki <jun784@gmail.com>
Diffstat (limited to 'skills/chapter25-codex-notes.md')
-rw-r--r--skills/chapter25-codex-notes.md1095
1 files changed, 1095 insertions, 0 deletions
diff --git a/skills/chapter25-codex-notes.md b/skills/chapter25-codex-notes.md
new file mode 100644
index 00000000..22c832f6
--- /dev/null
+++ b/skills/chapter25-codex-notes.md
@@ -0,0 +1,1095 @@
+# Codex notes for Simple
+
+This file is intentionally AI-facing. It records durable invariants, debugging
+habits, and project-owner preferences that are easy to miss when reading only
+the implementation. Check current code and Git state rather than treating old
+session handoffs as live status.
+For cross-chapter work, read [the backport queue](../docs/chapter-backports.md).
+It owns pending work; these notes capture reusable lessons. Completed work and
+superseded validation history remain in Git.
+The [completed split](../docs/chapter-split.md) maps the former combined 24/25
+to 24a/24b (SCCP), 25 (incomplete types), 26 (construction), 27 (escape), and
+28a/28b (modules/separate compilation). The old combined snapshots are retired.
+Earlier entries below retain historical chapter numbers and test names; old
+Chapter 25 implementation references describe the lineage now in 25–28b, with
+the complete implementation in 28b. Use current paths and teaching boundaries.
+The construction/escape border remains open to semantic review.
+Future work belongs in the [roadmap](../docs/roadmap.md), including the separate
+[incremental build design](../docs/incremental-builds.md).
+For shared viewer work, read [the graph viewer notes](graph-viewer-codex-notes.md).
+
+## Collaboration preferences
+
+- For densely keyed integer maps, use indexed `Ary<T>` storage rather than
+ `HashMap<Integer,T>`.
+- Build with Make alone; Maven must never be required. Cliff prefers few tests:
+ avoid tests for every small viewer edit or rapidly changing display experiment.
+ Use focused existing tests and disposable probes when they answer a real risk.
+- Jig tests are temporary debugging placeholders. Keep every `testJig*` method
+ `@Ignore`, retain its scratch body, and exclude it from spill cohorts/replays.
+ They must not become benchmarks or regression tests just because their current
+ contents compile. Cohorts 23/24 now contain 24/58 allocations (199 total replay
+ entries), after removing all 15 frozen Jig entries.
+- Historical replay adaptations must preserve operand-valued short-circuit
+ expressions: replacing a `||` RHS with `!!rhs` changes its result. Compare
+ actual results as well as allocation legality when rewriting these fixtures.
+ Static `_spillScaled` also counts each inlined loop copy separately, without
+ credit for eliminated calls; inspect the optimized call graph before calling
+ a higher cross-chapter score an allocator regression.
+- Prefer concrete ArrayList to List, common base classes to unnecessary
+ interfaces, and short names (`pred`, `proj`, `def`, `use`). Avoid redundant
+ Objects.requireNonNull. Prefer int[]/AryInt to List<Integer>; avoid large boxed
+ ID collections (use arrays/BitSet or NBHML as appropriate). Node ID 0 means null.
+- Do not introduce Java streams. Use loops and explicit sorts; ask Cliff before
+ making any exception.
+- Cliff often debugs the same failure in parallel. If asked to work in a side
+ repository, update `tmp/` and do not edit the live files he is debugging.
+- Stop and discuss before changing a central lattice, constructor, memory, or
+ call-graph invariant if the required fix starts cascading. Reduced tests and
+ concrete node-number traces are preferred over speculative broad rewrites.
+- Printers are valid debugging tools. If printing changes behavior, fixing the
+ printer/accessor side effect is immediately high priority.
+- Keep edited text files LF-only, including on Windows.
+- When mixing `||` and `&&` in one expression, always parenthesize the groups
+ explicitly rather than relying on operator precedence.
+- For any node consuming or producing both control and memory, control belongs
+ in slot 0 and memory in slot 1; other inputs/results follow. This applies to
+ Start, Return, and future forward ports of Call, CallEnd, and other such nodes.
+- Preserve unrelated dirty changes. Emacs lock/backup files are common and are
+ not permission to clean the tree.
+- Do not push without explicit approval. Prefer commits at meaningful test
+ frontiers with messages describing the architectural change.
+- Give brief progress updates during long investigations and test runs. Cliff's
+ PowerShell UI can appear blank while Codex is thinking; report concrete
+ findings, the current check, and any blocker.
+
+## Mutability and chapter boundaries
+
+- Separate compilation retains the Modules parser's constructor local values
+ and escape fixes while adding saved IR and import ownership. Save owned nested
+ struct class declarations even when graph types omit them: callers need the
+ allocation and constructor signatures before bodies load. Preserve the nearest
+ file owner so imported children are not claimed by a compiled parent. The
+ library/client example checks three seeds and repeated native calls; native
+ callback exposure is also checked after saving and loading the library.
+
+- The four README intention questions are kept in
+ [the review agenda](../docs/chapter-backports.md#four-readme-intentions-to-review).
+ Final data fields are writable constructor locals, including in loops; final
+ local bindings and shared function declarations stay fixed. Do not classify
+ a constant instance default as a shared field or skip its loop Phi merely
+ because the published field is final. Class constants keep their static image.
+
+- The modules snapshot is source-only: LocalIds reserves class slots within one
+ invocation; ParseAll reads discovered parents and children; FinishCalls removes
+ analysis call edges before scheduling/selection. No Serialize, ElfReader,
+ GlobalBits or imported type reconstruction. Keep the constructor and native
+ exposure cases in BoundaryTest when extracting separate compilation.
+- A symbolic Store with alias 1 is a barrier to Load forwarding: until its
+ field resolves it may write the queried alias. Skipping it can turn a byte
+ compound assignment at 255 into a stale read of 255 instead of 2. Both
+ incomplete-types and construction keep this barrier; exercise worklist seeds.
+- Constructor fields stay in local SSA values and ordinary Phis. Emit ordinary
+ initializing Stores at helper exits and package precise aliases in MemMerge.
+ Private memory carries fields between helpers and into publication; it does
+ not carry each loop update. Keep converted field values in private summaries
+ and keep unwritten fields visible to completion checks. Constructor bodies
+ are parsed after the field list closes, so later fields also have local slots.
+ The discarded InitStore/InitPhi design must not be brought into the EA split.
+ Constructor reads may see an unfinished object, but never an uninitialized
+ field. Exact self aliases must read the current local value, including inside
+ loops, rather than loading the allocation before exit Stores have run.
+ Construction privacy follows language rules; global escape discovery is a
+ separate topic, with no required teaching order between them.
+
+- The escape snapshot carries function/alias exposure sets in TypeMem. Stop
+ feeds externally visible results/memory back to Start; SCCP restores unknown
+ callers only for exposed function identities. Their parameters use declared
+ bounds, never the narrower values inferred from known calls. External callers
+ already hold their object arguments, so writes through them expose addresses.
+ Native calls are exposure roots even when their path has no live Return.
+ FinalizeIR freezes this feedback before replacing call edges with emission
+ roots; the analysis cycle is not runtime CFG. Keep construction's local SSA
+ representation and early-self restrictions for the planned boundary review.
+
+- Native function addresses in AMD64 ELF use low-address tail-jump thunks.
+ Static fields and computed addresses must name the same weak entry across
+ objects; direct calls can still use the native PLT. A native FunPtr has no
+ local FunNode, so Encoding must check externFunc before the dead-function
+ identity fallback. Do not suppress relocation overflow checks or make class
+ storage read-only to accept a narrow dynamic relocation. Preserve constant
+ initialization Store folding. See StaticDataTest.testNativeFunctionIdentity.
+- Long-term pointer direction: compact code and data arenas, which may be
+ separate; full-width native pointers have separate types and lifetime rules.
+ Keep static class images viable for ROM and eventual removal of initialization
+ checks. These are future changes, recorded in the Chapter 25 roadmap; the
+ native thunk fix keeps current four-byte function/eight-byte data pointers.
+- The class-init-cycle project is closed. Cycle checking, diagnostics,
+ serialization/import handling, and runtime lowering are complete for the
+ agreed scope. Argument arrays and initialization optimizations remain
+ separate ROADMAP work; they do not keep this project open.
+- Native boundary decision (2026-10-18): no classes for native code; do not
+ design a native-startup policy. Ordinary supported FFI calls are separate.
+ Chapter 25's README owns the tutorial explanation of class initialization;
+ ROADMAP owns future work. Do not recreate an initialization progress document.
+- GCM memory-Phi anti-dependencies constrain paths whose input differs from
+ the load's memory, not paths carrying that memory unchanged. Bound the idom
+ walk by the memory definition's depth: an alternate Phi input need not be
+ dominated by that definition. Also stop at `defblk.idom()`: when the memory
+ definition is the function entry, its null idom is the valid sentinel.
+ Include the boundary depth; a strict `>` skips required same-block edges
+ and can make a read observe a later store. Keep `testCapitalize`,
+ `testConditionalClassInitX86`, and
+ `testReadBeforeConditionalStore` passing; dropping Phi handling entirely
+ miscompiles the latter (140 instead of 120).
+ This rule applies from Chapter 13 onward. The nested-branch regression
+ `SchedulerTest.testMemoryPhiAcrossSiblingBranch` checks the disjoint walk;
+ removing the depth bound reproduces a null-stblk crash. Retain each early
+ chapter's evaluator scheduling rules when porting the boundary.
+- CLInit lowering (2026-10-17): TypeCheck queues surviving markers; optional
+ Serialize saves opaque ideal IR, then Lower runs Iter before LoopTree.
+ Serialization includes disconnected Returns and filters cross-object links
+ without mutating the live graph. Parent initialization precedes the child's
+ flag test/call; the true flag store remains inside the initializer. Runtime
+ flag reads follow ordinary Load idealization, including removing control.
+ Handle x86 condition-flag lifetime constraints in the backend, not by pinning
+ ideal Loads. Generated calls remain unlinked and
+ conservative; do not reopen parameter flow into SCCP-optimized bodies.
+ Private/imported initializer bodies retain Start because opaque CLInit has no
+ ordinary Call edge yet; this does not change public reachability.
+ The selected entry gets the external arg; other clinits get zero for now.
+ The planned array ABI uses non-null entry args and null elsewhere; not yet
+ implemented. Eval2 shares singleton class objects and retains imported bodies.
+ Execution coverage includes native cold/hot calls, parent order,
+ ARM/RISC-V encoding, and nonreturning object reload.
+- Chapter 25 phase structure: Opto only runs SCCP, numeric-mode
+ resolution and worklist seeding. IterPostOpto does peepholes/inlining;
+ FinalizeIR does field layout/offset cleanup and function/CU finalization.
+ opto() requires an explicit prior iter(). Direct calls needing the old complete
+ optimization sequence also name iterPostOpto().finalizeIR(); driver() handles
+ the sequence for later targets. Iter setup, FinalizeIR, TypeCheck, Unlink,
+ and InstSelect own their algorithms in separate files. CodeGen's phase wrapper
+ handles assertions, timing (including failures), and successful viewer phase
+ notifications. Its driver lists the order and skips completed steps.
+ Serialize decides whether ideal IR is needed: explicit Serialize or file
+ Export, not Encoding or in-memory linking. A staged file export must request
+ Serialize before Lower; ideal IR cannot be recovered from machine nodes.
+ Unlink detaches imports for machine selection, including requests stopping at
+ Select, while ideal evaluation retains them. Schedule requires Unlink or Select;
+ use the driver instead of skipping Lower/LoopTree in hand-written test chains.
+ Earlier chapters expose only phases with real work. CodeGen and its resumable
+ driver start in Chapter 18, with explicit LoopTree and LocalSched boundaries.
+ Instruction selection arrives in 19, RegAlloc in 20, Encoding/Export in 21.
+ FinalizeIR starts in 22 with unused-function cleanup and gains layout in 23.
+ Opto (SCCP) and IterPostOpto start in 24; earlier peepholes are named Iter.
+ Serialize, Lower, and Unlink stay in 25. Use short explicit chains such as
+ parse().iter(), and driver(Phase.target, ...) for longer compilation paths.
+- Class-init memory checkpoint (2026-10-17) is implemented: keep CLInit
+ opaque through Iter/Opto, carry whole-memory effects plus the known-true
+ `$init` alias result, and use normal parser memory for redundant checks.
+ The flag store belongs inside the initializer. Parser creates the ready load
+ up front; ordinary Load/FldOff nodes resolve forward receivers. The fourth
+ CLInit input is always present in a constructed graph. If the receiver resolves
+ to an instance, CLInit and its speculative flag load disappear together.
+ Precise alias recovery can still be deferred. See `chapter28a/README.md`.
+ Runtime lowering is implemented above; argument arrays remain deferred. Public function fields
+ on instances of public classes must retain Start, just like class-object
+ fields. publicFIDX now checks both; this fixes sys.scan.match losing Start
+ while building sys.o and subsequently widening after import.
+ Do not weaken assertions or reset imported types
+ to conceal incorrect public classification.
+ Future optimization: move final, always-constant instance fields to the class
+ or fold them to constants and omit their instance storage. Access semantics
+ and public reachability remain unchanged; do not implement layout changes yet.
+ Frequency estimates belong
+ after Opto and lowering: function entry 1, unknown branches 50/50, estimated
+ loop trips 10, rare init/failure edges, non-null/null 90/10; bounded,
+ flow-consistent estimates shared by GCM and Encoding.
+- Class-init cycle validation (2026-10-14): reject an SCC only when it contains
+ more than one `<clinit>`. A self-edge or helper recursion with one initializer
+ is legal, regardless of inlining. Error reports list every internal edge of
+ the first rejected SCC, including helper calls; stack order is not a path.
+- Class-cycle diagnostics use `SourceError.format(CompUnit, byteOffset, message)`.
+ Start `file:line:column: error: message` in column one for Emacs compilation
+ buffers; put the source and caret on separate following lines. With no source,
+ report the path and saved byte offset, without inventing line/column numbers.
+ Lexer snapshots keep their original CompUnit through inlining and Call/CLInit
+ serialization; recover representative sites by rescanning only on rejection.
+ Source text is assumed available. Imported source paths are diagnostic data,
+ not `_smp` rebuild/discovery state. General error-message migration is deferred.
+
+- Constructors start in 16, independent binding/access permissions in 17a,
+ syntax sugar and inference in 17b. Historical Chapter 17 references mean 17b.
+- `!Point` is writable access, `~Point` deep read-only access; `!p` allows
+ rebinding, `~p` fixes a binding. Primitives default mutable; struct refs default
+ fixed/read-only. Arrays default writable and use `[~]` per layer. `var`/`val`
+ change only binding mutability, preserving initializer access.
+- Keep access in TypeMemPtr through named/cyclic type resolution. A read-only
+ load may observe writes through another alias; its memory edge is essential.
+ Writable array slots are invariant in element permissions. Allocation and
+ declaration spelling must agree on that element view.
+- Constructor initialization privileges apply only to allocation fields, even
+ inside nested blocks; they do not authorize writes to surrounding fixed locals.
+ In 23/24, a class-wide field needs an explicit initializer: implicit nullable
+ null alone must not turn an instance field into static storage.
+- Resolving a forward struct reference must preserve pointer access/nullability.
+ Phi idealization should report progress only when the referenced struct changes,
+ not merely because the canonical declaration has different pointer permissions.
+- In chapter25, complete named declarations as a group before
+ upgrading old graph types. Update stored declarations in New, Parm, Convert,
+ and similar nodes as well as their cached result types. A nested undeclared
+ struct must remain open, not become an empty finished definition. Unknown
+ expression types are distinct from explicit missing-initialization sentinels.
+- Construct cyclic pointers with their access bit already set before interning;
+ creating a provisional pointer and then changing access can orphan temporary
+ cyclic types. Serialized objects use the unversioned `C0DE` header; rebuild
+ objects after format changes. Backward compatibility is not required yet.
+
+## Tutorial backports
+
+- Shared ISA encoding lives in `isa/` for Chapters 21-25: x86 bytes behind
+ `CodeSink`, ARM/RISC-V word packing, and test-only evaluators. Chapter code still
+ owns machine nodes, register masks, operand/type interpretation, layouts and
+ relocations. Fix shared forms once there; keep their byte tests independent
+ of compiler IR and retain chapter execution tests. See `isa/README.md` for
+ the exact boundary. Only 21-25 add ISA main/test sources to their builds;
+ the linearized Maven build adds them at 21. Make's release jars still bundle
+ tests and their evaluators; Maven treats the evaluators as test sources.
+
+- Chapters 10-14 now introduce whole memory (10), lazy memory splitting (11),
+ references (12), GCM (13), and numeric types including floats (14).
+ Earlier notes using 10a/10b refer to today's 10/11. Float regressions are
+ named Chapter14FloatTest; reference tests are Chapter12Test and GCM tests
+ Chapter13Test. Every directory is a standalone compiler snapshot. Chapter 12
+ deliberately retains immutable shallow struct references resolved by name;
+ see its README's L0/L1 notes before trying to expand recursive types.
+ Keep Chapter 25's separate constructor-memory and incomplete-type design.
+ GCM readiness and anti-dependency checks must both
+ filter by alias and ignore MemMerge as a clobber; keep the evaluator's same
+ alias filtering too. MemMerge still needs ordinary data-dependency placement.
+
+- From Chapter 15, an array element Load's control stands for its range check.
+ Keep that edge during parsing and rewriting. A Load can push through a
+ memory Phi when its control is null or dominates the Region's idom; retain
+ that control on both copies. The Region itself or a later check cannot
+ supply control to predecessor loads. Dropping it permits an out-of-bounds
+ speculative read. The parser currently attaches the current control as a
+ placeholder, which can be the Region itself even in a valid graph.
+ `Chapter15Test.testArrayLoadControl` checks a guarded read after a memory
+ merge; `testArrayLoadDominatingControl` checks successful splitting under
+ an earlier common check. Length loads have no index;
+ Chapters 19+ already leave them unpinned, independently of element control.
+
+- Chapter 15 New consumes `{ctrl, $mem, size}` and produces `{ptr, $mem}`:
+ no control result, pointer slot 0, memory slot 1. Its TypeStruct/TypeMemPtr
+ defines the covered field aliases. Use a partial input MemMerge (null default),
+ one memory output projection shared across those aliases, and a whole-memory
+ aggregate preserving unrelated prior slices. Missing partial aliases are
+ uncovered, not zero or unknown memory. Typed alias queries follow New's input
+ and meet in each field's initializer; cached MemPhi types break cycles, and
+ explicit dependencies preserve worklist progress. Start's contents are empty
+ in this chapter because it has no heap arguments; revisit that in Chapter 18.
+ Both schedulers must follow aggregates to find New's effect for a read alias.
+ Only a proven distinct allocation permits Load to bypass New; unequal pointer
+ nodes alone do not prove disjoint objects.
+
+- The preferred direction is: introduce a fix in the earliest applicable chapter,
+ then use the same implementation in later snapshots where practical. Tutorial
+ progression takes priority over importing the fully general Chapter 25 solution.
+ Use a locally sensible fix when the general solution requires concepts not yet
+ introduced; report substantial representation changes for review.
+
+- Chapter 16 extends New inputs to `{ctrl, $mem, size, fields...}` while
+ retaining `{ptr, $mem}` outputs and partial alias coverage. Constructor values
+ determine alias contents; register dependencies on their nodes as well as
+ New. ScopeNode owns Var records and the single memory binding. The existing
+ sole-use Store-to-New guard is intentionally conservative with a shared memory
+ result; do not add alias traversal just to recover a small constant fold.
+
+- Chapter 18 keeps memory Parm 1 and CallEnd memory opaque: heap arguments make
+ the earlier empty-entry-heap assumption invalid. Local/return merges use
+ BulkMemPhi; calls carry the complete memory state and clobber all aliases.
+ MemMerge no longer tracks parser state. GCM can raise a Load before a Call's
+ terminating block position, but must not append anti-dependence inputs to
+ Call: the last input is its function pointer. The one-step Phi Load guard
+ stops at Calls. Eval2's New initializer indexing and tuple printer use ptr 0,
+ memory 1, size input 2, field inputs 3 onward.
+
+- Chapter 19 lowers nonzero initializers into explicit Stores. New has inputs
+ `{ctrl, $mem, size}` and results `{ptr, $mem}`; use zero field types for alias
+ contents queries, not constructor inputs. Both CPU selectors copy BulkMemPhi
+ and MemPhi with their alias metadata. GCM handles reads through MemOpNode,
+ including folded x86 arithmetic/comparison reads; selected TypeMem results
+ identify writers. Trailing anti-dependence edges on writers have no register
+ requirement. Direct machine Calls embed the target, but still must not gain
+ scheduling-only inputs. The one-step Phi Load guard remains unchanged.
+
+- Chapter 20 includes ARM and moves CodeGen to the codegen package. Memory
+ Phis and projections have no live range; the existing allocator already
+ excludes TypeMem. Preserve its inlining-safe Return compute and rejected-Phi
+ operand dependencies. Folded x86 Add requires a register value in slot 4 even
+ for a constant addend, because that value is also its two-address result.
+
+- Chapter 21 reverses addDep's direction: use `consumer.addDep(producer)`,
+ including explicit forward dependencies. New's register masks are cached in
+ the common node during allocation; size uses input 2 and pointer output 0.
+ Its GCM already places writers before adding read-before-write constraints;
+ preserve that chapter-specific rule. LoopNode.forceExit reconstructs memory
+ using actual MemPhi aliases and merges return memory with BulkMemPhi. Machine
+ execution tests must find main's encoded offset, rather than assuming PC 0.
+ Full-width x86 loads choose their instruction from the allocated register
+ bank, preserving integer bits when allocated to XMM. ARM's emulator needs
+ MOVK and full MOVZ immediate/shift support. Use current README measurements
+ for spill totals rather than historical memory-port baselines.
+
+- Synthetic never-exit TOP values remain in the graph for neutral type merges,
+ but have no runtime value to allocate. Chapters 20-25 exclude TOP definitions
+ and Phi arms from LRG construction, Phi-input liveness and spill copies; TOP
+ arms also do not contribute to spill loop-depth estimates. Register-bias walks
+ must stop at an input with no LRG. Do not give TOP a permissive register mask:
+ that still creates false liveness, interference and spills. Coalescing assumes
+ both ends of every actual copy have LRGs; fix the creation of bogus copies,
+ rather than adding a null guard there.
+
+- From Chapter 18, stop loop-tree discovery at each Return. Seed the function's
+ outer tree before the post-order body walk, since `forceExit` can add a return
+ Region and projections before the walk reaches the function entry. Those
+ exits inherit the function's outer tree; Never and the backedge projection
+ inherit the actual loop. Function bodies are depth 0, real loops add depth.
+ In Chapter 25, Stop/Start and StopCU/StartCU links describe external-world
+ type propagation, not executable loop backedges. Walking through them created
+ inconsistent depths and caused the RISC-V pointer-return spill-round failure.
+
+- Chapter 21's Load search is separate from the one-step factoring guard.
+ It creates no nodes or rewiring, but records dependencies and returns folding
+ witnesses. All arms of a non-loop memory Phi must succeed; a BitSet records
+ successfully checked merges. Only the original loop Phi closes an unchanged
+ path, and only for a loop-invariant pointer. Build value Phis after proving
+ profitability, and enqueue the root value Phi even when it folds locally:
+ self-edges otherwise keep an obsolete Phi alive outside the worklist. Reject
+ hoisting controlled array accesses. BrainFuck's program length now folds;
+ its RISC-V run loses 907 heap loads and all hot stack spills. Static move
+ counts initially rose from repeated initialization-store pointer copies;
+ adjacent-copy forwarding removes them. Measure executed memory traffic too.
+ This search is forwarded through 25.
+
+- Chapters 23-24 exclude class-wide (`Field._one`) fields from New's alias
+ coverage and keep symbolic field offsets until layout. Alias contents must
+ preserve cyclic field types and readonly information. Phi factoring must not
+ introduce a pointer/integer operand Phi even when the result is boolean.
+ A precise Phi must let its bulk input split before collapsing to it; inserting
+ a scalar-style upcast would hide the alias request from the bulk Phi.
+ Chapter 24 MemPhi skips both TOP and XCONTROL predecessor arms during SCCP;
+ New's outputs and alias-contents query stay high while its control is high.
+ Check registers before encoding, which may add untyped branch nodes.
+
+- Chapter 25 Load search follows Escape to private/public memory; New has no
+ public-memory input to bypass. Alias 1 on a Store is unresolved, so it blocks
+ the search. External storage cannot use allocation disjointness. Array length
+ Loads can drop control, while element Loads retain it. New integer value Phis
+ start at i64 and use Convert at the result to preserve the declared width;
+ decline hoisting when that would lose a sharper inferred range. Queue every
+ new value Phi, including nested merges, and queue new precise memory Phis
+ instead of eagerly folding them while bulk splitting is still in progress.
+ User-count dependencies must wake from Node.delUse, including unkeep.
+
+- Generalized Phi factoring now starts in 10 and is forwarded through 25.
+ Memory-specific eligibility lives in `MemOpNode.canDrop`. Store identity
+ equality now excludes Stores from this rewrite. Load owns `clobbered`: all
+ these chapters check only immediate memory users, without recursion or a
+ visited set. Stop at Stores, Phis, MemMerges (11+), New (15+), and Calls (18+);
+ do not chase aliases or aggregates just to save one Load. Keep this teaching
+ optimization simple and conservative.
+ Preserve exact operation attributes with clone-based `copyEmpty`, type each
+ operand Phi separately, reuse identical inputs, and reject type widening.
+ Loads inspect memory users for clobbers before GCM has made anti-dependence
+ edges. From 11, create precise memory
+ Phis for the factored operands. Do not factor aggregates or bulk Phis;
+ wait for other BulkMemPhis at the Region to disappear, since their slice
+ lookup assumes Region/alias identifies the completed memory point. `delUse`
+ wakes recorded dependents for user-count queries; Region's empty-diamond fold depends on
+ projection rewiring as well as projection types. These guards are already
+ forwarded through 25; preserve the later chapters' existing `copyEmpty`.
+ `subsume` must wake each rewired user's recorded dependents, just as `setDef`
+ does: operand identity changes can enable a Phi fold without changing the
+ intervening operation's type (`Chapter14Test.testCloneAnd` in 16).
+- Stores use identity equality from Chapter 10 onward. Equal Stores on two of
+ three paths must not merge and run on the third path. Sharing requires the
+ original Stores to cover every exit path, or GCM to split the shared Store
+ back onto those paths. GVN checks neither, and GCM does not split Stores.
+ This also disables generic Phi factoring of Stores, but keeps explicit
+ store-after-store elimination and the closed-loop sink introduced in 17b.
+ `Chapter10Test.testConditionalStoreSharing` returns the object to expose the
+ actual write; returning a Load could hide it through forwarding.
+ `Chapter17bTest.testSinkLoopStores` checks exit placement and zero-trip values
+ in 17b and every later snapshot. Do not restore Store GVN to recover a fold.
+- Find the chapter where the relevant feature first appears, not just the chapter
+ that can parse the original reproducer. Reduce away later syntax/features when
+ possible. Put the regression in that earliest `ChapterNTest.java`, and forward
+ port it into the same test class in every later affected chapter directory.
+- Every directory contains its own compiler snapshot. Testing Chapter 25's
+ inherited `Chapter10Test` does not validate the Chapter 10 compiler. Use the
+ top-level runner, e.g. `make -k tests CHAPTERS="chapter10 chapter11 chapter12"`, with all
+ affected directories explicitly listed. Establish the destination baseline
+ before changing it; older Makefiles may need a forced rebuild after API changes.
+- Keep unrelated discoveries separate in `docs/chapter-backports.md`. Completed
+ fixes leave the pending queue; record validation in the change description.
+ Keep unresolved discoveries in the actual queue. A green suite
+ after changing graph/test order does not prove an intermittent failure fixed.
+- Store disposable probes/logs under an appropriate ignored build directory.
+ Preserve durable reproducers in tests or the review record, not only scratch
+ files. The B13 `chapter28b/tmp` probes were removed at Cliff's request after he
+ reviewed and pushed the fix; future sessions must not rely on those files.
+
+## Dominator caches and searches
+
+- Real dominator searches start in Chapter 6 (If and Region), before CFGNode
+ appears in 13. Regions recompute their dominator from current predecessors
+ using the shared `domLCA` walk; do not cache a dominator pointer just because
+ the old node is still alive. Rewiring a merge can change its dominator.
+- Chapters 6-17b cache a `char` depth with zero meaning unset. From 18, inlining
+ requires a separate `char` cache version and a checked global version bump.
+ Keep depth/version as separate fields rather than packing arithmetic into an
+ int. Assert before narrowing a depth or incrementing the version: 65535 is
+ the largest representable value, and wrapping must fail clearly.
+- Copy both cache fields when copying a CFG node. Ordinary `Node.copy()` clones
+ already preserve them; CFG copy constructors must do so explicitly. Inlining
+ invalidates afterward, but future local CFG transforms may preserve valid
+ caches outside their edit region.
+- Every cached depth override, including Region, Loop and Stop, participates in
+ version validation. Start has fixed depth zero; an unfolded function is a
+ root, while a folding function uses its new caller-side depth. Preserve the
+ chapter's dependency direction and dead-predecessor rules when sharing walks.
+- Cliff requested removing the dedicated dominator bookkeeping tests/helpers;
+ do not restore them without a substantive failure to cover. Keep existing
+ printer cache snapshots checking both fields; cast char depths to int for
+ numeric display.
+- Keep single-return accessors and overrides compact on one line, including
+ `idepth()` and `validIDepth()`.
+
+
+## Global code motion and constant ownership
+
+- GCM starts in 13; function-local constant graphs start with functions in 18.
+ Use an early definitions-first walk and a late uses-first worklist. Visit
+ Region/Loop Phis during early scheduling, and wake loop Phis and waiting loads
+ during late scheduling. The two walks must agree: otherwise late scheduling
+ can reach unscheduled arithmetic through a loop Phi. For ArrayList-backed
+ outputs, walk the original index range when early scheduling appends uses.
+- `isPinned()` is unnecessary: early scheduling preserves an existing input 0
+ and computes a block only for nodes with a null input 0, after walking their
+ inputs. Proj input 0 names its producer, so it must not be replaced by control.
+ For ordinary values, existing control is an earliest-placement bound; late
+ scheduling can still move them downward. Chapters 13-14 retain their fixed
+ late-placement cases explicitly in GCM (Proj, New, Parser.ZERO, and Cast).
+ Later GCM already handles fixed CFG/Phi/Proj placement structurally.
+- After early scheduling, global constant-building operations have Start in
+ input 0. Snapshot and keep those originals, then clone their input graphs with
+ one identity map per function. Reuse each copy throughout its function; keep
+ originals intact until all users are rewritten. This covers Cast/Constant
+ stacks and machine expansions without recursively inferring users' ownership.
+- `Node.copyEmpty()` makes an exact-class clone with fresh ID and empty edges.
+ GCM wires it with `addDef`; do not use machine `copy()` overrides here, whose
+ rematerialization contracts differ about whether input edges are registered.
+ Preserve ordinary copy/cache fields; clear graph edges, dependencies and hash.
+- In 18-19, calls remain linked: a Parm input is evaluated in its caller, and
+ the unknown-caller input stays global. From 20, unlinked Parm values belong to
+ the callee. Preserve 25's compilation-unit ownership checks.
+- Let early scheduling place machine constant expansions. Pinning them during
+ instruction selection can attach them to the old ideal Start rather than the
+ selected Start, forcing later code to recover from the wrong root.
+- Keep anti-dependence marks in a pass-local array, not CFGNode. In 13-20 the
+ evaluator independently reschedules nodes, so retain the full store placement
+ range when constraining loads; using only GCM's final store block breaks
+ `SchedulerTest.testStoreInIf2`. Retain the dominator walk from 21 onward too:
+ a conditional Store can precede a Load's late block without dominating it.
+ Its exact block need not be marked, but an ancestor intersects the Load's
+ placement range. `Chapter21Test.testReadBeforeConditionalStore` checks this.
+ Preserve each chapter's alias/tuple memory representation when finding stores.
+- Changes in node order can expose encoding bugs. Chapter 21's empty-block scan
+ mistook entry to a nested loop for a backedge; require the same loop-tree node,
+ as in 22 onward. Native Sieve is sensitive to this and must actually execute.
+- Test function-local chains with two surviving function bodies. In 24, returning
+ function pointers alone does not put their bodies in the scheduled graph;
+ recursive calls keep this regression meaningful. Check all chain members,
+ registered data edges, and one shared copy per function.
+
+## Register allocation review
+
+- Phi self-conflict splitting must isolate inputs shared with another Phi,
+ not only the entry input and result. Two loop Phis sharing a backedge value
+ otherwise stay in one conflicting LRG across every round. The correction
+ starts in 20; 24-25 retain their first cold-only attempt.
+- Chapter 25's cloned-call relinking must visit old nodes in node-ID order.
+ Iterating the clone IdentityHashMap changed call edges and optimizer worklist
+ order across JVMs. The SystemV sys build reproduced on Windows with
+ `-XX:+UnlockExperimentalVMOptions -XX:hashCode=2`; WSL was not essential.
+ When investigating reproducibility, compare phase dumps before blaming the
+ allocator or target ABI. Even diagnostic identity maps can perturb JVM hash
+ assignment and expose an existing dependence on identity iteration order.
+
+- Allocation starts in 20; native encoding in 21. Backport legality and
+ no-progress fixes to the first applicable chapter, but introduce spill-quality
+ heuristics gradually. Do not import the complete Chapter 25 allocator into 20.
+ Printing, deterministic ordering, and diagnostic support may start in 20.
+- Compare measured spill totals over the same tests, targets/ABIs, and seeds.
+ Local increases are acceptable when offset elsewhere; report per-target and
+ overall totals. `_spills` counts surviving SplitNodes, and `_spillScaled`
+ weights those moves by `8^loopDepth`; neither means only memory stores.
+- Use a fixed optimizer seed for routine allocator/scheduler/encoder validation.
+ Seeds shuffle IterPeeps/Opto worklists; optimization is intended to normalize
+ to the same graph modulo node IDs and equivalent operand orderings. Repeating
+ the backend on essentially the same graph adds little coverage. Reserve seed
+ sweeps for investigating optimizer normalization, missing dependencies, or a
+ demonstrated order-sensitive failure; inspect post-Opto graph differences
+ before expanding backend runs. Prefer distinct programs, register constraints,
+ targets/ABIs, and actual execution for backend coverage.
+- Separate allocation completion/register legality/runtime results from quality
+ expectations. Reaching the round limit is a correctness bug; raising the
+ limit or changing spill goldens does not prove progress fixed. Heuristics
+ which defer necessary splitting must retain a bounded fallback.
+- Review the README with each allocator chapter. End it with a RegAlloc
+ improvements section, a measured matrix, and commentary. Rows are fixed
+ program cohorts, columns are compiler chapters through that directory, and
+ cells are scaled spills. Include one test-count column, no totals row, and
+ leave cells before a cohort's introduction blank. The full matrix lives in
+ [regalloc-spills.md](../docs/regalloc-spills.md). Root `make spill-stats` uses
+ `build-support/spill-matrix.py` to rerun and validate membership; its
+ `--reuse --update-docs` mode refreshes the marked tables. Include the BrainFuck and
+ MergeSort allocator cases in the Chapter 20 cohort (39 compilations, not just
+ Chapter20Test's 33). Keep diagnostic machine graphs separate from quality data.
+- `make spill-stats` starts in 20. Helpers explicitly name the program cohort;
+ the JUnit listener labels individual cases and sums CPU/ABI and cohort totals.
+ Assertions stay enabled and failures propagate. Do not silently change cohort
+ membership, seeds, or targets when comparing chapters.
+- The staged review is complete through 25. Keep 21's shortened README and its
+ separate encoding reference. The final narrow-store mask fixes changed the
+ measured totals for 20 and 21; use the current READMEs, not older review counts.
+- Chapter 21 changed several inherited inputs/ABIs. Its Chapter20Test now freezes
+ all 13 original programs; Chapter21AllocTest retains the four revised cases,
+ counted with the native variants as cohort 21. On Windows the cohorts are
+ 39 and 52 compilations. Preserve source/target membership when moving forward.
+- Chapter 21 adds adjacent-copy forwarding after coloring, retained through 25.
+ A Split with exactly one use, in the next scheduled instruction, can be removed
+ if the operand accepts the source register and is not two-address tied. Exclude
+ Phi/CFG users. Adjacency supplies the no-clobber proof; do not expand this into
+ CFG lifetime partitioning. Chapter 20 keeps its simpler baseline.
+- Chapter 22 adds stronger copy-chain/backedge bias and cheap-spill ordering;
+ popular-use grouping remains for 23. Preserve `person21` (64-bit age) separately
+ from 22's narrower person example, and keep its revised infinite-loop input in
+ cohort 22. The Windows cohort counts are now 39, 52, and 26; the two added C return
+ ABI checks have zero moves.
+- Chapter 23 groups popular single-def uses by compatible register masks. One
+ intersection pass suffices: narrowing a class preserves its earlier users,
+ and newly added classes are disjoint. Snapshot distinct users before rewiring,
+ ignore null register masks, and count a call once despite repeated arguments.
+ The grouping correction/regression is already forwarded through 25.
+- Chapter 23's current cohorts count 39, 52, 26, and 30 allocations. Preserve
+ `stringHash21` (with the old guard); its revised source and newly enabled Jig
+ tests belong to 23. Grouping on/off gives identical 933 raw / 1,745 weighted
+ moves here; report the zero gain rather than implying every heuristic helps
+ this suite. The earlier-cohort increase includes changed optimizer behavior.
+- Chapter 24 gives loop-Phi self-conflicts one cold-only splitting attempt;
+ remember Phi identity across rebuilt LRGs and force the ordinary fallback on
+ the next conflict. A backedge-only attempt can make no graph change and must
+ still consume that allowance. The same regression runs in 24 and 25.
+- Chapter 24 preserves cohorts 20-23 and adds 67 allocations for cohort 24.
+ Distinguish moved inline sources from changed programs: keep the old 64-bit
+ Person and small emulator argument list, and attribute revised inputs to 24.
+ Cold-first splitting saves only 6 raw / 6 weighted moves here; the much larger
+ cross-chapter reduction includes optimizer changes. Its frozen String program
+ also found a lost null guard in the inlining dominator walk; keep that fix on
+ both sides when measuring the allocator heuristic.
+- Validate the instruction masks independently of the chosen register: a broad
+ mask can hide behind favorable color bias. Byte/short x86 and RISC-V stores
+ must exclude floating-point registers. Size fields such as `_sz` may hold a
+ printable character: compare to `'4'`, not integer `2`. The narrow-store
+ restrictions are now present from x86 20 and RISC-V 21 onward.
+- The historical Chapter 22 seed sweep exposed failures outside allocation;
+ it is not a template for routine allocator validation.
+ Chapter 22's frozen String source fails before allocation and its returned
+ function-pointer case fails during relocation at seeds 0-29, on all targets.
+ Both reproduce in the original snapshot; report these failures explicitly.
+ Do not replace the seed or add a return merely to make the sweep green.
+- Use an ablation in the same compiler to measure each staged heuristic. Native
+ ABI/lowering changes make the Chapter 20 and 21 totals different even with the
+ same sources. Include the same legality fixes on both sides. Mask corrections
+ can substantially change coalescing results; refresh old tables and commentary.
+- Chapter 21's spill reporter defers quality-golden failures until reporting ends,
+ so all target/runtime checks still run. The command must still exit nonzero.
+ Keep full native process exit codes and check each emulator's own result memory.
+- An ARM store of Top can originate in the optimizer, before allocation. Inlining
+ deletes the function entry before its Return necessarily folds away; compute
+ the return type from its live inputs. Never infer unreachable return data from
+ `_fun.isDead()` alone. This correction is now present in 20-25, with a seeded
+ interpreted-result regression; 20 first uses linked Return types at call ends.
+- Validate beyond encoding. The repaired seed-9 String case then exposed missing
+ register-register SUB support in the ARM emulator (now corrected in 21-25).
+ Check real results and unchanged flags; successful allocation/encoding alone
+ does not establish execution correctness. The 1,170-case Chapter 21 seed sweep
+ now passes through encoding, and the original ARM/RISC-V hash results agree.
+- For ARM execution probes, finish in-memory export so external relocations
+ (especially calloc) are patched, then select the intended function entry.
+ Encoding alone leaves placeholder branches. Check return PC, SP, callee-saved
+ registers, and the result; passing an instruction budget is not completion.
+- ARM call/frame fixes are present in 21-25: ADD/SUB immediates are unsigned
+ magnitudes, frames use byte counts and 16-byte alignment, BL writes only X30,
+ and RET names X30 explicitly. Test instruction words independently of the
+ compiler so matching encoder/emulator mistakes cannot validate each other.
+- Copy cleanup/reuse must check kill masks even when an instruction has no LRG;
+ before coloring, a fixed-register definition can clobber despite `_reg==-1`.
+ Cloning is valid only if its output mask can satisfy the use. A fixed-register
+ clone with flexible uses needs use-side splits, not a no-op def-side path.
+- Chapters 20-25 flag spills use specialized splits that exclude FP registers.
+ x86 can transfer FLAGS directly to a GPR or stack slot. ARM's
+ `SplitFlagsARM` transfers NZCV through a GPR and overrides
+ `isNonStackSplit()`; RegAlloc includes these transfers in loop-depth
+ collection and subsequent splitting. `SplitGPRARM` then admits GPRs and
+ stack slots, preserving the FP exclusion through cascading spills.
+ Keep ordinary splits excluded from those allocator paths. Test both flags
+ clobbers and simultaneous flags/GPR clobbers: `testFlagSpills` executes the
+ allocated moves for all 64 x86 status-bit and 16 ARM NZCV combinations from
+ Chapter 21 onward. Chapter 20 checks allocation and masks only; encoding
+ begins in 21. Preserve each allocator's existing ordinary-split policy:
+ 20-22 always skip ordinary splits here, while 23-25 skip them when min!=max.
+- Check function ownership and register masks immediately after allocation,
+ before encoding rewrites tail calls or adds untyped branches. Call.regmap can
+ itself depend on CFG.fun/idom. The test-only CheckedCodeGen override keeps
+ these checks at the right phase without changing the driver; in 25 this also
+ preserves ideal-graph serialization/import unlinking before instruction selection.
+- Shared correctness/support can be forwarded as one review batch, while leaving
+ each chapter's quality heuristics and historical cohort/README review staged.
+ Compare each destination's unchanged local suite before/after for such a batch;
+ do not present those unequal local suites as the chapter progression table.
+- Chapter 25's spill corpus preserves the earlier 212 source/target entries,
+ with documented constructor, receiver, library and mutability adaptations.
+ These are allocation/legality replays, not historical runtime harnesses. Its
+ own cohort includes current Chapter25Test allocations and a fresh library
+ encoding. See `chapter28b/src/test/java/com/seaofnodes/simple/spill/README.md`.
+ Do not silently use current inherited tests as though their sources were frozen.
+- Rebuild and measure the system library even if sys.o is up to date. In the
+ final 25 review, the older ranking and a multi-use-clone preference looked
+ better on clients but exhausted allocation rounds on fresh sys compilation.
+ Both trials were rejected. Keep the existing area/cost ranking; client-only
+ totals do not prove whole-suite improvement. The driver uses seed 456, old
+ cohort replay uses 123, and current test helpers normally use 126.
+- The 2026-10-07 targeted exception prioritizes single-use function addresses
+ separated from their use in the same block. This avoids adding callee-save
+ copies to hold rematerializable constants and passes fresh Win64/SystemV sys
+ builds. It does not restore the old broad ranking or prefer multi-use clones.
+ Eleven historical fixtures now mark local helpers/classes private; the fixture
+ README records this baseline change. Keep public export/API cases public and
+ separate visibility gains from same-source allocator measurements.
+- `_approxUIDs` is a parse-time count, not the optimized inlining size. The
+ early size rejection blocks `_hashCodeString` (100 estimated / 25 live) and
+ `_merge` (182 / 54) in the private fixtures. Removing that gate in a shadow
+ build inlines both, but ordinary-function policy stays unchanged. Cliff has
+ deferred deterministic best-call selection and per-convergence size caching
+ until after splitting Chapter 25. Do not resume that redesign as routine cleanup.
+- The interim inliner now uses a bounded 15-node boilerplate recognizer for
+ oversized parse estimates, replacing the earlier 1,000-node constructor
+ allowance. Walk uses from Fun through Parm/Store/MemMerge to Return, bounding
+ fanout and stopping at Return; scanning sparse MemMerge inputs would defeat
+ the bound. Watch Return inputs and the inspected prefix for later folding.
+ Normal body limits remain 100, or 200 for `<new>`/instance `<init>`, including
+ copyBody's assertion. Six Bubble Sort constructor calls fold despite parse
+ estimates 276-479 (also checked at 10,000); their live bodies are 11-14 nodes.
+ Public definitions remain emitted. Costs match the larger-allowance trial
+ (cohort 24 -3, cohort 25 +33 relative to private-helper changes), so do not claim
+ a general RA improvement. Full Make tests and fresh Win64/SystemV builds pass.
+ Chapters 20-24 now reuse the bounded recognizer with restricted straight-line
+ Store/MemMerge copying, substituting arguments/control directly. A separate
+ inline worklist runs after shrinking peeps settle; ordinary single-caller
+ inlining remains in idealize. Preserve Chapter 20's reversed dependency API
+ (`observed.addDep(dependent)`); 21 onward use `dependent.addDep(observed)`.
+ These chapters expand constructor defaults directly and have no parse-size
+ gate. Their full suites and three-seed store probes pass; spill records are
+ unchanged. This does not implement the deferred general Chapter 25 redesign.
+- ARM load/store opcodes must follow the allocated register bank, not the
+ source Type. Emit the SIMD bit for both addressing modes, normalize D0-D31
+ to register numbers 0-31, and permit FP registers only at supported widths.
+ A GPR-only float-store mask made 25's old Newton programs repeatedly split
+ the wrong side of a loop. The paired fixes start in 21's executable backend.
+ Emulator FP memory operations must preserve bits and scale double offsets
+ by eight. Use independent instruction words as well as compiler round trips.
+- Fixed-neighbor bias must clear the computed register, including a neighbor's
+ sole allowed register before it is colored; `_reg` can still be -1. The shared
+ correction and regression start in 20. A passing corpus can miss this path.
+- Chapter 25's extra multi-def/fixed-use thresholds and kill/rematerialization
+ choices remain local strategies. Earlier splitEmptyMaskSimple lacks the extra
+ branch that needs 25's original-def-use-count snapshot. Do not label a textual
+ difference a missing correctness fix without establishing the affected path.
+
+## Null guards: B13 / issue #246 lessons
+
+- Null refinement starts in Chapter 10; arrays in 15; scoped expression guards
+ in 17; short-circuit syntax in 23. Chapters 10-16 refine local bindings with
+ casts/constants, 17-24 use scoped CastNode facts, and 25 uses GuardNode with
+ incomplete types. Before Chapter 13, the early-return regression needs an
+ explicit `else` to get the false-arm refinement.
+- `p != null` can become `!!p`. Control simplification may strip both negations
+ while guard discovery still misses non-null `p`. Compare the guarded load's
+ input after parsing, not just the simplified If predicate.
+- Existing code already handles one Not. Recurse for nested Not (or Not of a
+ short-circuit Phi where supported), flip the proven truth, and preserve the
+ Boolean value. Unconditionally duplicating single-Not guards caused a Dijkstra
+ SCCP assertion during B13 development; that approach was discarded.
+- Keep the predicate alive across recursive peepholes (`keep`/`unkeep`), and
+ separate recursive guard discovery from the public guard-set marker. Otherwise
+ temporary predicates can die or recursive calls can disturb guard removal.
+- Short-circuit refinement must not guard an RHS-only value where it is not
+ available. Chapters 23-24 use `availableAt` with existing CFG dominators;
+ Chapter 25 uses its existing `earlyCFG`/dominance machinery. Do not import this
+ machinery into Chapter 10 merely to handle nested negation.
+- `Chapter10Test.testNullGuards` and `testNullGuardErrors` cover positive and
+ rejected uses throughout 10-25; `testShortCircuitGuardScheduling` adds RHS-only
+ call-result coverage in 23-25. Check both runtime outcomes and scheduling, not
+ just successful type checking. Eval2's Not must handle pointers/null as well as
+ numeric zero; B13 ported that helper from 25 to 18-24.
+- As of the 2026-09-19 review, B13 is complete and Cliff reports it pushed.
+ The separate B14 cyclic-equality failure had a deterministic type-only
+ reproducer despite green B13 suites; see the type lessons below.
+
+## Central parser rule
+
+Simple parses directly to SSA without an AST. Forward declarations mean types
+can be incomplete during parsing. The governing rule is:
+
+> Syntax and resolved lexical binding choose SSA topology. Types may sharpen,
+> optimize, and reject the graph, but provisional types must not choose its
+> shape.
+
+Consequences:
+
+- `compute()` must accept the weakest legal inputs and remain monotonic.
+- Parser-only lower bounds such as the old Load/Phi `_con` fields are suspect.
+- Calls keep syntactic shape. `ptr.field(args)` supplies `ptr` in the hidden
+ self slot even before the eventual function kind is known.
+- Numeric operations carry an unresolved mode until graph evidence settles
+ integer versus floating behavior.
+- A Load flow type is not guaranteed during parsing. Static declaration facts
+ may instead be recovered from symbolic field offsets or lexical `Var` state.
+
+## Constructors
+
+- A user constructor is declared `new N = { args -> body }` and invoked as
+ `new N(args)`.
+- Allocation first calls the hidden private `<init>`, then the user constructor.
+- Constructor hidden arguments are public memory at index 1, self at index 2,
+ private self-memory at index 3, followed by user arguments.
+- Constructors return private self-memory. `EscapeNode`s publish it into public
+ memory after construction.
+- The hidden initializer supplies defaults/poison. User constructors must set
+ non-defaultable fields on every exit before the object escapes.
+- Constructor field state is tracked in parser `Var` metadata. Early reads of
+ possibly-uninitialized fields are errors.
+- Check constructor completion in the parser, independent of allocation and
+ optimized memory. Save each constructor's merged exit field types by Var
+ identity and validate against the complete declaration at struct close.
+ No user constructor means an implicit empty constructor using declaration
+ defaults; a non-returning constructor has no completion obligation. Nested
+ blocks retain the enclosing constructor context, but nested functions do not.
+- Recursive constructors may reasonably be rejected; constructor chaining must
+ preserve private-memory initialization state.
+
+## Memory and alias invariants
+
+- The parser carries one ordinary bulk-memory value. Precise alias partitioning
+ belongs to the graph and optimizer, not nested parser Scope state.
+- Alias `#1` is bulk/unresolved memory. A precise alias is `#N`, with `N != 1`.
+- Precise memory shapes always use `MemPhiNode`; bulk memory slices use
+ `BulkMemPhiNode`. Plain `PhiNode`s do not represent memory. If a memory bug
+ seems to require generic Phi memory state, suspect a representation/worklist
+ bug instead.
+- `MemMerge` inputs are disjoint slices whose union covers all memory. Its
+ default input means all aliases not explicitly split out.
+- `BulkMemPhi` carries `All - exclusions`; parallel `MemPhiNode`s carry the
+ aliases in the exclusion set.
+- A precise consumer may select `MemMerge.alias(N)`. A bulk consumer may not
+ silently replace a `MemMerge` with `alias(1)` because that discards every
+ precise side effect.
+- In particular, Store-after-MemMerge bypass is valid only when the Store
+ already has a precise alias. This is covered by Chapter25Test's chained
+ `grow(1).buf[len++]` regression.
+- When an unresolved Store sharpens, rebuild a precise Store plus `MemMerge`;
+ do not mutate a whole-memory Store in place after users have treated it as
+ the complete memory state.
+- Stores retain control when it represents conditional execution. Removing
+ the control from the Store after an early return can make the write execute
+ unconditionally.
+- Store width is semantic state (1/2/4/8 bytes, with 0 unresolved) and comes
+ from the target field declaration, never from the stored value's GLB or the
+ removed parser constraint.
+
+## Types and monotonicity
+
+- Integer widening starts in Chapter 24. `nonZero()` preserves `_widen` when
+ narrowing a range. B10's correction is in 24, with `TypeTest.testNonZeroWidening`
+ in 24-25. Both now use widening 0-3 and dual widening `3-widen`, preserve
+ singleton widening, and give TypeConAry the same widening coordinate. Loop Phis
+ widen only below-center nonconstant ranges: above-center widening can fall to
+ an unwidened constant, violating SCCP monotonicity. Existing Chapter14/21 tests
+ exposed this during the backport.
+- Recursion needs two widening points in Chapters 24-25: ParmNode for arguments,
+ and CallEndNode for results. The return cycle in `f(x-1)+1` never crosses the
+ Parm. Keep the comments at both sites and PhiNode cross-linked. Chapter 25's
+ FunPtrNode follows the live Return range; join that range before widening,
+ then bound widening by the callee signatures, not that live range. Apply the
+ same rule before and after linking, and retain narrow results such as i32.
+ `Chapter24Test.testRecursiveWidening` checks both cycles in both chapters.
+- TypeConAry's `_make(any,widen)` shares the backing array and returns an
+ uninterned type. `xdual()` must leave interning to Type.intern's dual-pair setup;
+ ordinary factories and ymeet's widening replacement intern their results.
+ Chapters 24-25 use this API and a distinct array tag. Chapters 22-23 only receive
+ compatible cleanup; their sentinel-based representations predate widening.
+- Cyclic structural equality starts in Chapter 23. Chapters 9-22 compare
+ interned children by identity and check type kinds in `Type.equals`.
+ In recursive equality, leaf dispatch must also check `_type` before calling
+ subclass `eq`: that method assumes matching kinds. B14 adds this check in
+ 23-24; 25 already had it. No earlier representation change is needed.
+- Struct hashes in 23 onward omit field types to handle cycles. Same-named
+ structs with different leaf kinds therefore collide intentionally. Exercise
+ that path deterministically by interning a BOTTOM-field struct before
+ meeting two float-constant variants; `TypeTest.testCyclicLeafKinds` in 23-25
+ checks the resulting field, canonical identity, and meet/dual behavior.
+- Function signatures acquire implicit open/closed argument tails in 23.
+ Normalize trailing defaults before interning: global BOTTOM/TOP in 23-24,
+ scalar BOT/TOP in 25. Preserve defaults before a later non-default argument.
+ Keep normalization out of raw cyclic allocation, whose child slots are still
+ incomplete. Add representative signatures to `TypeFunPtr.gather` for the
+ existing lattice-law tests; no separate test harness is needed.
+- The type lattice must remain complete, symmetric, and bounded. Run TypeTest
+ after changing Type/Field equality, hashing, duality, meet/join, interning,
+ serialization, or gather sets.
+- `TypeScalar` contains the scalar values ordinary Loads and Stores handle:
+ integers, floats, memory pointers, and function pointers. Scalar TOP/BOT sit
+ inside global TOP/BOT and avoid interpreting global BOT as all functions.
+- `TypeStruct._open` means fields may still be discovered. `_fref` means the
+ named structure has been referenced but never authoritatively defined. A
+ real definition meets away `_fref`; field discovery alone does not.
+- Moving an unresolved pointer or checking it for null is valid. Operations
+ requiring layout or fields must eventually diagnose a never-defined struct.
+- `TypeNil` owns pointer nullability. Required pointer fields begin nullable as
+ an initialization poison and must become their declared non-null type.
+- One-step idealization must preserve the type knowledge already established.
+ `IterPeeps.progressOnList` and `Opto.worklistCheck` are invariant checks, not
+ assertions to weaken.
+- Worklist order must not affect semantics or prevent optimizer normalization.
+ Use deterministic seed variation when investigating missing dependencies or
+ order-sensitive optimization; never fix a bug by merely favoring one order.
+
+## Functions and escape analysis
+
+- Unified function returns start in Chapter 18. Check return compatibility from
+ the optimized return expression, not a parse-time meet that includes dead
+ exits. Parse-time type-kind flags may describe an error, but must not decide
+ whether an error exists. Earlier snapshots keep separate Return nodes; they
+ already accept dead mixed-type exits and do not enforce one common return type.
+
+- Chapters 22-24 use Return-linked FunPtrNode constants. No linked calls does
+ not mean no callers when a function address survives. Direct call targets use
+ the existing call graph; any other address use conservatively preserves the
+ unknown-caller Start input. Phi-selected functions therefore keep general
+ parameter types. Escape propagation stays in 25; Cliff may split 25 into
+ smaller chapters later. Stop retains callable Returns through instruction
+ selection, which removes the lifetime edge from machine operands before
+ scheduling and cloning. Never substitute relocation-array growth or a linker
+ address scan for correct retention.
+- The direct-call exemption requires the pointer to occur only as the call
+ target: `call.fptr()==ptr && call._inputs.find(ptr)==call.nIns()-1`. A pointer
+ also passed as an argument still needs unknown callers retained.
+- Validate returned addresses by calling them. Include anonymous and library
+ functions, plus unused-body removal. Methods gain a receiver argument in 23;
+ native/emulator probes must honor it even when the body does not use self.
+ A wrong native signature initially passed because a stale argument register
+ happened to contain the right pointer; a passing call alone cannot prove ABI
+ correctness.
+- Chapter 21's empty-main heuristic can remove main for `return {->42;};`.
+ Use `if(arg) return {->42;}; return {->43;};` to probe returned addresses,
+ and invoke main explicitly at its encoding offset. Its CodeGen.driver cannot
+ resume after an explicit opto() call; use the remaining phases explicitly or
+ start a fresh driver.
+- Function-address encoding begins in 21. RISC-V AUIPC/ADDI occupies eight bytes;
+ patch the second instruction at opStart+4 and round the high part for signed
+ low bits. Anonymous bodies need emitted code, but no exported ELF symbol.
+- Removing a use can enable a distant rewrite without changing the definition's
+ type. Node.kill in 24-25 wakes that definition's dependencies immediately; only
+ queueing the definition missed single-call inlining after another call died.
+- A constant Simple function address must be a `FunPtrNode`, not an ordinary
+ `ConstantNode`, so the pointer retains an edge to the function Return.
+- Before Opto, a live FunPtr keeps its function callable even if no Call is
+ currently linked. After Opto, an orphan FunPtr may remain as a null/equality
+ sentinel while the function body becomes dead; this deserves careful error
+ checking rather than a pre-Opto deletion.
+- Extern C functions have no Simple FunNode/Return and are handled only while
+ linking.
+- From Chapter 22, retain native symbol names by function identity in CodeGen.
+ Selection and spill copies can turn ExternNode into a plain ConstantNode;
+ address relocation must consult that identity before looking for a local
+ function. ARM ADRP splits a signed page delta into unsigned instruction
+ fields: mask both pieces, including the high piece for backward addresses.
+- Unknown-caller Start edges and post-Opto compilation-unit ownership are
+ separate concepts. Do not overload Start edges merely to keep surviving
+ functions attached to a compilation unit.
+- A global escape fallback means all public functions, not all functions.
+ Public `<clinit>` functions escape when their class and all ancestor names
+ are public (no direct name component begins with `_`).
+- Escape summaries are monotone discovery facts. If a later SCCP state widens
+ to FULL/public-only, it must not forget precise private bits discovered from
+ the same Return earlier.
+
+## Serialization and code generation
+
+- Narrow direct C integer returns are normalized during instruction selection
+ in 22-25, using the declaration's width and signedness. An ideal cast would
+ fold away because the CallEnd already advertises the narrow semantic type.
+ In 25, identify external functions through their FIDX mapping: ExternNode can
+ fold into an ordinary ConstantNode. x86 upper return bits can be garbage;
+ RV64 sign-extends even u32 returns, which Simple must zero-extend again.
+ Native/emulator regressions start in Chapter22Test; Chapter25Test's errno
+ check also verifies the negative close result after importing sys.o.
+- Machine expansions must use the copying constructor and directly populate
+ input arrays, leaving reverse edges to CodeGen._instOuts. Using the ordinary
+ edge-registering constructor duplicates uses; the narrow-return expansion
+ exposed this during FileIO register splitting.
+- Keep Serialize's magic C0DE unchanged for now. Cliff explicitly unwound the
+ proposed magic bump; a real format version is future work. Rebuild all object
+ files after incompatible layout changes while there are no external users.
+- Class storage must be writable during `<clinit>`, even when all final field
+ values are constant. `Encoding.Relo.readOnly()` owns this distinction for
+ pool emission, ELF symbols, in-memory linking, and assembly printing. Ordinary
+ constant objects remain read-only. `val x = 42; return 0;` exposed a native
+ read-only write that emulator memory did not reject.
+- Object files carry canonical types plus ideal Simple IR. Every new semantic
+ type field, node field, tag, alias map, or mode needs balanced serialization,
+ deserialization, type upgrading, equality, and bijection testing.
+- Avoid smart peepholes after Opto. Machine selection briefly has incomplete
+ graph edges, and GVN already guarantees uniqueness where required.
+- `PtrToIntNode` is the explicit non-null pointer-to-integer conversion used for
+ C FFI calls. User code must handle null before converting.
+- x86 REX arguments are `(ModRM.reg, ModRM.r/m, SIB.index)`. Keep that ordering
+ aligned with `modrm`; reversing it silently selects the wrong extended
+ registers. The Chapter 25 Bubble Sort failure exposed this in `MulIX86`.
+
+## Side-effect-free diagnostics: B09 lessons
+
+- Shared debug printing lives in `print/`; see its README for the API. All
+ chapter Nodes extend `BaseNode<Node>`, which owns the ID field, unique naming,
+ print/toString, and default expression formatting. Compiler constructors pass
+ IDs from their existing allocator to super; clone still assigns a fresh ID.
+ Simple hooks override `format()` with input slots (`%1`, `%10`, literal `%%`).
+ Custom protected `_print1` hooks use `p.p(...)`, `p.n(...)`, open/close, and
+ matching-suffix unchar. Every print tracks repeats; short constant types expand
+ repeatedly. Both IR and graph adapters consume the common indexed edge and
+ dependency accessors. Graph record/edge construction belongs in GraphAdapter;
+ chapters supply only their semantic roles, names, projections and grouping.
+- Shared IR ordering finishes CFG RPO before placing data. Walking control and
+ data uses together can move a loop's closing Region before its body via Phi
+ backedges. Keep Loop/Region Phis contiguous with their header without pulling
+ backedge definitions up. Parm inputs belong to callers; stop at function
+ boundaries, and print globally shared floating expressions once with globals.
+ All traversal/placement state uses private identity maps and raw edges, without
+ invoking scheduling or dominator queries. Machine hooks must format correctly
+ into an initially empty buffer; multiline expansions are laid out centrally.
+
+- `Node._inputs` remains allocated after `kill()`. Use `isDead()` when printing
+ or filtering dead nodes; an empty input array alone does not imply death.
+ Bounds-safe edge inspection only needs null-node and input-count checks.
+
+- Type construction and interning are allowed during printing. Do not add
+ alternate linker-key comparisons or other machinery merely to avoid interning;
+ canonical types and fast identity comparisons are normal type-system operations.
+- Type layout queries are forbidden during printing, including queries that
+ currently return cached answers. Layout lives in TypeStruct for convenience,
+ but is separate from type identity and lattice operations. It may eventually
+ move elsewhere to support removing dead fields or packing fields with limited
+ states. Capture size, alignment, and section choice during encoding; printers
+ display that recorded layout instead of asking Types to compute one.
+- Use the `_` prefix for no-side-effect variants of accessors: `CodeGen._link`,
+ `RegAlloc._lrg`, `Var._type`, and the existing leaf `_isConstant` predicates.
+ Shared printer placement must not populate compiler dominator caches.
+ Keep mutating compiler accessors available for their normal jobs.
+- Audit the whole call chain from `Node.p(depth)`, `print`/`toString`, labels,
+ scope/graph viewers, and machine `asm` methods. `link` prunes dead functions
+ starting in Chapter 24; 19-23 can use ordinary `link`, including its interned
+ return-erased key. Scope memory reads can create lazy Phis, `Var.type()` can
+ resolve declarations, and register lookups can compress union-find links.
+- Use identity bookkeeping for graph inspection: `Node.hashCode()` writes
+ `_hash`, which also controls GVN locking. Leaf diagnostic type accessors can
+ use their existing `_` implementations without entering shared Type.VISIT
+ recursion; this is separate from permission to intern types.
+- Test cold caches, unresolved declarations, lazy memory, stale linker entries,
+ uncompressed register chains, and forbidden layout queries. Do not assert that
+ printing leaves the type intern table unchanged. B09 regressions start in
+ Chapter13Test/18Test/19Test/20Test/23Test/24Test and propagate to later snapshots;
+ Chapter25Test also covers constant-pool display.
+
+## Debugging workflow
+
+1. Reduce the first semantic or invariant failure before changing architecture.
+2. Refer to graph nodes by class and node ID, e.g. `Store#107`.
+3. Find the first phase or IterPeeps count where the graph becomes wrong.
+4. For memory bugs, verify both exactly-once alias coverage and that a precise
+ consumer never requests an alias excluded from bulk without a parallel slice.
+5. Distinguish ideal-graph correctness from selection, scheduling, register
+ allocation, and byte encoding. Compare IR, `CodeGen.asm()`, and `objdump`.
+6. Rebuild `sys.o` after compiler changes that affect serialized IR or native
+ code; stale system objects can make results appear inconsistent.
+7. Preserve the full process exit status and capture stderr. Chapter 22's Hello
+ World printed its expected output before crashing; Cygwin returned 2816,
+ which `(byte)waitFor()` turned into zero. Expected stdout alone cannot prove
+ successful execution. Normal TestC/driver execution now throws on failure;
+ the legacy module-test helper explicitly treats exit codes as results.
+8. Chapter 25's `make -j 4 tests` runs test groups in separate JVMs. Native
+ artifact names must be distinct across groups: Chapter22Test uses
+ `helloWorld`, Chapter25Test uses `helloWorldSys`, and driver variants have
+ separate output directories. `NativeExecutionTest` covers overwriting an
+ executable between link and execution, full exit statuses, and driver errors.
+9. CI uses `make lib tests CTAGS=` so native runtime and sys prerequisites are
+ built. Chapter 25's default ABI must match TestC: win64 on Windows, SystemV
+ elsewhere. Native spill expectations can differ by ABI (Newton is 42/34);
+ validate Linux as well as Windows when changing native test setup.
+
+Useful test ladder:
+
+```text
+focused test
+chapter test
+make tests_raw0
+make tests_raw1
+make tests_alone
+TypeTest after lattice changes
+serialize/deserialization bijection after persisted graph changes
+```
+
+Default fuzzer wrappers use explicit regression seed lists; exploratory fuzzing
+is opt-in. A passing wrapper with an empty list is not exploratory coverage, and
+`OPEN_FAILING_SEEDS` remain unresolved until explicitly verified and promoted.
+For dated suite counts and known failures, consult the backport validation record
+rather than treating an old count as the current baseline.
+
+Peepholes are always enabled in every chapter, by Cliff's request on 2026-09-26.
+Do not reintroduce an unoptimized compiler mode. Incomplete Loop/Phi nodes still
+defer rewrites that require their missing inputs; this is part of normal graph
+construction. Early README diagrams showing unfurled arithmetic are schematics.
+
+Chapter 8 fuzzing checks compilation/evaluation for failures; 9 onward compare
+optimizer worklist seeds. Historically 8-18 compared peepholes off/on. A change
+of oracle does not prove an old discrepancy fixed. Preserve actual generated
+source when comparing chapters, because the generators and dialects change.
+Cliff removed the frozen Chapter 18 fixture, replay tests, related reductions,
+and seed-list entry on 2026-09-27 as unnecessary test overhead. Retain the
+evaluator fixes; do not recreate that test suite. Do not describe timeout-skipped
+comparisons as equal runtime results.
+
+- Dynamic ARM/RISC counts must validate results, not just termination. RISC
+ equality expands to SUB then unsigned SLTIU; the second node must consume
+ the SUB, not copy its inputs. ARM register 31 is XZR for load destinations,
+ so narrow loads use WMASK, never RMASK. Logical-not uses CMP to XZR followed
+ by CSET to its result register. Shared emulator fixes and Chapter 21-25
+ backports are recorded in `docs/chapter-backports.md`.
+- Cohort 23's dynamic increase from compiler 24 to 25 is primarily class-field
+ initialization: Ops 630->1106. RA 160->442 includes 224 extra pointer-address
+ and 56 integer rematerialization instructions, but only two extra splits.
+ Do not call that 282 additional spills. ARM and RISC Bubble Sort both improve
+ when executed. The measured breakdown lives in `docs/regalloc-spills.md`.