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.
The compiler itself emits QBE IR, one unit at a time. M2make
--separate drives the whole object build and link:
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/<Module>.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:
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.
main. Every library object exports <Module>_init,
even when the module has no BEGIN body.main; it calls all
imported module inits in dependency order, then the program's own
init, then returns ExitCode.<Module>__<name>.
Globals stay <Module>_<name>; literals, constructors and sets are
module-qualified (<Module>_strN, <Module>_ctoN,
<Module>_setN); class vtables are <Module>__<Class>__vt.export data, so a program
object can reference an imported module's exported variable.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 |
M2 -I import scanner yet. M2 -c uses explicit interfaces;
use M2make --separate for transitive resolution.--separate rebuilds every object. Stale
objects are not detected; a .def/object mismatch surfaces as a
normal linker error..def (no implementation
object) must still have its vtable materialised by a user object.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.