Plan: mc — a self-hosting mini compiler, teachable via its surface

Read docs/plan.md before any work: it fixes the language, the teaching surface, the architecture, the budget, and the milestones. This file just summarizes the operating rules.

Context #

Empty repository (main with no commits). The goal is a compiler that is deliberately tiny at its core — basic types, an opaque pointer, arithmetic/logic, bitwise/shift, loop {} — whose distinguishing feature is teaching tooling via the surface: the source code itself registers new lexemes/tokens, extends the parser, operates on the AST, and emits object-file bytes saying which section/symbol they go into.

Decisions fixed with the user:

DecisionChoice
stage0 hostC23, as small as possible, compiled by clang exactly once
Critical requirementStage1 onward is self-hosted: never uses gcc/cc/clang again
Initial targetAArch64 + Mach-O (this machine: darwin arm64, Xcode 26 / ld-1267)
ExtensionVia the surface, in the language itself; other ISAs/outputs are taught this way later
OutputPhase 1: .o (MH_OBJECT) + Apple's ld. Phase 2 (post fixed point): direct executable with an ad-hoc signature
Syntax"schoolbook C / Arduino C": type name, no fn/->/:; a single, opaque uptr pointer (no * sigils); #... directives to teach the compiler
External referencesWrite everything from scratch in this repository

Organizing thesis: stage0 doesn't need to compile "the language"; it needs to compile one program — src/mc.mc. Every scope question is answered with "does this appear in the compiler's own source?".

Name: .mc files, mc binary.


The language, by example (this is what stage0 compiles) #

#include "sys.mc"                 // textual include, once-only

#define HEAP_SIZE 1048576         // constant folded at compile time (constant expressions only)

u8  heap[HEAP_SIZE];              // global array = reservation in __bss; the name is worth a uptr
i64 hp = 0;                       // global in __data

uptr alloc(i64 n) {
    uptr p = heap + hp;           // uptr is opaque: byte arithmetic, no scaling
    hp = hp + ((n + 7) & ~7);
    return p;
}

