Spec M10 — Tier 2: passes and backends taught through the surface

Prerequisite: M9 closed (golden updated). Proves the central requirement: a backend written in .mc, outside the compiler, produces __text byte for byte identical to the built-in one.

Core (small, on both sides) #

  1. Function address: &name where name is a function/extern → uptr (adrp/add with reloc PAGE21/PAGEOFF12 for _name).
  2. Indirect call: intrinsic callp(p, a1, ..., a7) → args in x0..x6, blr via x8? No: load p into x16 (IP0, caller-saved, outside x0..x7) and blr x16. Saving live depths is the same as for bl. Return type i64.
  3. User hook: src/mc.mc includes src/user.mc, which by default includes lib/user_default.mc containing void user_init() { }. The driver calls user_init() before parsing. Teaching the compiler = editing src/user.mc to include your modules and running make mc1 (mc recompiles itself with the module included). No dylib, no ABI.
  4. Registration (src/hooks.mc, part of the compiler): pass(uptr fn) appends to a linear table; backend(uptr name, uptr fn) likewise. Driver: after parse_unit and fold, applies each pass in order (root = callp(fn, root)); picks the backend via --backend=NAME (default macho, built in) and calls callp(fn, root, out_path).
  5. Splitting gen: gen_unit(root) now has two halves with public names: gen_lower(root) (AST → per-function Ins buffers + global/string tables, no encoding) and gen_encode_all() + macho_write(out). The built-in macho backend = gen_lower + gen_encode_all + macho_write. Each function's Ins buffer and the tables are reachable via accessors (ins_count(f), ins_at(f, i), func_count(), func_name(f) …).

Proof (in the surface, under lib/) #

Budget #

C: &fn + callp + splitting gen + hooks ≈ 80 lines (target ≤ 2980). .mc: same, plus the demonstration modules (which don't count against the budget).

Edit this page