Spec M41 -- override, debloat and re-arch: <mc/core> becomes composable, and a recreated compiler is smaller than mc by what it omits
Owner's direction (2026-09-04, docs/specs/M40.md § Amendment, quoted): "the developer RECREATES
the compiler -- without touching src/ -- with overrides of the core's remaining fixed decisions,
new primitives, a new machine, and the REMOVAL of the primitives, writers and machines the target
does not use; then builds the program with that compiler." And: "<mc/core> becomes composable (a
minimal core -- lexer, parser, resolver, walker, hooks -- with the object writers, the two host
machines, the driver, the bundle and the sysroot resolver as parts an entry file includes or
omits), and primitives the target does not use can be removed (types and intrinsics unregistered;
writers, machines and targets simply not registered). examples/avr is the first recreated,
debloated compiler: no Mach-O, ELF64 or COFF writer, no arm64 or x86-64 machine, uptr of two
bytes, the AVR machine and its ELF32 writer only."
Goal: five bundled parts, one core-provided mc_main(), three registries that make the optional
parts self-registering, two removal mechanisms for core language primitives, and one machine-declared
override (type_width(TY_UPTR)). Measured target: a compiler built from the minimal core plus one
machine and one writer is about a third of build/mc1's size, and the full assembly still equals
src/mc.mc byte for byte.
Sequencing: after M24 (type_new/type_width registry, walk_depth_type, slot_new(type_width),
syntax_lit, intrinsic, machine_tab/machine_slot). M40 is the first consumer. Line references
are to main at 73d1b97; all sizes were measured on this machine on 2026-09-04 with
build/mc1 --dump-syms src/mc.mc.
What already exists #
- Partial include lists already work.
src/astdump.mc:17-23is a working compiler front end built from six of the core's files (arena,lz,macho,lex,ast,parse,hooks) andsrc/lexdump.mc:4-5from two.src/m0.mc:19-20isarena+machoalone. Nothing aboutsrc/core.mcis atomic today except the file itself. #includeis once-only (src/lex.mc:663,lex_seen), so a part may include its own dependencies and an entry may name a part twice.- Relative includes inside a bundled file resolve by last path component
(
src/lex.mc:640-647,src/bundle.mcbundle_find_base), which is what makes<mc/core>'s own#include "arena.mc"work.tools/bundle.mcrefuses a manifest where two entries share a last component (docs/reference/bundle.md:165), so a new part file's basename is its bundle name. - A wrong split fails loudly at compile time: a call into an omitted part is
call to unknown function(src/gen_resolve.mc:389), not a silent link failure. - The registries are all one shape -- linear table, registration order, last registration wins:
pass(src/hooks.mc:53),backend(:61),syntax*(:227-278),type_alias(:321),machine(:378,MAXMACHINES 8at :372),target(:434,MAXTARGETS 16at :421). user_init()timing is already fixed and documented: aftertok_init()(the idsK_U8..K_EXTERNare frozen at 256..269) and before the first token,src/main.mc:218.--machine=is applied after it (:220) and--backend=at :232.- The bundle is data, not a dependency of teaching.
mc build's[compiler]step generates#include <mc/host>+#include <mc/core>+ the modules (src/driver.mc:350-376) and compiles it with the HOST's executable backend (:546-558). The blob that gets embedded is the building compiler's; whether the built compiler carries one is decided by its own include list. src/limits.mcis already a pure add-on:arena.mcowns the table registry (T_*atsrc/arena.mc:66-102), the seeds (:115) andlim_note(:141);limits.mcadds the planner and the report, and reaches the bundle only through the lexer'sbopen_fnpointer, never throughsrc/bundle.mc.- Measured today (
build/mc1, 600 032 bytes on disk):__text282 168,__cstring14 527,__data293 864 of which the bundle blob is 285 102 (tools/bundle.list, 50 entries),__bss33 957 168 (the 32 MiB static heap,src/arena.mc:157, not in the file).
Design #
1. Five parts, and src/core.mc is their sum #
Two file splits, five new part files, one rewritten core.mc.
| part (bundle name) | members, in order | __text measured |
|---|---|---|
mc/core_min | arena lz objmodel lex ast parse gen_resolve gen_walk hooks cli | 136 536 |
mc/core_machines | machine_arm64 machine_x86_64 | 36 464 |
mc/core_writers | sha256 macho backend_exe backend_elf backend_coff | 48 348 |
mc/core_build | toml driver sysroots sysroot stubs limits (+ sha256, once-only) | 56 580 |
mc/core_bundle | bundle_data bundle | 3 456 + 285 102 __data |
src/core.mc | the five parts, then main.mc | 282 168 total |
src/core.mc becomes six #include lines. That is the design's load-bearing property: the full
assembly is literally the parts, so there is no second list to drift.
Split 1 -- src/macho.mc -> src/objmodel.mc + src/macho.mc. The walker and the parser do
not need a Mach-O writer, they need the object model: parse.mc:349 calls sec_new (via
sec_make) and parse.mc:338-343 registers the R_* constants as internal defs; gen_walk.mc
uses sec_at/sec_data/sec_zsize (:358-364), sym_new/sym_ref (:434, :559) and reloc_add
(:375). objmodel.mc takes macho.mc:1-244 (the three record layouts, R_*, S_*, TEXT_FLAGS,
sec_new, sym_new, sym_set_value, reloc_add) plus sym_class/sym_order/out_name16/
dump_syms (:258-324) -- sym_order because backend_elf.mc:292 and backend_coff.mc:315 need
it and neither is Mach-O, dump_syms because it prints the model and is check-obj's oracle.
macho.mc keeps the MH_*/LC_*/N_*/CPU_* defines (:246-256, also used by
backend_exe.mc:553,634,763) and macho_write (:325-467). Measured: 6 592 bytes of model,
5 092 of writer. Function definition order across the two files is unchanged, so the split
alone is byte-neutral.
Split 2 -- src/main.mc -> src/cli.mc + src/main.mc. See § 2.
Naming. mc/core_min, not mc/core-min: a bundle name is the file's basename
(tools/bundle.list is NAME<TAB>PATH and the last-component fallback matches on the basename),
and core_min.mc cannot be reached as core-min. core_* as the prefix keeps mc/driver (the
module) distinct from mc/core_build (the part).
What an entry file says. Nothing new: it names the parts.
#include <mc/host>
#include <mc/core_min>
#include "machine_avr.mc"
#include "image_avr.mc"
i64 main(i64 argc, uptr argv, uptr envp) { host_init(envp); return mc_main(argc, argv, envp); }
void user_init() {
machine_avr_init();
backend("avr-image", &backend_avr);
backend_default("avr-image");
}
For mc build: [compiler].core is a path today (src/driver.mc:362-364). One rule, ~4 lines:
if the value starts with <, emit it verbatim instead of rewriting it with ../. A project then
writes core = "<mc/core_min>" and its module includes whatever else it wants. examples/kernel
and examples/lang, which already #include <mc/core> in their module, are unaffected.
2. The entry: mc_main() and three self-registering parts #
src/main.mc:129-242 does eight things a recreated compiler must not have to copy: host_init,
the machine/backend/target/sysroot registrations, the bundle hook, the subcommand dispatch,
lim_plan, the argv loop with -o/--backend=/--machine=/--include=/--dump-*/--host,
user_init() at the one correct moment, and the parse -> passes -> fold -> dump/backend pipeline.
Six of the eight are core-min's; two are the optional parts'.
src/cli.mc (core_min) gets opt_val, usage, dump_host, the M_* constants and
i64 mc_main(i64 argc, uptr argv, uptr envp) -- everything from src/main.mc:158 on, unchanged,
with four edits:
machine_use(host_machine())becomes conditional (machine_find(...) >= 0), so a compiler with only an AVR machine does not die at startup. New:i64 machine_use_if(uptr name)inhooks.mc.- the default backend (
main.mc:198-203) tries the target registry first and, ifntargets == 0, falls back to whatbackend_default(name)recorded; with neither,die("no backend: use --backend=NAME"). New:void backend_default(uptr name)(~8 lines). No "if exactly one backend is registered" magic -- the module says which one. lim_plan(...)(:212) moves behindvoid on_plan(uptr fn)(~12 lines, theon_stmtshape), registered bylimits.mc. Unregistered, nothing is pre-sized and the tables grow from the seeds inarena.mc:115-- which is exactly whatastdumpdoes today.- the three
str_eq(argv[1], "build"|"limits"|"sysroot")lines (:154-157) move behindvoid subcommand(uptr name, uptr fn, uptr usage)(~30 lines inhooks.mc), andusage()prints the core line plus each registered subcommand's usage string.core_buildregisters three,sysroot's string carrying its two lines, somcwith no arguments prints byte-identical text.
src/main.mc keeps backend_macho (it calls macho_write, so it moves into macho.mc),
host_bundle_open (moves into bundle.mc beside bundle_open, and bundle.mc gains
mc_bundle_init() = lex_set_bundle(&host_bundle_open)), and shrinks to a main() that calls
host_init, then mc_machines_init(), mc_writers_init() (the six backend() and five
target() calls of :132-146), sysroots_init(), mc_bundle_init(), mc_build_init() (the three
subcommand() calls and the on_plan registration), then return mc_main(argc, argv, envp).
Each *_init lives in its own part; main.mc is the only file that names all five, and it is the
file a recreated compiler replaces.
3. Removal, honestly #
(a) Not including a part. Zero mechanism. This is what the owner's list actually asks for: no
Mach-O/ELF64/COFF writer is core_writers omitted; no arm64/x86-64 machine is core_machines
omitted; no mc build, no mc sysroot, no mc limits is core_build omitted; no bundle is
core_bundle omitted. Measured saving: 141 392 bytes of __text and 285 102 of __data.
(b) Unregistering. Not needed, and recommended against. Once the registrations move into
mc_machines_init / mc_writers_init, "not registered" is the default and backend_remove /
machine_remove / target_remove have no caller -- the same reason M24's D8 refused
type_set_width as a free-standing setter. The tables stay append-only.
(c) Core language primitives. Two mechanisms, both consulted before the core's own answer.
void type_disable(i64 ty)(~10 lines: onei64 ty_offbitmask inhooks.mc, one test at the head oftype_of_token,src/parse.mc:677-686). Diagnostic at the token:u32: removed by this compiler. It removes the word from the surface, not the type from the model --ld8()still yieldsTY_U8(src/gen_resolve.mc:300-304) andtype_widthstill answers for it. Say so inhooks.md. Disablingi64,uptrorvoidis permitted and makes the language unusable (a literal isTY_I64,&fand every pointer isTY_UPTR, a function returnsTY_VOID); no special case, one sentence of documentation.void intrinsic_disable(uptr name)(~14 lines: a name table inhooks.mc, one test inintrin_id,src/gen_resolve.mc:284-296, applied to M24's registry too so the rule is uniform). Diagnostic:ld64: removed by this compiler. This one has a real caller today -- M40 § 2B's mitigation is precisely "portable sources useldw/stwinstead ofld64/st64", and without removal the wide pair stays reachable and silently wrong on a 2-byte word.- Directives and keywords: no. Removing
#dylib,#embedorloopneeds a table in the lexer and the parser's own grammar, and has no caller. Deferred, unpriced.
4. Overrides of the core's remaining fixed decisions #
| decision | where it lives today | verdict |
|---|---|---|
word width of uptr | src/ast.mc:213-218 | mechanism, M41. ~15 lines, § 4a |
| frame cap 4095 | src/gen_walk.mc:893 | already the machine's problem (docs/reference/machine.md:176-199); a machine may diagnose a smaller one. No mechanism. |
| local-array cap 4095 | src/gen_walk.mc:686, src/parse.mc:1047, :2011 | follows type_width for free, and is M40 § 2B's semantic trap: the same source is refused on arm64 and accepted on AVR. Documented, not fixed. |
MAXDEPTH 64 | src/gen_walk.mc:55 | expression depth, not a target fact. Leave. |
MAXPARAMS 12 | src/arena.mc:58 | the ABI, and M38 made the machines implement the stack half. Leave. |
section names __TEXT,__text etc. | src/gen_walk.mc:973-979 | opaque labels the writer maps -- backend_elf and backend_coff already do. No mechanism. |
entry symbol _main | src/backend_exe.mc:758 | the writer's. Already replaceable by not including it. |
| default backend / machine | src/main.mc:198-203, :131 | backend_default() + machine_use_if(), § 2 |
| string-literal section | src/gen_walk.mc:977 | as the section names. No mechanism. |
HEAP_SIZE 32 MiB | src/arena.mc:157-158 | no. It is bss, absent from the file, and dynamic since M23 (grow/mmap). A static array's size cannot be set from user_init in this language. Document it. |
| the bundle | core_bundle | omit the part, or ship your own (§ 5) |
4a. type_width(TY_UPTR), machine-declared. M40 § 1b's C1+C3+C4+C5, on top of M24's M1/M5,
priced there at ~15 core lines and adopted here as the mechanism M40 consumes:
- C1
src/ast.mc:213-218--type_widthreads the registry (M24's M1) and, forTY_UPTRalone, a machine-declared width.void type_set_width(i64 ty, i64 w)refuses everytybutTY_UPTR, which is the "machine-declared form, never a free-standing setter" of M40 D2 and reverses M24 D8 because the caller now exists. - C3
src/gen_walk.mc:321-324--slot_new's(size + 7) & ~7granule becomes the declared word (~3 lines). Byte-identical at width 8. - C4
src/gen_walk.mc:360-361,:689,:892-- the three roundings to 16 become a declared frame alignment (~3 lines). - C5
src/gen_walk.mc:374-376-- a string inside auptr[]initializer writesbuf_u64(b, 0)+R_UNSIGNEDat length 3; on a 2-byte word it writes two bytes with a machine-declared pointer relocation kind (~5 lines). - C2 (
:693,:881) and C6 (:924-933) follow from M24'sslot_new(type_width(ty))and the existingbuf_u16branch at:381for free.
Inert by construction: with nothing declared the width is 8, the granule 8, the alignment 16 and the initializer 8 bytes -- exactly today.
5. Measurement and the debloated bundle #
Per-file __text was measured by taking the offset of each file's first function out of
build/mc1 --dump-syms src/mc.mc and differencing (the walker lays functions out in definition
order). The acceptance harness re-measures the same way, so no new tool is needed.
| compiler | __text | __data | on disk |
|---|---|---|---|
build/mc1 today | 282 168 | 293 864 (285 102 blob) | 600 032 |
| core_min only (+ host, no machine, no writer) | ~137 100 | ~8 800 | ~176 000 |
| recreated AVR compiler (core_min + AVR machine ~20 K + ELF32/image writer ~8 K) | ~166 000 | ~9 000 | ~205 000 |
About a third of mc. Of the ~395 KB saved, 285 KB is the bundle and ~141 KB is code.
Can a debloated compiler ship a smaller bundle? Yes, at zero core lines. tools/bundle.mc:22-25
is arena + lz + bundle_data + bundle, and all four are already bundled names, so a project
writes the same four-line tool against <mc/arena>, <lz>, <mc/bundle_data>, <mc/bundle>,
generates its own bundle_data.mc from its own manifest, and its compiler includes that file plus
<mc/bundle> instead of <mc/core_bundle>. bundle.mc needs only BUNDLE_COUNT and the two
arrays, which the generator emits. Worth a paragraph in docs/reference/bundle.md; no code.
What a debloated compiler gives up, stated plainly in the guide: without core_build it has no
mc build, so it is a leaf -- it compiles programs, not compilers; without core_bundle it has no
#include <name> at all, so its programs use relative includes, as examples/kernel/lib already
does.
6. The seed, and how the manifest grows #
stage0 compiles src/mc.mc and cannot parse <name>: stage0/parse.c:799 requires a
T_STR after #include. So every part is a file under src/ included relatively by
src/core.mc, and separately bundled under an mc/... name for taught compilers -- which is
already how mc/core works. Nesting goes from 4 (mc.mc -> core.mc -> arena.mc ->
prelude.mc) to 5, against stage0/lex.c:9 MAXOPEN 16; file count goes from ~30 to ~37 against
MAXINC 256; string literals stay at 840 against MAXSTRS 2048 (stage0/gen_arm64.c:28) and
functions at ~1 100 against MAXFUNCS 2048. No stage0 change, no budget change.
tools/bundle.list grows from 50 to 57 entries: mc/cli, mc/core_build, mc/core_bundle,
mc/core_machines, mc/core_min, mc/core_writers, mc/objmodel, each NAME<TAB>src/NAME.mc
with a unique last component. scripts/check-bundle.sh:84 derives its expected index size from the
manifest (entries * 4), so it needs no edit; src/bundle_data.mc is regenerated by make bundle
and grows by roughly 4 KB (the part files are comments and includes; objmodel/cli are moves).
Out of scope #
examples/avritself, the AVR machine, the ELF32 writer and the.mmcusection -- M40.mc buildresolving a module-registered[target]-- M39.5.- Removing directives or core keywords (§ 3c).
- A per-part
[compiler].parts = [...]TOML key.core = "<mc/core_min>"plus the module's own includes covers it; a second spelling of the same list is what this milestone exists to delete. - Making
HEAP_SIZEconfigurable. - Any change to
stage0/. - Fine-grained parts (
<mc/core/lex>,<mc/core/parse>). Five parts are the seams that exist; ten would be ten names to keep true.
Files and estimated deltas #
| file | lines | what |
|---|---|---|
src/core.mc | 69 -> ~55 | six includes and the prose that explains the parts |
src/core_min.mc | ~45 | new: ten includes + the contract (what a minimal compiler is) |
src/core_machines.mc | ~15 | new |
src/core_writers.mc | ~25 | new; mc_writers_init() (the six backend, five target) |
src/core_build.mc | ~25 | new; mc_build_init() (three subcommand, on_plan) |
src/core_bundle.mc | ~15 | new |
src/objmodel.mc | ~330 | moved out of macho.mc (model + sym_order + dump_syms) |
src/macho.mc | 467 -> ~150 | the Mach-O writer alone, plus backend_macho moved in |
src/cli.mc | ~205 | moved out of main.mc: mc_main, opt_val, usage, dump_host |
src/main.mc | 242 -> ~55 | main(): host_init, five *_init, mc_main |
src/hooks.mc | +95 | subcommand (30), on_plan (12), backend_default (8), machine_use_if (5), type_disable (10), intrinsic_disable (14), type_set_width (8), accessors |
src/parse.mc | +6 | type_of_token consults the disable mask |
src/gen_resolve.mc | +8 | intrin_id consults the disable table |
src/ast.mc | +6 | type_width for TY_UPTR (on top of M24's registry read) |
src/gen_walk.mc | +12/-6 | slot granule, frame alignment, pointer initializer (C3/C4/C5) |
src/bundle.mc | +14 | host_bundle_open moved in, mc_bundle_init() |
src/limits.mc | +4 | registers on_plan |
src/driver.mc | +6 | [compiler].core starting with < is emitted verbatim |
tools/bundle.list | +7 | the new entries |
src/bundle_data.mc | regenerated | +~4 KB |
scripts/check-parts.sh | ~170 | new: the four proofs of Acceptance 1-4 |
scripts/check-docs.sh | +1 | the coverage families gain type_, intrinsic, subcommand |
Makefile | +6 | check-parts, inside check: |
docs/reference/hooks.md | +90 | the eight new registrations and their limits |
docs/reference/bundle.md | +50 | the parts table, the own-bundle recipe, the naming rule |
docs/build.md | +25 | core = "<mc/core_min>" |
docs/reference/cli.md, docs/surface.md, docs/plan.md | +30 | mc_main as Tier 3 surface |
docs/guide/98-recreating-the-compiler.md | ~200 | new: "I want a compiler for X and nothing else" |
stage0/, lib/, tests/ | 0 | untouched |
Net new src/ lines ~150; moved ~530.
Acceptance #
- The parts are the core. An object built from
#include <mc/host>+ the five parts spelled out +<mc/main>+<user_default>is byte-identical (cmp) to one built from#include <mc/host>+<mc/core>+<user_default>. This is the anti-drift proof and it fails the momentcore.mcand the part files disagree. check-standaloneunchanged and green: step 4 still compares<mc/host>+<mc/core>+<user_default>againstbuild/mc2.o, byte for byte. The reference moves with the milestone; the invariant ("the core inside the bundle is the core insrc/") does not.- Inertness, in M17 step A's protocol.
src/mc.mc's own source order changes (three files move: the Mach-O writer,hooks,limits), sotests/golden/mc2.sha256is rewritten once, and only after: a copy ofbuild/mc1taken before the change and the post-changebuild/mc1produce byte-identical objects for all 32tests/*.mc, forexamples/api,examples/lang,examples/concandexamples/kernel; the--dump-asmdiff betweenmc1andmc2is empty;cmp build/mc2.o build/mc3.opasses.check-obj32/32 against the frozen C seed;check-lex/check-ast/check-asm/check-surface/test-exe/test-linux/test-linux-x86_64/test-windows/check-kernelunchanged. - Debloat is measured, not asserted.
scripts/check-parts.shbuilds acore_min-only compiler (with a two-slot probe machine and a null writer registered fromuser_init) and prints theexamples/minimaltable for it: file size,__text,__cstring,__data. It asserts__dataunder 16 KB (no blob), and that--dump-symsof its own object contains no_macho_write,_elf_write,_coff_write,_bundle_open,_drv_build,_m_arm64. The ceiling is a number in the script, incheck-minimal's style. - Removal is proved by refusal. The same source that
build/mc1compiles is refused by a compiler that calledintrinsic_disable("ld64"), withld64: removed by this compiler, and by one that calledtype_disable(TY_U32), withu32: removed by this compiler. Both compilers are built insidecheck-parts.shfrom the bundle, incheck-standalone's style. - The override is proved and inert. A probe module calling
type_set_width(TY_UPTR, 2)shows auptrlocal in a 2-byte slot and auptr[]global at 2 bytes per element under--dump-asm/--dump-syms;type_set_width(TY_U64, 2)is refused; and with no call, every object in Acceptance 3 is unchanged. The real consumer is M40. mcbehaves identically.mcwith no arguments prints byte-identical usage text (the subcommand strings reassembledrv_usage's four lines);mc build,mc limits,mc sysroot listandtests/golden/sysroot-list.txtare unchanged;check-build's four diagnostics andcheck-limitsare unchanged.- The bundle is reproducible.
make bundletwice gives identical bytes;check-bundlegreen at 57 entries with<mc/bundle_data>still one#embednode plus a 228-value index. - Docs.
make check-docsgreen with every new symbol documented and the guide page compiled as a sample. git diff --stat stage0/ lib/ tests/is empty.
Risks #
- The golden rewrite is the milestone's only irreversible step. It is a reorder, not a
behaviour change, and Acceptance 3 is the M17 step A protocol that proved exactly this class
before. Do not fold any other
src/change into the same commit. core.mcand the parts drifting apart. The design makescore.mcbe the parts, and Acceptance 1cmps the two spellings anyway. If the owner prefers to keepcore.mc's flat list for a zero-golden-rewrite landing (D2's alternative), this risk becomes the milestone's main one and Acceptance 1 becomes mandatory rather than belt-and-braces.mc_maingrowing a flag that only an optional part understands.--dump-machine(M24),--sysroot-dir(core_build) and--configare the precedent. Rule to write down incli.mc: a flag whose handler lives in a part is registered, notif-ed.--sysroot-dirmoves into thesysrootsubcommand's own parsing where it already is.user_inittiming regressing.mc_mainmust keepuser_init()aftertok_init()and before the first token (src/main.mc:218), and the part*_initcalls must stay inmain(), beforemc_main, because they only touch the backend/target/machine tables and must nottok_add. A part that ever needstok_addis auser_initclient like everyone else.- A recreated compiler with no machine at all silently produces nothing until the first
mach(...)call dereferencesmach_tab == 0.mc_mainshould sayno machine registeredbeforegen_lower; three lines, in the same commit. type_disableread as "the type is gone". It removes a word from the surface;ld8still yieldsTY_U8internally. Every documentation sentence about it has to say so, or the next milestone will file a bug.- Two more taught compilers to keep alive (
check-parts's probes). They are leaves and they are the only things that exercise a partial core, which is the regression class M41 introduces.
Decisions (architect, 2026-09-04 -- every recommendation below is adopted) #
- D1 -- coarse parts or fine (
<mc/core/lex>,<mc/core/parse>)? Recommend five coarse parts. The seams that exist are the ones the owner listed: writers, machines, driver, bundle. A per-file part list would be twelve names, every one of them a promise that the file is independently includable -- whichparse.mcandgen_walk.mcare not. - D2 -- is
src/core.mcthe sum of the parts, or a parallel flat list? Recommend the sum, and pay the one golden rewrite. The alternative (keep today's flat list, add the parts beside it) leaves the golden untouched but makes "the full assembly is the parts" a claim a script has to defend rather than a fact the source states. If the owner wants the golden untouched, take the alternative with Acceptance 1 as a required check. - D3 -- split
src/macho.mc? Recommend yes, intoobjmodel.mc+macho.mc. Without it the owner's "no Mach-O writer" is unreachable, since the walker's section/symbol model lives in the same file. The cut is a pure move that preserves definition order. - D4 -- split
src/main.mc? Recommend yes, intocli.mc(mc_main, core_min) +main.mc(the registrations).mc_mainis the milestone's public API:examples/kernelandexamples/avrwrite a five-linemaininstead of copying 200. - D5 --
subcommand()table, or leavebuild/limits/sysrootinmain? Recommend the table. It is the eighth registry of the same shape, it is what makesmc builda part rather than a core fact, and it keeps the usage text byte-identical by carrying one string per registration. - D6 -- a removal API for backends, targets and machines? Recommend no. Once the
registrations live in the parts, "not registered" is free and
*_removehas no caller -- the reason M24 D8 refusedtype_set_width, applied to itself. - D7 --
type_disableandintrinsic_disable: in, or deferred? Recommend both in, ~24 lines together.intrinsic_disablehas a named caller (M40 § 2B'sldw/stwmitigation);type_disableis the smaller half of the same idea and the owner's sentence names types first. Document loudly that they remove words, not types. - D8 -- directive and keyword removal? Recommend no, unpriced, no caller.
- D9 -- does
type_set_width(TY_UPTR, w)land in M41 or M40? Recommend M41, with the probe test of Acceptance 6, so that M40 is a module and nothing else. This is M40's amended D1/D2 and reverses M24 D8 exactly as the amendment says. - D10 --
HEAP_SIZEoverridable from[limits]? Recommend no. It isbss, absent from the file, and dynamic since M23; a static array's size is not reachable fromuser_init. - D11 --
[compiler].core = "<mc/core_min>"? Recommend yes, ~4 lines: a value starting with<is emitted verbatim. It is the only driver change the milestone needs, and without it a project cannot askmc buildfor a partial core. - D12 -- part naming. Recommend
mc/core_min,mc/core_machines,mc/core_writers,mc/core_build,mc/core_bundle-- underscores, because a bundle name is the file's basename and the last-component fallback resolves on it;core_*becausemc/driveralready means the module.
Adopted, one line each: D1 five coarse parts; D2 core.mc is the sum and the golden is rewritten
once under M17 step A's protocol; D3 objmodel.mc split; D4 cli.mc/mc_main split; D5 the
subcommand table; D6 no removal API for the registries; D7 type_disable and
intrinsic_disable both in, documented as word removal; D8 no directive removal; D9 the uptr
width mechanism lands here and M40 consumes it; D10 HEAP_SIZE stays; D11 the <...> form of
[compiler].core; D12 underscore names. The architect's additions: (a) Acceptance 1 (cmp of the
two spellings) is a required check, whichever way D2 goes; (b) mc_main says no machine
registered before gen_lower (risk 5), in the same commit; (c)
docs/guide/98-recreating-the-compiler.md is written for the developer of M39's original question
and states, in one table, what each omitted part costs in bytes and in capability.
Implementation notes (mc-dev, 2026-09-04): where the landed milestone deviates #
Ten places where what landed differs from § Design, each with the reason. Everything else is the spec as written, including D1..D12 and the architect's three additions.
backend_macholives insrc/core_writers.mc, not insrc/macho.mc. § 1 puts it in the writer's file; it callsgen_lower/gen_encode_all, which are<mc/core_min>'s, so putting it there would makesrc/macho.mcunincludable on its own -- andsrc/m0.mc(the M6 driver that builds the object model by hand and writes it) includes exactlyobjmodel.mc+macho.mc. The part that REGISTERS a backend is the part that defines it.host_bundle_openlives insrc/core_bundle.mc, not insrc/bundle.mc. It callshost_include(), andtools/bundle.mcincludessrc/bundle.mcwith no host layer at all.sysroots_init()is called frommc_build_init(), not frommain()as § 2 sketches. A part owning its own initialisation is what makesmain()a list of parts and nothing else.intrinsic_disableis tested at the head ofres_call, not insideintrin_id. Two reasons: the diagnostic gets a POSITION (prog.mc:4: ld64: removed by this compilerinstead of a baremc:line), andintrinsic()'s own guard,if (intrin_id(name)) die2("cannot shadow a core intrinsic", name), keeps working -- a dyingintrin_idwould have refused a module that disablesld64and then registers its own.-
subcommand,on_planand theintrinsic_disablename table use fixed ceilings (16, 8,- rather than M23's growable tables, by M23's own rule: none of the three scales with the
program being compiled. Same reasoning as
MAXMACHINESandMAXTARGETS.
- rather than M23's growable tables, by M23's own rule: none of the three scales with the
program being compiled. Same reasoning as
- Four names had to move for the parts to be parts, and § 1's file list does not mention them:
tm_catandtm_num_strfromsrc/toml.mctosrc/arena.mc,MODE_755fromsrc/backend_exe.mctosrc/arena.mc, andR_X86_PC32/R_X86_PLT32fromsrc/machine_x86_64.mctosrc/objmodel.mc. The full assembly hides a cross-part dependency completely;scripts/check-parts.shcase 1b (below) is what found all four. glob_place'sal = 16for a global ARRAY is left alone. § 4a C4 names three roundings to 16 (the two zerofill lines, the local array, the frame) and this is a fourth, in the data section rather than the frame. It is wasteful on a two-byte word and harmless; changing it is M40's call, with a real target to measure against.- The probe modules are files in
lib/and are NOT bundled. § 5's acceptance says "built insidecheck-parts.shfrom the bundle, incheck-standalone's style"; the entry files do take their core from the bundle, but the fivelib/user_*.mcfixtures are reached by relative#includefrombuild/parts/. Adding them totools/bundle.listwould move the blob -- and the five goldens -- for something no compiler includes. - The goldens were rewritten once per commit, four times, not once for the milestone. Every
commit that touches
src/regeneratessrc/bundle_data.mc, which is part ofsrc/mc.mc, sobuild/mc2.omoves in each of them;make checkrunscheck-bundlebeforebootstrapand a stale bundle is a hard failure, so the alternative was a redmake checkin three of the four commits. Risk 1's intent -- "do not fold any othersrc/change into the same commit" -- is honoured: each rewrite follows an empty--dump-asmdiff betweenmc1andmc2and a passingcmp build/mc2.o build/mc3.o, andscripts/check-inert.shruns across each step. scripts/check-parts.shproves one thing more than § Acceptance asks: case 1b, that<mc/core_min>plus EACH optional part compiles on its own. That is the property the five names of deviation 6 were violating while the whole assembly stayed green, and it is the regression class this milestone introduces.