Spec M52 -- the standard library becomes installable, and the binary carries only the compiler's own source
Written against minicompiler/mc main at c7b8cfc (version constraints for [deps]), read-only
session except for the probes of § 1.3, 2026-09-08, on the Apple M4 / macOS 26.6.2 host
../comparison.md measures. There is no docs/plan.md row to quote: this
milestone starts from the owner's direction, and ../plan.md's row is written from
this spec and not the other way round.
The owner's direction, verbatim (2026-09-08):
M51 não violaria se fosse lib, não bundle, e mesmo os bundles podemos converter em libs que podem ser instaladas (caso não existam) no momento do build de uma aplicação. Dessa forma o binário do mc fica mais enxuto, portando somente o que é estritamente essencial e verdadeiramente dependência.
The one-paragraph version. The measurements move this milestone from "carve the libraries out into
packages" to something much smaller, and they do it twice. First, the size: applying the owner's own
rule per bundle row -- a row stays iff the compiler itself is built from it -- takes 41 of 101 rows
and 123 315 of 678 368 blob bytes out, which is 10.0% of the binary (1 408 576 -> 1 268 276 B,
measured by regenerating the blob and rebuilding, § 1.2) and not one byte of __text, because the
essential set is the compiler's own source and that is 82% of the blob. Second, the mechanism: the
road that serves a library name the blob no longer carries already exists and needed no design at
all -- resolution step 3, <libs>/mc/v<version>/, reached on a bundle miss
(../reference/packages.md § 2), which § 1.3 proves end to end on this
host: a thinned compiler refuses #include <sys>, mc install --from-tree . populates the tree in
0.28 s with no network, and the same compiler then builds a taught float compiler out of
<mc/core> + <user_float> whose object is cmp-identical to the one the unthinned compiler's
taught compiler writes. So M52 is not a packaging milestone: it is a one-predicate change in
tools/bundle.mc, two more roots in dp_mc_load so that the tree ships beside the binary and the
offline promise survives, one widened refusal, and the test and doc migration. Zero #include
lines change anywhere -- <float> is spelled <float> before and after -- which is what makes the
0.16.0 cut safe for the consumer. Publishing float, i128, io as registry packages of their own
stays priced and deferred (§ 9), for one measured reason: only ONE bare name per package survives (a
package's lib), so <machine_arm64_float> would have to become <float/machine_arm64_float.mc> in
every consumer, and the single-file CLI has no lock road at all.
0. What this stands on (read, not assumed) #
- The three resolution roads.
lex_include_name(src/lex.mc:1041) strips a trailing.mcand tries, in order:lex_include_libs(name, 0)-- the lock road, only when the name's first path component is a packagemc.locknames and the including file may reach it (:1046);lex_include_bundled(name, 0)-- the blob (:1048,lex_include_bundledat:971, a no-op whenbopen_fn == 0,:713); andlex_include_libs(name, 1)-- the installedmcpackage (:1050). Only then does it refuse. The two openers arrive as function pointers,bopen_fnfromlex_set_bundle(:715) andlopen_fn/lhint_fnfromlex_set_libs(:745), so the lexer depends on neithersrc/bundle.mcnorsrc/deps.mc. - Stage 1 is not gated on the flavour.
libs_open(src/deps.mc:1313) forwards any non-zero stage todp_mc_open(:1270), which reads<libs>/mc/v<mc_version()>/withbundle.listat its root as the NAME<TAB>PATH map (dp_mc_load,:1226). Nothing there tests whether this binary has a blob; the comment says "a full binary answers every one of these names from its blob and never reaches this step", which is a statement about today's manifest, not a guard. Only the hint is gated:lex_include_name:1060asksdep_include_hint(src/deps.mc:1300) only whenbopen_fn == 0. - Step 3 verifies nothing.
dp_mc_loadreadsbundle.listanddp_mc_openreads the named file throughlex_readable; there is no hash, no manifest check, and a missing file simply falls through to the next road.<libs>is--libs-dir DIRwhen given, elsehost_home()/.mc/libs(deps_libs_root,:1209;deps_set_libs_dir,:1219), and--libs-diris parsed bymc build,mc pkg,mc install,mc upgradeandmc tool-- not by the single-file CLI (src/core_build.mc:54,src/core_pkg.mc:37-55; noopt_val(a, "--libs-dir")insrc/cli.mc). mc installalready installs the compiler's own package (src/install.mc, M44 step 4):mc install [VERSION] [--from-tree DIR]resolves themcrow from the registry, or hashes and copiesmc.toml+[package].filesout of a checkout, and thenins_bundle_list(:45) copiestools/bundle.listto the root of the tree -- the file step 3 reads. The rootmc.tomldeclares 105 files (grep -c '^ "' mc.toml), which is every path oftools/bundle.listplussrc/bundle_data.mc,tools/bundle.list,src/mc_linux_x86_64.mcandsrc/user.mc. So the libraries are already files of themcpackage; nothing has to be moved for them to be installable.host_self_path()exists in all three host layers (src/host_macos.mc:114,src/host_linux.mc:82,src/host_windows.mc:91) and is read bysrc/upgrade.mc:333andsrc/tool.mc:344/422. It is_NSGetExecutablePath/readlink /proc/self/exe/GetModuleFileNameA, neverargv[0].mc buildnever downloads, and that is proved rather than asserted:../reference/packages.md§ 9, withscripts/check-pkg.shrunning the whole suite under acurl, awgetand ataronPATHthat exit 97 if invoked.../determinism.md's rule is that one source gives one answer on every machine.- A taught compiler's modules are included in array order.
drv_gen_compiler(src/driver.mc:505) writes#include <mc/host>, then[compiler].core(or<mc/core>by default,:528), then one line per[compiler].modulesentry throughdrv_include(:479), which emits a value starting with<verbatim. Somodules = ["<float/float.mc>", "<mine.tk>"]already orders a library ahead of a language module with no new mechanism. - The blob's generator.
make bundle(Makefile:140) isbuild/mc1 --exe tools/bundle.mcthenbuild/bundle tools/bundle.list src/bundle_data.mc.tools/bundle.mcwrites names and file bytes and nothing else -- "no path, date or address reaches the blob" is its own comment, andM47-S5.md§ 4.2 verified it. - The reserved names are
mc,mc/...,depsandbuild(dep_reserved,src/deps.mc:106).float,io,sys,i128,f16are not reserved: any account could register them today. - What
src/actually includes fromlib/. Two files, measured withgrep -rn 'lib/' src/*.mc:../lib/prelude.mc(16 files --backend_coff,backend_coff_exe,backend_elf,backend_elf_exe,backend_exe,driver,gen_resolve,limits,macho,objmodel,sandbox,sha256,stubs,sysroots,toml, andcore_mintransitively) and../lib/user_default.mc(src/user.mc:17). Nosrc/file includes any otherlib/file, and nosrc/file has a#include <name>of a library at all -- the compiler declares the system calls it needs in its own host layer. - The slim flavour (
../reference/bundle.md§ The slim flavour):src/core_slim.mcis the five parts without<mc/core_bundle>and<mc/core_sandbox>, andsrc/main_slim.mcthemain()that matches. It is whatmc install's refusal (dep_include_hint) was written for. - The ten goldens (
ls tests/golden/):mc2,mc2-opt, and the four foreign ones each with an-opttwin.src/mc_seed.mc-- what the frozen seed compiles since the bootstrap decoupling -- has relative includes only and touches no bundle.
1. The measurements (step 0), and what they decide #
1.1 The blob today, with the owner's rule applied per row #
src/bundle_data.mc's bundle_idx carries a csize and an rsize per entry, so the cost of every
name is readable without a build: 101 entries, raw 1 479 468 B, compressed 677 117 B, blob
678 368 B (the blob is the names plus the streams; the index is another 3 232 B). Applying "a row
stays iff the compiler is built from it" -- which by § 0's last-but-three bullet is the path is
under src/, plus prelude and user_default:
| class | rows | raw | in the blob |
|---|---|---|---|
stays -- every src/ path (58), prelude, user_default | 60 | 1 223 798 | 554 279 |
leaves -- every lib/ path but two, and the two tests/mc/bundle/ payloads | 41 | 255 670 | 122 838 |
The 41 rows that leave, by what they are and what they cost (csize, bytes of blob):
| what | rows | names | blob |
|---|---|---|---|
| the float library and its two machines | 6 | float 7 363, machine_x86_64_float 11 184, machine_arm64_float 10 736, float_rt 2 071, user_float 499, mc_float 986 | 32 839 |
| the wide integers | 4 | i128 16 563, u128 414, mc_i128 230, mc_u128 225 | 17 432 |
| half precision | 2 | f16 3 653, mc_f16 1 064 | 4 717 |
| the system layers and stdio | 10 | sys_windows_host 7 658, sys_windows 5 200, sys_linux_x86_64 3 736, sys_linux_aarch64 2 868, sys 2 493, sys_windows_start 1 952, sys_linux 1 496, sys_svc 1 337, io 792, u128's door counted above | 27 532 |
the teaching demos and probes (check-surface, check-standalone, check-asm fixtures) | 17 | user_syntax_demo 21 005, machine_probe 4 574, backend_arm64 4 546, syntax_demo_test 1 965, user_badmach 1 186, pass_demo 942, user_tokadd 793, mc_syntax_demo 733, user_dupop 693, user_param_nop 544, user_lit_nop 519, user_demo 478, user_dupty 400, user_dupintrin 388, mc_probe 328, mc_badmach 217, mc_lit_nop 173, mc_param_nop 170 | 39 654 |
the #embed test payloads | 2 | embed_demo 641, embed_demo.txt 23 | 664 |
Two facts worth naming before any design. The single largest row that leaves is
user_syntax_demo, 21 005 B of blob -- a check-script fixture, not a library; and lz
(src/lz.mc, 4 103 B) stays although its bundled name is flat, because src/core_min.mc
includes lz.mc and the blob is what serves that relative include for <mc/core>. The rule is
about the path, not the spelling, and lz is the row that proves it.
1.2 The three binaries, measured #
Measured by copying tools/bundle.list aside, writing the 60-row subset, running build/bundle, and
building with build/mc1 --exe src/mc.mc; then restoring both files and confirming with cmp:
| binary | __text | __cstring | __DATA,__data | blob | |
|---|---|---|---|---|---|
mc today | 1 408 576 | 540 192 | 31 024 | 691 456 | 678 368 |
mc with the 41 rows out (measured, not projected) | 1 268 276 | 540 192 | 31 024 | 566 832 | 555 053 |
mc-slim (no blob at all) | 626 420 | 476 280 | 27 248 | 8 664 | 0 |
- The cut is −140 300 B, −10.0% of the binary and −18.0% of
__DATA,__data. The blob is 98% of that section (678 368 of 691 456), so the section and the blob move together. __textdoes not move at all. A bundle row is data. The other 476 280 B is code, and the only measured lever on it is the flavour:mc-slim's__textis 63 912 B smaller because<mc/core_bundle>and<mc/core_sandbox>are not in it. That is where the rest of the "enxuto" lives, and it belongs to whichever milestone decides whethermc sandboxis part of the default compiler (§ 13.7). M52 does not touch it.mc-slimis 44.4% of today's full binary and 49.4% of the thinned one: after M52 the two flavours differ by exactly one thing, the compiler's own source, which is the honest definition they never quite had.
1.3 The four probes: does the road that is already there actually serve a library? #
Run on this host with the thinned compiler of § 1.2 (/tmp/mc-thin), a private HOME, and no
network. Transcript in the commit that lands step A; the four results:
| probe | command | result |
|---|---|---|
| 1 | thinned compiler, nothing installed, #include <sys> | m52probe.mc:1: unknown bundled include: sys, exit 1 |
| 2 | unthinned build/mc1, same source | compiles, runs, exit 42 |
| 3 | mc install --from-tree . --yes into the private HOME | package mc 0.0.0-dev -> …/.mc/libs/mc/v0.0.0-dev/, exit 0, 0.28 s, 107 files, 3 308 KiB |
| 4 | thinned compiler again, same source, single-file CLI, no mc.toml, no lock | compiles, runs, exit 42 |
Then the road a taught compiler takes, which is the consumer's road:
| probe | command | result |
|---|---|---|
| A | thinned compiler + installed tree builds #include <mc/host> + #include <mc/core> + #include <user_float> | exit 0, 1 324 746 B |
| B | that compiler compiles and runs a program using <sys> and <float_rt> | prints 10.000, exit 42 |
| C | the same taught compiler built by the unthinned build/mc1 | exit 0 |
| D | the object each taught compiler writes for the same float program | cmp equal |
Probe D is the inertness proof of the whole milestone: a library's bytes are the same bytes whether
the blob or the installed tree hands them over, so nothing the compiler emits can move. Probe 4 is
the reason design (I) wins § 3: the single-file CLI has no lock road at all
(../reference/packages.md § 2), so step 3 is the only road that can
serve tests/float/*.mc, check-float, check-wide and a bare mc prog.mc -o prog.
1.4 What that decides #
- The size gain is real but modest (10%), and it is all data. The spec says so in its own headline instead of letting a reader expect a smaller compiler (§ 10.1).
- The mechanism is not to be designed -- it is to be reached. The compiler-side work is a predicate in the generator, two more roots in one function, and one widened message (§ 2, § 4).
- Because the spelling of every
#include <name>is unchanged, the migration is not a source migration. It is a provisioning migration: the tree has to be somewhere. § 4 decides where. - The 41 rows are not 41 libraries. 17 of them, and the largest single row, are check-script
fixtures -- which is an argument against publishing them as packages and for keeping them files
of the
mcpackage (§ 3).
2. The essential set -- decision (design question 1) #
D1. A bundle row stays iff its path is under src/, plus exactly two named exceptions. One
predicate, in tools/bundle.mc, ~8 lines:
in the blob <=> path starts with "src/" or name is "prelude" or name is "user_default"
The two exceptions are not taste, they are the compiler's own #includes: sixteen files under src/
include ../lib/prelude.mc and src/user.mc includes ../lib/user_default.mc (§ 0), and inside the
blob those relative includes resolve by name to prelude and user_default (M15's last-component
fallback). Drop either and <mc/core> is broken and check-standalone fails at once. They are
"verdadeiramente dependência" in the owner's exact sense.
Three alternatives, and why not:
- A third column in
tools/bundle.list(blob/lib). Honest and greppable, but the classification would then live in the manifest anddp_mc_loadwould have to stop the path at the second TAB (it takes the rest of the line today,src/deps.mc:1233). The predicate is one place; a column is two, plus a format change to a file an installed tree carries. - A second list file (
tools/bundle.blob). One more generated file to keep from drifting. - Moving
lib/prelude.mcandlib/user_default.mcintosrc/, which would remove the two exceptions. It would tell the truth -- they are part of the compiler's source -- and the bundled NAMES would not move, because the name column is independent of the path. But it churns 17#includelines for two lines of predicate, and<prelude>is genuinely both: the core'swhileandfor, and a library every program uses. Not done; recorded here so the next reader does not rediscover it.
tools/bundle.list itself is unchanged -- all 101 rows stay, and that is load-bearing: the file
is the NAME->PATH map step 3 reads out of the installed tree (dp_mc_load), so a row that left the
blob must still be in the map or the library is unreachable by either road. The root mc.toml's
105 files are unchanged for the same reason. The blob shrinks; the package does not.
3. Where the libraries live -- decision (design question 2) #
D2. They stay files of the mc package and are served by resolution step 3. M52 publishes no new
registry package. Proven by § 1.3, at a cost of zero new resolution roads.
The alternative -- one package per library (float, i128, f16, io, sys_linux,
sys_windows, and rt/http/sqlite from examples/api), priced in
M47-S5.md § 4 as ~150 changed lines plus the moves -- is refused for M52 on three
measured grounds, not on cost:
- Only one bare name per package survives.
libs_open(src/deps.mc:1313) matches a lock row by the name's FIRST path component and then joins either the rest of the path or, for a bare<geo>, the package'slibentry. So a package namedfloatserves<float>and<float/machine_arm64_float.mc>-- but not<machine_arm64_float>. Every one of the 41 names except at most six would have to be respelled in every consumer, including the teko compiler, which includes<machine_x86_64_float>and<machine_arm64_float>by their bare names today (§ 7). Design (I) changes no spelling at all. - The single-file CLI has no lock road, so
tests/float/*.mc,tests/wide/*.mc,tests/windows/07*.mc, every<sys>program inbench/, andmc prog.mc -o progitself could not resolve a package name without each becoming a project with anmc.toml. Probe 4 shows step 3 serving exactly those. - 17 of the 41 rows are check fixtures (§ 1.1).
user_syntax_demo,machine_probe,pass_demo,user_dupopare not a standard library; they are this repository's test corpus and they have no consumer and no version series. A package for them would be a published artefact nobody installs.
What D2 gives up, and what replaces it: a project cannot pin a newer float than its compiler
ships. It can pin a newer everything -- [deps] mc = "0.17.0" is a legal row, mc is the registry's
own published package and the tree is hashed and locked like any other -- and when per-library
packages arrive (§ 9) the layering works with no change here, because step 1 precedes step 3: a
[deps] float pin overrides the compiler's own copy by construction.
D3. The libraries are versioned with the compiler, because they are files of the mc package and
mc install writes <libs>/mc/v<mc_version()>/. No second version series, no 1.0.0 freeze
commitment for float, no lockstep to maintain. This is M48.md § 2's mclib rationale
with the package it already had instead of a new one -- see § 9.1 for what exactly of M48 § 2 is
superseded.
4. The library root, and the offline promise -- decision #
D4. dp_mc_load gains two more roots, tried after <libs>, derived from host_self_path(); and a
release ships the tree beside the binary. ~15 lines in src/deps.mc, and it is what keeps the
property M15 earned and ../bootstrap.md asserts: untar, compile a float program,
no network.
Order, and why:
| order | root | who puts it there |
|---|---|---|
| 1 | <libs>/mc/v<version>/ -- --libs-dir DIR or $HOME/.mc/libs | mc install, mc upgrade |
| 2 | <dir of host_self_path()>/lib/mc/v<version>/ | the release tarball, and make |
| 3 | <dir of host_self_path()>/../lib/mc/v<version>/ | a packager who puts the binary in bin/ |
<libs> is first so that an explicit mc install still wins over the copy that shipped -- a user who
installs a newer tree gets it, which is the same precedence --libs-dir has over $HOME today.
Roots 2 and 3 are two lex_readable probes; root 3 exists because /usr/local/bin/mc has to find
/usr/local/lib/mc/v0.16.0/, and a packager who cannot do that will vendor the blob back in.
The tree beside a binary is a partial tree and that is fine by construction: dp_mc_load reads
bundle.list and dp_mc_open falls through on any name whose file is not readable (§ 0), so the
release stages only bundle.list and the lib/ files -- about 312 KiB of source, and nothing of
src/ (a full binary answers every mc/* name from its blob; a slim one needs mc install, exactly
as today).
D5. make populates root 2 for the whole suite. Four lines in the Makefile put bundle.list
and lib/ under build/lib/mc/v<version>/, beside build/mc1, and every check script then works
with no new flag, no --libs-dir and no dependence on $HOME -- which is the CI rule
(../reference/packages.md § 2's last paragraph) satisfied by the layout
instead of by an argument. This is why --libs-dir is deliberately not added to the single-file
CLI: nothing needs it once the root is beside the binary.
D6. The refusal is widened. dep_include_hint is asked only when bopen_fn == 0 today, so the
thinned full binary of probe 1 says unknown bundled include: sys and names no road. After M52 a
binary asks the hint whenever nothing answered, and the hint distinguishes three states:
| state | message | exit |
|---|---|---|
| no root found at all | prog.mc:1: #include <sys>: not in this compiler and mc 0.16.0's library tree was not found: run mc install | 1 |
| a root found, the name is not in its map | prog.mc:1: unknown bundled include: sys (unchanged) | 1 |
| a slim binary, no root | prog.mc:1: #include <prelude>: not bundled in this compiler and mc 0.16.0 is not installed: run mc install (unchanged) | 1 |
The middle row is the one that must not move: a misspelled name is still a misspelled name, and
check-mc/check-surface assert that text.
5. Install on demand at a build -- decision (design question 3) #
The direction asks for libraries "que podem ser instaladas (caso não existam) no momento do build".
Three roads were weighed against two promises -- mc build never downloads
(../reference/packages.md § 9, proved by check-pkg's failing curl)
and one source gives one answer on every machine
(../determinism.md).
| road | determinism | offline promise | verdict |
|---|---|---|---|
| (a) refuse, and name the command | kept | kept | the default (D6) |
| (b) resolve the newest implicitly and write the lock | broken: the answer is a function of the day the build ran | broken silently | refused |
| (c) an explicit flag that runs the sync step first | kept -- sync writes the lock, and the lock is what the build reads | kept: without the flag, nothing reaches the network | D7 |
D7. mc build --sync lands, and it is the only road from a build to the network. It is
pkg_sync(dir, yes) followed by the build, ~20 lines in src/driver.mc, no new resolution
semantics, the same plan and the same [[permission]] consent mc pkg sync --yes already prints
(../reference/packages.md § 10). Without the flag mc build is byte
for byte the command it is today, and check-pkg's curl-that-exits-97 keeps proving it.
(b) is refused with a number: a project whose [deps] names no version would resolve float to
whatever the index holds at that minute, so two machines and two days give two objects -- and every
gate in this repository that says "the same source twice" would be measuring the registry.
What the common case does after D4 is reach the network not at all: the tree ships beside the
binary, so --sync is for a project with real [deps] rows, which is what it was always for.
6. The compiler's own tests -- decision (design question 4) #
D8. make check runs with no network and no $HOME, through root 2 (D5). No vendored deps/, no
local registry, no --libs-dir plumbing: one Makefile rule, and every script that reaches a
library name keeps the name it has.
What each gate needs, read from the scripts (18 of them name a library path or a bundled library name):
| gate | what it does today | after M52 |
|---|---|---|
check-float (16/16 on three run legs, 14/14 objects on two Windows legs) | builds lib/mc_float.mc by path; tests/float/*.mc say #include <sys>, <float_rt> | unchanged; the two names come from root 2 |
check-wide | lib/mc_i128.mc, lib/mc_u128.mc, lib/mc_f16.mc by path; tests/wide/*.mc say <sys>, <i128>, <u128>, <f16> | unchanged, same reason |
check-surface | builds taught compilers from lib/mc_syntax_demo.mc etc. by path | unchanged (paths, not names) |
check-standalone | #include <user_syntax_demo> and <syntax_demo_test> as bundled names (scripts/check-standalone.sh:61,65) | the two names come from root 2; the <mc/host> + <mc/core> + <user_default> half is untouched and still cmp-identical to src/mc.mc |
check-lex, check-ast, check-asm | glob lib/*.mc and compare mc_seed against mc1 | unchanged: they read files, not names. Counts do not move |
check-obj, check-inert, bootstrap | compare artefacts | unchanged; probe D says why |
check-bundle | reproducible + fresh + the <mc/bundle_data> shape | +1 case: the blob holds exactly the 60 rows the predicate admits, and every one of the other 41 resolves through root 2 |
test-linux, test-windows, test-windows-exe | generate a TOML per test; the sources say <sys>, <io>, <sys_linux>, <sys_windows> | unchanged; the cross-built compiler on the macOS side reads root 2 |
check-mc | tests/mc/073-embed-bundle.mc uses #include <embed_demo> and #embed of <embed_demo.txt> | the two payloads come from root 2 |
check-pkg (145/145) | the curl/wget/tar shim, mc install --from-tree, § 32 | +1 section: root 2 and root 3 each serve a name the blob lacks, and with both removed the D6 message is exact |
check-docs | compiles every ```mc fence | the fences that say #include <sys> need root 2 on PATH-adjacent terms: one line in the script |
check-parts | the six parts, <mc/core_min> alone, the slim assembly | unchanged |
A new gate, scripts/check-libroot.sh (~90 lines, in make check), is the one thing D4 adds that
nothing else covers: the three roots in precedence order, a partial tree, a name in the map whose file
is absent (falls through, no crash), and the D6 message with every root removed.
7. The consumer -- decision (design question 5) #
teko reported its bundle surface on 2026-09-13, measured by grep over its own modules:
| what | includes from the bundle |
|---|---|
the teko compiler (teko*.tk, core_teko.mc) | <sys>, <mc/core_writers>, <mc/core_machines>, <mc/core_bundle>, <mc/core_build>, <machine_x86_64_float>, <machine_arm64_float>, <float> |
lib/rt.tk, lib/time.tk | <name> through <mc/core_bundle> only -- no <io>, no <i128> |
teko_std | <teko/lib/rt.tk> from the registry; zero direct bundle |
D9. The 0.16.0 cut needs no change in teko at all, and no deprecation window. Every name in that
table keeps its spelling: the four mc/* ones stay in the blob (§ 2), and <sys>, <float>,
<machine_arm64_float> and <machine_x86_64_float> come from the library root the release ships
(§ 4). The exact [deps] lines teko will need are: none.
That is the whole value of D2 over per-library packages, and it is worth stating as the counterfactual:
under design (II) teko's compiler would need [deps] float = "0.16.0" plus three respellings --
<float/float.mc>, <float/machine_arm64_float.mc>, <float/machine_x86_64_float.mc> -- because only
a package's lib keeps a bare name (§ 3.1), and its registry modules array would have to carry them
ahead of <teko/teko.tk>.
teko's one open question -- "como um pacote de linguagem declara 'preciso do pacote float carregado
antes dos meus módulos'" -- is answered today and needs no feature: [compiler].modules is an ARRAY
and drv_gen_compiler emits one #include per entry in array order (§ 0), with a <...> value
passed through verbatim. So the answer is one line ahead of the language module, whether the name is
a bundled one now or a package one later:
[compiler]
modules = ["<user_float>", "<teko/teko.tk>", "<teko/user.mc>"]
D12 (step D, added 2026-09-14 from the consumer's dry run against main 62f0cdd/21d081b). The
table above is about a program teko's compiler compiles. What it did not cover is teko's compiler
itself: a [compiler] product is a second binary in the project's own build/, so roots 2 and
3 are relative to IT and the tree beside mc is invisible. Measured by the consumer: the product
works when mc build spawns it (the parent hands its root over, deps_libs_for_child) and fails
when run standalone -- build/teko --entry-only ..., its own bootstrap.sh, its check-docs.sh --
with lib/rt.tk:40: unknown bundled include: sys, 3 of 64 fixtures; --libs-dir or a copied
tree gives 64/64 and FIXPOINT OK. Three shapes were weighed:
| shape | verdict |
|---|---|
(a) mc build STAGES the tree beside the [compiler] product | taken: a taught compiler leaves mc build as self-sufficient as the release tarball |
| (b) bake the builder's absolute root into the binary | refused: a path in the object, against ../determinism.md |
(c) --libs-dir on every invocation | refused: it pushes the contract onto every consumer's scripts, and breaks a user who is handed build/teko |
So D9 stands for the SOURCES and gains one sentence for the BINARY: the consumer's --libs-dir
workaround is no longer needed on any road.
D10. No deprecation window, and no alias. The roadmap's 1.0.0 policy (add the new, keep an alias
for one major) is about a renamed or removed surface. Nothing here is renamed and nothing is
removed: #include <float> resolves to the same bytes before and after, by a different road. A
deprecation message would fire on a spelling that is still correct. What 0.16.0 owes the reader is a
release note that says the library tree ships beside the binary and that a binary copied out of the
tarball alone loses it (§ 13.2).
8. Goldens, the bootstrap and the flavours -- decision (design question 6) #
D11. All ten goldens move once per step that regenerates the blob, each only after its own
criterion -- the empty --dump-asm diff on both roads and cmp mc2.o mc3.o / cmp mc2o.o mc3o.o
for the two local ones, make check-linux-host for the Linux pairs, the cross-computation of
../../tests/golden/README.md for the Windows pairs. Steps A and B
each regenerate it (A changes src/deps.mc, B changes the blob's contents), so the goldens are
rewritten twice over the milestone, once per commit -- the M41 precedent, recorded so nobody
reads a double rewrite as a mistake.
build/mc_seed is unaffected: src/mc_seed.mc has relative includes only and never reaches a bundle
(§ 0). stage0/ is untouched, 2848/3000.
D12. Both flavours survive, and both ship the library root. After M52 the difference between them
is exactly one sentence -- the full binary carries the compiler's own source and can therefore teach
a compiler with no root at all; the slim binary carries none and needs the root for everything. The
ten -slim release archives stay, scripts/release-assets.sh --slim stays, and the slim tarball
becomes a complete offline toolchain for the first time: today it needs mc install before
#include <prelude> works, and after D4 the root travels with it.
The measured pair to put in ../reference/bundle.md: full 1 268 276,
slim 626 420, 49.4%.
9. Priced, not built #
9.1 What of M48.md § 2 is superseded, and what survives #
M48 § 2 designed mclib: lib/ as ONE registry package under lib/mc.toml, versioned with the
compiler, published as a mc-libs-<ver>.tar.gz release asset, spelled <mclib/float.mc>, and
explicitly not overriding the bare bundled names ("<float> still comes from the blob unless the
project pins a package named float"). M52 keeps the goal and drops the package:
| M48 § 2 | M52 |
|---|---|
a second package, mclib, with its own mc.toml, files, index row, admin registration and release asset | none. The libraries are already 41 of the mc package's 105 files (§ 0) |
spelling <mclib/float.mc> | <float>, unchanged |
<float> keeps coming from the blob | <float> comes from the library root; the blob has only the compiler |
§ 2.6's reassignment of mc-libs-<ver>.tar.gz and mc install --from-bundle | superseded: mc install already fetches the mc row (that half of § 2.6 landed in M44 step 4) and D4 ships the tree in the binary's own tarball, so there is no second archive at all |
§ 2.1's value (c), "a newer lib/ on an older compiler" | deferred to § 9.2; [deps] mc = "<newer>" is the road that exists |
M48 § 2's measurements and its road-A/road-B table stay useful and are cited by § 9.2. Its mclib
name should be treated as retired, exactly as CLAUDE.md's C3 entry already records.
9.2 Per-library registry packages -- deferred, with the price and the one new fact #
M47-S5.md § 4 prices the moves (lib/float/, lib/i128/, …), what breaks (the
lib/*.mc globs in three scripts, the paths check-float/check-wide/check-surface name, ~15
places in docs/), and answers the nesting question (lex_root_of keeps the LONGEST matching root,
so an inner package wins -- untested, and a nested fixture in tests/pkg/ is the first thing that
work should add). Two things to add to that page's price:
- The spelling cost, measured: 41 names, of which at most 6 could keep a bare spelling as some
package's
lib. The other 35 become<pack/file.mc>in every consumer, every test and every doc that names them (18 scripts, 25docs/files, § 6). - The single-file road: a package name is unreachable without a project (§ 3.2), so the move
turns
tests/float/*.mc,tests/wide/*.mcand the<sys>programs ofbench/into projects, or requires a new resolution road for them.
When it happens, the shape is one package per directory under lib/<name>/, [package] name/lib/
files/mc/licence, [[permission]] empty (a library asks for nothing -- it runs inside the caller's
program, which is what packages.md § 3 already says of a library's permissions), versions starting
at the compiler's own minor, and the names registered by the owner first: float, io, sys,
i128, f16, prelude are not in dep_reserved's set (§ 0) and any account can take them today.
That is a registry action, not a compiler one, and it is the one item of § 9.2 worth doing before
0.16.0 ships (§ 13.4).
9.3 M51's <http> #
D13. M51 ships http as a registry package, never as a bundle row. The direction's first clause
-- "M51 não violaria se fosse lib, não bundle" -- is settled by § 2's predicate: lib/http.mc would
have a lib/ path and would therefore not be in the blob, so the question is only whether it is a
file of the mc package or a package of its own. It is a package of its own, because HTTP is not a
dependency of the compiler and its release cadence is not the compiler's -- which is the owner's rule
applied to the one library that has a consumer outside this repository. M51 is therefore the first
package of § 9.2, and it inherits § 9.2's two costs: bench/http/'s single-file programs
(forkka.mc 53 lines, fork1.mc 50, httpmin.mc 124) become a project with an mc.toml, and the
name http wants registering before the first tag. examples/api/lib/http.mc (253 lines) and
rt.mc (253) are where the shape comes from; bench/http/mc/forkka.mc is the shape M51 packages.
10. Acceptance (runnable; numbers, not adjectives) #
- Size, this host,
build/mc1 --exe src/mc.mc: 1 268 276 B (±0 -- it was measured, not projected),__DATA,__data566 832, blob 555 053, and__textexactly 540 192, byte for byte unchanged.size -min the commit. The blob holds 60 of 101 rows. - Nothing the compiler emits moves.
check-obj32/32 against the frozen seed;check-inertidentical on both roads for the 33 objects and all five taught examples; the--dump-asmdiff betweenmc1andmc2empty on both roads;check-lex/check-ast/check-asmat their current counts with no newseed-skip;git diff --stat stage0/empty. - Offline.
make checkgreen end to end with no network reachable, andcheck-pkg'scurl/wget/tarshim (exit 97 if invoked) unchanged and still not invoked. check-standalone:<mc/host>+<mc/core>+<user_default>still compiles to an objectcmp-identical tobuild/mc1 src/mc.mc, and the taught half still builds from<user_syntax_demo>and compiles<syntax_demo_test>to exit 42 -- now through root 2.- Every library leg unchanged in outcome:
check-float16/16 on macos/aarch64, linux/aarch64 and linux/x86_64 and 14/14 objects on each Windows leg, with the four llvm-mc sweeps at 60/60/249/232 distinct instructions and 0 mismatches;check-wideok;check-surface32/32 plus every M21/M24/M31/M41.5 case;test-linuxandtest-windowsat their counts;check-mcat its count with073-embed-bundleamong them. check-libroot(new): root 1 beats root 2 beats root 3; a partial tree serves what it has and falls through for what it does not; with all three absent, the D6 message is exact, exit 1.- The consumer, measured as a probe and not asserted: a program that says
#include <sys>and a taught compiler that says#include <user_float>both build from a release-shaped directory (mcbesidelib/mc/v<ver>/) with$HOMEunset and no network; and the object the taught compiler writes iscmp-identical to the one today'smcwrites (probe D). mc build --sync: with a[deps]row and nothing fetched,mc buildalone ismc: float 0.16.0 is not fetched+ therun:line, exit 2, and touches no network;mc build --syncfetches, writes the lock and builds. Asserted incheck-pkgagainst the directory registry it already builds.check-limits17/17 under 90%,globalsunchanged (every new field goes indp, § 12), andmc limits src/mc.mcin the commit.- Ten goldens rewritten once per step, each after its own criterion (D11);
make check-linux-hostRC 0 over all four cells with the cross proof. -
Docs with
check-docsgreen:bundle.md(the catalogue split in two, the new size table),packages.md§ 2 (the three roots),cli.md(--sync),diagnostics.md(the widened hint),bootstrap.md(what "the binary alone is the toolchain" means after M52),build.md,guide/00-getting-started.md,guide/80-footprint.md,plan.md,CLAUDE.md§ State; every
11. Steps -- gated commits #
| order | step | content | lines (src/ code, est.) | gain | gate |
|---|---|---|---|---|---|
| 1 | A the library root and the refusal — LANDED | D4's two extra roots in dp_mc_load; D6's widened dep_include_hint and its three states; D5's Makefile rule; scripts/release-assets.sh stages lib/mc/v<ver>/; scripts/check-libroot.sh; docs | 86 added, 37 code (est. ~40) | none by design -- the blob still has all 101 rows, so every name still resolves from it; the new road is proved by asking for a name the blob lacks | § 10.2, § 10.6, ten goldens once |
| 2 | B the cut — LANDED | § 2's predicate in tools/bundle.mc; make bundle; check-bundle's 60-row case; the check-pkg section; docs' catalogue split — plus the two roads a library name could not reach any more, § Implementation notes 4 and 5 | 52 code lines in src/ (est. ~0: see notes 4 and 5), 8 in tools/bundle.mc | −132 098 B measured, −9.4% of the binary and −18.0% of __DATA,__data, __text unchanged to the byte | all of § 10, ten goldens once |
| 3 | C mc build --sync — LANDED | D7: the sync step before the build, the two flags, and the pointer that keeps the driver free of the package half. The run: line is unchanged — see § Implementation notes -- step C, 5 | 79 added, 30 code (est. ~20) | the direction's "instaladas no momento do build", explicitly | § 10.8, check-pkg § 38, check-parts § 4c, ten goldens once |
| 4 | E the child's roots and the symlink — LANDED | D12's two follow-ups from the consumer's dry run: mc build forwards --libs-dir only when it was given one (a derived root made the child look for the entry's [deps] under the mc tree), and host_self_path() resolves a symlink on macOS — see § Implementation notes -- step E | 44 added / 35 removed, code +4 / -7 -- net 3 code lines REMOVED (deps.mc +14/-21, driver.mc +12/-10, host_macos.mc +18/-4) | a [compiler] project with [deps] builds with no flag; ~/bin/mc -> build/mc1 finds the tree | check-pkg § 39, check-libroot (h), ten goldens once |
| -- | D per-library packages, http first | § 9.2, § 9.3 | ~0 here; the moves and the registry rows | a project can pin a newer library than its compiler | deferred: M51 owns the first one |
Needed for the 0.16.0 cut: A and B. C is the direction's own clause and landed with them. D is M51's.
12. Files and estimated deltas #
| file | lines | what |
|---|---|---|
src/deps.mc | +40 (~30 code) | D4's roots 2 and 3 in dp_mc_load (host_self_path() + two lex_readable probes, the chosen root in DP_MCDIR as today); D6's three-state dep_include_hint. No new global -- the state is already dp's |
src/lex.mc | +6 | lex_include_name asks the hint whenever nothing answered, not only when bopen_fn == 0 |
src/driver.mc | +20 | D7's --sync |
src/core_build.mc | +1 | the usage line |
tools/bundle.mc | +15 | § 2's predicate, with the two exceptions named in a comment |
tools/bundle.list, mc.toml | 0 | § 2's last paragraph: the map and the package keep all 101/105 rows |
Makefile | +10 | D5's build/lib/mc/v<ver>/; check-libroot in check and .PHONY |
scripts/check-libroot.sh | ~90 | new |
scripts/check-bundle.sh | +25 | the 60-row case; every dropped name reachable through root 2 |
scripts/check-pkg.sh | +40 | the three roots, the D6 message, --sync against the directory registry |
scripts/check-docs.sh | +3 | the root for fences that include a library |
scripts/release-assets.sh | +30 | stage bundle.list + lib/ under lib/mc/v<ver>/ in both flavours' tarballs; INSTALL.txt |
.github/workflows/release.yml | +0 | the tarballs are built by the script the jobs already call |
docs/ | ~220 | § 10.11 |
src/bundle_data.mc, ten goldens | regenerated | twice, once per step (D11) |
Net new src/ code: about 55 lines. stage0/: 0. That number is the milestone's own argument:
the mechanism was built in M44 and this spec mostly reaches it.
13. Risks #
- The consumer. Measured at zero include rewrites (§ 7) and zero
[deps]lines. The residual is provisioning: if teko's CI copies themcbinary out of a tarball and discards the rest,<float>stops resolving. Named in the release note, andcheck-librootis the gate that keeps the three roots working. - "The binary alone is the toolchain" is weakened for library names. This is real and it is the
price of the direction: before M52 a copied binary compiled a float program; after it, the binary
plus the
lib/directory beside it does.docs/bootstrap.md's claim must be rewritten to say the compiler's own source is inside the binary -- which is still the strong claim, and the onecheck-standaloneproves. A user who wants the old behaviour hasmc installand a project has[deps] mc. - The library root is unverified. Step 3 hashes nothing (§ 0), so a tampered
lib/beside the binary is compiled without complaint, where the blob was inside a signed executable. Exactly as true ofmc-slimtoday, which shipped. The answer for a consumer who needs verification is the lock road --[deps]+mc.lock, hashed per file -- and a hash check on root 2/3 is priced as a follow-up (av<ver>.tomlcache manifest already exists formc install's road; the beside-the- binary road would need one written at release time). - Name squatting.
float,io,sys,i128,f16,prelude,httpare not reserved (dep_reserved, § 0) and § 9.2/§ 9.3 will want them. Registering them before 0.16.0 is a registry action with no compiler cost, and it is the cheapest item on this page. - Registry availability at build time is a risk only for
mc installon a slim binary and for D7's--sync.mc buildwithout the flag still never calls out (§ 5), so a registry outage cannot stop a build of a locked or vendored project. - CI minutes for per-package repositories -- deferred with § 9.2. One
publish-to-registryjob per package is the shape; M48 § 2.2's road-A table is the price. - The other 476 KB is code, and this milestone does not reach it. § 1.2 measures where it is
(
__text540 192, of which 63 912 is<mc/core_bundle>+<mc/core_sandbox>). If the owner wants the binary leaner than 1.27 MB the lever is the flavour, not the blob, and the question is whethermc sandboxbelongs in the default compiler -- M43's call, not M52's. - Two golden rewrites in one milestone (D11) read like a mistake in a log. Recorded in the
step table and in
CLAUDE.md§ State. - A future
src/file that should NOT be in the blob, or alib/file that should be, would make § 2's path rule wrong. The escape is the third column § 2 priced and refused; it is 10 lines whenever the first counter-example arrives. check-docsfences. Sixdocs/pages have ```mc fences that include a library name; if the root is not visible to the script, the gate fails loudly rather than silently, which is the behaviour wanted.
14. Decisions #
| decision | why | |
|---|---|---|
| D1 | A bundle row stays iff its path is under src/, plus prelude and user_default; the predicate lives in tools/bundle.mc; tools/bundle.list and mc.toml are unchanged | the owner's rule stated once and applied per row; the two exceptions are the compiler's own #includes (16 files and src/user.mc), and lz proves the rule is about the path and not the spelling |
| D2 | The libraries stay files of the mc package and are served by resolution step 3. M52 publishes no new registry package | the road exists and was proven end to end (§ 1.3); no #include spelling changes; a package would keep only ONE bare name and the single-file CLI has no lock road; 17 of the 41 rows are check fixtures |
| D3 | The libraries carry the compiler's version | they are files of <libs>/mc/v<mc_version()>/; no second series, no lockstep |
| D4 | dp_mc_load gains <exe>/lib/mc/v<ver>/ and <exe>/../lib/mc/v<ver>/ after <libs>, and a release ships that tree | keeps "untar and a float program compiles, offline"; <libs> first so an explicit mc install still wins; a partial tree is safe because step 3 falls through |
| D5 | make populates root 2 beside build/mc1 | every check script keeps working with no flag and no $HOME; this is why --libs-dir is NOT added to the single-file CLI |
| D6 | dep_include_hint is asked whenever nothing answered, and distinguishes three states | the thinned full binary must not say unknown bundled include and name no road; a misspelled name must keep the message the gates assert |
| D7 | mc build --sync is the only road from a build to the network; implicit newest-resolution is refused | determinism: an implicit resolution makes the object a function of the day; sync writes the lock and the lock is what a build reads |
| D8 | make check stays offline through root 2 -- no vendored deps/, no local registry for the libraries | one Makefile rule against 18 scripts of plumbing |
| D9 | No [deps] line and no respelling for teko; the cut is source-compatible | measured from teko's own grep (§ 7); the counterfactual under per-library packages is three respellings and a [deps] row |
| D10 | No deprecation window and no alias | nothing is renamed or removed; a deprecation message would fire on a spelling that is still correct. The release note carries the provisioning change instead |
| D11 | Ten goldens, rewritten once per step (twice over the milestone), each after its own criterion; mc_seed unaffected | steps A and B both change what goes into src/mc.mc |
| D12 | Both flavours survive and both ship the root; the ten -slim archives and --slim stay | after M52 they differ by exactly one sentence, and the slim tarball becomes offline-complete for the first time |
| D13 | M51's http is a registry package, the first of § 9.2 | HTTP is not a dependency of the compiler and its cadence is not the compiler's |
| D14 | mclib (M48.md § 2) is retired, not implemented | the mc package already carries lib/; a second package would add an index row, an asset, an admin registration and a second spelling for nothing (§ 9.1) |
15. The 0.16.0 cut #
The CUT is coordinated with teko; the design is not. What each side does:
| here, before the tag | steps A, B, C merged, make check and make check-linux-host green, the ten goldens re-recorded, docs/ and CLAUDE.md § State updated; the release note's provisioning paragraph (§ 13.2); the registry names of § 13.4 registered |
| here, at the tag | release.yml produces the ten archives as it does today, each now carrying lib/mc/v0.16.0/ beside the binary |
| teko, before the tag | nothing to change. A dry run of its own recipe against a 0.16.0 pre-release binary in a directory shaped like the tarball, which is the one thing that can go wrong (§ 13.1) |
| teko, after the tag | MC_VERSION to 0.16.0 in its own crumb, as it does for any minor |
| registry, before the tag | MC_VERSION raised from 0.15.20 to 0.16.0 (§ 16) -- a manifest naming [package].mc or a version constraint (= ~ ^ * >=) is refused by the older validator |
M52 is a minor and not a major because no source stops compiling: § 7's table is the evidence, and D10 is its consequence.
16. The registry side #
minicompiler/mc-registry carries docs/spec-M52-registry.md, written 2026-09-07 from an earlier
sketch of this milestone (a minicompiler/stdlib repository publishing a stdlib package in
lockstep with the compiler). Its opening was aligned to THIS spec on 2026-09-14
(minicompiler/mc-registry PR #29): the design of record is D2 above -- the libraries stay files
of the mc package, served from lib/mc/v<ver>/ beside the binary. No stdlib package and no
minicompiler/stdlib repository exist in 0.16.0; D14 retires mclib.
What the registry's step S0 (minicompiler/mc-registry PR #20) built stands under the new design:
pkg_reserved_exact gained stdlib and std, and jb_reserved_by_mc mirrors this compiler's
dep_reserved (mc, deps, build) independently, on the registry's own side of the wire. Both
are harmless under D2 -- nothing publishes either name -- and they keep the deferred per-library
road (§ 9.2, http first) open without a second reservation pass later.
Risk 4's names are being reserved on the registry as step S1 (minicompiler/mc-registry, in
flight as this section lands): float, io, sys, i128, u128, f16, prelude, http join
pkg_reserved_exact, so a public registration of any of them is refused while the admin road
still accepts one. The compiler's own dep_reserved (§ 0) is UNCHANGED by S1 -- a consumer still
has to be able to pin a future http package ([deps] http = "1.0.0"), and reserving a name only
against PUBLIC self-registration is exactly what that needs.
The cut checklist (§ 15, "here, before the tag") gains one operational row: the registry's own
pinned MC_VERSION (a Dockerfile ARG, 0.15.20 at the time of writing, kept in step across
six spellings by its own scripts/check-version.sh) has to be raised to 0.16.0 BEFORE any
consumer publishes a manifest that says [package].mc (this compiler at 0.15.27) or writes a
version constraint (= ~ ^ * >=, this compiler at 0.15.28) -- the 0.15.20 validator does not know
either spelling and refuses both.
Implementation notes -- step A #
Written after the fact, against the tree that landed. The design above is unchanged; what follows is what the code and the gates actually do, and the four places the text and the tree had to be reconciled.
- The three roots are one function,
dp_mc_root()insrc/deps.mc, anddp_mc_loadcalls it where it used to build<libs>'s path inline. A root is recognised by itsbundle.listand by nothing else: the first root that HAS that file is the only one read, so a<libs>tree that exists but is empty does not silently fall through to the one beside the binary and hand a reader a library he did not install. Roots 2 and 3 come frompath_join(host_self_path(), ...)--path_joincuts its base at the last slash, which is exactly "the directory of this file", and normalises the..of root 3 lexically.
- A self path with no slash in it is refused.
host_self_path()is_NSGetExecutablePath/readlink /proc/self/exe/GetModuleFileNameAand all three normally answer a path with a directory in it, butpath_joinwith a base that has none answers a RELATIVE path, which would make the root a function of the working directory. Four lines say no instead (docs/determinism.md: one source, one answer, wherevermcwas run from).
- D6 reads
bopen_fn.dep_include_hinthas to tell "no root and no blob" (mc-slim, the unchanged sentence) from "no root and a blob" (the new one), and whether this binary carries a blob is one global insrc/lex.mc, set bymc_bundle_init().src/deps.mcalready callslex_readable, and it is compiled aftersrc/lex.mcin every assembly that contains it (<mc/core_build>stands on<mc/core_min>), so the read costs nothing and no argument was added to the hook.
scripts/libroot.shis the one definition of the tree and both theMakefileandscripts/release-assets.shcall it. Writing the copy twice was the alternative and it is the one drift nobody would notice: a release whose library root is laid differently from the one every gate runs against is a release nothing tested.
scripts/check-standalone.shcase 5 changed, and that is D6 working rather than a regression. That script copies the binary into an empty directory and runs it there -- no tree beside it, andHOMEis now an empty directory too, on purpose, so the answer does not depend on whoever ranmc installon the machine. It is therefore exactly state 1 of § 4's table, and it now asserts the sentence that names the road. The middle sentence,unknown bundled include, is asserted twice inscripts/check-pkg.sh(both with the full binary inbuild/, which has root 2 beside it) and once more inscripts/check-libroot.shcase (e).
- The gate proves which road answered, with a name the blob does not have. In step A the blob
still carries all 101 rows, so any real name resolves at step 2 and a test that included
<sys>would have passed with the roots deleted. Every positive case inscripts/check-libroot.shtherefore adds one row to the tree under test --m52probe, three lines of mc returning 42 -- and compiles, links and RUNS a program that calls it. Case (d) gives the two trees different bodies (42 and 7) and reads the precedence off the exit code.
- Measured teeth. Run against a
build/mc1built frommain(the commit this branch forked from),scripts/check-libroot.shreports 3/7 with four failures -- the refusal, root 2, root 3 and the unpacked tarball, eachunknown bundled include: m52probe-- and exit 1. The three that pass are root 1 (which M44 already implemented), the middle message and the tarball's file count (scripts/release-assets.shis this branch's).
- Deviation, on record: case (f) compiles
<m52probe>and not<float>. § 10.7 asks for a<float>program out of a release-shaped directory;lib/float.mcis a compiler MODULE (type_new,intrinsic, asyntax_lithandler) and compiling it as a program iscall to unknown function, so it needs a taught compiler built first -- minutes, in a gate that runs in seconds. What case (f) does instead is stronger about the root and weaker about the library: it untars the archivescripts/release-assets.shwrites, asserts thatlib/mc/v<ver>/came out of it withbundle.listand all 41 library files, adds the probe row to the UNPACKED copy and compiles a program through it with an empty$HOME, no--libs-dirand no network. Step B, where<float>LEAVES the blob, is where the literal form of § 10.7 becomes measurable, andscripts/check-float.shis the gate that will see it.
- What did not need a line.
lex_include_name's three steps,libs_open,dp_mc_open,mc install, the cache manifest and the closure rule are all untouched: a root is a root whichever directory it was found in, and the only thing M52 step A changed about step 3 is WHERE the search starts.
Implementation notes -- step B #
Written after the fact. The predicate and the sizes are the design's; what follows is the two places the design did not look, and the measurements that settled everything else.
- The predicate is
bl_in_blob+bl_cutintools/bundle.mc(~45 lines with the comment, 8 of code).bl_verifystill runs over all 101 rows -- sorted, unique, nomc/bundle_data, no two sharing a last component -- and only then doesbl_cutcompact the arrays to the 60 the predicate admits. So the manifest's reviewability invariants keep covering the whole map, which is what the map has to be: a row that left the blob is still the only thing that turns<sys>intolib/sys.mcfor the tree.bl_reportgained one line (if (pad < 1) pad = 1;): a name past the 20th column used to be glued to its byte count, andcheck-bundleparses that report now.
scripts/libroot.shstages every row whose path is not undersrc/-- 43 files: the 41 the blob lost, pluspreludeanduser_default, which are lib/ paths and ride along for free (the blob answers them first and the copies are never read). It gained a--libs DIRmode that lays the same tree atDIR/mc/v<ver>/instead ofDIR/lib/mc/v<ver>/-- that is resolution root 1, whatmc install --libs-dir DIRwrites, and three gates need a<libs>that is whole and not just a directory of packages.
check-bundlecompares the generator against the manifest, then the tree against the compiler. The first half is adiffof the 60 namestools/bundle.mcreported against the predicate restated overtools/bundle.listinawk, so a row that moved in either direction is a named diff; the<mc/bundle_data>shape check'sINTcount comes from the same helper (240 values, four per blob row, not per manifest row). The second half offers each of the other 41 names to the real compiler, which has root 2 beside it, and requires that neither refusal appears -- most of those files are compiler modules that mean nothing on their own, so resolving and compiling are different questions and only the first is asked. Then the same compiler copied into an empty directory refuses<sys>with the sentence that namesmc install: the literal form of § 10.7, which step A could only prove with a synthetic row.
- Deviation:
src/was touched, and the cut is why.mc buildwith a[compiler]section writes a taught compiler into the project's ownbuild/directory and spawns it there, so roots 2 and 3 -- both relative tohost_self_path()-- are invisible to it. Measured on the unmodified tree:mc build examples/desktop --config ui.tomlansweredmain.ui:16: #include <sys>: not in this compiler and mc 0.0.0-dev's library tree was not found: run mc install, with the tree sitting besidebuild/mc1all along. That is a regression for every user with a[compiler]and a library include, not a test-plumbing problem, so it is fixed at the root:deps_libs_for_child()(src/deps.mc) answers the<libs>a child must be told about -- 0 when--libs-dirwas given (already forwarded) and 0 when root 1 answered (the child inheritsHOME), else the tree beside THIS binary -- anddrv_teachpasses it as--libs-dir.dp_mc_rootwas refactored intodp_beside_libs+dp_root_inso that "beside the binary" has one definition. 22 added code lines (src/deps.mc18,src/driver.mc4), no new global (mc limits src/mc.mcreportsglobals 446/512before and after). The two-step road --mc build --compiler-only, then that compiler with--entry-only-- forwards nothing by construction, and there the caller names--libs-diritself:scripts/check-opt.shandscripts/check-inert.shdo, anddocs/build.md§ M52 says so for a user's own script.
- Deviation: the sandbox had to bring the tree into the box. With the cut,
make test-sandboxcame back 51 ok, 22 failed: every case whose program says#include <sys>(refused: syscall 78 (readlinkat), the compiler asking where its own binary is). Inside the box neither beside-the-binary root can work --/mcis a single bound FILE and the box has no/proc-- so the box mounts the tree where<libs>looks:HOMEis/src, hence/src/.mc/libs/mc/v<ver>/, bound read-only on the overlay's upper layer, granted read-only by Landlock (a mount of its own, so the/srcrule does not reach it), with the host path resolved before the unshare indp_mc_root's order and without naming one of its functions --src/sandbox.mcis<mc/core_sandbox>, whichcheck-partsbuilds on<mc/core_min>alone. 30 added code lines (src/sandbox.mc20,src/sandbox_box.mc9,src/seccomp.mc1), one appended field insb_state(SB_LIB, so no offset moved), no new global. On the overlay fallback road/srcis a read-only bind and there is nowhere to make the mount point: the field is cleared and a library name is refused with the sentence that namesmc install, which is the truth about that box. Result: 73 ok, 0 failed, 1 skipped.readlinkatalso joined the four compile profiles -- measured withsh scripts/sandbox-trace.sh --unionon Ubuntu 26.04 (aarch64, glibc 2.43, the Lima cell) and on Alpine 3 (aarch64, musl), both of which answerreadlinkat; the trace runs OUTSIDE the box, where the compiler does take that road. The two x86-64 rows are the same name, unmeasured from this Mac (stracedecodes nothing under its amd64 emulation), and the one residual is on record indocs/reference/sandbox.md§ The profiles: musl on x86-64 implementsreadlink()with thereadlinksyscall, which noSN_*name covers, so on such a host a box with no tree reportsrefused: syscall 89 (readlink)instead of the sentence -- one--unionrun there fixes it, and it changes nothing for a box that has a tree.
check-docsneeded no line, against the step table's guess: the fences are compiled bybuild/mc1, which has root 2 beside it, and thetaught=DIRfences go throughmc build, which forwards it (note 4). 52 samples, 514 links, green with no edit.
check-pkggrew a section and lost an assumption.--libs-dirREPLACES<libs>, so every hand-laid fixture<libs>is now laid whole (libroot.sh --libs, once each, at the top of the script): without it the taught compilers oftests/pkg/appandapp-float, which both say#include <sys>, have no road to a library at all. Two assertions moved with it, and both are sharper for it: § 15's bundle-less probe wants a<libs>with nothing in it and now names$tmp/none, because a probe that found a partial tree would getunknown bundled include(the middle state of D6) instead of the slim sentence; and § 18's "nothing was fetched" test now says "nothing but themctree", which is what it always meant. The new § 37 is the D6 table on a real name: roots 2 and 3 each compile, link and RUN a program that says#include <sys>(exit 42,libon stdout), and with neither root the refusal is exact. 186/186.
check-standaloneis where the scope of "alone" is now written down. The copied binary gets the library tree beside it, exactly as a release tarball carries it, so cases 1-3 (<sys>+<prelude>, a taught compiler from<mc/core>+<user_syntax_demo>,<syntax_demo_test>) pass through root 2; case 4 --<mc/host>+<mc/core>+<user_default>compiled to an objectcmp-identical tosrc/mc.mc's -- is untouched, which is the statement that matters: the compiler's own source is inside the binary. Case 5 needed a compiler with no tree anywhere near it and runsalone/bin/mc, two levels down, so that neither root 2 (its own directory) nor root 3 (one above) finds the tree in$tmp.
-
Measured sizes. The cut in isolation -- the predicate and nothing else, against the tree this branch forked from:
binary __text__cstring__DATA,__datablob all 101 rows 1 400 552 540 924 31 111 692 576 678 368 60 rows 1 268 454 540 924 31 111 567 952 556 168 −132 098 B, −9.4% and −18.0% of
__DATA,__data, with__textand__cstringbyte-identical -- § 1.2 predicted −140 300 against a tree that has grown since, and the shape is exactly what it said. The branch's own final binary is 1 285 334 B (__text542 272, +1 348 for the nine new functions of notes 4 and 5;__DATA,__data570 352; blob 558 552).
- Nothing the compiler emits moved.
scripts/check-inert.shagainst abuild/mc1built from the fork point: 33 objects identical on the plain road and 33 with--opt=1(tests/*.mcandsrc/mc.mc), and byte-identical artefacts forexamples/api,lang,conc,desktopandkernelthrough the taught compiler each side builds. The compiler's OWN body is confined too: compiling the fork point'ssrc/mc.mcand this tree's with the same compiler gives 9 added function labels, 0 removed, and 12 of 1935 shared functions differing -- the six that were edited (dp_mc_root,drv_teach,sb_go,sb_build_tree,sb_landlock_apply,sb_rec) and the sixbundle_*readers, whose immediates are the blob's own count going from 101 to 60.
- A foreign host needed one more line, and
make check-linux-hostis what found it. A Linux or Windows cell starts from abuild/that holds the compiler and nothing else --scripts/check-linux-host.shuntars the checkout EXCLUDINGbuild/, and a CI leg links the object it was handed -- so the tree has to be laid there before the suite runs: 12 FAILs per cell, all of themtests/mc/09[7-9]/10[01](the M49 tests, the only ones in that corpus that say#include <sys>).librootis now the first prerequisite of the Linux and Windowschecksubsets, andscripts/bootstrap-linux.sh/scripts/bootstrap-windows.shlay it themselves right before the suite -- which also covers the gnu cell and CI, neither of which goes throughmake checkfor the chain. The chain above it needs nothing:src/mc_linux.mcandsrc/mc_windows.mchave relative includes only.
- Roots 2 and 3 never worked on a Windows host, and the cut is what exposed it.
host_self_path()there isGetModuleFileNameA, which answersD:\a\mc\mc\build\mc.exe-- and every path function in this compiler cuts on/alone (path_join,path_norm,dep_in), so a backslash path has no directory as far as they are concerned and step A's own no-slash guard refused it. While the blob carried every name nothing ever asked, so the two CI legs were green through step A; with the cut they answered#include <backend_arm64>: not in this compiler and mc 0.0.0-dev's library tree was not foundfor all 41 names, with the tree beside the binary.src/host_windows.mcnow hands back one spelling -- forward slashes, which Win32 takes in every call -- so the whole compiler reads one kind of path andmc upgrade/mc tool, the other two readers, get it too. The loop is the coreloop/breakform and notwhile: that file is compiled on its own bycheck-asm, with no<prelude>in scope.
- Two more places a fresh checkout needed the tree, both found by CI and neither reachable
from a developer's machine, where
build/is never empty:check-bundlenames the FILE targetbuild/mc1while$(LIBROOT)hung off the PHONYmc1, so on a cleanbuild/the tree was never laid (it is an order-only prerequisite ofbuild/mc1now -- a newerbundle.listre-lays the tree without relinking the compiler); and the two sandbox CI jobs download the compilers intobuild/and run neithermake checknor a bootstrap script, soscripts/ci-sandbox-cell.shlays it before the cell.
readlinkis notreadlinkat, and only the CI cell could say so. § 5's residual -- "musl on x86-64 implementsreadlink()with thereadlinksyscall, which noSN_*name covers" -- was measured on the sandboxlinux/x86_64job and is wider than it was written: glibc 2.39 on x86-64 usesreadlinktoo, sosh scripts/sandbox-trace.sh --checkthere failed withsrc/sysno.mc has no index for: SN_READLINK.SN_READLINKis index 95 now (SN_COUNT96), 89 on x86-64 andSN_ABSENTon AArch64, which has no such syscall at all; both x86-64 compile lists carry the row besidereadlinkat, and the AArch64 pair carryreadlinkatalone -- which is exactly what each host's own trace records. The residual is gone: a box with no tree now reports the sentence that namesmc installon every cell.
Implementation notes -- step C #
Written after the fact. The flag is the design's; what follows is the one place the design did not
look, the one number pkg_sync could not answer on its own, and what the gates measure.
- The driver may not NAME
pkg_sync.src/driver.mcis<mc/core_build>andsrc/pkg.mcis<mc/core_pkg>, andcheck-parts§ 1b requires each part to compile on<mc/core_min>alone -- so a direct call would have made the driver drag the fetcher, the registry and the lock writer into every compiler that hasmc build, which is exactly the splitdocs/reference/packages.md§ 9 rests on ("a compiler assembled without<mc/core_pkg>cannot download even in principle"). The sync step therefore arrives as a pointer,drv_set_sync(&pkg_sync_for_build)frommc_pkg_init()-- the shapelex_set_bundleandlex_set_libsalready have. It is a field of the driver's own record (DRV_SYNCFN), not a global, so the globals row did not move. Measured:--dump-symsof a compiler assembled from<mc/core_min>+<mc/core_machines>+<mc/core_writers>+<mc/core_build>has_drv_buildand no_pkg_sync, andmc build DIR --syncon it ismc: --sync needs the package half of this compiler: mc pkg is not in it, exit 1 (scripts/check-parts.sh§ 4c).
pkg_syncreturns 0 for two different outcomes, and a build has to tell them apart. Formc pkg sync"the lock is written" and "I printed a plan and did not run it" are the same answer -- it said what it would do -- and both are exit 0. For a build they are not: in the second case the lock was not rewritten, so the build would either read a stale one or refuse. The wrapperpkg_sync_for_build(dir, cfg, yes)is what adds that one bit, and it adds nothing else:pkg_open_config+pk_set_yes+ the samepkg_sync, thenif (!pk_yes() && (pk_nplan() > 0 || pkg_perm_ask())) return -1;.-1is "stop, exit 0": the plan is on the screen and nothing else happened. The permission half is not decoration -- a build that walked past an unaccepted[[permission]]set would be a second road around the consentmc pkg syncasks for.
- The flag is not forwarded to the
--entry-onlychild, and that is by construction rather than by a line.drv_teachwrites the child's argv name by name (build, the directory,--config, the config,--entry-only, then--libs-dir/--opt=/--limitswhen they apply), so a flag it does not mention cannot travel. The gate measures the consequence instead of the code:tests/pkg/sync/teach.tomlis the same project with a[compiler]section, andmc build --sync --yeson it prints onelockline and twofetchrows, not two and four. A second sync in the child would have rewritten the lock a second time and said so.
mc buildhas no--registry, and does not get one. The registry is[registry].urlor the default. A build whose answer depended on a registry named in the shell line would be a build two people cannot reproduce from the same checkout, which is the same argument § 5 used to refuse implicit resolution.scripts/check-pkg.sh§ 38 names its fixture registry bysed-ing a[registry]table into a copy of the project's config, which is what a private tap looks like in real life.
- The
run:line did not change, against the step table's "therun:line it makes unnecessary" read as a licence to rewrite it.mc: mc.lock is staleandmc: <pack> <ver> is not fetchedstill sayrun: mc pkg sync --yes, for two reasons: the refusal is raised bysrc/deps.mc, which is<mc/core_build>and has no business naming a flag that needs the other part; and the advice is right on every road --mc pkg sync --yesworks whether or not the reader is inside amc build. What--syncmakes unnecessary is the second COMMAND, not the sentence.check-mcandcheck-pkgassert that text and neither moved.
--yeson its own is refused, not ignored (mc: --yes applies to --sync: mc build downloads nothing without it) -- the post-M42 rule for a flag read by nobody. Both flags live in ONE record field,DRV_SYNC(0, 1, 2), sodrv_runneeded no new parameter and its three callers (drv_build,drv_limits,sysroot_cmd) were not touched;mc limitsandmc sysroot stubparse no--syncand read 0.
- A part-less compiler still ADVERTISES the flag. The usage line is
subcommand("build", ...)'s, registered by<mc/core_build>: a subcommand disappears with its part (M43 § 4b,mc sandbox), a flag inside one cannot. The refusal is what tells the truth, and it names the missing half. Re-registeringbuildfrommc_pkg_init()with a longer usage string was the alternative and it puts the same text in two files -- the one drift nobody would notice.
- A no-op sync stays a sync.
mc build --syncon a project with no[deps]and no[tools]issync: no dependencies, an emptymc.lock, and the build: that is whatpkg_syncdoes there, and making the build skip the step would have been a second behaviour to remember.
- Cost: 79 added lines in
src/, 30 of them neither comment nor blank (src/driver.mc+48/-1,src/pkg.mc+25,src/core_pkg.mc+5,src/core_build.mc+1/-1), against the step table's ~20. Zero new globals:mc limits src/mc.mcreports the sameglobalsrow before and after. Nothing the compiler emits moved --scripts/check-inert.shagainst the fork point is 33 objects identical on the plain road and 33 with--opt=1, plus the five taught examples.
Implementation notes -- step D (the taught compiler's tree) #
Written after the fact, from the consumer's measurement of 2026-09-14 (§ 7, D12).
- The gap reproduced here, in both shapes, before a line was written. With
build/mc1ofmain21d081b:examples/avr's product (core = "<mc/core_min>", no blob) answered#include <sys>: not bundled in this compiler and mc 0.0.0-dev is not installed: run mc install, andexamples/lang's (the default<mc/core>, with a blob) answered#include <sys>: not in this compiler and mc 0.0.0-dev's library tree was not found: run mc install. Both exit 1, run standalone with an empty$HOMEand no--libs-dir. The consumer's own message,unknown bundled include: sys, is the third: a product that has<mc/core_build>and therefore a resolution road, with no tree at the end of it.
make check-langpasses today for a different reason than adjacency, and that was checked rather than assumed.examples/lang/build/mc-langis nowhere near the repository'sbuild/lib/; what saves it is thatgrep '#include <' examples/lang/*.lx lib/*.lx *.mcfinds no library name at all -- the only bundled include in the directory is<mc/core>in a comment. It never asks step 3 a question.
- The staging is
drv_stage_libroot(drv_path(cbin))indrv_teach, one call for both roads (the--compiler-onlyhalf returns after it). The source is the root the RUNNINGmcresolved for itself, read off the mapdp_mc_loadalready parses and caches --src/deps.mcis included beforesrc/driver.mcin<mc/core_build>, so the driver callsdp_mc_load()anddp_state()and readsDP_MCDIR/DP_MCN/DP_MCPATHthrough that file's own#defines. The alternative,bundle_readof eachlib/row, is not open to the driver: it is the same part split step C's note 1 records --src/driver.mcis<mc/core_build>andbundle_readis<mc/core_bundle>, so it would need a fourth function pointer. And after step B it would answer almost nothing: the blob keepssrc/*pluspreludeanduser_default, so a binary with no root has no library to stage.
3b. The whole thing is ONE function, and the frozen seed is the reason. The first draft was
five -- three readers in src/deps.mc and a drv_stage_file helper -- and it turned
make check red in a place nothing else in this branch touches: scripts/check-asm.sh
compiles lib/mc_i128.mc and lib/mc_u128.mc with build/mc0, and those are
#include "../src/core.mc" plus a module, i.e. the whole compiler in one translation unit.
mc limits puts them at 2044 of the seed's MAXFUNCS 2048 on main; five more is 2049
and build/mc0 answers mc: too many functions, exit 1, 176/178 files identical.
stage0/ is frozen, so the budget is what had to give: one function is 2045, three of
headroom. Worth recording for whoever writes the next src/ milestone -- that ceiling, and
not check-limits' own rows (which measure src/mc_seed.mc), is the tight one.
- The rule is
scripts/libroot.sh's, restated in one loop:bundle.listplus every row whose path is not undersrc/, and only the files that are there. Writing a different rule in the compiler than the one the release tarball is laid with is step A note 4's drift with the two sides swapped. Measured: 44 files, the same count and the same bytessh scripts/libroot.shwrites.
- Idempotence is a byte compare, not a timestamp. The loop reads the destination and does
not write when the lengths and the bytes match, so a second
mc buildrewrites nothing -- proved with a marker file andfind -newer: 0 of 44. A timestamp comparison would have been wrong on a checkout, where every source is newer than every artefact.
deps_libs_for_childis kept, and the reason is worth a line: with the staging in place the spawned child would find its own root 2 anyway, but the parent's--libs-dirstill wins (root 1 beats root 2, D4) and it is what makes the child's resolution order identical to the parent's on the road where the two could differ -- a<libs>the parent read and the child's freshly staged copy are the same bytes, but only one of them is what the parent actually compiled against. It is also why the staging changes no artefact: on themc buildroad the staged tree is never read.
- The gate is
scripts/check-libroot.shcase (g), which is where the other roots are proved. Its fixture is the consumer's shape reduced to what a gate can run in seconds:core = "<mc/core_min>"and a module that isexamples/avr/mc-avr.mcminus the AVR --<mc/core_machines>,<mc/core_writers>,<mc/core_build>, amain()and an emptyuser_init(). No<mc/core_bundle>, so the product has no blob and a successful#include <sys>is road 3 through the staged tree and nothing else. Three assertions: the tree is there (44 files), the product run standalone compiles and RUNS the program (exit 42) with an empty$HOMEand no--libs-dir, and a second build rewrites 0 files.<mc/core_build>is in that list for a measured reason: without itmc_build_init()never runs,lex_set_libsis never called, and the product has no road 3 to reach a tree with -- the first draft of the fixture left it out and answeredunknown bundled include: syswith the tree sitting beside it.
- Measured teeth. Against
build/mc1ofmain21d081b,scripts/check-libroot.shreports 7/10 with three failures -- the staged tree, the standalone product and the idempotence case, which refuses to pass vacuously when nothing was staged -- and exit 1. With this branch it is 10/10.
- Cost: 67 added lines in
src/, 34 of them neither comment nor blank, all insrc/driver.mc, and one new function. Zero new globals --check-limitsreports the sameglobalsrow before and after. Nothing the compiler emits moved: the staging writes files BESIDE the product and not into it, andscripts/check-inert.shagainstmain21d081b is 33 objects identical on the plain road, 33 with--opt=1, and the five taught examples.
Implementation notes -- step E (the child's roots and the symlinked binary) #
Written after the fact, from the consumer's dry run of 2026-09-14 against main d18930d. Two
defects, both real, both reproduced before a line was written.
deps_libs_for_childturned themctree into the whole package root in the child. Step B added it for the road step D had not built yet: with no--libs-dirand nothing under$HOME/.mc/libs, the parent handed the spawned taught compiler--libs-dir <dir of mc>/lib. But<libs>serves two roles -- themclibrary tree under<libs>/mc/v<ver>/AND the installed packages under<libs>/<pack>/v<ver>/-- anddeps_libs_root()is whatdep_resolvereads for the second. So a[compiler]project whose ENTRY has[deps]died in the child. Reproduced ontests/pkg/app(which has both a[compiler]and three[deps]), packages installed under a scratchHOME, no--libs-dir:compiler build/mc-app.mc -> build/mc-app,compile main.mc -> build/app, thenmc: geo 1.2.0 is not fetched, exit 1 -- the consumer's ownteko 0.11.0 is not fetchedwith a different name.mc build . --libs-dir $HOME/.mc/libsworked, which is the shape that made it look like a flag was missing. Fixed at the root: the parent forwards--libs-dironly when it was given one (dp_libs_opt), never a derived one, anddeps_libs_for_childis DELETED -- since step D the child has a root 2 of its own and needs no derived answer. The order was checked rather than assumed:drv_stage_libroot(drv_path(cbin))is called indrv_teachbeforedrv_finishand before thedrv_spawn, so the tree is beside the child when the child runs. No case remains where the child would have a tree the parent did not stage:drv_stage_librootstages from the root the runningmcresolved by roots 1/2/3, and if it found none there is no library for either of them. Cost:src/deps.mc+14/-21 (the function and its comment, replaced by a note) andsrc/driver.mc+12/-10 -- 6 lines of code removed and none added.
host_self_path()answered the symlink's path on macOS.~/bin/mc -> .../build/mc1is how a developer puts one compiler onPATH, and_NSGetExecutablePathanswers the path as invoked, so roots 2 and 3 were computed from~/bin/and the tree beside the real binary was invisible:#include <sys>: not in this compiler and mc 0.0.0-dev's library tree was not found: run mc install, exit 1, measured.src/host_macos.mcpasses the answer throughrealpath(3)(libSystem, bufferxalloced, so no global) and keeps the raw path when that fails. The other two hosts need nothing and that was read, not assumed:readlink("/proc/self/exe")returns the target the kernel resolved, andGetModuleFileNameAanswers the module's own path. The doc row in../reference/hooks.md§ 6 said the opposite in prose ("the answer is not resolved any further") and is corrected with the code. One observable consequence, and it is the fix working:mc upgradewith no--toprints the RESOLVED path on itsintoline, so it replaces the compiler and not the link.scripts/check-pkg.sh§ 33i asserted that line with$tmpspelled out, and$TMPDIRon macOS is/var/folders/..., a symlink to/private/var-- the case now asserts the suffix and prints the whole line, which is where the/private/var/...in the run's output comes from.
- The gates.
scripts/check-pkg.sh§ 39:mc pkg sync --yeswith no--libs-dirand a scratchHOMEwrites$HOME/.mc/libs, thenmc buildon the[compiler]config with no flag at all builds and the program runs (exit 42,plot 110) -- which proves both halves in one command, since the entry also says#include <sys>and therefore needs the staged tree; then the same config with an explicit--libs-dirstill builds and runs, so the forwarded flag still wins.scripts/check-libroot.shcase (h): a symlink in a directory with nolib/of its own compiles#include <sys>-- a REAL name, becausem52probeexists only in the trees that script lays and would say nothing about the tree besidebuild/mc1.