diff options
| author | Cliff Click <cliffc@acm.org> | 2026-10-08 16:52:27 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-10-08 16:52:27 -0700 |
| commit | 7096ba70c05eabfe96fd7e69d276b16ac1bd0bb9 (patch) | |
| tree | bda5cb52577488cb61544c671afa5080297d0381 /chapter25 | |
| parent | 87a75c83b75480e65147a4d425fddd14a05df4d9 (diff) | |
| download | Simple-main.tar.gz Simple-main.zip | |
Support putting static data into ELF constant poolmain
Mostly fcn inits in <clinit>
Diffstat (limited to 'chapter25')
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 |
