M47 S5 (compiler side) — minicompiler/mc as the first registered package
The registry's step 5 (minicompiler/mc's spec § 5) says the first packages the
server validates and serves are the compiler's own. This is the half that lives
here: the root mc.toml, the release job that announces a tag, what was
measured while wiring it, and the price of the second half — the standalone
libraries of lib/ as packages of their own — which is priced and not built.
Everything below was measured against build/mc1 on this branch, macOS/arm64.
1. What shipped #
1.1 mc.toml at the repository root #
[package]
name = "mc"
lib = "src/core.mc"
check = ["src/mc_linux_x86_64.mc"]
files = [ … ]
name = "mc"— M44's amendment D15': the packagemcis the whole bundle at the compiler's version. It is the one name the registry's reserved rule admits, and only from this repository.lib = "src/core.mc"— what a bare#include <mc>means.src/core.mcis the compiler withoutuser_init: it is what[compiler].coredefaults to (<mc/core>), what every taught compiler in this tree includes, and the only file whose name is the whole product. It reaches the rest of the tree with quoted relative includes, so a package tree serves it exactly as the repository does. The alternatives weresrc/mc.mc(which pins the host to macOS and definesmain, so a consumer could not add auser_init) andsrc/core_min.mc(a part, not the product).check— the translation units the registry compiles, one box each (§ 5.1 below). Added after § 3.3's recommendation was adopted there.-
files—cut -f2 tools/bundle.list | LC_ALL=C sort -u, plus four files that list cannot name:src/bundle_data.mc, the blob. The bundle cannot contain itself, so it has nobundle.listrow;src/core_bundle.mcincludes it by path. Without it infilesthe closure rule refuses the build withmcpkg/src/bundle_data.mc:1: not declared in mcpkg's [package].files— measured, § 2.2.tools/bundle.list, theNAME<TAB>PATHmap.#include <name>served from an installed tree (packages.md § 2, step 3) reads it.src/mc_linux_x86_64.mc, the unitchecknames, andsrc/user.mc, the one line it includes that no bundle row carries: an entry point and an extension point are files a compiler is BUILT from, not names it serves. Acheckentry has to be infiles— the tree hash coversfilesand nothing else, so a unit the package does not ship is one no consumer, and no validator, would receive (§ 5.1).
97 entries, in byte order: under a UTF-8 locale macOS collates
_before., solib/float_rt.mcsorts abovelib/float.mcand a developer withLANGunset and a CI runner with it set disagree about an order the tree hash is taken over.scripts/check-pkg.sh§ 30a regenerates the array underLC_ALL=Cand fails on drift, so a new bundle entry that is not added here is a redmake check. (Found by CI, which is the only macOS in this loop with a locale set.) - No
[project].makebuilds this repository, notmc build; the five real project configs aresrc/mc.<target>.toml, one per cross target. A[project]here would also make the registry's validator run the compiler with no arguments inside the box, which prints usage and exits 1 (measured).mc build .at the root is thereforemc.toml: missing key: project.entry(exit 1, measured), on purpose; nothing in the repository or inscripts/reads a config at the root (grepped).
1.2 release.yml: publish-to-registry #
One job after publish, minicompiler/register-action@v1, no secret, no
checkout, permissions: contents: read, timeout-minutes: 20,
continue-on-error: false. It is needs: publish and not a step inside it
because § 13b makes a GitHub Release the thing the validator looks for, and
because a refused validation must not be able to unmake a published release —
a rerun of the job alone is the retry.
Until a person registers minicompiler/mc on https://minicompiler.dev/me the
registry answers 404 not registered and this job is red while everything
above it is green. That is the intended first-run state, not a defect.
1.3 One compiler fix, and one script fix #
dep_underrefused every entry when the package directory was..path_normanswers"."for a directory with no segments (.,./,a/..), andpath_jointhen drops that segment from the result — so the join of a contained entry does not begin with"./"and the prefix test could never match.mc pkg hash .wasmc: ./: files entry escapes the package: <first entry>, exit 2, for any package at all: reproduced ontests/pkg/src/mathx-1.0.0before anything in this branch existed.src/deps.mc, +8 lines of code: when the normalised base is".", containment is whatdep_rel_okalready answered plus "the join did not become absolute". The four escaping shapes are still refused with.as the base (check-pkg§ 30e) and every other base behaves exactly as before —mc pkg hash <abs>of every fixture is byte for byte the hash it was.scripts/pkg-hash.shcould not read a multi-linefilesarray. Its own comment said so; the fixtures all write one line and the root manifest cannot.files_ofnow takes the firstfiles = [through the]that closes it, in either shape, and extracts the quoted element of each line. The thirteen fixture hashes are unchanged.
2. What was measured #
2.1 The tree hash #
$ build/mc1 pkg hash .
930d9e1c…29eab
$ sh scripts/pkg-hash.sh .
930d9e1c…29eab
The two implementations agree on the root package, which is what check-pkg
§ 30c asserts on every run. (The value moves with every commit that touches a
listed file; it is the hash of this tree, not a constant.)
2.2 A consumer, end to end #
A project outside this repository, with the tree vendored at deps/mcpkg/ and
an mc.lock carrying the hash:
[project]
entry = "main.mc"
out = "build/app"
kind = "exe"
[deps]
mcpkg = "0.0.0-dev"
#include <mcpkg/src/lz.mc> // a bundle file, through the package road
#include <mcpkg/src/host_macos.mc>
#include <mcpkg> // the lib = src/core.mc, which carries main()
#include <mcpkg/lib/user_default.mc>
mc build . produces a 1 153 024-byte compiler; it answers
mc 0.0.0-dev and os macos / arch aarch64 / sys sys; and the object it writes
for src/mc.mc is byte for byte the one build/mc1 writes (1 269 016
bytes). Deleting src/bundle_data.mc from the manifest turns the same build
into mcpkg/src/bundle_data.mc:1: not declared in mcpkg's [package].files, so
the files boundary is doing its job on this tree.
The name is mcpkg and not mc because of § 3.1 below.
3. Findings for the registry (do not fix here) #
Three things stop minicompiler/mc from being validated and consumed as the
package mc today. None of them is in the registry's code; two are in this
compiler and are the owner's call, one is in the registry's § 4.2.
3.1 The compiler's reserved rule refuses the name mc on every road #
dep_reserved (src/deps.mc) refuses mc, deps and build in [deps], in
[replace], in mc pkg add and in mc pkg check's reading of an index file.
Measured, verbatim:
$ mc build . # [deps] mc = "0.0.0-dev"
mc.toml:7:6: reserved package name: deps.mc
$ mc build . # [replace] mc = "…" alone
mc.toml:7:6: reserved package name: replace.mc
$ mc pkg check reg/index/mc.toml --libs-dir …
reg/index/mc.toml:2:15: reserved package name: package.name
The last one is the registry's own gate: docs/reference/packages.md § 10 calls
mc pkg check "what the registry's CI runs on every changed index file", and it
cannot be run on the index file this repository would produce.
The reason the rule exists is written down (src/deps.mc, packages.md § 6):
<mc/core> is the source of the compiler that is running, and a taught
compiler assembled from a foreign one would not be the compiler that built it.
That argument is about <mc/...> resolution, and it is untouched by
publishing a package named mc: the road a consumer of the published package
takes is § 2 step 3, the installed tree at <libs>/mc/v<version>/, which
dp_mc_open serves and which dep_reserved has nothing to do with.
Three ways out, for the owner:
- Leave it. The package
mcis published formc install/mc upgrade(M44 steps 4-5) and formc-slim, and is never named in a[deps]. Thenmc pkg checkneeds an exemption for exactly this row, or the registry's CI stops using it formc. - Narrow the rule to
[deps]and[replace]only, leavingpkg checkand the index able to speak the name. Smallest change; keeps the resolution argument intact. - Drop it and let a project pin
[deps] mc = "…". This needs an answer to "whichmc/…names does the lock own and which does the running binary own", and § 3.2 says that answer is not free.
3.2 The bundle's namespace is not the package's namespace #
<mc/core> in the bundle means src/core.mc; <mc/core> through the locked
package road means <pkgdir>/core.mc, because libs_open joins the name's tail
to the package directory and there is no NAME → PATH map on that road. So a
consumer of the package writes <mc/src/core.mc>, not <mc/core>. Two
consequences:
<mc/host>— the synthesized name that means "this compiler's own host file" — has no meaning through the package road at all; a consumer must namesrc/host_macos.mcorsrc/host_linux.mcitself, which is host-specific.- An installed tree (§ 2 step 3) does have the map:
dp_mc_loadreadsbundle.listat the root of<libs>/mc/v<version>/, nottools/bundle.list. A tag archive of this repository puts it attools/bundle.list, so whatever installs themcpackage has to copy it to the root — which is exactly whatscripts/check-pkg.sh's step-3 fixture does by hand today (cp tools/bundle.list "$mcd/bundle.list"). That is a note for M44 step 4 (mc install), not a defect here.
3.3 § 4.2's check.mc cannot include every files entry #
check.mcincludes<name>(the package'slib) and every file of[package].files
Written verbatim against this package, mc build check is:
compile check.mc -> build/check
deps/mcpkg/src/sandbox_profiles.mc:36: initializer must be constant
The rule cannot hold for a package that carries alternatives, and there are four independent classes of them here. Each was reproduced on its own:
| what | the two files | the message |
|---|---|---|
the lib needs a host layer chosen first | <mcpkg> before src/host_macos.mc | src/sandbox_profiles.mc:36: initializer must be constant |
| two host layers | src/host_macos.mc + src/host_linux.mc | src/host_linux.mc:14: duplicate #define (O_CREAT is 0x200 on macOS and 0x40 on Linux) |
| two system layers | lib/sys.mc + lib/sys_linux.mc | lib/sys_linux.mc:32: duplicate #define |
a files entry that is not mc source | tests/mc/bundle/embed_demo.txt, tools/bundle.list | …:1: type expected at top level |
and, less fundamentally, a module that needs another one included first
(lib/i128.mc after lib/f16.mc is
lib/i128.mc:38: array size must be a positive constant, because the type it
declares is not there yet).
This is general, not a property of mc. Any package that ships a per-host
or per-architecture file — which is most of the ones worth publishing — has the
same shape.
What the validator should do instead, in the order this branch would recommend:
- An optional
[package].checkarray: the entries a package declares as translation units, each compiled ON ITS OWN, onemc buildper entry. Absent, it defaults to["<the lib>"]. It costs the compiler nothing — nothing insrc/reads it and an unread[package]key is inert — and it costs the validator one loop. For this repository it would be a single two-line file that is exactly § 2.2'scheck.mc. - If a new key is unwanted: the
libalone, and accept that a package whoselibdoes not stand up on its own (this one) declares a[project]instead and is exercised by the second box (§ 18's "the package's own[project]is a second box").
The closure rule — the thing § 4.2 says the "every file" shape is FOR — is not
lost either way: it is enforced on every file the build reads, not on the
files the check names, and § 2.2 above shows it firing on a package tree with a
single #include.
Not recommended: dropping the rule and hashing only. The check is what proves a tag compiles, and a package that does not compile should not be published.
4. Priced, not built: the libraries of lib/ as packages #
The registry's § 5 second bullet asks for float, i128, f16, prelude,
io, rt, http, sqlite, sys_linux, sys_windows as packages of their
own. A package is a directory with an mc.toml at its root and a files
array that may not contain .. (dep_rel_ok), so each one is a directory move.
4.1 The moves #
| package | files today | becomes | lines |
|---|---|---|---|
float | lib/float.mc, lib/float_rt.mc, lib/machine_arm64_float.mc, lib/machine_x86_64_float.mc, lib/user_float.mc, lib/mc_float.mc | lib/float/*.mc + lib/float/mc.toml | 2 006 |
i128 | lib/i128.mc, lib/mc_i128.mc | lib/i128/ | 505 |
f16 | lib/f16.mc, lib/mc_f16.mc | lib/f16/ (depends on float) | 206 |
prelude | lib/prelude.mc | lib/prelude/ | 37 |
io | lib/io.mc | lib/io/ | 34 |
sys_linux | lib/sys_linux.mc, lib/sys_linux_aarch64.mc, lib/sys_linux_x86_64.mc | lib/sys_linux/ | 291 |
sys_windows | lib/sys_windows.mc, lib/sys_windows_host.mc, lib/sys_windows_start.mc | lib/sys_windows/ | 545 |
rt, http, sqlite | examples/api/lib/*.mc — not bundled at all | a new lib/rt/ etc., or left in the example | 629 |
mc_float.mc and mc_f16.mc include "../src/host_macos.mc" and
"../src/core.mc"; after a move those become "../../src/…". They are entry
points for the check scripts, not library code, and could stay in lib/
instead — but then the package's files cannot name them, which is the whole
constraint.
4.2 What does not move #
- The bundled names.
tools/bundle.listisNAME<TAB>PATH; only the PATH column changes.<float>stays<float>. - The blob, and therefore the five goldens.
tools/bundle.mcwrites the names and the file bytes and nothing else — "no path, date or address reaches the blob" is its own comment, and it is true. A pure move with the PATH column updated produces a byte-identicalsrc/bundle_data.mc, somake bundleis a no-op and no golden moves. This is the single most valuable fact in this section and it should be verified withcmpbefore the work is believed. - Relative includes inside bundled files. They resolve by name with a
fallback to the last path component (M15), and
lib/user_float.mc's#include "float.mc"keeps working because the file is beside it either way.
4.3 What breaks, and what it costs #
| risk | why | fix |
|---|---|---|
check-lex, check-ast, check-asm | all three iterate tests/*.mc tests/lib/*.mc lib/*.mc src/*.mc; lib/*.mc does not descend | one more glob per script, lib/*/*.mc, and a re-count of the three "N/N files" numbers |
check-float, check-wide, check-surface | they name lib/float.mc, lib/mc_float.mc, lib/mc_f16.mc, lib/mc_i128.mc by path | a path each |
check-inert | compares artefacts, not paths | nothing, if 4.2 holds |
docs/ | bundle.md, build.md, machine.md, guide/96-a-new-primitive.md, three specs/ files name the paths | make check-docs catches a broken relative link; a stale path inside prose it does not |
the root mc.toml | files is generated from tools/bundle.list | regenerated by the same one-liner check-pkg § 30a asserts |
| a package inside a package | lib/float/ sits under the mc package's root, and its files are in mc's files too | see below |
That last row is the one question that is not a path edit, and the code already
answers it: two trees would claim the same bytes — the mc package, whose
files names lib/float/float.mc, and the float package, whose root is
lib/float/ — and lex_root_of (src/lex.mc) keeps the longest matching
root, not the first, so the inner package wins and float is closed. That is
by construction and not by accident, but no fixture has ever nested one root
inside another, so it is untested: a nested pair in tests/pkg/ is the first
thing this work should add, before a file is moved.
The alternative, and the one worth weighing against the whole of § 4: publish
the libraries from a separate repository. Then the price here drops to zero,
nothing nests, and tools/bundle.list goes on pointing at the copies this
binary ships. It is what mc-geo in the docs' own examples implies and what
every other ecosystem does.
4.4 Size #
Mechanical, if (a) 4.2 is verified and (b) the nesting question is answered:
~8 new mc.toml files (~10 lines each), ~25 file moves, ~10 path edits in
scripts/, ~15 in docs/, one regenerated root manifest — call it a day's
work and a PR of ~150 changed lines plus the moves, with the cmp of
src/bundle_data.mc as the gate that says nothing else moved.
If instead the answer is "a separate repository", the price here is zero and
moves there: a project that wants a newer float than its compiler ships pins
[deps] float = "…", which packages.md § 6 already says is allowed and safe.
5. The outcome — what the registry did with § 3, and what came back #
minicompiler/mc-registry PR #7 (aa34c7c, S5b) rehearsed § 4.2 of its own spec
against this repository's root manifest and recorded every measurement in its
docs/spec-M47.md § 22. Three of the findings are ports into this tree; this
section is what each one cost here.
5.1 [package].check, and two more files entries #
§ 3.3's first recommendation was adopted verbatim: an optional array of
translation units, each compiled on its own, in a box of its own, defaulting
to [<the lib>] when absent, refused when it leaves the package or when it
names a file [package].files does not declare. The compiler still reads
nothing (grepped: the only package.* reads in src/ are package.files,
package.name, package.lib, package.repo and the lock's
package.<n>.deps), so the whole port here is manifest text:
check = ["src/mc_linux_x86_64.mc"]
plus src/mc_linux_x86_64.mc and the src/user.mc it includes added to
files (scripts/check-pkg.sh § 30a's extras list grew with them, and § 30b'
now asserts that every check unit is on disk and declared).
The unit is per-host and the registry's worker is linux/x86_64, which is
why the entry point named is that one: the check project the validator writes
carries no [target], so the box compiles for the worker's own host pair.
Rehearsed here, in the shape web/job.mc writes — a project with
kind = "obj", [target] os = "linux" arch = "x86_64", one dependency
vendored at deps/pkgcheck/ (the reserved name mc cannot be a [deps]
key, which is the registry's own § 22.2 finding; pkgcheck is the local name it
uses) holding mc.toml plus every files entry and nothing else, an mc.lock
row carrying the tree hash, and
// check.mc
#include <pkgcheck/src/mc_linux_x86_64.mc>
| the vendored manifest | build/mc1 build check |
|---|---|
| this branch's | compile check.mc -> build/check.o, exit 0, ELF 64-bit LSB relocatable x86-64, 1 494 016 bytes |
origin/main's (no check entry in files) | pkgcheck/src/mc_linux_x86_64.mc:1: not declared in pkgcheck's [package].files, exit 1 |
So the closure rule is what refuses the manifest without the addition, and the
addition is what a validator needs — both measured, not argued. The [replace]
spelling was tried first and is not usable, exactly as the registry recorded
(mc: pkgcheck 0.0.0-dev is not fetched, exit 2: [replace] is consulted after
the tree is resolved). The vendored tree is what a consumer gets from
mc pkg sync anyway, and it hashes.
A second unit is possible and is not added here. src/mc_linux.mc — the
aarch64-hosted entry point — compiles on an x86_64 worker in the same shape
(measured: exit 0, build/check.o 1 494 032 bytes, an x86-64 object). Nothing
in a host file is architecture-gated at compile time; the layer only answers
strings at run time, so "a unit the worker cannot compile" does not apply to it.
What it would buy is coverage: src/mc_linux.mc is the only path to
src/host_linux_aarch64.mc and src/sysno_linux_aarch64.mc, which are in
files and are compiled by no check today. What it costs is one more box per
tag (~1.5 s of the ~1.6 s job) and one more files entry. Recommended, for
the architect to take or leave; the same argument would extend to
src/mc_windows*.mc and to src/mc.mc, which is where it should stop — those
have no bundle row either and each is another box.
5.2 The sandbox defect #
The check project is a compile, and every compile-only box exited 126 after
succeeding. Fixed here, reproduced on the Lima oracle first:
M43.md § Implementation notes — the compile-only box. It deletes
jb_box_ok's exemption on the registry's side.
5.3 The tree hash moves, and that is the design #
dep_hash_tree digests mc.toml first, so adding check and two files
entries changes the package's hash. Nothing pins it: the value is recomputed on
every build and compared against a lock a consumer writes, and
scripts/check-pkg.sh § 30c compares the compiler against
scripts/pkg-hash.sh rather than against a constant.
5.4 What is still the registry's, and not ported #
The reserved name on both roads (§ 22.2 there — the [deps] key finding is
§ 3.1's here, still unfixed and still recorded), the tag with no [package]
(§ 22.3), and the caps (§ 22.5: CPU 1.39 s of 60, wall 1.48 s of 180, 90 716 KiB
of 512 MiB, an output of 1 595 016 bytes of 64 MiB — the whole two-tag job in
1631 ms). No cap change is proposed there and none is needed here.