# Plan — separate compilation for V3 (future work) **Status: deferred.** Finish the compiler as it is first (corpus, language gaps, robustness). This document records the design so the work can be picked up later without re-deriving it. **Baseline at the time of writing:** `v3-smallfix`; suite 259/259, fixpoint OK 4,028,447 bytes, corpus 511/612, 0 crashes. --- ## 1. What separate compilation means ```text compile time link time .def (interface) ──► type checking .mod (body) ──► codegen ──► .o ──┐ ├──► cc/ld ──► executable runtime shim + libc ──────────────────────┘ ``` - Every compilation reads the **`.def` of every imported module** (transitively, because a def can re-export types from another def). It does **not** read the imported module's `.mod`, `.o`, or any previous compilation state. - A `.def` change requires recompiling its importers (like a C header change). No link-time type checking. - The linker receives only `.o` files, the runtime shim and system libraries. **`.def` files are never passed to the linker.** ## 2. Why V3 cannot do it today V3 is a single-session, whole-program compiler: - `M2.mod` parses all units on the command line into one `SymTab`, then `Lower.EndSession` calls `QbeGen.EndModule`, which writes one `gen_ssa/.ssa`. - Procedure symbols are **`_`**, where `uid` comes from `SymTab.nextUid` in session parse order (`QbeGen.Mangled`). Separate compilations assign different uids to the same procedure → undefined symbols at link time. - Vtable symbols are **`$vt_`** (`QbeGen.VtRef`); type indices are session-local too. - Literal/constructor pools are session-global: `$strN`, `$setN`, `$ctoN`. - `QbeGen.EndModule` flushes all pools and emits `main`; there is no per-module object boundary. - Module init bodies already emit `_init` (`QbeGen.BeginInit`) and `main` calls the init functions encountered in the same session. Cross-object init ordering is the one new runtime concern. - Generics re-parse the generic body in the client session (`M2.mod` `DoInst`). ## 3. Required design ### 3.1 Stable symbol names (critical path) | Entity | Current | Needed | | --- | --- | --- | | Module-level procedure | `_` | `__` | | Module-level variable | `_` (already stable) | keep | | Nested procedure / method | `_` | `__` or deterministic local id | | Class vtable | `$vt_` | `____vt` | | String literal | `$strN` | `__strN` | | Set constant | `$setN` / named `_` | module-qualified; exported consts keep global names | | Constructor | `$ctoN` | `__ctorN` | | External C procedure | C link name | keep | Module-level names are unique in Modula-2; module qualification is enough. Nested procedures need a deterministic path (not the session counter). Generics use the declared instance name, so `__` is stable. ### 3.2 Per-unit codegen - `M2 -c Foo.mod` emits one QBE module for `Foo` with `export function` for public procedures and `data` for globals. - Library modules emit **no `main`**; only the program module emits `main`, the init calls and the `ExitCode` return. - Split `QbeGen.EndModule` into “end object” and “end program”. - Pools are per-object; exported set/string `CONST`s are materialised by the client from the `.def` value or referenced by stable symbols. ### 3.3 Module initialization - Each module with a `BEGIN` body/class init exports `_init` (already the case). - The program object's `main` calls the init functions of all linked modules **once, in dependency order**, before the program body. - Simplest implementation: the driver/M2make computes the import DAG and emits an array of init function pointers in the program object. - Nested modules are initialised by their enclosing module's init. - Alternative: emit constructors into `.init_array` (needs C/asm glue; less portable with QBE). Start with the generated list. ### 3.4 Interface resolution / driver - Add `-c` (compile one unit) and `-I ` search paths. - Read the target `.mod` plus every direct/transitive `.def`; never the imported `.mod`. - Keep the current one-shot multi-file mode for tests/corpus. - Optional `.sym` cache: serialise parsed def symbol tables, keyed by def hash + compiler version + flags. Not required initially; stale caches are a real risk. ### 3.5 Generics - ISO generic modules are compile-time templates. A client instantiation still needs the **generic body**, not just the `.def`. - Choose one: - instantiate into the client object (generic `.mod` must be on the search path), or - emit a separate instance module/object (`Inst.o`) with stable instance names and link it. - The `.def` alone is enough for type checking, not for emitting code. ### 3.6 Classes / sets / strings - Class layouts come from the `.def`; vtables/methods live in the defining module's object under stable names. - Exported set/string `CONST`s can be re-materialised from the `.def` value or referenced by a stable exported symbol. - Avoid cross-object dependencies on private literal pools. ## 4. The linker Normal `cc`/`ld`; no compiler-specific linker needed: ```sh M2 -c -I runtime/syslib:lib/iso Lib.mod -o Lib.o M2 -c -I runtime/syslib:lib/iso Prog.mod -o Prog.o cc Prog.o Lib.o runtime/syslib/shim.o -lm -o Prog ``` - `.o` files + shim + libc; **no `.def` at link time**. - `main` only in the program object. - `Lib__P`, `Lib__x`, `Lib__Class__vt` are ordinary symbols. - `Lib__init` is exported and called from `Prog`'s `main` via the generated init list. - Undefined `Lib__P` at link time = stale object or `.def`/object mismatch → rebuild the importer. - Runtime (`shim.c`, `-lm`) is linked once, as today. - No linker script needed initially. ## 5. Phases (do not start until the current compiler work stops) 1. **Stable names + per-unit `.ssa` in one session.** Keep the whole-session driver, emit one `.ssa` per unit, link them. Validates symbols, pools, vtables and the init list. 2. **`-c` / `-I` driver mode.** Compile one `.mod` against transitive `.def`s; emit `.o`; link with `cc`. 3. **Init order + program/main split.** Generated init table, module bodies, nested modules. 4. **Generics and classes.** Instance emission strategy; stable vtable/method symbols. 5. **M2make incremental builds.** Per-module `.o` targets, `.def` dependencies, relink. 6. **Self-host with separate compilation.** The compiler as N objects; adjust the fixpoint gate (compare per-object `.ssa` or the linked binary). ## 6. Tests / gates for each phase - A two-module program (`Lib.def/.mod` + `Prog.mod`) compiled as separate objects, linked, run. - Globals, exported procedures, set/string `CONST`s, class vtable dispatch, generic instances. - Module init order (an init that depends on an imported module's init). - Recompiling only the program after an unrelated module change. - Recompiling an importer after a `.def` change. - `cc` link failure on a stale object is expected and documented. - The existing suite/fixpoint/corpus gates must stay green; the fixpoint may need a new comparison for multi-object builds. ## 7. Risks - **Stable naming** is the highest-risk item: it touches imports, generics, classes, vtables, bootstrap and self-hosting. - **Generics** need the generic implementation at client compile time; there is no “precompiled generic interface” shortcut for the body. - **Init order** must be deterministic and dependency-aware. - **ABI/versioning:** objects from different compiler versions/flags must not be mixed silently. Record a compiler/ABI version in the object or build system. - **Fixpoint:** the “byte-identical single `.ssa`” gate must be redefined for multi-object builds. - **Debugging:** separate objects make it easier to inspect a module, but harder to reproduce whole-program diagnostics until the driver is solid. ## 8. Non-goals / keep it simple - No dynamic linking, no incremental type checking beyond `.def` mtimes, no linker plugin, no binary `.def` format initially. - Keep `.def` as the source of truth; a `.sym` cache is optional. - Keep the one-shot mode for the test suite/corpus. ## Appendix — related future work: dialect library folders The question “can PIM, M2R10 and ISO libraries live in separate folders?” is independent of separate compilation, but it compounds the same interface-resolution work. A possible layout: ```text runtime/syslib/ SysShim, FileIO, Utf8 (shared runtime) lib/pim/ InOut, RealInOut, NumberIO, FpuIO, StrIO, ... lib/iso/ STextIO, SWholeIO, SRealIO, TextIO, IOChan, StreamFile, ChanConsts, IOConsts, SIOResult, ConvTypes, Conversions, RealStr, WholeStr, LongStr, LongMath, Strings, Storage, ProgramArgs, SysClock, TERMINATION, ... lib/m2r10/ future R10 redesigned library (not ISO/PIM) lib/gm2/ (optional) gm2-only modules (DynamicStrings, Files, ...) lib/common/ modules genuinely identical across dialects ``` Rules: - **One dialect per compilation session.** PIM and ISO can both define `InOut`/`RealInOut`/`Storage` with different signatures; never put both folders on the same `-I` path for one session. - The build system (M2make) selects `lib/pim` or `lib/iso` (or `lib/m2r10`), plus `runtime/syslib`. - M2R10 is a **different language revision**, not a dialect of ISO/PIM; its library must not be mixed with either. - The corpus harness currently uses one `V3_LIBS`; it would need to compile PIM and ISO corpora with different library paths (or audit that no colliding module is used across both). - Keep `stdlib/` as a compatibility aggregate only during the transition; remove it once the test/corpus scripts use the dialect folders. - Classification of `SysIO`, `Files`, `DynamicStrings`, `CharClass`, `WrapC`/`WrapClock`/`WrapTime`, `TimeString` needs an audit: ISO, gm2-only, or V3-specific. This is a low-risk directory/build change once the compiler is stable; it should be scheduled after the current corpus/language work, not during it.