summaryrefslogtreecommitdiff
path: root/chapter25
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-10-08 16:52:27 -0700
committerCliff Click <cliffc@acm.org>2026-10-08 16:52:27 -0700
commit7096ba70c05eabfe96fd7e69d276b16ac1bd0bb9 (patch)
treebda5cb52577488cb61544c671afa5080297d0381 /chapter25
parent87a75c83b75480e65147a4d425fddd14a05df4d9 (diff)
downloadSimple-main.tar.gz
Simple-main.zip
Support putting static data into ELF constant poolmain
Mostly fcn inits in <clinit>
Diffstat (limited to 'chapter25')
-rw-r--r--chapter25/README.ja.md2
-rw-r--r--chapter25/README.md19
-rw-r--r--chapter25/initialization.md481
-rw-r--r--chapter25/module.ja.md275
-rw-r--r--chapter25/module.md279
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java2
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java3
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java45
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java95
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalBits.java6
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java3
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/StaticData.java195
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java11
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Chapter20Test.java2
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java2
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Chapter22Test.java18
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/StaticDataTest.java112
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/TestC.java1
18 files changed, 883 insertions, 668 deletions
diff --git a/chapter25/README.ja.md b/chapter25/README.ja.md
index cfb953c6..989161eb 100644
--- a/chapter25/README.ja.md
+++ b/chapter25/README.ja.md
@@ -74,7 +74,7 @@ return new Box(new i64[1]);
宣言で割り当て補助関数を生成します。これは割り当てを行い、隠れた `<init>` とユーザーコンストラクタを順に呼び、`EscapeNode` を通じて初期化済みの非公開メモリを公開します。パーサーの `Var` メタデータは、早すぎるフィールド読み取りやコンストラクタ出口での未初期化を検出します。Store の幅は保存する値ではなく、対象の宣言から得ます。
-ファイルレベルのコードは、インスタンス初期化とは異なるクラス初期化子(`<clinit>`)で表します。古い [モジュール設計ノート](module.ja.md) と [ロードマップ](ROADMAP.ja.md) には、特に初期化順序に関する提案や代替案があります。実装済み規則すべての仕様ではありません。
+ファイルレベルのコードは、インスタンス初期化とは異なるクラス初期化子(`<clinit>`)で表します。[遅延初期化の設計(英語)](initialization.md) は、一度だけ実行するためのチェック、モジュール間の非循環な依存関係、シリアライズの対応を提案しています。現在の実装すべてを記述する仕様ではありません。[ロードマップ](ROADMAP.ja.md) にも追加の提案があります。
## システムライブラリと例
diff --git a/chapter25/README.md b/chapter25/README.md
index 6d2a376b..4029827f 100644
--- a/chapter25/README.md
+++ b/chapter25/README.md
@@ -135,9 +135,10 @@ and missing initialization on constructor exits. Store widths come from target
declarations, not from the values being stored.
File-level code is represented by a class initializer (`<clinit>`), distinct
-from instance initialization. The older [module design notes](module.md) and
-[roadmap](ROADMAP.md) contain proposals and alternatives, especially concerning
-initialization order; they are not a specification of all implemented rules.
+from instance initialization. The [lazy initialization design](initialization.md)
+specifies proposed run-once checks, acyclic dependencies across modules, and
+serialization support. It describes planned work, not all currently implemented
+behavior. The [roadmap](ROADMAP.md) contains additional proposals.
## System library and examples
@@ -194,12 +195,12 @@ metric; see the [measurement details](../docs/regalloc-spills.md) for coverage.
<!-- spill-matrix:start -->
| Test cohort | Cases D / X | Ch 20: Ops / RA / X | Ch 21: Ops / RA / X | Ch 22: Ops / RA / X | Ch 23: Ops / RA / X | Ch 24: Ops / RA / X | Ch 25: Ops / RA / X |
|---|---:|---:|---:|---:|---:|---:|---:|
-| Ch 20 | 22 / 13 | — / — / 161 | 55,173 / 5,407 / 174 | 58,751 / 9,099 / 175 | 58,736 / 9,084 / 184 | 58,720 / 9,068 / 183 | 59,043 / 9,094 / 209 |
-| Ch 21 | 20 / 18 | | 6,207 / 261 / 461 | 6,324 / 379 / 475 | 6,326 / 381 / 475 | 6,325 / 380 / 470 | 6,233 / 219 / 533 |
-| Ch 22 | 14 / 8 | | | 198 / 60 / 7 | 194 / 56 / 7 | 194 / 56 / 7 | 332 / 136 / 11 |
-| Ch 23 | 14 / 8 | | | | 630 / 160 / 36 | 630 / 160 / 36 | 1,106 / 442 / 36 |
-| Ch 24 | 36 / 20 | | | | | 4,923 / 1,341 / 790 | 5,044 / 1,470 / 609 |
-| Ch 25 | 2 / 12 | | | | | | 20 / 0 / 1,862 |
+| Ch 20 | 22 / 13 | — / — / 161 | 55,173 / 5,407 / 174 | 58,751 / 9,099 / 175 | 58,736 / 9,084 / 184 | 58,720 / 9,068 / 183 | 58,965 / 9,076 / 208 |
+| Ch 21 | 20 / 18 | | 6,207 / 261 / 461 | 6,324 / 379 / 475 | 6,326 / 381 / 475 | 6,325 / 380 / 470 | 6,196 / 219 / 534 |
+| Ch 22 | 14 / 8 | | | 198 / 60 / 7 | 194 / 56 / 7 | 194 / 56 / 7 | 252 / 92 / 11 |
+| Ch 23 | 14 / 8 | | | | 630 / 160 / 36 | 630 / 160 / 36 | 864 / 358 / 36 |
+| Ch 24 | 36 / 20 | | | | | 4,923 / 1,341 / 790 | 4,646 / 1,335 / 609 |
+| Ch 25 | 2 / 12 | | | | | | 20 / 0 / 1,856 |
<!-- spill-matrix:end -->
diff --git a/chapter25/initialization.md b/chapter25/initialization.md
new file mode 100644
index 00000000..d03c2eb7
--- /dev/null
+++ b/chapter25/initialization.md
@@ -0,0 +1,481 @@
+# Lazy class initialization design
+
+Design proposal, 2026-10-08. This specifies the next implementation; the current
+compiler does not yet enforce this protocol. Declared constant final fields
+already have static-data lowering, but general lazy run-once initialization,
+dependency validation, and initialization guards remain to be implemented.
+
+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.
+- Initialization requirements form a DAG across the complete linked program,
+ including module boundaries. Reject cycles, including potential cycles
+ behind runtime conditionals. Ordinary recursive functions are still legal.
+- 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. 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.
+Do not infer permission for arbitrary callbacks or same-class helper calls from
+the fact that their owner is currently being initialized. Such helpers need a
+proof that they do not observe unfinished state; otherwise reject their use.
+
+## 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.
+
+Add a small `ClassInit` record owned by a CompUnit. The file class is available
+as `CompUnit._init`; a canonical map in CodeGen resolves records by full class
+name. Classes nested within a file do not each have a CompUnit today. They can
+have records owned by that same unit, including records with no dynamic body.
+This avoids treating an instance `<init>` as a class `<clinit>`.
+
+Suggested information, with names provisional:
+
+| Member | Meaning |
+|---|---|
+| `_name` | Stable, fully qualified class identity |
+| `_owner` | Defining CompUnit; owns native data and initializer definitions |
+| `_parent` | Enclosing class record, if any |
+| `_body` | Optional current `<clinit>` FunNode; absent for unloaded/static-only bodies |
+| `_deps` | Lazily allocated, deduplicated direct initialization dependencies |
+| `_complete` | All declarations, call effects, and dependency targets resolved |
+| `_static` | Proven that this class's own initialization requires no execution |
+| `_flagAlias` | Alias of the hidden `$init` field in this class object |
+
+An absent body is not proof of `_static`: it may simply not have been loaded.
+Metadata flags describe compiler knowledge, not additional runtime states.
+An explicitly defined namespace-only parent can have an empty initialization
+record; a missing parent definition must not silently become such a record.
+FunNodes can refer back to their initialization record; the record survives
+inlining, dead-function elimination, and conversion of initialization to data.
+
+`addInitDep(target, reason)` adds a direct edge only once and invalidates the
+cached graph validation/order. Retain a source location and, for indirect
+dependencies, a call-site witness for diagnostics. Do not turn every dependency
+addition into a scan of all initializer bodies.
+
+## 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.
+
+Record semantic touches before inlining and guard elimination. Collect direct
+touches in ordinary function bodies too, and propagate their initialization
+effects through the 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 all possible targets' effects.
+
+The initial policy is conservative: a runtime conditional contributes both
+branches' possible requirements. Do not let inlining choices or random peephole
+order decide whether a cycle is legal. Dependencies may be added as forward
+references and call targets resolve. Before declaring a graph complete, every
+reachable 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
+
+Introduce an effectful `CLInitNode` with a stable ClassInit 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.
+Record the semantic requirement even when no check needs inserting. Passive
+Simple class references remain subject to checks at their 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:** `_static` 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 compilation-local class IDs. These are path
+facts, not mutable global properties of ClassInit or its declared TypeStruct:
+
+- On an active touch, record the dependency, then consult the facts. If C and
+ its required ancestors are known ready, omit the CLInitNode.
+- 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 ClassInit record.
+
+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 class 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 and validation
+
+1. Add ClassInit identities and a distinct lazy dependency collection; preserve
+ passive class references. Add direct/function effect summaries and cycle
+ diagnostics, including unresolved external-call handling.
+2. Serialize/load the directory and summaries. Validate combined graphs before
+ imported-body unlinking and at final composition. Preserve ordinary function
+ entries; establish startup handling and diagnostics for unsupported native
+ handoffs before claiming initialization coverage at the FFI boundary.
+3. Add serializable CLInitNode IR, the hidden field in each class object, and
+ the flag Store in `<clinit>`. Use the same node for ordinary checks and
+ startup. Add parser flow tracking and the basic constant/dominating-check
+ peepholes as part of this implementation, rather than relying on a later
+ cleanup pass to remove every repeated check.
+4. Refine flow proofs and fully-static classification. Extend constant-memory
+ lowering only with the required observation/identity proofs.
+
+Validation should cover the actual boundaries, with a small number of focused
+fixtures: parent-before-child order; an untouched branch performing no I/O;
+two callers executing one initializer once; direct, indirect, sibling and
+cross-module cycle rejection; passive/static reference cycles; a shared class
+and flag reached through two separately compiled importers; a join/loop where
+the first check cannot be removed; and rejection of unsupported native class
+handoffs. Check function-pointer acquisition at the class field, with no new
+ordinary function wrapper. Check parser omission and ideal elimination separately: a dominating
+check, a true-test branch, an unchecked merge arm, and a zero-trip loop. Verify
+one body-owned flag store before inlining, and that folding its early true
+value cannot hide a cycle or bypass field initialization checks. Preserve entry
+arguments/results and verify after serialization reload.
+Run existing Chapter 25 suites and ARM/RISC/native execution checks for each
+implementation frontier. No earlier-chapter backport is implied by this design.
+
+## 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.
diff --git a/chapter25/module.ja.md b/chapter25/module.ja.md
deleted file mode 100644
index 2746ad1e..00000000
--- a/chapter25/module.ja.md
+++ /dev/null
@@ -1,275 +0,0 @@
-# Simple のモジュール
-
-[English](module.md) | 日本語
-
-この文書は [Modules for Simple](module.md) の日本語訳です。原文はモジュールの設計案です。
-
-目標:
-
-- 有効なプログラムを1行で書けるようにする(チュートリアルや初心者の導入を簡単にする)。
- - 最小限の手間でコンパイルして実行する。
-- 分割コンパイル、名前空間、外部ライブラリを扱う。
- - ELF など、リンク可能・実行可能な形式へコンパイルする。
-- フィールドやクラスを簡単に非公開にする。
-- 出力ファイルを、既存の .o/.exe/lib/dll の仕組みと互換にする。
-- `#include` や `import` 用のファイルを不要にする。
-- モジュールごとに構造・ファイルシステムの階層を持たせる。
- - 複数のファイルで1つのモジュールを構成することも考えられる。
- - モジュール内で簡単に名前空間を使う。
- - 親ファイルまたは入れ子のファイルに、入れ子の名前を置く。
-- クラス初期化子について次を保証する。
- - 正確に1回だけ実行する。
- - 最初にアクセスされた時点で遅延実行する。
- - 最初のインスタンスが作られる前、またはその時点までに完了する。
- - `<clinit>` 内の特別な原型インスタンス(golden instance)は例外として認める。
-
----
-### 目標:最小限の手間で有効な1行プログラムを書く
-
-現在の作業ディレクトリの名前から、モジュールとクラス `hello` を作ります。
-追加の引数なしでコンパイルすると、リンクと実行も行います。
-
-ファイル:`hello.smp`
-```
-sys.io.p("Hello World!");
-```
-
-コンパイルして実行します。
-```
-> smp hello.smp
-Hello World!
-```
-
-モジュール名とクラス名は、ファイル名から取り出した `hello` です。
-メモリ内でコンパイルし、`exec` で実行します。ファイルは作りません。
-これは、その場でコンパイルして実行するためだけのモードです。
-
----
-### 目標:分割コンパイル
-
-コンパイル時に出力先のビルドディレクトリを指定すると、`.o` ファイルを得られます。
-出力先ディレクトリの構造はソースと一致し、場所は自由に選べます。
-
-前と同様に、コンパイルのコマンドでモジュール名を指定していないため、
-`Pet` がモジュールにもクラスにもなります。
-
-ファイル:Pet.smp
-```
-// Class initializer <clinit> is any code outside a struct declaration
-// and is guaranteed to run-once on first touch.
-
-// static fields listed here:
-int default_leggies = 4;
-
-// A default (no argument) <init> is code inside struct with same name as the
-// file; this is the same behavior as field initializers for Java, although
-// any code is allowed, not just expressions.
-
-struct Pet {
- str name;
- int leggies = default_leggies;
-}
-
-float yet_another_static_field;
-```
-
-`build/` ディレクトリ内の `Pet.o` へコンパイルします。
-```
-> smp Pet.smp -o build/
-> ls build
-Pet.o
-```
-
----
-### 目標:入れ子の名前空間
-
-`myRepo` 内のファイル名から、モジュールとクラス `Pet` を作ります。
-入れ子のクラス `Pet.Vet` も作ります。これらを別のモジュール `Owner` から使います。
-
-ディレクトリ:`~/myRepo/`
-```
-~/myRepo/
- Pet.smp
- build/
-
-~/otherRepo/
- Owner.smp
-```
-
-ファイル:`myRepo/Pet.smp`
-```
-int default_leggies = 4;
-
-struct Pet {
- str name;
- int leggies = default_leggies;
-}
-
-// Nested class Vet inside of file Pet
-struct Vet {
- str name;
- str address;
- long credit_card_num;
-}
-```
-
-ファイル:`otherRepo/Owner.smp`
-```
-Pet dog = Pet{name="Fido"};
-Pet snake = Pet{name="slither", leggies = 0; };
-Pet.Vet vet = Pet.Vet{name="Linda"; address="101 Main St"; credit_card_num=1234};
-
-sys.io.p("I took "+dog.name+" to "+vet.name+"\n");
-```
-
-`build/` ディレクトリ内の `Pet.o` へコンパイルします。
-コマンドで別のモジュールのルートを指定していないため、`Pet` がモジュールであり、クラスでもあります。
-`Pet` がモジュールなので、`Vet` はモジュール `Pet` 内のクラスです。
-
-```
-> smp Pet.smp -o build/
-> ls build
-Pet.o
-```
-
-`Pet` と `Pet.Vet` のコードは、同じ `Pet.o` ファイルに入ります。
-
-自由変数 `Pet` と `Pet.Vet` を解決するために `Pet` のリポジトリを指定して、
-`Owner.smp` をコンパイルし、実行します。
-
-```
-> smp Owner.smp -lib ~/myRepo/build
-I took Fido to Linda
-```
-
----
-### 目標:入れ子のファイル内の入れ子の名前空間
-
-前の例と同じですが、ファイルを入れ子にします。
-
-```
-~/myRepo
- Pet.smp
- Pet/
- Vet.smp
- build/
- Pet.o
- Pet/
- Vet.o
-
-~/otherRepo
- Owner.smp
-```
-
-ファイル:Pet.smp
-```
-int default_leggies = 4;
-
-struct Pet {
- str name;
- int leggies = default_leggies;
-}
-```
-
-ファイル:Pet/Vet.smp
-```
-// Nested class Vet inside of directory Pet
-struct Vet {
- str name;
- str address;
- long credit_card_num;
-}
-```
-
-`Owner.smp` をコンパイルし、実行します。
-```
-> smp Owner.smp -lib ~/myRepo/build
-I took Fido to Linda
-```
-
----
-### 目標:クラス初期化子の遅延実行
-
-```
-Parent.smp
-Parent/
- ChildA.smp
- ChildB.smp
-```
-
-ファイル:`Parent.smp`
-```
-sys.io.p("In Parent <clinit>");
-sys.io.p(ChildB.str);
-```
-
-ファイル:`ChildA.smp`
-```
-// <clinit> for ChildA
-sys.io.p("In ChildA <clinit>");
-str str = "A";
-```
-
-ファイル:`ChildB.smp`
-```
-// <clinit> for ChildB
-sys.io.p("In ChildB <clinit>");
-str str = "B";
-```
-
-コンパイルして実行します。
-```
-> smp Parent.smp
-In Parent <clinit>
-In ChildB <clinit>
-B
-
-```
-
-### 目標:簡単な非公開フィールド
-
-名前の先頭にアンダースコアを付けると、フィールドやクラスが非公開になります。
-
-### 目標:C コンパイラのドライバに似た構文
-
-```
-Simple --cpu x86_64_v2 --abi Win64 --norun -o lib/lib/sys_x86_64_v2_win64.o src/main/smp/sys.smp
-```
-
-モジュールを指定していないため、最上位のファイルがモジュール `sys` になります。
-モジュールディレクトリも指定していないため、ファイル名のディレクトリ部分 `src/main/smp` になります。
-ビルドディレクトリを指定していないため、出力ファイルのディレクトリ部分 `lib/` になります。
-出力ファイル名の既定値は `smp.o` ですが、ここでは別名を指定しています。
-
-ある `<clinit>` は別の `<clinit>` を参照でき、最初のアクセス時に実行されます。
-設計上、子より先に親へアクセスする必要があります。ただし、子 A は子 B を参照できます。
-Parent.B へのアクセスは、次を意味します。
-
-- 最初に Parent にアクセスし、`<clinit>` を開始する。
-- そこからアクセスするものにも、A や B を含め、再帰的に `<clinit>` を実行する。
-- Parent の処理が完了する。
-- `<clinit>` 内では別の*モジュール*を参照できない。
-- 静的関数は別のモジュールを参照*できる*が、その場合、実行される可能性がある……。
-
-目標:モジュール内の `<clinit>` を決定的にする。
-
-- 背景:複雑な `<clinit>` の順序のせいで、子が使う時点で参照先の static final フィールドがまだ初期化されていないことが、何度もあった。
-- 副目標:親が子より先になるなど、`<clinit>` の*順序*を決定的にする。
- - 問題:Parent の `<clinit>` の途中で Child を参照すると、Parent が完了する前に Child の `<clinit>` が走る。
- - 部分的な回答:親が先で「親は子を知っている」ため、これはよい。
- - 追加の問題:モジュールをまたぐと成り立たない。あるモジュールは別のモジュールの親ではない。
- - 問題:`<clinit>` 中に呼び出す Parent の関数が、非決定的に子を初期化できる。
- - `fcn = { -> rand ? A.fld : B.fld }`
-- 回答案:Parent の `<clinit>` は、直接にも間接にも子を参照しては*ならない*。
-- 例:`B b = new B; // Illegal ref to child in Parent.<clinit>`
- — Parent の初期化子から子への不正な参照。
-- 例:`B? b = fcn(); fcn = { -> rand ? null : new B; } // Illegal call to fcn referring to B inside Parent <clinit>`
- — Parent の初期化子内で B を参照する関数への不正な呼び出し。
-- 例:`B? b; fcn = { -> new B; } // OK, no B.<clinit> during Parent.<clinit>`
- — Parent の初期化中に B の初期化子を実行しないため、これは許される。
-
-目標:外部とのリンクでも、1回限りの初期化機能を使えるようにする。
-
-回答案:外部から `Foo.foo()` を呼ぶときは、まず `Foo.<clinit>` が実行済みであることを確認してから、
-`Foo.foo()` を呼び出す。任意の最適化として、成功後は呼び出し元を変更し、
-`<clinit>` の確認を省略する。
diff --git a/chapter25/module.md b/chapter25/module.md
deleted file mode 100644
index 4cd801e7..00000000
--- a/chapter25/module.md
+++ /dev/null
@@ -1,279 +0,0 @@
-
-# Modules for Simple
-
-English | [日本語](module.ja.md)
-
-Goals:
-- 1-liner valid program (for easy tutorial/newbie starts)
-- - Compile and run with minimal effort
-- Separate compilation and namespaces; external libs
-- - Compile to ELF or other linkable/executable format
-- Easy private fields & classes
-- Output files are compatible with existing .o/.exe/lib/dll infrastructure
-- No `#include` or `import` files
-- Structure / file system heirarchy per module
-- - Perhaps many files make a module
-- - easy namespaces within a module
-- - Nested names in parent file or nested file
-- Class initializers guarenteed:
-- - execute exactly once
-- - lazily on first touch
-- - complete on or before first instances are made
-- - - (an exception is allowed for a golden instance in `<clinit>`)
-
-
----
-### Goal: Valid 1-liner, minimal effort
-
-This makes a module and a class `hello` out of the current working directory
-name. Compiling without more arguments also links and executes.
-
-File: `hello.smp`
-```
-sys.io.p("Hello World!");
-```
-
-Compile and run:
-```
-> smp hello.smp
-Hello World!
-```
-
-Module and class name is `hello` extracted from file name, compiled in memory
-and `exec'd`. No files made, this is strictly a run and gun mode.
-
-
----
-### Goal: separate compilation
-
-Name a build directory target during compilation to get `.o` files.
-The build target directory structure will match the source, and
-can be placed anywhere.
-
-As before, since no module name is mentioned on the compile line, `Pet` becomes
-both the module and a class.
-
-File: Pet.smp
-```
-// Class initializer <clinit> is any code outside a struct declaration
-// and is guaranteed to run-once on first touch.
-
-// static fields listed here:
-int default_leggies = 4;
-
-// A default (no argument) <init> is code inside struct with same name as the
-// file; this is the same behavior as field initializers for Java, although
-// any code is allowed, not just expressions.
-
-struct Pet {
- str name;
- int leggies = default_leggies;
-}
-
-float yet_another_static_field;
-```
-
-Compile to `Pet.o` in the `build/` directory:
-```
-> smp Pet.smp -o build/
-> ls build
-Pet.o
-```
-
-
----
-### Goal: nested namespaces
-
-This makes a module and class `Pet` based on the file name in `myRepo`. It
-also makes a nested class `Pet.Vet`. These are used from a different module
-`Owner`.
-
-
-Directory: `~/myRepo/`
-```
-~/myRepo/
- Pet.smp
- build/
-
-~/otherRepo/
- Owner.smp
-```
-
-File: `myRepo/Pet.smp`
-```
-int default_leggies = 4;
-
-struct Pet {
- str name;
- int leggies = default_leggies;
-}
-
-// Nested class Vet inside of file Pet
-struct Vet {
- str name;
- str address;
- long credit_card_num;
-}
-```
-
-File: `otherRepo/Owner.smp`
-```
-Pet dog = Pet{name="Fido"};
-Pet snake = Pet{name="slither", leggies = 0; };
-Pet.Vet vet = Pet.Vet{name="Linda"; address="101 Main St"; credit_card_num=1234};
-
-sys.io.p("I took "+dog.name+" to "+vet.name+"\n");
-```
-
-Compile to `Pet.o` in the `build/` directory; since the compile line does not
-mention some other module root, `Pet` must be the module (and is also a class).
-Since `Pet` is the module, `Vet` is a class within the module `Pet`.
-
-```
-> smp Pet.smp -o build/
-> ls build
-Pet.o
-```
-
-The code for both `Pet` and `Pet.Vet` are in the same `Pet.o` file.
-
-Compile and run `Owner.smp`, specifying the `Pet` repo to satisfy the free
-variables `Pet` and `Pet.Vet`.
-
-```
-> smp Owner.smp -lib ~/myRepo/build
-I took Fido to Linda
-```
-
----
-### Goal: nested namespace in a nested file
-
-Same as the last example but with nested files:
-
-```
-~/myRepo
- Pet.smp
- Pet/
- Vet.smp
- build/
- Pet.o
- Pet/
- Vet.o
-
-~/otherRepo
- Owner.smp
-```
-
-
-File: Pet.smp
-```
-int default_leggies = 4;
-
-struct Pet {
- str name;
- int leggies = default_leggies;
-}
-```
-
-File: Pet/Vet.smp
-```
-// Nested class Vet inside of directory Pet
-struct Vet {
- str name;
- str address;
- long credit_card_num;
-}
-```
-
-Compile and run `Owner.smp`:
-```
-> smp Owner.smp -lib ~/myRepo/build
-I took Fido to Linda
-```
-
-
----
-### Goal: Class Initializers are Lazy
-
-```
-Parent.smp
-Parent/
- ChildA.smp
- ChildB.smp
-```
-
-File: `Parent.smp`
-```
-sys.io.p("In Parent <clinit>");
-sys.io.p(ChildB.str);
-```
-
-File: `ChildA.smp`
-```
-// <clinit> for ChildA
-sys.io.p("In ChildA <clinit>");
-str str = "A";
-```
-
-File: `ChildB.smp`
-```
-// <clinit> for ChildB
-sys.io.p("In ChildB <clinit>");
-str str = "B";
-```
-
-Compile and run:
-```
-> smp Parent.smp
-In Parent <clinit>
-In ChildB <clinit>
-B
-
-```
-
-### Goal: Easy private fields
-
-Leading underscore makes field or class private.
-
-### Goal: similar to C compiler driver syntax
-
-```
-Simple --cpu x86_64_v2 --abi Win64 --norun -o lib/lib/sys_x86_64_v2_win64.o src/main/smp/sys.smp
-```
-
-Module not mentioned, so top-level file is module: `sys`
-Module directory not mentioned, so is prefix of file name `src/main/smp`.
-Build dir not mentioned, so is prefix of output file: `lib/`
-Output file is default `smp.o`, here renamed.
-
-
-
-One <clinit> can refer to another, and will be executed as first touched.
-Parents must be touched before children by design.
-However a child A can refer to a child B.
-Touching Parent.B means
-- Parent is touched first, and begins <clinit>
-- Anything it touches, gets recursively <clinit> including e.g. A or B
-- Parent completes.
-- Nothing in a <clinit> can refer to another *module*.
-- Static functions *can* refer to another module, but then can be executed...
-
-Goal: deterministic <clinit> within a module.
- History: plenty of times I've had complex <clinit> orders, and then discovered
- some referred static-final-fields are not yet init'd when child uses.
- SubGoal: deterministic <clinit> *order*, Parent before Child, etc.
- Issue: mid-Parent <clinit>, refers to Child, so Child <clinit> before Parent completes.
- SubAnswer: Since parent first, and "parent knows child", this is OK.
- SubIssue: this does not work across modules; one module is not a parent of another
- Issue: Parent fcns called during <clinit> can non-determistic init child.
- "fcn = { -> rand ? A.fld : B.fld }"
- Answer?: Parent <clinit> *may not* direct or indirect refer to a child.
- Example: `B b = new B; // Illegal ref to child in Parent.<clinit>`
- Example: `B? b = fcn(); fcn = { -> rand ? null : new B; } // Illegal call to fcn referring to B inside Parent <clinit>`
- Example: `B? b; fcn = { -> new B; } // OK, no B.<clinit> during Parent.<clinit>`
-
-Goal: external linkage also enjoys the 1-shot init feature.
- Answer?: extern call to 'Foo.foo()' first checks Foo.<clinit>
- has run, before calling `Foo.foo()`. Optional: Caller is
- modified to bypass <clinit> check after success.
- \ No newline at end of file
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
index 86e83372..6fc66e4f 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
@@ -733,6 +733,8 @@ public class CodeGen {
// make a machine node for "this".
private Node _instSelect( Node n, IdentityHashMap<Node,Node> map ) {
if( n==null ) return null;
+ if( n instanceof StoreNode st && StaticData.preinitialized(this,st) )
+ return _instSelect(st.mem(),map);
Node x = map.get(n);
if( x !=null ) return x; // Been there, done that
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java
index 2681066e..d1326cee 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java
@@ -30,9 +30,6 @@ public class CompUnit {
//BAOS _serial; // Serialized IR for this ELF file
Ary<CompUnit> _deps; // CompUnits that this CompUnit depends on
- //// List of symbols exported by this compilation unit, and their Node
- //// definitions.
- //public HashMap<String,Node> _exported;
// Per-Compilation-Unit Start/StopNodes, keeping alive all exported Nodes.
// Null means no code loaded (yet).
public StartCUNode _start;
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java
index 7423edbd..1b6281b8 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java
@@ -133,16 +133,22 @@ public class ElfWriter {
// The relocations; 3 words per relocation
public class ReloSection extends DataSection {
ReloSection( int info ) {
- super(".rela.text", SHT_RELA, SHF_INFO_LINK, 24/*entsize*/);
+ this(".rela.text",info);
+ }
+ ReloSection(String name,int info) {
+ super(name, SHT_RELA, SHF_INFO_LINK, 24/*entsize*/);
_link = 2;
_info = info;
}
void writeReloOffPlus(int offset, int symidx, byte relo) {
+ writeRelo(offset,symidx,relo,-4);
+ }
+ void writeRelo(int offset,int symidx,byte relo,long addend) {
assert symidx > 0;
write8(offset);
write8(((long)symidx << 32L) | relo);
- write8(-4);
+ write8(addend);
}
}
@@ -287,7 +293,7 @@ public class ElfWriter {
_strtab = new StringSection();
// place all the symbols
- int nlocals = 1/*null*/+7/*number of sections*/+1/*Simple section*/;
+ int nlocals = 1/*null*/+9/*number of sections*/+1/*Simple section*/;
SymbolSection symbols = new SymbolSection(nlocals);
// we've already constructed these entire sections in the encoding phase
@@ -324,12 +330,35 @@ public class ElfWriter {
relocations.writeReloOffPlus(offset, symidx, (byte)2/*PC32*/);
}
- // Write relocations for the constant pool
- String cpoolPrefix = _code.entryClinitName().replaceAll("[^A-Za-z0-9_$]", "_") + "$CPOOL$";
+ // Stable function identities are also addressable from data, including
+ // anonymous functions which have no source-level exported name.
+ HashMap<String,Integer> dataSymbols=new HashMap<>();
+ for( FunNode fun : _code._linker )
+ if( fun!=null && !fun.isDead() && _code.owns(fun) && enc.opStart(fun)>=0 ) {
+ String name=_code._fidxs.symbol(fun.sig().fidx());
+ dataSymbols.put(name,symbols.symbol(name,text._index,SYM_BIND_GLOBAL,SYM_TYPE_FUNC,enc.opStart(fun),0));
+ }
+ for( StaticData.ObjectData obj : enc._data.objects.values() ) {
+ int section=obj.external ? 0 : obj.readOnly ? rodata._index : rwdata._index;
+ dataSymbols.put(obj.symbol,symbols.symbol(obj.symbol,section,SYM_BIND_GLOBAL,SYM_TYPE_OBJECT,
+ obj.external ? 0 : obj.offset,obj.external ? 0 : obj.size));
+ }
for( Encoding.Relo relo : enc._bigCons.values() ) {
- DataSection data = relo.readOnly() ? rodata : rwdata;
- int symidx = symbols.symbol(cpoolPrefix+symbols.gidx(), data._index, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, relo._target, relo._t.size());
- relocations.writeReloOffPlus(relo._opStart+relo._off, symidx, relo._elf );
+ StaticData.ObjectData obj=enc._data.object(relo._t);
+ relocations.writeReloOffPlus(relo._opStart+relo._off,dataSymbols.get(obj.symbol),relo._elf);
+ }
+ ReloSection roRelos=new ReloSection(".rela.rodata",rodata._index);
+ ReloSection rwRelos=new ReloSection(".rela.data",rwdata._index);
+ for( StaticData.Relocation relo : enc._data.relocations ) {
+ String name=enc._data.targetSymbol(relo.value());
+ Integer sym=dataSymbols.get(name);
+ if( sym==null ) {
+ sym=symbols.symbol(name,0,SYM_BIND_GLOBAL,SYM_TYPE_NOTYPE);
+ dataSymbols.put(name,sym);
+ }
+ // AMD64 absolute 32/64-bit addresses; linker rejects overflow.
+ byte kind=(byte)(relo.logSize()==2 ? 10 : 1);
+ (relo.object().readOnly ? roRelos : rwRelos).writeRelo(relo.object().offset+relo.offset(),sym,kind,0);
}
// Create a "Simple" section for Simple types and ir
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
index 7cb92884..8e9b99db 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
@@ -41,6 +41,8 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
public final BAOS _cpool = new BAOS(); // Constant r/o pool
public final BAOS _sdata = new BAOS(); // Static r/w pool
public final HashMap<Node,Relo> _bigCons = new HashMap<>();
+ public final StaticData _data;
+ public final HashMap<String,Integer> _externalAddresses = new HashMap<>();
// Big Constant relocation info.
public static class Relo {
@@ -70,6 +72,7 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
Encoding( CodeGen code ) {
_code = code;
+ _data = new StaticData(this);
}
// Shortcut to the defining register
@@ -217,10 +220,8 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
// encodings and compact the code, changing all the offsets.
compactShortForm();
- // Write the constant pool
- writeConstantPool(_cpool,true );
- // Write the static memory
- writeConstantPool(_sdata,false);
+ // Place pool objects and record relocations within their contents.
+ _data.write();
// Patch RIP-relative and local encodings now.
patchLocalRelocations();
@@ -517,52 +518,6 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
}
}
- // --------------------------------------------------
- // Write the constant pool into the BAOS and optionally patch locally
- void writeConstantPool( BAOS bits, boolean ro ) {
- padN(16,bits);
-
- // radix sort the big constants by alignment
- Ary<Relo>[] raligns = new Ary[5];
- for( Node op : _bigCons.keySet() ) {
- Relo relo = _bigCons.get(op);
- if( relo.readOnly() != ro )
- continue;
- int align = relo._t.alignment();
- Ary<Relo> relos = raligns[align]==null ? (raligns[align]=new Ary<>(Relo.class)) : raligns[align];
- relos.add(relo);
- }
-
-
- // Types can be used more than once; collapse the dups
- HashMap<Type,Integer> targets = new HashMap<>();
-
- // By alignment
- for( int align = raligns.length-1; align >= 0; align-- ) {
- Ary<Relo> relos = raligns[align];
- if( relos == null ) continue;
- for( Relo relo : relos ) {
- // Map from relo to constant start and patch
- Integer target = targets.get(relo._t);
- if( target==null ) {
- targets.put(relo._t,target = bits.size());
- // Write constant into constant pool
- switch( relo._t ) {
- case TypeTuple tt -> throw Utils.TODO("no tuples here, use structs instead");
- case TypeStruct ts -> addStruct(bits,ts);
- // Simple primitive (e.g. larger int, float, function ptr)
- default -> addN(align,relo._t,bits);
- }
- }
- // Record target address and opcode start.
- // Target is relative to the cpool/sdata start.
- relo._target = target;
- relo._opStart= opStart(relo._op);
- }
- }
-
- }
-
// Emit a single scalar as bits
static void addN( int log, Type t, BAOS bits ) {
long x = switch( t ) {
@@ -577,36 +532,22 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
addN(log,x,bits);
}
- // Structs use internal field layout
- private void addStruct( BAOS bits, TypeStruct ts ) {
- // Field order by offset
- int[] layout = ts.layout();
- int off=0; // offset in the struct
- for( int fn=0; fn<ts._fields.length; fn++ ) {
- Field f = ts._fields[layout[fn]];
- if( f._extern ) continue;
- int foff = ts. offset(layout[fn]);
- // Pad up to field
- while( off < foff ) { bits.write(0); off++; };
- // Constant array fields are special
- if( f._fname=="[]" ) { // Must be a constant array
- ((TypeConAry)f._t).write(bits);
- off += ((TypeConAry)f._t).len();
- } else {
- int log = f._t.log_size();
- addN(log,f._t.isConstant() ? f._t : f._t.makeZero(),bits);
- off += 1<<log;
- }
- }
- }
-
-
// A series of libc/external calls that Simple can link against in a JIT.
// Since no runtime in the JVM process, using magic numbers for the CPU
// emulators to pick up on.
public static int SENTINEL_CALLOC = -4;
public static int SENTINEL_WRITE = -8;
+ int externalAddress(String name) {
+ Integer address=_externalAddresses.get(name);
+ if( address!=null ) return address;
+ return switch(name) {
+ case "calloc" -> SENTINEL_CALLOC;
+ case "write" -> SENTINEL_WRITE;
+ default -> throw new IllegalArgumentException("Unresolved external function '"+name+"'");
+ };
+ }
+
void patchGlobalRelocations() {
for( Node src : _externData.keySet() ) {
String name = _externData.get(src);
@@ -618,11 +559,7 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
for( Node src : _externals.keySet() ) {
int start = opStart(src);
String dst = _externals.get(src);
- int target = switch( dst ) {
- case "calloc" -> SENTINEL_CALLOC;
- case "write" -> SENTINEL_WRITE ;
- default -> throw Utils.TODO();
- };
+ int target = externalAddress(dst);
((RIPRelSize)src).patch(this, start, opLen(src), target - start);
}
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalBits.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalBits.java
index b0060024..bc9c9d39 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalBits.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalBits.java
@@ -129,6 +129,12 @@ public class GlobalBits {
return next(clz,order);
}
+ // Stable native symbol identity, independent of this compilation's dense IDs.
+ String owner(int local) { return _local2Clz.at(local); }
+ String symbol(int local) {
+ return _local2Clz.at(local)+"$F$"+_local2Order.at(local);
+ }
+
// Write out to BAOS enough bits to unwind the local back to global index
void packed( BAOS baos, HashMap<String,Integer> strs ) {
// Unwind a local index to the clz.
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java
index d249b80c..252b5bf0 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java
@@ -23,10 +23,11 @@ public class LinkMem {
// Patch local ops, e.g. loading float constants from the constant pool
for( Node op : enc._bigCons.keySet() ) {
Encoding.Relo relo = enc._bigCons.get(op);
- int target = relo._target+(relo.readOnly() ? cpool : sdata);
+ int target = enc._data.address(enc._data.object(relo._t),cpool,sdata);
((RIPRelSize)relo._op).patch(enc, relo._opStart, enc.opLen(relo._op), target - relo._opStart);
}
+ enc._data.link(cpool,sdata);
return _code;
}
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/StaticData.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/StaticData.java
new file mode 100644
index 00000000..f9f16008
--- /dev/null
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/StaticData.java
@@ -0,0 +1,195 @@
+package com.seaofnodes.simple.codegen;
+
+import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.BAOS;
+import java.util.*;
+
+// Pool objects have identity independently of their initial field values.
+// References in their contents use the same symbols in ELF and emulator images.
+public final class StaticData {
+ public static final class ObjectData {
+ public final Type type;
+ public final String symbol;
+ public final boolean readOnly, external;
+ public final int alignment, size;
+ public int offset;
+ ObjectData(Type t, String name, boolean ro, boolean ext) {
+ type=t; symbol=name; readOnly=ro; external=ext;
+ alignment=t.alignment();
+ int align=1<<alignment;
+ size=t instanceof TypeStruct ts ? (Math.max(1,ts.size())+align-1)&-align : align;
+ }
+ }
+ public record Relocation(ObjectData object, int offset, int logSize, Type value) {}
+
+ final Encoding enc;
+ final LinkedHashMap<Object,ObjectData> objects = new LinkedHashMap<>();
+ final ArrayList<Relocation> relocations = new ArrayList<>();
+ final String prefix;
+
+ public ArrayList<ObjectData> entries() {
+ ArrayList<ObjectData> entries=new ArrayList<>(objects.values());
+ entries.sort(Comparator.comparingInt((ObjectData obj) -> obj.alignment).reversed());
+ return entries;
+ }
+
+ StaticData(Encoding enc) {
+ this.enc=enc;
+ prefix=enc._code.entryClinitName()+"$DATA$";
+ }
+
+ static boolean isClass(Type t) {
+ return t instanceof TypeStruct ts && Parser.startsClzPrefix(ts._name);
+ }
+
+ static CompUnit owner(CodeGen code,TypeStruct ts) {
+ return owner(code,ts._name.substring(Parser.CLZ.length()));
+ }
+ static CompUnit owner(CodeGen code,String name) {
+ if(name==null) return null;
+ CompUnit owner=null;
+ for( CompUnit cu : code._compunits.values() )
+ if( cu._cname!=null && (name.equals(cu._cname) || name.startsWith(cu._cname+".")) &&
+ (owner==null || cu._cname.length()>owner._cname.length()) ) owner=cu;
+ return owner;
+ }
+
+ // A final field whose declaration already denotes this exact constant is
+ // available as that value to every ideal Load. Materialize it in the object
+ // image instead of emitting the redundant initializer Store. This lowering
+ // leaves serialized ideal IR intact, so imports retain their memory edges.
+ static boolean preinitialized(CodeGen code,StoreNode st) {
+ if( !(st.ptr()._type instanceof TypeMemPtr ptr) || !ptr._one || !isClass(ptr._obj) ) return false;
+ CompUnit owner=owner(code,ptr._obj);
+ if( owner==null || owner._src==null ) return false;
+ if( !(Parser.TYPES.get(ptr._obj._name) instanceof TypeStruct ts) ) return false;
+ Field fld=ts.field(st._name);
+ if( fld==null || !fld._final || !fld._t.isConstant() || !st.val()._type.isConstant() ) return false;
+ Type value=st.val()._type, declared=fld._t;
+ if( declared instanceof TypeFunPtr fp && value instanceof TypeFunPtr vp ) return fp.fidx()==vp.fidx();
+ if( declared instanceof TypeMemPtr ptr0 && value instanceof TypeMemPtr ptr1 )
+ return pointerConstant(ptr0) && ptr0._obj==ptr1._obj;
+ return declared==value;
+ }
+
+ ObjectData object(Type t) {
+ Object key=isClass(t) ? ((TypeStruct)t)._name
+ : t instanceof TypeFunPtr fp ? functionSymbol(fp) : t;
+ ObjectData old=objects.get(key);
+ if( old!=null ) return old;
+ boolean clz=isClass(t);
+ if( clz && Parser.TYPES.get(((TypeStruct)t)._name) instanceof TypeStruct canonical ) t=canonical;
+ CompUnit owner=clz ? owner(enc._code,(TypeStruct)t)
+ : t instanceof TypeFunPtr fp ? owner(enc._code,enc._code._fidxs.owner(fp.fidx())) : null;
+ String symbol=clz ? ((TypeStruct)t)._name.substring(Parser.CLZ.length())+".$class"
+ : t instanceof TypeFunPtr fp ? functionSymbol(fp) : prefix+objects.size();
+ ObjectData obj=new ObjectData(t,symbol,!clz && (!(t instanceof TypeStruct ts) || ts.isConstant()),
+ owner!=null && owner._src==null);
+ objects.put(key,obj);
+ // Register before following fields: class objects may refer to each other.
+ if( !obj.external && t instanceof TypeStruct ts )
+ for( Field f : ts._fields )
+ if( !f._extern && f._t.isConstant() ) target(f._t);
+ return obj;
+ }
+
+ private static boolean pointerConstant(TypeMemPtr ptr) {
+ return ptr.notNull() && (isClass(ptr._obj) || (ptr._obj.isConstant() && (!ptr._obj.isAry() || ptr._obj.isConAry())));
+ }
+
+ private void target(Type t) {
+ if( t instanceof TypeMemPtr ptr && pointerConstant(ptr) ) object(ptr._obj);
+ if( t instanceof TypeFunPtr fp && fp.notNull() &&
+ enc._code.link(fp)==null && !enc._code._externFunc.containsKey(fp.fidx()) )
+ // A dead function still needs a unique non-null identity for comparisons.
+ object(fp);
+ }
+
+ void write() {
+ ArrayList<Encoding.Relo> refs=new ArrayList<>(enc._bigCons.values());
+ refs.sort(Comparator.comparingInt(r -> r._op._nid));
+ for( Encoding.Relo ref : refs ) object(ref._t);
+ // Definitions belong to their compilation unit, even if every local use folded.
+ ArrayList<CompUnit> units=new ArrayList<>(enc._code._compunits.values());
+ units.sort(Comparator.comparing(cu -> cu._cname==null ? "" : cu._cname));
+ for( CompUnit cu : units ) if( cu._src!=null && cu._clz!=null ) object(cu._clz);
+ for( ObjectData obj : entries() ) {
+ if( obj.external ) continue;
+ BAOS bits=obj.readOnly ? enc._cpool : enc._sdata;
+ Encoding.padN(1<<obj.alignment,bits);
+ obj.offset=bits.size();
+ if( obj.type instanceof TypeStruct ts ) writeStruct(obj,ts,bits);
+ else Encoding.addN(obj.alignment,obj.type,bits);
+ while( bits.size()<obj.offset+obj.size ) bits.write(0);
+ }
+ for( Encoding.Relo ref : refs ) {
+ ObjectData obj=object(ref._t);
+ ref._target=obj.offset;
+ ref._opStart=enc.opStart(ref._op);
+ }
+ }
+
+ private void writeStruct(ObjectData obj, TypeStruct ts, BAOS bits) {
+ int[] layout=ts.layout();
+ for( int idx : layout ) {
+ if( idx==ts._fields.length ) continue;
+ Field f=ts._fields[idx];
+ if( f._extern ) continue;
+ int off=ts.offset(idx);
+ while( bits.size()<obj.offset+off ) bits.write(0);
+ if( f._fname.equals("[]") ) ((TypeConAry)f._t).write(bits);
+ else {
+ Type value=f._t.isConstant() ? f._t : f._t.makeZero();
+ int log=f._t.log_size();
+ Encoding.addN(log,value,bits);
+ if( (value instanceof TypeFunPtr fp && fp.notNull()) ||
+ (value instanceof TypeMemPtr ptr && pointerConstant(ptr)) )
+ relocations.add(new Relocation(obj,off,log,value));
+ }
+ }
+ while( bits.size()<obj.offset+ts.size() ) bits.write(0);
+ }
+
+ String functionSymbol(TypeFunPtr fp) {
+ String ext=enc._code._externFunc.get(fp.fidx());
+ return ext!=null ? ext : enc._code._fidxs.symbol(fp.fidx());
+ }
+
+ String targetSymbol(Type value) {
+ if( value instanceof TypeMemPtr ptr ) return object(ptr._obj).symbol;
+ TypeFunPtr fp=(TypeFunPtr)value;
+ return enc._code.link(fp)==null && !enc._code._externFunc.containsKey(fp.fidx())
+ ? object(fp).symbol : functionSymbol(fp);
+ }
+
+ void link(int cpool, int sdata) {
+ for( Relocation rel : relocations ) {
+ long address;
+ if( rel.value instanceof TypeMemPtr ptr ) address=address(object(ptr._obj),cpool,sdata);
+ else {
+ TypeFunPtr fp=(TypeFunPtr)rel.value;
+ String ext=enc._code._externFunc.get(fp.fidx());
+ FunNode fun=enc._code.link(fp);
+ if( ext!=null ) address=enc.externalAddress(ext);
+ else if( fun==null ) address=address(object(fp),cpool,sdata);
+ else {
+ if( !enc._code.owns(fun) ) throw new IllegalArgumentException("Unlinked function "+functionSymbol(fp));
+ address=enc.opStart(fun);
+ }
+ }
+ // Emulator native-call sentinels are signed 32-bit addresses.
+ if( rel.logSize==2 && (address<Integer.MIN_VALUE || address>0xffff_ffffL) )
+ throw new IllegalArgumentException("Function address outside 32 bits: "+address);
+ BAOS bits=rel.object.readOnly ? enc._cpool : enc._sdata;
+ int off=rel.object.offset+rel.offset;
+ for( int i=0; i<(1<<rel.logSize); i++ ) bits.buf()[off+i]=(byte)(address>>>(8*i));
+ }
+ }
+
+ int address(ObjectData obj,int cpool,int sdata) {
+ if( obj.external ) throw new IllegalArgumentException("Unlinked class object "+obj.symbol);
+ return obj.offset+(obj.readOnly ? cpool : sdata);
+ }
+}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java b/chapter25/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
index 994065d8..e8218afd 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java
@@ -79,12 +79,19 @@ public final class ASMPrinter extends AssemblyAdapter<Node> {
if( !encoded() ) return pools;
int base=(codeEnd+15)&-16;
if( _enc._cpool.size()>0 )
- pools.add(new Pool("Constant Pool",_enc._cpool.buf(),0,base,entries(_enc._bigCons.values(),true)));
+ pools.add(new Pool("Constant Pool",_enc._cpool.buf(),0,base,objects(true)));
base=(base+_enc._cpool.size()+15)&-16;
if( _enc._sdata.size()>0 )
- pools.add(new Pool("Static Data",_enc._sdata.buf(),0,base,entries(_enc._bigCons.values(),false)));
+ pools.add(new Pool("Static Data",_enc._sdata.buf(),0,base,objects(false)));
return pools;
}
+ private ArrayList<Data> objects(boolean ro) {
+ var data=new ArrayList<Data>();
+ for( var obj : _enc._data.entries() )
+ if( !obj.external && obj.readOnly==ro )
+ data.add(new Data(obj,obj.symbol+": "+obj.type.str(),obj.alignment,obj.size));
+ return data;
+ }
// Retain this diagnostic entry point for checking that pool printing never lays out types.
static int printConstantPool(int address, SB sb, BAOS bits, HashMap<Node,Encoding.Relo> relos,
boolean ro, String name) {
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter20Test.java
index 40e9172e..b46a54b2 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter20Test.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter20Test.java
@@ -258,7 +258,7 @@ val _hashCodeString = { !String self ->
};
""";
testCPU(src,"x86_64_v2", "SystemV",21,null);
- testCPU(src,"riscv" , "SystemV",10,null);
+ testCPU(src,"riscv" , "SystemV",8,null);
testCPU(src,"arm" , "SystemV",8,null);
}
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java
index 8010e245..0cfdb577 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -384,7 +384,7 @@ val _hashCodeString = { !String self ->
};
""";
testCPU(src,"x86_64_v2", "SystemV",21,null);
- testCPU(src,"riscv" , "SystemV",10,null);
+ testCPU(src,"riscv" , "SystemV",8,null);
testCPU(src,"arm" , "SystemV",8,null);
}
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter22Test.java
index e8c080fd..7484a6ae 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter22Test.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter22Test.java
@@ -146,9 +146,9 @@ struct _Person { i32 age;};
p.age = (arg<<17)>>17;
return 0;
""";
- assertEquals(82, testCPUSize(src, "x86_64_v2","win64", 3 ));
- assertEquals(96, testCPUSize(src, "riscv", "SystemV", 4 ));
- assertEquals(96, testCPUSize(src, "arm" , "SystemV", 4 ));
+ assertEquals(65, testCPUSize(src, "x86_64_v2","win64", 3 ));
+ assertEquals(76, testCPUSize(src, "riscv", "SystemV", 4 ));
+ assertEquals(76, testCPUSize(src, "arm" , "SystemV", 4 ));
// do assertEquals here
EvalRisc5 R5 = TestRisc5.build( src, "sext_str_not_fold_away", 0, 4, false);
@@ -178,9 +178,9 @@ return 0;
int trap_arm = A5.step(100);
assertEquals(0,trap_arm);
- assertEquals(82, testCPUSize(src, "x86_64_v2","win64",3 ));
- assertEquals(96, testCPUSize(src, "riscv", "SystemV",4 ));
- assertEquals(96, testCPUSize(src, "arm", "SystemV",4 ));
+ assertEquals(65, testCPUSize(src, "x86_64_v2","win64",3 ));
+ assertEquals(76, testCPUSize(src, "riscv", "SystemV",4 ));
+ assertEquals(76, testCPUSize(src, "arm", "SystemV",4 ));
}
@@ -202,9 +202,9 @@ return 0;
int trap_arm = A5.step(100);
assertEquals(0,trap_arm);
- assertEquals(72, testCPUSize(src, "x86_64_v2","win64",3 ));
- assertEquals(92, testCPUSize(src, "riscv", "SystemV",5 ));
- assertEquals(92, testCPUSize(src, "arm", "SystemV",5));
+ assertEquals(55, testCPUSize(src, "x86_64_v2","win64",3 ));
+ assertEquals(72, testCPUSize(src, "riscv", "SystemV",5 ));
+ assertEquals(72, testCPUSize(src, "arm", "SystemV",5));
// do assertEquals here
}
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/StaticDataTest.java b/chapter25/src/test/java/com/seaofnodes/simple/StaticDataTest.java
new file mode 100644
index 00000000..070b7a9e
--- /dev/null
+++ b/chapter25/src/test/java/com/seaofnodes/simple/StaticDataTest.java
@@ -0,0 +1,112 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.codegen.StaticData;
+import com.seaofnodes.simple.type.TypeStruct;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import com.seaofnodes.isa.eval.EvalArm64;
+import com.seaofnodes.isa.eval.EvalRisc5;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class StaticDataTest {
+ private static final String SRC="val n=42; val f={int x -> x+n;}; return 0;";
+
+ private static StaticData.ObjectData root(CodeGen code) {
+ return code._encoding._data.entries().stream()
+ .filter(obj -> obj.symbol.endsWith("Test.$class")).findFirst().orElseThrow();
+ }
+
+ // Read the image before <clinit>, then call the function through its data slot.
+ @Test public void testEmulatorImage() {
+ for( String cpu : new String[]{"arm","riscv"} ) {
+ CodeGen code=new CodeGen(SRC).driver(cpu,"SystemV",true,false);
+ var e=code._encoding;
+ var obj=root(code);
+ TypeStruct ts=(TypeStruct)obj.type;
+ int cp=(e._bits.size()+15)&-16, sd=(cp+e._cpool.size()+15)&-16;
+ byte[] image=new byte[1<<20];
+ System.arraycopy(e._bits.buf(),0,image,0,e._bits.size());
+ System.arraycopy(e._cpool.buf(),0,image,cp,e._cpool.size());
+ System.arraycopy(e._sdata.buf(),0,image,sd,e._sdata.size());
+ int n=sd+obj.offset+ts.offset(ts.find("n"));
+ int f=sd+obj.offset+ts.offset(ts.find("f"));
+ if( cpu.equals("arm") ) {
+ var arm=new EvalArm64(image,1<<16);
+ assertEquals(42,arm.ld4s(n));
+ arm._pc=arm.ld4s(f);
+ arm.regs[0]=7;
+ assertEquals(0,arm.step(1000));
+ assertEquals(49,arm.regs[0]);
+ } else {
+ var r5=new EvalRisc5(image,1<<16);
+ assertEquals(42,r5.ld4s(n));
+ r5._pc=r5.ld4s(f);
+ r5.regs[10]=7;
+ assertEquals(0,r5.step(1000));
+ assertEquals(49,r5.regs[10]);
+ }
+ }
+ }
+
+ @Test public void testNativeImage() throws Exception {
+ Path dir=Path.of("build/objs/static-data");
+ Files.createDirectories(dir);
+ CodeGen code=new CodeGen(null,dir.toString(),null,"Test",SRC,123L,true,
+ TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ TypeStruct ts=(TypeStruct)root(code).type;
+ Path driver=dir.resolve("driver.c");
+ Files.writeString(driver,"""
+ #include <stdint.h>
+ #include <string.h>
+ extern unsigned char data[] __asm__("Test.$class");
+ typedef int64_t (__attribute__((CALL_CONV)) *fun)(int64_t);
+ int main(void) {
+ int64_t n; uint32_t f;
+ memcpy(&n,data+%d,sizeof(n));
+ memcpy(&f,data+%d,sizeof(f));
+ return n!=42 || ((fun)(uintptr_t)f)(7)!=49;
+ }
+ """.formatted(ts.offset(ts.find("n")),ts.offset(ts.find("f"))));
+ String exe=dir.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString();
+ TestC.linkExe(dir.resolve("Test.o").toString(),
+ TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi",
+ driver.toString(),null,exe);
+ assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe));
+ }
+
+ // Two separately compiled users must reference the same mutable class object.
+ @Test public void testSharedClass() throws Exception {
+ Path dir=Path.of("build/objs/static-data-shared");
+ Files.createDirectories(dir);
+ Path lib=dir.resolve("lib");
+ Files.createDirectories(lib);
+ var paths=new Ary<>(new String[]{lib.toString()});
+ String[] names={"Globals","A","B"};
+ String[] sources={"int count=0;",
+ "val a={ -> Globals; };", "val b={ -> Globals; };"};
+ for( int i=0; i<names.length; i++ )
+ new CodeGen(null,(i==0?lib:dir).toString(),i==0?null:paths,names[i],sources[i],123L,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ Path driver=dir.resolve("driver.c");
+ Files.writeString(driver,"""
+ #include <stdint.h>
+ __attribute__((CALL_CONV)) extern int64_t *a(void),*b(void);
+ int main(void) {
+ int64_t *x=a(),*y=b();
+ if(x!=y) return 1;
+ *x=37;
+ return *y!=37;
+ }
+ """);
+ String exe=dir.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString();
+ TestC.linkExe(lib.resolve("Globals.o").toString(),
+ TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi",
+ driver.toString(),new Ary<>(new String[]{dir+"/A.o",dir+"/B.o"}),exe);
+ assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe));
+ }
+}
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/TestC.java b/chapter25/src/test/java/com/seaofnodes/simple/TestC.java
index 64518b0d..b08be824 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/TestC.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/TestC.java
@@ -167,6 +167,7 @@ public abstract class TestC {
// pointers as 4bytes.
var params = new Ary<>(String.class);
params.add("gcc");
+ params.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie");
if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main`
params.add(obj);
// Standalone Simple programs export `simple_main`; the small C