# Separate compilation — implementation note **Status: core shipped (2026-10-24).** V3 now supports per-unit objects. The original one-shot multi-file mode is still the default for the test suite and corpus; separate compilation is additive. ## Workflow The compiler itself emits QBE IR, one unit at a time. `M2make --separate` drives the whole object build and link: ```sh M2make --separate --dialect iso \ --m2bin ./compiler/M2 --shim runtime/syslib/shim.c \ -o Prog Prog.mod ``` It resolves the imported `.def` files transitively, compiles each `.mod` against interfaces only, writes objects to `gen_ssa/.o`, and links them with `cc` plus the shared runtime shim. No `.def` is passed to `cc`. For a single unit, `M2 -c` emits QBE IR directly: ```sh M2 -c Lib.mod # picks up Lib.def automatically M2 -c Lib.def Prog.mod # explicit imported interface qbe -o gen_ssa/Lib.s gen_ssa/Lib.ssa cc -c gen_ssa/Lib.s -o Lib.o ``` `M2 -c` requires the imported interfaces on the command line (except the target's own `.def`, which is found beside the `.mod`). M2make is the transitive-interface resolver. ## Object model - **Library objects** contain declarations and bodies for one module and **no `main`**. Every library object exports `_init`, even when the module has no `BEGIN` body. - **The program object** is the only object with `main`; it calls all imported module inits in dependency order, then the program's own init, then returns `ExitCode`. - Module-level procedures use stable symbols `__`. Globals stay `_`; literals, constructors and sets are module-qualified (`_strN`, `_ctoN`, `_setN`); class vtables are `____vt`. - All top-level data is emitted with QBE `export data`, so a program object can reference an imported module's exported variable. - Nested procedures remain object-local (module prefix + uid). - Module init order comes from the interface order; M2make's topological sort supplies dependencies first. The implementation is split across: | Area | Change | | --- | --- | | `SymTab` | uid → module/depth/class tables; stable type/class names | | `QbeGen` | stable `Mangled`, module-qualified pools, `export data`, library/program split, external init list | | `Lower` | `-c` mode, empty init for bodyless objects, target `.def` data emission, class/vtable stability | | `compiler.frm` / `M2.mod` | `-c`, sibling `.def` lookup, imported-def init list, target def matching | | `M2make` | `--separate`: per-module `M2 -c` + qbe + cc, final link | ## Limits / next phases - **No `M2 -I` import scanner yet.** `M2 -c` uses explicit interfaces; use `M2make --separate` for transitive resolution. - **No incremental build:** `--separate` rebuilds every object. Stale objects are not detected; a `.def`/object mismatch surfaces as a normal linker error. - **Generics** still need the generic implementation source at the client; separate generic instances are not emitted as their own objects yet. - **Classes** have stable vtable/method symbols for normal imported class use; a class that exists only in a `.def` (no implementation object) must still have its vtable materialised by a user object. - **ABI/version marker** in objects is not implemented. - The fixpoint gate still compares the one-shot compiler image, not a multi-object rebuild. ## Tests `compiler/run_tests.sh` has separate-mode gates: - `SepProg` — exported data, procedure call, library `BEGIN`, link and run (`42`), library object must not define `main`. - `SepEmptyProg` — a library with no `BEGIN` still exports `SepEmpty_init` and links (`42`). Manual class check: a class in `CLib.def/.mod` linked into `CProg` returns `42` through a virtual call, with vtable/method symbols resolved across objects.