i64 fib(i64 n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

void putnum(i64 v) {
    u8 buf[24];                   // local array = space in the frame
    i64 i = 24;
    loop {
        i = i - 1;
        st8(buf + i, '0' + v % 10);   // memory access by explicit width
        v = v / 10;
        if (v == 0) break;
    }
    write(1, buf + i, 24 - i);
}

i64 main(i64 argc, uptr argv) {
    uptr first = ld64(argv);      // argv[0] with no sigil: ld64 reads 8 bytes at argv
    putnum(fib(24));              // prints 46368
    write(1, "\n", 1);            // string literal is worth a uptr into __cstring
    return 0;
}

Core — what stage0 implements (and nothing else) #

Types (7 words): u8 u16 u32 u64 i64 uptr void. i64 is the working integer type; u8 for bytes; u16/u32 because Mach-O fields require them (n_desc, n_strx); uptr is the only pointer — opaque, no pointee type, byte arithmetic. No i8/i16/i32, no float, no bool (comparisons yield i64 0/1). Comparisons are always signed (addresses < 2^63; documented, not enforced). Since M45 the self-hosted compiler registers i32 for itself at start-up, with the same type_new a module uses — it is not a keyword, not in the ladder, and stage0 still has exactly these seven words.

Memory (intrinsics, not syntax): ld8 ld16 ld32 ld64 (read, zero-extend) and st8 st16 st32 st64(p, v) (write); &x gives the uptr of a local/global; an array name decays to uptr. No *p, no p->f, no p[i].

Operators: + - * / % · & | ^ ~ << >> (>> arithmetic only on i64) · == != < <= > >= · && || ! with short-circuit evaluation (required: p != 0 && ld8(p) == 'x') · unary - and & · C-style cast (u32) x (unambiguous: a type keyword always follows () · assignment is = only.

Control: if (c) stmt [else stmt], loop { }, break; / break 2; (N levels, no labels needed), continue;, return [e];. No while/for/switch/goto — those come from the prelude.

Declarations (schoolbook C): type name(type a, ...) { } (max. 8 params → never passes an argument on the stack; exceeding it is an error; no varargs); type x = e; local; type x[N]; local/global array (N constant); top-level globals with a constant initializer; extern type name(type a, ...); (undefined symbol — this is how write/open from libSystem come in; the compiler prefixes _). Two top-level passes → mutual recursion without forward declarations.

Literals: decimal/hex integer, char 'a' '\n', string (goes to __cstring, NUL-terminated).

Allocation: a static u8 heap[...] arena in __bss + bump pointer, no free. Comes zeroed by the kernel. No malloc, no mmap.

I/O: only what's in lib/sys.mc: open read write close exit. Default impl = libSystem extern; alternate impl via #opcode svc (number in x16, svc #0x80; SYS_exit=1 read=3 write=4 open=5 close=6, verified against the SDK's sys/syscall.h), selected by a flag.

Mandatory style for mc.mc: never a raw ld64(n + 16) in the middle of the code; always #define NODE_LHS 16 + accessors node_lhs(n) / set_node_lhs(n, v). When struct arrives via the surface, you swap 20 accessors, not 3,000 call sites.

Core lexemes: 7 types + if else loop break continue return extern + directives #include #define #token #infix #prefix #rule #section #opcode + punctuation ( ) { } [ ] , ; + the operators above. Everything else is taught.


Teaching surface (Tier 1 — already works in the C stage0) #

Preprocessor-style #... directives, processed at compile time, in order of appearance, mutating the core's tables:

// 1. Lexer: new lexeme (sequential id >= 256, matched by longest prefix)
#token "<=>"
#token "+="

// 2. Expression parser: Pratt table. $1/$2 are the operands; the expansion is
//    parsed right away and becomes an AST with holes.
#infix  "<=>" 6 left   cmp3($1, $2)
#prefix "~~"           bitrev($1)

// 3. Statement parser: flat pattern -> template. Each item is a literal token
//    or "nt $name" (nt: expr | stmt | block | ident | type), read as a C parameter.
//    The 1st item is always a literal token: rules are indexed by it (zero backtracking).
#rule stmt: while ( expr $c ) block $b
    => loop { if (!$c) break; $b }

#rule stmt: for ( stmt $init expr $cond ; ident $i = expr $step ) block $b
    => { $init loop { if (!$cond) break; $b $i = $step; } }   // the step is an assignment: in the core `=` is a statement

#rule stmt: ident $x += expr $e ;
    => $x = $x + $e;

// 4. Placement: everything emitted afterward goes to this section until the next #section
//    ("say where they go"). Default: __TEXT,__text for code, __DATA,__data
//    for initialized globals, __DATA,__bss for arrays without an initializer.
#section __DATA __mytable 0
u64 table[64];

#section __TEXT __text 0x80000400

// 5. Encoders: teach one instruction. Called with constant arguments, it emits the
//    folded word directly into the current function's code stream.
#opcode mov16(rd, imm)   0xD2800000 | (imm << 5) | rd
#opcode svc(imm)         0xD4000001 | (imm << 5)

i64 sys_write(i64 fd, uptr buf, i64 n) {
    mov16(16, 4);            // x16 = SYS_write; x0..x2 already carry the args on entry
    svc(0x80);               // result ends up in x0, which is the return value
}

// 6. Raw bytes and relocations, for what #opcode doesn't cover:
//    emit(u32 constant); reloc(TYPE, "_symbol") binds a relocation to the next word.
void call_helper() {
    reloc(BRANCH26, "_helper");
    emit(0x94000000);
}

Rules that keep the mechanism small:

  1. A #rule pattern is a flat sequence — no alternation, optional items, or recursion in the pattern.
  2. The first item is a literal token (optionally preceded by a single ident $x, already read via the normal path) → indexed by token, no backtracking.
  3. The template is parsed by the existing parser at definition time ($name becomes Hole(i)); expansion is a tree copy — never textual substitution, so there are no precedence bugs.
  4. Hygiene: gensym only — $$tmp in the template becomes a fresh local per expansion. Nothing else.
  5. Re-expansion of the result, capped at 64 levels.
  6. Frame size is computed after expansion (gensyms are locals).
  7. #define is a folded constant, not a textual macro; #opcode only accepts constant arguments (otherwise it's an error).

Tier 2 — programmatic (stage1+, zero cost in C): since the compiler is written in .mc, a new AST pass or backend is a .mc module included via #include that calls pass(fn) / backend("name", fn) at init time. Recompiling mc with that module is teaching the compiler. No interpreter, no dylib, no plugin ABI. The AST is flat data in an arena with #define offsets, and the output primitives are ordinary functions: sec_new(seg, sect, flags), sym_def(name, sec, off, global), reloc_add(sec, off, sym, type, pcrel, len), emit_u32(sec, w). A surface backend is just code that calls them.


Architecture #

 L1  surface (.mc)       #token #infix #prefix #rule #section #opcode  emit() reloc()
                         + .mc modules with pass()/backend()  (stage1+)
 ───────────────────────────────────────────────────────────────────────────────────
 L0  core                lexer w/ mutable token table
     (stage0 in C,        table-driven Pratt + statements + #rule expander
      later in mc.mc)     flat AST in arena → minimal type checking
                         linear instruction buffer → AArch64 encoders → Mach-O writer

Repository layout #

mini_compiler/
  Makefile                    targets: stage0, test, bootstrap, budget
  stage0/                     C23, <= 3000 lines (checked in CI); only open/read/write/close/exit from libc
    arena.c  lex.c  parse.c  ast.c  types.c  gen_arm64.c  macho.c  main.c  mc.h
  lib/
    sys.mc                    open/read/write/close/exit (extern libSystem by default; #opcode svc behind a flag)
    prelude.mc                while/for/+=/-=/++/-- via #rule (M9) — only via explicit #include, versioned
  src/                        self-hosted compiler (same split as stage0)
    mc.mc  lex.mc  parse.mc  ast.mc  types.mc  gen_arm64.mc  macho.mc  obj.mc
  tests/
    NNN-name.mc              header `// expect-exit: N` / `// expect-stdout: ...`
    golden/                   SHA-256 of mc2.o
  scripts/
    build-stage0.sh  test.sh  link.sh  bootstrap.sh  loc-budget.sh
  docs/
    core-language.md  surface.md  determinism.md  macho-notes.md

stage0 budget (target <= 3000 lines): lexer + token table 350 · Pratt + table 250 · statements + #rule 400 · #define/#section/#opcode + constant folder 150 · types 150 · symbols 200 · instruction buffer + ~40 encoders 700 · Mach-O 550 · driver/arena/errors/dumps 250.

Codegen and Mach-O #


Determinism (docs/determinism.md) #

  1. Never hash pointers; never iterate a hash table to produce output — a parallel array in insertion order.
  2. Symtab via a stable partition; no qsort.
  3. stage0's C I/O has the same shape as the .mc version (open/read in a loop/close), no stdio.
  4. No __FILE__, date, absolute path, N_OSO/stabs, ar. LC_BUILD_VERSION hardcoded.
  5. Zero every padding/alignment byte explicitly.
  6. Reference build of stage0 with -O1; additional CI with -O0 -fwrapv -fno-strict-aliasing -fsanitize=undefined,address.
  7. --dump-tokens/--dump-ast/--dump-syms/--dump-asm with deterministic text since M1.
  8. Compare .o files, not linked executables. Versioned golden SHA-256 of mc2.o.

Milestones #

#MilestoneAcceptanceWhere it usually breaks
M0stage0/macho.c hand-writes a .o with movz x0,#42; ret in _mainlink.sh && ./t; echo $?42missing LC_BUILD_VERSION, n_sect being 1-based, segment offsets
M0.5same .o with _start + svc #0x80 writing hi (link -static -e _start)prints hisyscall number, ld refusing a static link
M1lexer + #token + Pratt + #infix/#prefix + i64 main() { return 40 + 2; }exit 42; stable --dump-tokens/--dump-astsp alignment, x29/x30
M2locals, if/else, loop/break N, calls, recursion, / %, local arrays, ld*/st*fib(24) prints 46368 via putnum in .mccalling convention, spilling before bl
M3globals, global arrays, strings, &x, #include, #define, extern, arenaprogram opens its own source and prints the line countsign extension, u16/u32 alignment
M4tokenizer written in .mcown-source token histogram == the C stage0's— (free cross-check)
M54 relocations, #section, #opcode, emit()/reloc(); sys.mc via svcotool -r/nm look sane; sys_write via svc runsPAGEOFF12 in add vs ldr, r_extern
M6src/mc.mc complete, core only (no #rule, no prelude) → mc1mc1 passes the same suite as stage0everything — this is where the --dump-* flags pay off
M7fixed pointmc1 mc.mc → mc2.o, mc2 mc.mc → mc3.o, cmp identical; golden recordedtable ordering, padding, short reads
M8cut the cordmake bootstrap uses clang only for stage0; binaries not versioned; loc-budget.sh <= 3000
M9#rule in stage0 and in mc.mc; lib/prelude.mc with while/for/+=/++ (struct deferred: it requires type $t and a layout, more than #rule delivers; accessors + #define cover the case)a program with while/struct produces an identical .o under stage0 and mc1; migrate a leaf module of mc.mc and recheck M7expansion order, gensym
M10backend taught via the surface: a .mc module with backend("asm", fn) emitting AArch64 textthe surface backend's __text byte-for-byte equal to the built-in one, across the corpusinsufficient emission primitives (which is what this milestone exists to discover)
M11direct executable (MH_EXECUTE): __PAGEZERO/__TEXT/__DATA/__LINKEDIT, LC_LOAD_DYLINKER, LC_LOAD_DYLIB libSystem, LC_MAIN, binding _open/_read/_write/_close/_exit, ad-hoc LC_CODE_SIGNATURE (CodeDirectory v0x20400, SHA-256 per 4 KiB page, CS_ADHOC)mc --exe runs without ld; codesign -dvvv valid; the fixed point still holds along this pathSHA-256 in .mc (~150 lines), execSegBase/Limit, bind opcodes
M12Tier 3, .mc only: syntax hooks (syntax/syntax_stmt + the parser's public API), type_alias, #dylib; examples/api (HTTP + SQLite + class/interface taught by oop.mc)make -C examples/api test green with an --exe binary; make check with check-examplesinsufficient parser API; bind by ordinal; sockets
M13(backlog) size the program's arena at compile time — profiling, annotation, or bound analysis (docs/specs/M13.md)--mem-report + HEAP_SIZE written by the compilerundecidable in general; only profiling is universal

Non-negotiable order: M6 and M7 before M9. A prelude before the fixed point couples the two hardest problems and blocks bisection.


Verification #

Risks that kill the project (and the brake on each one) #

  1. mc.mc without accessors → M7 becomes unbearable. Accessors from the first line.
  2. A prelude before the fixed point. M9 only after M7.
  3. The surface mechanism turning into a Scheme. 3000-line cap in CI.
  4. --dump-* left for later. It's in M1.
  5. Without M10, extensibility is just a hypothesis. M10 is part of the scope.
  6. M11 (signing) delaying everything. Stays behind the fixed point; ld remains a valid path.

Phase 2 — standalone distribution and cross-OS targets #

Owner's direction (2026-09-03): a developer downloads the mc binary and compiles their own code with no make/clang/gcc and without cloning this repository; everything the compiler needs is served by the self-hosted binary. Programs must be buildable for other operating systems, with the developer telling mc how (object format, linker, static libraries such as musl libc.a or mingw kernel32.lib). A TOML definitions file is read by default. Bundled includes are resolved with #include <name>, #embed (optionally compressed) is available to programs, and mc finds what to compile with little ceremony.

Principles carried over #

Target order decided by the owner (2026-09-03) #

Linux arm64 -> Linux x86 (32-bit) -> Linux x64 -> Windows arm64 -> Windows x64, in stages. mc itself keeps running on macOS arm64 for now (cross-hosting comes later with CI). Foreign targets are produced as objects and linked by an external linker configured in the TOML (ld.lld, lld-link). Linux binaries are executed for real in Docker (linux/arm64 native; amd64 via emulation).

Architect's caveat, to be decided when the stage arrives: 32-bit x86 is expensive for semantic, not encoding, reasons — i64 needs register pairs and uptr becomes 4 bytes, which breaks the 8-bytes-per-field layouts the language (and the compiler itself) assume. Recommended: x64 before x86, and x86 only if still wanted afterwards.

Rule for every change (owner, 2026-09-04) #

Every pull request that touches src/, lib/, examples/, stage0/ or tools/ also updates docs/ (guide, reference or example pages); the Docs updated check enforces it and the site is regenerated from docs/ on merge. A change without documentation is not finished.

Rule for every new target (owner, 2026-09-03) #

A milestone that adds an OS or an architecture ships, in the same PR, its CI leg (a job that links and RUNS the suite on a runner of that platform: ubuntu-latest for linux/x86_64, windows-11-arm and windows-2025 for Windows, node for wasm) and the architect adds that job to the main branch protection as a required status check at merge time. No target is "supported" without a gate.

Milestones (specs in docs/specs/M14.md...) #

#DeliverableAcceptance
M14Project driver + TOML: mc build [dir] reads mc.toml ([project] entry/out/kind, [target] os/arch, [linker] cmd/args with {out} {obj} {libs}, [libs] paths, [externs] symbol-or-prefix = lib, [include] paths, [compiler] modules to build a taught compiler first); TOML subset parser in .mc with line/column errors; externs' libraries from the TOML feed the same ordinal table as #dylibexamples/api builds and passes with mc build alone (no Makefile); tests/toml/* dumps match; make check green, golden rewritten once
M15Bundled standard library + #embed: #include <name> served from a deterministic LZ-compressed bundle embedded in the binary (tools/bundle.mc generates src/bundle_data.mc from lib/ and the compiler core; <mc/core> lets a taught compiler be built anywhere); #embed name "file" [lz] declares a byte array (+ sizes) for programs; src/lz.mc implements both directionsmc copied alone into an empty directory compiles a program using <sys>/<prelude> and a taught compiler from <mc/core>; make bundle is reproducible byte for byte; fixed point holds with the bundle
M16Linux arm64: ELF64 relocatable writer (R_AARCH64_CALL26, ADR_PREL_PG_HI21, ADD_ABS_LO12_NC, LDST*_ABS_LO12_NC, ABS64), <sys/linux> (syscalls via svc #0, number in x8), _start shim or musl libc.a, linker invocation from TOMLthe test suite compiled with [target] os = "linux", linked with ld.lld + musl from Alpine, executed in Docker linux/arm64 with identical stdout/exit
M17x86 family groundwork: split gen_lower into a target-independent walker (frames, depth stack, labels, calls) and a machine interface (~30 primitives) implemented by arm64; then the x86-64 machine (SysV ABI, ModRM/SIB/REX encoder) as a .mc backend; ELF for Linux x64 executed in Docker linux/amd64suite green under emulation; arm64 objects unchanged byte for byte after the refactor
M18Linux x86 (32-bit) — only if still wanted: i64 via register pairs, uptr = 4 bytes, layout audit of the language and the runtimesuite green in Docker linux/386
M19Windows arm64: COFF writer (IMAGE_REL_ARM64_BRANCH26/PAGEBASE_REL21/PAGEOFFSET_12A/ADDR64), <sys/windows> on kernel32 (WriteFile, ReadFile, CreateFileA, ExitProcess), lld-link from TOML against mingw kernel32.libobjects link; PE inspected with llvm-readobj; executed when a Windows host exists
M20Windows x64: COFF x64 relocations (IMAGE_REL_AMD64_REL32/ADDR64, no addend) parameterised into the same writer, the Win64 ABI as a second machine x86_64-win sharing every encoder with x86_64, and the entry shim split out into <sys_windows_start> so the layer is architecture-neutral (docs/specs/M20.md)same as M19: objects match clang --target=x86_64-windows-msvc -c field for field, lld-link -machine:x64 links them, and the windows-latest CI leg runs the suite on real hardware
M21Tier 3 completion: syntax_expr, syntax_infix (code, not template), syntax_type (generic instantiation at use), on_func_end, token record/replay with identifier substitution (docs/specs/M21.md)check-surface covers each hook; make check green
M22examples/lang: a higher-level language from a prelude — fn, classes with single inheritance and virtual/override, interfaces, generics with C#-style where constraints and const N: i64, ref parameters, namespaces as sugar over includes (namespace, import, using, qualified names), automatic memory (reference counting with free lists) (docs/specs/M22.md)examples/lang/test.sh green with the taught compiler; 100k-object churn inside a 4 MiB arena proves real deallocation
M21.5Tier 3 follow-ups from examples/lang: bundle as #embed bytes (zero nodes), arena exhausted with position and estimate, mc build --compiler-only, on_stmt hook, parse_block through syntax_stmt("{") (docs/specs/M21.5.md)examples/lang builds on <mc/core> with the default arena; demos in check-surface; objects inert
M23Dynamic limits: growable tables in the self-hosted mc, best-effort estimate (static pre-scan + remembered usage) reserved at estimate * (1 + tolerance), single [limits] tolerance float in [0,1] (basis points internally), mc limits checker (ok/grew/tight, CI exit codes), mc build --fix-limits adjusting only the tolerance with consent, check-limits on the seed's headroom (docs/specs/M23.md)5k-function/5k-string program builds with no TOML change; tolerance = 0 grows and is reported; make check warns before the seed's tables fill
M24Tier 4 -- primitives and hardware instructions taught from the surface (owner's principle, 2026-09-04: "capabilities become surface"): eight mechanisms in src/ (~187 lines: a type registry type_new, the literal's type surviving resolve, fold guards, the depth type as walker functions, type-sized frame slots, syntax_lit, intrinsic, machine_tab/machine_slot) and --dump-machine; f32/f64 become the FIRST LIBRARY (<float>: literals, arithmetic, casts, ldf*/stf*, AAPCS64/SysV/Win64 float ABI, putf64) with f16, i128 and one AVX instruction from a module as the proof of generality; #machine dropped (docs/specs/M24.md)step A inert (objects identical to the seed, byte-identical across the pre/post compiler over the whole corpus); tests/float/ bit-exact on all five legs; the three generality modules with an empty git diff src/
M25Sysroots and cross-compilation resolution: explicit path -> running system -> ~/.mc/sysroots cache -> precise instructions; Apple: synthesized text stubs from the program's externs by default, or mc sysroot fetch macos-* from community SDK mirrors with the mc.toml lines printed (nothing redistributed by mc); mc sysroot fetch for musl (Alpine apk) and mingw-w64 import libs (llvm-mingw) with checksums and offline fallback; mc sysroot list (docs/specs/M25.md)link on macOS without an SDK via synthesized stubs; fetch into a temp cache and build for linux; offline paths exit with instructions
M31Concurrency taught by a module (owner's test, 2026-09-03): threads with mutex/semaphore, spawn f(args) fire-and-forget with channels, await res = f() without async (the call widens to Intent/Intent<T>; intent allowed only on locals), thread-safe reference counting via #opcode/#machine atomics — design panel first to find what the core lacks (docs/specs/M31.md)delivered as an example under examples/ (a concurrency module for lx or a sibling example) with its own tests in make check: producer/consumer over channels, parallel sum, an await chain; determinism of the compiled output; the core-gap list is empty or spec'd
M32Desktop UI test (owner, 2026-09-03): examples/desktop drives GTK4 from mc via extern + [externs] prefix mapping over four dylibs (windows, header bar, buttons, entry, list, transient dialog, callbacks by &fn), then a declarative UI language taught by the surface lowering to the same calls; --self-test for CI, screenshots via the desktop tooling for the visual check (docs/specs/M32.md)both variants build with --exe, otool -L shows gtk4/gobject/gio, self-tests identical; check-desktop skips without GTK4
M33WebAssembly (owner, 2026-09-03, last in the queue): a .mc backend consuming the AST (structured control flow, call_indirect for callp, imports for extern, data segments; wasm32 by default with uptr still 8 bytes — only the address operand narrows — and wasm64 opt-in) with two writers, binary .wasm and text .wat; <sys/wasi> and <sys/browser>; examples/wasm with a WASI CLI (run under wasmtime) and a browser page with JS glue (checked in the in-app browser); architecture sweep first to list what the core would need (docs/specs/M33.md)suite subset runs under node's WASI (wasmtime when present); the page runs in the browser; .wat round-trips through wat2wasm when installed; depends on M17 step A (gen_resolve, target registry)
M34examples/minimal: the smallest executable per target with a reproducible measure.sh (size, code bytes, segments, max RSS, footprint, own mappings, startup); baseline macOS --exe 16 692 B / 28 B of code / 1.33 MB RSS; Linux -nostdlib as the true floor; ceilings guarded in make check (docs/specs/M34.md)table printed; ceilings hold; guide page on the site
M35Benchmark and memory tooling as bundled .mc modules activated by flags: <bench> (--bench, CSV history, CI job, numbers page) and <memcheck> (--paranoid / [profile] mem = "paranoid": shadow arena, red zones, poisoning, use-after-free, double free, leaks, UBSan-style checks via a Tier 2 pass; the seed keeps stage0-san) (docs/specs/M35.md)tests/mem/* each caught with the right message; clean program reports nothing; overhead documented; CSV rows deterministic except timings
M36Multi-target builds (owner, 2026-09-03, last): [[targets]] in mc.toml, mc build producing every output in one run (--target=NAME, --list-targets, --keep-going), per-target overrides of linker/sysroot/libs/limits/run, taught compiler built once, byte-identical outputs alone or in batch (docs/specs/M36.md)examples/minimal and examples/wasm build all available targets in one command; CI covers macOS, wasm and Linux entries
M37mc hosted on Linux (owner, 2026-09-04, promoted: the cloud runs only Linux; releases must ship mc for every landed OS/arch, and a target alone does not make a host): host layer (host_macos/host_linux), mc-linux-<arch> cross-built and released, scripts/bootstrap-linux.sh fixed point on Linux from a published seed, Linux make check subset, CI jobs on both Linux runners, Linux release assets (docs/specs/M37.md)fixed point on linux/x86_64 and linux/arm64; mc-linux src/mc.mc == macOS mc2.o; CI green
M38mc hosted on Windows (owner's rule, 2026-09-04: every landed OS/arch gets a release asset of mc itself): MAXPARAMS 12 with stack parameters 9..12 on all three machines (CreateProcessA takes ten), src/host_windows*.mc + lib/sys_windows_host.mc over kernel32 (spawn via CreateProcessA/WaitForSingleObject, VirtualAlloc for the arena, CreateDirectoryA/DeleteFileA), host_exe_suffix(), scripts/{link,bootstrap}-windows.sh, the Windows make check subset, two CI host jobs and two release assets (docs/specs/M38.md)fixed point on the Windows runners; release assets for both Windows targets
M39An architecture taught from the surface (owner, 2026-09-04): examples/kernel, a bare-metal RISC-V 64 micro-kernel (reset, NS16550A UART, mtvec trap, two cooperative tasks, ok, SiFive test-device exit) compiled by a taught compiler that registers a RISC-V machine, a flat-image writer and a bare-metal layer, all in .mc under examples/ -- zero lines in src/; run under qemu-system-riscv64 -machine virt -bios none as the CI oracle; the gaps this exposes in the core are priced as follow-ups (M39.5: mc build with a module-registered [target]; M40: the word-size sweep AVR/PIC need) (docs/specs/M39.md)git diff --stat src/ stage0/ lib/ tests/ empty; the image boots and exits 0 in QEMU with the exact transcript; llvm-mc re-assembles every distinct instruction byte for byte; golden unchanged
M39.5mc build with a module-registered [target] (gap G1 of docs/specs/M39.md, decision D2, done 2026-09-04): the deferral form only -- drv_run keeps [target].os/.arch as strings, drv_entry passes a role (obj/exe), and drv_backend_for resolves the pair inside drv_parse right after user_init() and before parse_unit(); the two registry-built diagnostics and the requires [linker] check move unchanged, drv_teach's host lookup is untouched, and there is never a second user entry point. examples/kernel is the consumer: [target] os = "none" / arch = "riscv64", registered by its own compiler with rv-image in both roles, so mc build examples/kernel writes the image end to end18 added / 15 removed code lines in src/driver.mc; the image byte-identical to the one the single-file CLI wrote; check-build 16/16 with its [target] messages unchanged; a pre-change mc1 writes identical objects for every tests/*.mc, for src/mc.mc and through all four taught compilers
M40The narrow word (owner, 2026-09-04): examples/avr, a bare-metal ATmega328P image (blink, console ok, one timer ISR, exit through simavr) built by a taught compiler under design A -- the word stays 8 bytes and the AVR machine widens, zero lines in src/; ELF32 EM_AVR written by the module with the .mmcu section; simavr primary and qemu-system-avr second as oracles; narrow u8/u16 arithmetic via M24's depth type, declared; PIC excluded (banked memory and a hardware call stack are a different frame model); G10 repriced from ~400 lines to ~15 plus one semantic decision, superseded the same day by the owner's override direction: the AVR module declares uptr = 2 from the surface and the recreated compiler is debloated (docs/specs/M40.md § Amendment; after M24 and M41)git diff --stat src/ stage0/ lib/ tests/ empty; the image runs under both oracles with the exact transcript; the ELF and the vector table match an avr-gcc reference; llvm-mc -triple=avr re-assembles every distinct instruction
M41Override, debloat and re-arch (owner, 2026-09-04): the compiler as a product of the developer's own build -- <mc/core> composable (a minimal core of lexer, parser, resolver, walker and hooks; writers, host machines, driver, bundle and sysroot resolver as optional parts), primitives removable (types and intrinsics unregistered; writers, machines, targets simply not registered), and the core's remaining fixed decisions reachable as overrides from user_init() (the word width of uptr first); examples/avr is the first recreated compiler (docs/specs/M41.md; after M24). Done 2026-09-04 in four gated commits: five parts (<mc/core_min>, <mc/core_machines>, <mc/core_writers>, <mc/core_build>, <mc/core_bundle>) with src/core.mc as their sum, mc_main() in src/cli.mc, the subcommand/on_plan/backend_default/machine_use_if registrations, type_disable/intrinsic_disable/type_set_width, and scripts/check-parts.sh as the gate; a core_min-only compiler is 219 417 bytes against mc's 759 875, 29%a taught compiler built from the minimal core is smaller than mc by the parts it omits, measured; a program the stock compiler accepts is refused by a debloated one that removed the primitive; objects identical for every compiler that overrides nothing

Phase 3 — documentation, website, identity, editor experience #

Owner's direction (2026-09-03): detailed documentation of every use case and of every element of the mc API; a site/ directory with a static site generator written in mc (Hugo-like) producing the documentation website for GitHub Pages; designer agents, an attractive but simple layout and an icon that expresses mc; and a VS Code extension with an LSP and a debugger. The LSP must give developers back the syntax they created themselves (colors, navigation, tracking of definitions and uses).

Architecture decisions #

Milestones #

#DeliverableAcceptance
M26Documentation set: docs/guide/ (getting started, single file, project + mc.toml, teaching the compiler, Tier 1-3 recipes, cross-compiling, examples walkthroughs) and docs/reference/ (language, directives, CLI, TOML keys, parser/hook API with the meaning of every public function, object primitives, machine tasks, diagnostics catalogue); scripts/check-docs.sh verifies every public p_*/hook/CLI flag appears in the reference and every code sample compiles (docs/specs/M26.md)make check gains check-docs; zero undocumented public symbols
M27site/: mcsite static generator in mc (Markdown subset -> HTML, code fences highlighted by the bundled lexer, nav/search index/sitemap from site/site.toml, templates with placeholders), layout by the designer agent, SVG icon/favicon, GitHub Pages workflow YAML (docs/specs/M27.md)mc build site renders site/public from docs/; pages validate (HTML, links, a11y review); icon delivered as SVG + PNG set
M28mc lsp: JSON-RPC/stdio server in .mc (JSON in .mc, UTF-16 offsets), incremental full-document reparse, semantic tokens incl. taught words/operators, diagnostics, go-to-definition, references, hover, document symbols; project awareness via mc.toml (taught compiler) (docs/specs/M28.md, design panel first)LSP conformance tests with a scripted client; examples/lang files colored/navigable through the taught compiler
M29VS Code extension (editor/vscode): TextMate baseline grammar, LSP client, build/run commands, problem matcher, lldb-dap launch configs; packaged .vsix (docs/specs/M29.md)extension installs from .vsix; smoke test in the Extension Host
M30DWARF in .o/--exe (macOS) and ELF (Linux): .debug_line, .debug_info/.debug_abbrev with subprograms and locals, .debug_str; lldb steps through .mc and .lx sources (docs/specs/M30.md)lldb breakpoints by file:line and frame variable show mc locals; extension debug session works end to end

Edit this page