diff options
| author | Cliff Click <cliffc@acm.org> | 2026-10-18 22:09:58 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-10-18 22:09:58 -0700 |
| commit | 300d2a9c3672acacc8bdd8d5370a424651a00571 (patch) | |
| tree | f33d895f6ec4e08692efa4c090c681257553097d /chapter25/initialization.md | |
| parent | 0be52f8dd82f70d4c557fe916d47dc425401f848 (diff) | |
| download | Simple-300d2a9c3672acacc8bdd8d5370a424651a00571.tar.gz Simple-300d2a9c3672acacc8bdd8d5370a424651a00571.zip | |
The phase rework, made new phases explicit and add them to the CodeGen driver. Fix the flag-spilling on X86 and ARM. Fix the GCM conservative anti-deps and backport.
Diffstat (limited to 'chapter25/initialization.md')
| -rw-r--r-- | chapter25/initialization.md | 842 |
1 files changed, 0 insertions, 842 deletions
diff --git a/chapter25/initialization.md b/chapter25/initialization.md deleted file mode 100644 index 9996a3a3..00000000 --- a/chapter25/initialization.md +++ /dev/null @@ -1,842 +0,0 @@ -# Lazy class initialization design - -Design and implementation plan, revised 2026-10-10. Checkpoint 1 implements -post-Opto initialization dependencies and cycle checking for review. Declared -constant final fields already have static-data lowering. General lazy run-once -initialization, serialized dependency validation, and initialization guards -remain to be implemented; the compiler does not yet enforce the full protocol. - -Initialization metadata belongs with class identity and compilation-unit -ownership, not solely on the `<clinit>` FunNode. Represent active touches in -ideal IR, validate an acyclic graph across source and imported objects, and -lower remaining checks to a hidden flag in the class object and a conditional -call. The initializer itself owns the store that sets the flag. -Use one runtime bit in the first, single-threaded implementation. - -## Language rules - -- Dynamic class initialization runs once, on the first active touch. A class - never actively touched executes no initialization code. -- A dynamic parent completes before a dynamic child starts. There is no early - publication point or special tail-call escape from this rule. -- Outside the initializer's own checked initialization context, no code may - observe fields before their required initialization completes. -- Class identity exists before initialization. Taking a class reference, as - in `sys`'s `val io = io`, is passive and does not execute the child's body. - Storing a function identity is also distinct from invoking it. -- Proven static-data accesses require no dynamic work. This does not authorize - a called function to observe unfinished dynamic state in an ancestor or any - other class. -- After Opto, build the combined function-call and class-initialization graph, - including module boundaries. Reject an SCC only when it contains more than - one `<clinit>`. Ordinary recursion, initializer self-edges, and helper cycles - involving only one initializer are legal. Unresolved conditional branches - contribute edges; eliminated branches and call targets do not. -- Initialization order follows actual execution and parent requirements, not - parser discovery order, worklist seed, or filesystem enumeration order. - -For example, A conditionally reading `B.foo`, with B initializing `foo` from -`A.bar`, is rejected if that read survives Opto. Neither an initial zero nor an early evaluation of -`A.bar` resolves that cycle. If B is A's child, the parent requirement already -provides the reverse dependency. - -An initializer may read its own previously initialized fields under the existing -definite-initialization rules. That is not an ordinary external active touch. -Same-class helper calls are allowed by the cycle checker, whether or not they -inline. Ordinary field-initialization and type checks remain separate. - -The cycle diagnostic lists every internal edge of the first rejected SCC, -including helper calls. Every listed edge participates in a cycle; this reports -all cyclic connections in that component without enumerating individual paths. -On failure, rescan the component's functions to choose one surviving touch or -call site for each edge. `SourceError.format(CompUnit, byteOffset, message)` prints -`file:line:character: error: message` starting in column one, followed by the -source line and a separate caret line. Lexer snapshots retain their -original CompUnit through inlining; Call and CLInit serialization retain the -offset and source identity/path. Imported source is read only on error. Parent -requirements are labeled explicitly rather than assigned a fabricated position. -This is a focused diagnostic utility, not a migration of all parser errors. - -## Ownership and identity - -Keep `CompUnit._deps` for source/object discovery and rebuild dependencies. -Do not reuse it as the initialization graph: passive references belong there, -and its elements are files rather than necessarily individual classes. - -Use CompUnit directly for the current runtime initialization graph. The parser -creates a file `<clinit>` in `parseStruct(true, ...)`; nested struct declarations -use `parseStruct(false, ...)` for instance initialization. Do not introduce a -separate ClassInit object or invent a dynamic class initializer for those -instance constructors. - -Reuse `_cname` for stable identity, `_par` for the filesystem parent, `_clz` for -the canonical class object and hidden field, and existing function identities -for the body. Add only initialization-specific information: - -| Member | Meaning | -|---|---| -| `_classInitDeps` | Lazily allocated `Ary<CompUnit>` of direct initialization requirements | -| `_initComplete` | All initializer touches and relevant call targets have resolved | -| `_initStatic` | Proven that this unit's own initializer needs no execution | - -Keep diagnostic witnesses in the checker's temporary function summaries: source -locations and indirect call witnesses where applicable. CompUnit retains only -the edges and analysis flags after validation. Derive the flag alias from `_clz`'s -`$init` field rather than maintaining a duplicate mapping. Locate the body by -its stable function identity; a transient FunNode cache may be useful but does -not define class identity. An unloaded or eliminated FunNode is not proof of -static initialization. - -Static class objects for nested struct declarations keep their existing named -TypeStruct identity and defining CompUnit. Their materialized data need no new -runtime graph vertex; an active use that requires the enclosing file initialized -maps to its CompUnit. Preserve initialization-independent static accesses. If a -future language feature adds an independent dynamic initializer inside one file, -then introduce the additional per-class graph identity that feature needs. - -`CompUnit.addClassInitDep(code, target)` allocates the collection lazily, deduplicates -edges, and invalidates a compilation-wide validation stamp. Keep self-edges so -they can be diagnosed. Explicit namespace-only parents have no dynamic body; -a missing parent definition must not silently become an empty initializer. - -There are two different dataflows. Runtime `$init` values use ordinary memory -aliases and support flag-load/CLInitNode optimization. Dependency discovery -records which initializers may be requested, independently of incoming flag -values and normal return. A dedicated analysis-only memory slice could encode -that second forward dataflow, but the first implementation uses explicit -CompUnit edges plus small function summaries. It does not add a second family -of memory aliases, and it does not infer cycles from runtime `$init` types. - -## Discovering and validating dependencies - -Use the convention `A -> B` to mean that initializing A may require B completed. -Topological output therefore places prerequisites before dependents. A dynamic -child has a structural dependency on its parent. An active touch of another -class during A's initializer contributes an edge; a passive reference does not. - -Insert active touches as CLInitNodes on executable control. After Opto finishes -SCCP, inlining, and cleanup, collect surviving touches and calls. Use final -function-pointer target sets and propagate initialization effects through the -resulting call graph with a worklist. Recursive function SCCs reach a fixed -point of finite dependency sets; they are not themselves initialization cycles. -Indirect calls contribute the union of targets still possible after optimization. - -Do not accumulate historical targets or source touches eliminated by Opto. -There is no side table watching Node replacements or type changes. An inlined -helper's surviving CLInitNodes belong to its caller; an uninlined helper's -summary propagates through its surviving calls. Walk executable control -forwards so touches before nonreturning calls and infinite loops are included. -Preserve initializer boundaries until validation; expanding/inlining CLInit -operations early must not erase the class identity needed for checking. - -A field value becoming constant does not prove its initializer unnecessary. -CLInitNode survives independently of the load. Dead control may remove both; -a static-initialization proof may remove the obligation. Such proofs must not -assume the very acyclicity being checked. - -Acceptance now depends on what Opto proves. Worklist determinism remains an -implementation goal, but preserving every intermediate target set is not the -language rule. Before declaring a graph complete, every surviving required -initialization-time call must have a complete effect summary. - -An unknown function-pointer target or native callback is not an empty summary. -Reject such a call during initialization unless an explicit trusted contract -establishes its initialization effects, including that it cannot call back into -unfinished Simple initialization. Ordinary native I/O can use such a contract; -this is independent of whether the call has memory or I/O effects. - -Proving a class entirely static is a separate, deterministic classification -step. Static object/reference cycles are legal and need no dynamic ordering. -Dropping a dynamic requirement requires proof that the relevant operation needs -no initialization, not merely that its guard or result optimized away. A real -cycle must not bootstrap its own static-ready proof. - -Validate the union of parent edges and active initialization requirements after -the source/import closure is resolved and again after loading or replacing -summaries. Report a cycle with class names and the source/call witnesses for -its edges. An incomplete library may retain explicitly unresolved obligations; -it cannot be certified as a complete runnable program. - -Use a stable name order to break ties in diagnostic/topological output. Cache -module-local results, but validate cross-module edges at final composition. -One module's list is not necessarily a contiguous block: `X.A -> Y.B -> X.C` -in prerequisite-first order requires interleaving X and Y. Retain edges as the -authority rather than serializing a local order as a global proof. - -The topological order validates permitted completion order; it is not a startup -schedule. If A conditionally uses independent B, B initializes only on the -taken path. A may start before B and finish after it. That is safe precisely -because B cannot directly or indirectly require unfinished A. Do not eagerly -initialize every possible dependency, or a prefix of the topological list. - -## Ideal IR and runtime protocol - -### Compiler phase boundaries - -The Chapter 25 driver runs `Parse -> Iter -> Opto -> IterPostOpto -> FinalizeIR --> TypeCheck -> LoopTree`, followed by the existing serialization and backend -phases. `Opto` performs SCCP, resolves numeric modes, and seeds the worklist; -it does not invoke either Iter pass. `IterPostOpto` performs peepholes and -inlining using the SCCP results. `FinalizeIR` resolves field layouts and offsets, -runs the resulting cleanup, roots surviving functions, and releases temporary -CU memory keeps. Numeric resolution remains in Opto because it can require -another SCCP propagation before the types are ready for Iter. - -Direct callers explicitly run `parse().iter().opto().iterPostOpto().finalizeIR()` -before `typeCheck()`. The driver handles that sequence when a later phase is -requested. CLInit expansion after successful TypeCheck remains unimplemented; -its phase and the serialization boundary are separate decisions. - -### Memory IR checkpoint (2026-10-17) - -This checkpoint supersedes the older representation details below. Keep CLInit -opaque through Iter/Opto and the class-cycle check; introduce neither its -conditional call CFG nor frequency estimates in those phases. - -Use the existing mutable `$init` field and alias. The implemented node shape is: - -```text -ready = Load(C.$init, incoming memory) -CLInit(ctrl, wholeMemory, classPtr, ready) - -> (ctrl, wholeMemory, initAliasMemory) -``` - -The third result is an alias slice containing true on normal continuation. -Merge it over the whole-memory result using MemMerge. The whole-memory result -conservatively accounts for arbitrary initializer effects; changing just the -flag slice would be incorrect. The flag result is a fact about the operation's -result, not an extra runtime store at each touch. Keep the class pointer and -source location on the operation; resolve the flag alias from the class type, -including after forward-reference resolution and object loading. - -The parser creates the ready load up front, using the existing receiver node. -For an unresolved receiver, ordinary Load/FldOff machinery resolves the alias -and offset later; no additional named FRef is needed. If the receiver becomes -an ordinary instance, the speculative CLInit and flag load disappear together. -If the parser cannot yet identify the flag alias, precise Loads and MemPhis -recover that slice from the whole-memory projection once the type resolves. - -Known-true input bypasses the operation, returning incoming control/memory; -false or unknown retains it. Parser memory uses the result, so subsequent flag -loads, MemPhis, and ordinary peepholes can establish redundant checks without -a separate parser set of initialized classes. A merge establishes true only -when every live incoming path does. Initially an ordinary unknown call may -lose a known-true fact; that is conservative, and preserving monotone flag -facts across arbitrary calls can follow later. - -The static flag starts false. Its true store belongs inside `<clinit>`, not at -each CLInit site. It is installed at body entry so the body knows its own flag -is true. The parent requirement remains structural; runtime lowering must -execute it before entering the body. This implements the accepted same-class -reentry rule; it does not exempt cross-class cycles from validation. Retain -initializer boundaries and their dependency edges until the post-Opto check; -do not propagate an in-progress caller's flag into another initializer to -erase that check. Ordinary field-initialization checking remains separate. - -Memory/projection wiring, parser updates, the initializer-owned flag store, -and copying/serialization are implemented. Rebuild serialized objects for the -new four-input node shape. Runtime lowering, entry adaptation, and executable -run-once behavior remain a subsequent step. - -Public function fields on instances of public classes must retain their Start -caller, just like public class-object fields. `CodeGen.publicFIDX` now checks -both, preserving private-field/class and hidden-constructor exclusions. This -fixes the `sys.scan.match` widening exposed by conservative CLInit memory -effects; neither imported-type resets nor SCCP assertions needed changes. - -Validation: 395 raw0 tests, 37 raw1 tests, 9 standalone tests, the fuzzer -regression wrapper, 25 Chapter 25 tests, and 38 additional tests all pass. -The additional group includes 16 class-init tests -covering cycles, serialization, repeated touches, branch merges, flag stores, -and removal of a speculative flag load when a forward call returns an instance. - -### Deferred constant instance fields - -A final instance field proven to have the same constant value in every instance -can eventually be stored on the class object, or folded directly to a -ConstantNode, and removed from instance layout. Instance-field access semantics -remain unchanged. Public function reachability must not depend on that storage -choice: an externally accessible function field is callable by anyone whether -stored in the instance or class. This optimization is not needed for the current -initialization checkpoint; instance layout is unchanged. - -### Deferred CFG frequencies - -After Opto and cycle validation, lower surviving CLInit operations to flag -tests, conditional calls, and control/memory merges. Mark the needs-init edge -rare. Once the resulting CFG and loop tree are stable, estimate frequencies -before GCM and reuse them in Encoding block layout. Frequencies are derived -backend data, not ideal-IR types or inputs to Iter/Opto. - -Start each function at 1.0; use 50/50 for unknown tests, 90/10 for non-null/null, -and a small positive minimum probability for initialization and known-failure -edges. Use an estimated loop trip count of 10 unless a proven count is known; -distinguish body iterations from header tests and account for early exits. -Compute cyclic flow using loop-aware propagation, not an unbounded walk around -backedges. Regions sum incoming edge flows, and branch outputs sum to their -input. Bound probabilities and loop amplification and normalize outgoing -probabilities; do not independently clamp successor counts and break those -relationships. Unreachable blocks remain zero. Bound the resulting estimates -against overflow. Later profiling can supply probabilities/trip estimates -through the same interface; switch distributions are deferred. - -GCM chooses the lowest-frequency legal placement, with deterministic tie breaks. -Encoding prefers common fall-through paths, forward cold branches, and backward -loop edges. Recompute/invalidate estimates if later CFG changes require it. - -Introduce an effectful `CLInitNode` with a stable target CompUnit identity: - -```text -CLInit(C, ctrl, memory) -> (ctrl, memory) -``` - -Control is input/output slot 0 and memory slot 1. Store the target class as -semantic metadata or a separate constant input, never in the control/memory -slots. Include target identity in equality, copying, printing, and serialization. -Its normal return guarantees the required initialization of C and its ancestors. -It can execute arbitrary initializer effects, so it is not a pure flag test. - -At dynamic class-field reads and writes, required instance creation, and calls -requiring initialized class state, consult parser flow facts first and insert -CLInitNode only if the required initialization is not already established. -A check omitted by a valid dominating initialization proof needs no separate -historical dependency record: its prerequisite is represented on the covering -path. Passive Simple class references remain subject to checks at later field accesses. -Native escape of class references is a separate boundary described below. -Forward references must preserve the pending check until the target is known. - -For `ary.copy(args)`, check the class at the field access: - -```text -CLInit(ary) -f = load ary.copy -call f(args) -``` - -The ordinary function header, calling convention, and static function-pointer -field do not change. There is no per-function checked wrapper. Folding the -field load to a constant function pointer does not remove the preceding -initialization effect unless that effect is independently redundant. Once the -checked load yields `f`, it can be passed along and called without repeating -the check for `ary`. Materializing a function address in static data is not a -source-level field access. An initializer's raw body remains private compiler -machinery, not a callable reset operation. - -Keep CLInitNode in ideal IR so its peepholes can eliminate or simplify frequent -checks. Lower a remaining node to ordinary Load/If/Call/Region/Phi IR before -machine selection, using a shared class-initialization helper where an -out-of-line guard is useful. The checking code contains no flag store: - -```text -CLInit_C(ctrl, memory): - (ctrl, memory) = CLInit_parent_if_required(ctrl, memory) - if load(C.$init, memory): - return (ctrl, memory) - return call C.<clinit>(ctrl, memory) - -C.<clinit>(ctrl, memory): - memory = store(C.$init, true, memory) - ... original initializer body ... - return (ctrl, memory) -``` - -Entry to `<clinit>` already means that its check took the not-initialized path. -Place the flag store in the initializer, naturally at its entry. This covers -all its normal exits without replicating a store in each CLInitNode expansion. -Inlining the body may of course copy its store, just as it copies other body -instructions. The body must not guard itself through ordinary method-entry -instrumentation. - -There is no requirement to schedule the store last. With a validated acyclic -initialization graph and one thread, no other user can observe this class's -flag while its initializer is running. A nonreturning initializer can leave -the bit set because no permitted observer can resume. The physical flag store -is not an early publication point: the class's initialization still completes -only when its body returns. Cycle validation and definite-initialization checks -must remain independent of that store and of optimizations which expose it. - -The first runtime contract is single-threaded initialization, with no recovery -and retry after initializer failure. The validated DAG rules out recursive -entry to an unfinished initializer. Thus no runtime `initializing` state is -needed. Concurrent first touches or recoverable initialization failures require -an extended protocol; the flag is not a synchronization mechanism. - -## Hidden flag and known completion - -Add a hidden boolean field `$init` to the ordinary `<clinit>` class object. -There are few such objects, one per class; no separate allocation, symbol, or -special bit-packing scheme is needed. Use the existing small-integer field -representation and normal struct layout/offset machinery. It is a logical bit, -initially false for a dynamic body, stored true by that body, and inaccessible -to user writes. Separate importers share it through the existing `C.$class` -identity and its canonical field layout. - -Allocate/remap these aliases during class discovery, before the current -`freezePublicInterface()` boundary. Hidden flags are not source-visible fields, -but their memory effects must participate in call summaries and memory merging. -Keep CLInitNode's complete public-memory effect until a narrower summary is -proved; tracking only the flag would lose the initializer's field and I/O effects. - -There are two different facts: - -1. **Static body completion:** `_initStatic` proves that C's own initial contents - are supplied by data and there are no remaining initializer effects. Its - `$init` field starts true, and its redundant body/store can disappear. -2. **Completion at a program point:** normal return from CLInitNode establishes - readiness in that control/memory state. At a permitted external touch, a - true flag branch proves completion of C's own body; its ancestors must also - be known complete before concluding that an ordinary active use is safe. - -Inside C's own initializer, the flag can already be true while fields are still -being initialized. Keep that lexical initialization context and its field-read -checks; a folded true flag does not grant access to unfinished fields. In -particular, it must not make a callback or cyclic initialization dependency -legal. Record and validate those dependencies before suppressing their checks. - -Neither fact means mutable fields remain constant. Do not refine their declared -types to their static initial values. Also, a static-only child with a dynamic -ancestor is not globally ready for every active use: ordinary ensures still -establish the ancestor. Passive references and proven initialization-independent -static accesses need no such ensure, including during the parent's initializer. - -## Parser flow and ideal peepholes - -CLInitNode will occur frequently. Avoid constructing obvious duplicates in the -parser, then use normal ideal optimization as more types, call effects, and -control-flow facts become available. A separate late must-analysis is optional, -not a prerequisite for removing straightforward redundant checks. - -Carry known-initialized class identities with parser control flow, for example -as a BitSet in ScopeNode keyed by the existing remapped `$init` alias IDs. These -are path facts, not mutable global properties of CompUnit or its declared -TypeStruct: - -- On an active touch, consult the facts. If C and its required ancestors are - known ready, omit the CLInitNode. Otherwise insert the obligation in the IR; - post-Opto analysis collects surviving requirements. -- Normal return from CLInit(C) adds C and its required ancestors. Do not add - every possible dependency: a conditional initializer may never touch one. -- Copy facts on a branch and intersect them at a reachable merge. An abrupt - or nonreturning path contributes no completion facts to a reachable merge. -- A loop's first iteration has only entry-path guarantees. Body/backedge facts - alone cannot suppress its first check or establish readiness after a - potentially zero-trip loop. Preserve pre-loop facts and refine only with a - proof covering all relevant incoming paths. -- A call cannot reset established facts. Add new ones only from a proven normal - return summary; do not seed arbitrary exported function entries with facts - learned at one caller. Checks at field-access sites establish their facts. - -Give CLInitNode ideal peepholes for these cases: - -| Proof | Rewrite | -|---|---| -| Fully static class / flag known true at a permitted touch | Remove the class's own check and call; retain any still-required ancestor initialization | -| Flag known false at this memory/control point | Replace the test with an unconditional initializer call; retain the call's effects and results | -| Earlier normally returning CLInit of the same class dominates | Forward control/memory through the redundant node | -| Dominating true flag test covers this path | Remove the covered check, as with redundant if-test reasoning | -| All reachable arms of a merge establish completion | Fold the repeated check after the merge | - -Known false must come from the memory reaching the node, not simply the false -byte in the original object image. Known true on one branch is not known true -after a merge with an unchecked branch. Match stable class identity and required -ancestors, not just a pointer-shaped value or a similar field name. - -Use the existing dependency/wakeup mechanism when a node consults a flag value, -memory state, or another node's proof, so later refinement revisits the check. -Retain control and whole-memory ordering through the conditional call: removing -a check must preserve preceding initialization effects, and hoisting a check -onto an untaken path would violate laziness. In particular, CLInitNode is not -freely speculatable or an ordinary pure expression suitable for unrestricted GVN. - -Parser omission must have an enduring IR justification: earlier effectful -CLInit/control edges, a proven caller precondition, or static metadata. -When bodies are inlined or deserialized, those proofs travel with the IR; -transient parser facts alone cannot justify an unchecked exported body. A -shared declared type must not be mutated to encode a path-local true flag. - -## Static data classification - -A class is fully static only when every required initial value is representable -in data/relocations and no runtime effect remains: no I/O, observable allocation, -potential failure, or required dynamic dependency. A missing/dead body alone -does not prove this, and startup arguments are not static inputs. - -Ignore the compiler-inserted `$init = true` bookkeeping store when checking -whether the original initializer is entirely static. Otherwise every inserted -store would prevent this optimization. Once the proof succeeds, emit `$init` -as true in the object image and eliminate the runtime store with the body. - -Mutable fields may have static initial values. Folding their initializing -stores must still preserve reads and escapes during the initializer. Merely -knowing the final value and that the body runs once is insufficient. Preserve -object identity and canonical layout separately from initial contents, and do -not collapse distinct class objects with equal bytes. - -This extends the current StaticData machinery; it does not change pointer -widths or require class objects to become read-only. An object with a dynamic -`$init` field stays writable even if its source fields are final. A fully static -object whose source fields are also immutable can later use read-only storage, -with its hidden field already true and no runtime writes to it. - -## Serialization and final composition - -Add a class-initialization directory to the `.simple` data that can be read -without materializing every function body. Serialize: - -- Stable class and parent names, defining-unit identity, and initializer symbols. -- Static/dynamic/unknown classification and summary-completeness information. -- Direct initialization requirements and function-effect summaries, including - unresolved obligations and useful cycle-diagnostic locations. -- The hidden `$init` field's type, alias identity, initial image value, and - canonical layout, alongside the other class fields. Remap aliases using file - identity and local order rather than copying process-local numbers. -- CLInitNode operations and the initializer's own flag Store in ideal IR, - including all semantic fields needed to rebuild them. - -Do not serialize transient node references, a cached global topo index, or the -runtime value of a dynamic flag as an unconditional type fact. A definition -and its imported declarations must resolve to the same owning CompUnit and -canonical class/flag identity. - -On load, register identities first, resolve parent/dependency edges second, -then resolve function summaries and IR. Check conflicting owners, missing -definitions, alias remapping, summary completeness, and combined cycles. -Loaded bodies must agree with their summaries; report newly found requirements -and revalidate rather than trusting an earlier module-local topo sort. - -Preserve these summaries before `unlinkImports()` removes imported bodies. -Serialize ideal CLInitNode operations before lowering to machine code. Native -objects retain checks at their class-field access sites even when a particular -importing compiler inlines and removes its local checks. Ordinary function -symbols and function-pointer constants are not replaced by checked wrappers. - -Do not serialize a parser's transient BitSet as unconditional entry knowledge. -Its local alias IDs can change on reload; retained checks, control flow, and -explicit entry contracts must support any omitted checks. Recompute flow facts -for loaded graphs and verify hidden-field ownership/layout and that the flag -store belongs to `<clinit>`, not to each checking caller. - -Final validation needs a Simple-aware executable/library composition step. -An ordinary native linker cannot validate dependencies stored only in `.simple`. -Enumerate the complete intended Simple object set, including exported entry -points that native code could invoke; loading only the source-discovered -references is insufficient. Validate that metadata set before handing objects -to GCC/the native linker. Dynamically loading additional Simple modules is -outside this first protocol and would require validation before activation. - -Keep the current unversioned `C0DE` convention and require rebuilding objects -when the serialized layout changes. Retain/validate summaries for the exact -objects being linked, not cached descriptions of older builds. - -## Program entry and other boundaries - -Program startup is another CLInitNode on the starting class. In a fresh runtime -image with a dynamic starting class, its `$init` is known false, so the check -folds to the initial `<clinit>` call. The starting class sets its own bit just -like every other class; there is no adapter-specific flag store or protocol. -A proven fully static starting class needs no dynamic initialization call. - -The current file `<clinit>` can receive `arg` and return a program/test result. -Preserve those arguments and the call result on the known-first-call startup -path. Ordinary class-use checks need only control and memory; they do not need -to cache or invent a result for the skip path. For fully static startup, the -program result must also be available as a constant or retained computation; -static object contents alone do not prove the entry computation unnecessary. -Existing tests executing file code with different arguments use independent -runtime images, not a reset of an already initialized class. The startup fact -must not be reused for an arbitrary native entry into a live image. - -Own-initializer helper calls and construction of a special instance during -class initialization need an explicit definite-initialization proof. Do not -carry forward the old module notes' unrestricted “golden instance” exception. -Initially reject cases that would publish or inspect unfinished class state. - -## Native handoff - -Native callers are not required to understand or explicitly run Simple class -initialization. The compiler/runtime must establish initialization before -exposing a supported value, or reject an unsupported handoff. - -For the first implementation, reject passing a class object or raw `<clinit>` -function to native code, including through a class-to-address conversion. -Do not confuse these with an ordinary function pointer obtained by a checked -field access: loading `ary.copy` already initializes `ary`, so that value does -not need a new initialization wrapper when handed to native code, wherever the -ordinary FFI supports its signature. Initialization-time callbacks still need -the effect analysis described above; an uninitialized class cannot be hidden -behind a callback to evade cycle validation. - -Later, support a selected class value at the handoff point: - -```text -X = rand ? B : C // Passive class references -CLInit(selected class X) // Initialize the actual value being passed -libc.native(X) -``` - -Validate the initialization graph for all possible targets, B and C, including -their parents and transitive requirements, regardless of the selected value. -At runtime initialize only the selected target before the native call. This -can lower to checks on the corresponding control-flow arms, or dispatch on a -known target identity. The plan's initial CLInitNode has a fixed class identity; -this dynamic selection is a later extension, not an assumed existing facility. - -Initializing every possible target before handoff is an alternative policy, -but it runs effects for the unselected class. Prefer the selected-target form -to retain the rule that untouched classes run no code. Unknown target sets or -native traversal to further uninitialized class objects require additional -analysis; initializing just X is not a blanket proof for everything reachable -through X. Until supported, reject the handoff rather than transferring that -responsibility to native code. - -A native call directly to an exported symbol, without a preceding Simple -handoff, likewise needs compiler/runtime initialization support. Its eventual -export/startup mechanism is deferred; it must not require the native caller to -invoke `<clinit>`. Existing native-linking tests are not evidence that this -future initialization guarantee is already implemented. - -## Implementation sequence - -Implement in Chapter 25 only, with the following reviewable checkpoints. Keep -existing source/native entry conventions and pointer widths. Each checkpoint -must build; do not claim the runtime guarantee until the executable protocol, -serialization, and final-composition validation are all present. - -### 1. CompUnit dependencies and cycle checking - -Reviewed (2026-10-10). Files: `CompUnit.java`, `ParseAll.java`, -`Parser.java`, `ClassInitDependencies.java`, `CLInitNode.java`, `CallEndNode.java`, -`Opto.java`, and `CodeGen.java`. Generic `Node` needs no changes. - -- CompUnit holds direct `_classInitDeps`, `_initComplete`, - and reserved `_initStatic`. Existing file-discovery `_deps` is unchanged. - Diagnostic witnesses belong to the dependency checker and serialized summaries, - not CompUnit. -- Parser inserts a control-flow CLInitNode for an active class-field touch or - a receiver whose class is not yet resolved. Ordinary instance receivers - fold the obligation away when resolved. Passive references and direct - own-initializer field accesses do not insert a check. -- This checkpoint's ideal node has control at input 0 and receiver at input 1; - it neither consumes nor produces memory yet. The full runtime form described - above will add memory at slot 1 and move the receiver to slot 2. No hidden - flag or runtime initialization call is generated in this checkpoint. -- After Opto, the collector walks live control forwards in each surviving - function, reads final call-target sets, and propagates sparse may-touch - summaries. Ordinary helper inlining naturally relocates its checks into the - caller. Class initializers are not inlined before validation. -- Reachable nonreturning calls retain a conservative CallEnd during dataflow. - Calls left out-of-line retain their continuations, so otherwise unreachable - touches after them can contribute initialization dependencies. This extra - ordering is accepted; users can remove such unreachable code. - Inlining discovers bodies forwards from their entry, including non-exiting - paths, and explicitly retains the Return node even when disconnected from - executable control. Nonreturning functions can therefore inline normally. -- TypeCheck builds fresh CompUnit requirements from those summaries and parent - edges. It rejects cycles with source/call witnesses and computes a stable - prerequisite-first order. No source-history or Node watcher table is kept. -- Following validation and serialization, the temporary CLInit obligations are - erased for the existing backend. Executable lowering remains a later - checkpoint. The full lazy-initialization runtime guarantee is not implemented. - -Unknown native effects and unresolved targets leave `_initComplete` -false; imported summaries are now available in checkpoint 2. The returned order -covers known edges and is not a completeness -certificate. `_initStatic` remains false until static classification is added. - -Validation covers direct, helper-mediated, forward, conditional, indirect, -sibling, and parent/child cycles; legal ordinary recursion and passive -references; unknown native/imported effects; dependencies eliminated by SCCP; -unused helpers; discarded field values; and effects before versus after a -nonreturning call. Cycle and optimized-target cases run with three worklist -seeds. A bounded Eval2 run verifies that a retained nonreturning call executes -rather than falling through; its x86, ARM, and RISC-V encoding checks also pass. -All 492 tests in the parallel Chapter 25 Make suite pass. The encoding checks -were added afterward and pass in the focused suite. Two existing RISC-V spill -expectations change from 9 to 7; execution and allocation checks pass. -Generated fixtures are under -`chapter25/build/init-deps/`; full-suite logs are under `build/init-deps-opto*.log`. - -### 2. Serializable flags and dependency summaries - -Implemented for review (2026-10-10). Files: `Parser.java`, `Serialize.java`, -`ElfReader.java`, `ParseAll.java`, `ClassInitDependencies.java`, `CodeGen.java`, -and `CLInitNode.java`. Existing `GlobalBits` and `StaticData` mechanisms handle -alias/function remapping and the zero flag image without special cases. - -- Add hidden `$init` fields with the normal field/alias/layout mechanism before - `freezePublicInterface()`. Declared flag type is boolean; initial image value - is false unless static initialization has been proved. Do not make a dynamic - flag's declared type the constant false. -- Serialize per-unit initialization metadata and function may-touch summaries - in `.simple`, early enough for graph validation without loading every body. - Preserve stable owner/alias/function identities and diagnostic witnesses. -- Register imported units first, resolve/remap edges and hidden fields second, - then validate the combined graph. Neither a module-local topological order - nor a summary from an older object file is a final-link proof. -- Keep metadata for fully static or body-eliminated units. Rebuild objects - under the existing unversioned `C0DE` convention; add no compatibility layer. - -Checkpoint: a two-module cycle rejected from source is also rejected after -serialization; an acyclic diamond resolves to one shared class and flag. -Two local topo orders can be merged even when their modules must interleave. - -The object header carries owned class identities, initialization edges, -completeness/static flags, and per-function may-touch summaries with diagnostic -witnesses. Imports use those summaries even when their initializer bodies are -not live in the importing compilation. Class layouts survive body elimination. -Each `ClassCyclicError` witness holds existing owner/field name references, a -numeric line/column, and a shared suffix for indirect calls; a null field denotes -a call. Diagnostic text is formatted only when a cycle is rejected. Serialization -stores shared witnesses once and preserves numeric locations without requiring -the original source text. -Ideal CLInitNodes also survive serialization, including source positions, so -inlining an imported helper retains its active touches. Erasure now happens in -Unlink, after serialization; executable checks still belong to checkpoint 3. - -Object lookup distinguishes class definitions from references to other objects. -Objects serialize their own bodies; imported function pointers retain symbolic -identities. Loading registers all classes supplied by each object, resolves -prerequisites against existing source/imported units, and selects the requested -nested class's type rather than always the object's root class. Cross-object -calls are relinked after all bodies load, before Iter validates saved types. - -Validation covers separately compiled cyclic objects, a shared diamond across -three worklist seeds, interleaving orders from two multi-class objects, imported -helpers with and without inlining, type-only loading with stable flag aliases, -and the emitted zero flag byte. The C0DE layout changed: rebuild existing objects. -The runtime still does not test or set the flag, and static classification remains -deferred. All 494 tests pass in the Chapter 25 Make suite. Logs: -`build/init-checkpoint2-focused.log` and -`build/init-checkpoint2-all.log`. - -### 3. CLInitNode and the executable protocol - -Files: new `node/CLInitNode.java`; `Node.java`, `Parser.java`, `CodeGen.java`, -`Serialize.java`, `Eval2.java`, and the printer adapters as needed. - -- Add an effectful, serializable CLInitNode targeting the resolved CompUnit, - with control/memory inputs and outputs in slots 0/1. Pending targets remain - explicit until resolved. Its type computation is conservative about the - initializer's whole memory effects; final normal output guarantees completion. -- Integrate its possible call with existing Call/CallEnd linking, argument and - return propagation, and body liveness. A reference to CompUnit metadata alone - must not let SCCP discard the initializer as uncalled. Reuse the call machinery - or a generated guard helper, rather than implementing a second independent - calling convention in the node. Test an out-of-line imported initializer as - well as an inlined one. -- Insert checks at class-field accesses, including function-pointer loads; - use the same operation for required instance creation and startup. Do not - instrument ordinary function headers or rewrite their function-pointer data. -- Insert `$init = true` once in each dynamic `<clinit>` body, naturally at entry. - The checking node performs the test/conditional call and contains no store. - A false startup test becomes a direct call preserving `arg` and the result. -- Give Eval2 the same shared-object/flag behavior. Lower checks to existing - control, memory, and call IR for machine compilation; use an out-of-line - helper if necessary rather than adding target-specific machine operations. -- Reject the presently unsupported native class/raw-initializer handoffs. Keep - ordinary checked-field function pointers unchanged. Do not add an explicit - initialization obligation to C drivers or other native callers. Direct native - entry into a dynamic class remains outside the new guarantee until a - compiler-managed startup/export mechanism exists. - -Checkpoint: sequential touches run the body once; parent effects precede child -initialization; an untaken branch runs no child code. Verify result/argument -preservation and flag sharing in Eval2, ARM/RISC emulation, and native execution -through Simple startup. Existing direct native tests must not be mistaken for -coverage of the deferred dynamic-export case. - -### 4. Parser flow and ideal simplification - -Files: `ScopeNode.java`, `Parser.java`, `CLInitNode.java`, and existing -control/memory lookup helpers. - -- Add path-local known-initialized facts to ScopeNode. Copy in `dup`, intersect - reachable predecessors in merges, and respect entry/backedge distinctions in - loop completion, break, continue, and return handling. The established - control/memory path must justify each check omitted by the parser. -- Implement constant-true, constant-false, preceding dominating CLInit, and - dominating true-test peepholes. Use dependencies to revisit proofs after SCCP - or inlining sharpens values. Keep remaining parent requirements when only a - child's own body is static. -- Preserve initialization effects when folding field loads into function - constants. Never hoist a check onto a path that previously did not touch the - class. Do not confuse an early true flag inside `<clinit>` with permission - to read that initializer's unfinished fields. -- Add fully-static classification using the initializer body and its effects, - excluding the generated flag store from the test. Emit a true initial flag - only after proof; remove the now-redundant body/store/checks. Extend mutable - constant-field lowering only where observations during initialization remain - correct. Do not use classification to erase evidence of a genuine cycle. - -Checkpoint: distinguish a dominating check from a one-arm-only check, a -zero-trip loop from a definitely executed touch, and a static child from an -unfinished dynamic parent. Check that early flag folding cannot hide a cycle. - -### 5. Phase ordering and final composition - -Files: `CodeGen.java`, `ParseAll.java`, `Serialize.java`, `ElfReader.java`, -`Main.java`, and the native linking path in `TestC.java` as appropriate. - -The current driver runs Parse, Iter, Opto, IterPostOpto, FinalizeIR, TypeCheck, LoopTree, -`unlinkImports`, serialization, then selection and scheduling. Add explicit -initialization-analysis boundaries without relying on a particular peephole -worklist order: - -1. During parse/load, register identities and represent active initialization - obligations in ideal IR. Ordinary Opto resolves targets and removes dead - control; do not collect a historical union of temporary types or calls. -2. After Opto finishes, collect surviving CLInitNodes and calls, complete - function summaries, classify provably static work without circular - assumptions, and validate the graph. Any remaining unknown required - initialization effects prevent certification. -3. Before `unlinkImports`, retain imported summaries needed for validation and - serialization. Serialize ideal CLInitNodes and their proof-bearing control - and memory edges, plus the independent requirements. -4. Lower remaining CLInitNodes before final loop-tree construction and machine - selection. Because lowering introduces branches/calls, rebuild affected - control information and run ordinary ideal/type validation on that graph. - Move the final LoopTree step if needed rather than using a stale tree from - before expansion. Keep the serialized representation ideal and reloadable. - Ensure the generated call participates in call linking before the final - unlink step; imported bodies may remain represented by symbolic targets. -5. Before native executable composition, load metadata for the exact full - Simple object set being linked, merge requirements, and validate again. - A plain GCC/ELF link is not an initialization-graph checker. Do the same - validation for a complete in-memory image. - -If optimization or loading discovers another target, mark summaries dirty and -rerun validation before emission. No runtime `initializing` state is introduced; -compile-time DFS bookkeeping is independent of the single runtime flag. - -Checkpoint: source, imported ideal IR, and final linked metadata agree on -acceptance/rejection for equivalent optimized graphs. Exercise different -worklist seeds and object-discovery orders to check determinism; eliminated -branches and targets must not leave stale dependencies. Missing required -metadata fails diagnostically. - -### Validation and scope - -Use a small set of focused fixtures covering several boundaries each, plus the -existing full Chapter 25 Make suites. Include effects before a nonreturning -call so dependency checking cannot accidentally depend on returned memory. -Exercise duplicate touches, conditional cycles, native-handoff diagnostics, -serialization alias remapping, class/flag ownership, and normal startup results. -Run the fixed allocation cohorts only after runtime semantics stabilize; graph -and spill changes are expected, and any new golden counts must follow execution -and register-correctness checks. Keep Jig tests ignored. - -Do not backport this feature, change pointer widths, introduce concurrent or -recoverable initialization, add dynamic module loading, or solve the general -native-export policy in these checkpoints. The extra analysis-only alias slice -remains an alternative representation if explicit summaries prove awkward; -implementing both would add work without improving the initial guarantee. - -## Module goals retained from the old notes - -Keep one-line programs easy to compile/run, filesystem-based nested namespaces, -leading-underscore privacy, separate compilation without source include files, -and ordinary object-file/native-linker interoperability. Classes may be declared -within a file or supplied by nested files. Source and object builds must have -the same initialization semantics. The current CLI is documented in the -[chapter README](README.md); historical command examples and conflicting -initializer-order proposals are superseded by this design. |
