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 = [ … ]

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 #


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:

  1. Leave it. The package mc is published for mc install / mc upgrade (M44 steps 4-5) and for mc-slim, and is never named in a [deps]. Then mc pkg check needs an exemption for exactly this row, or the registry's CI stops using it for mc.
  2. Narrow the rule to [deps] and [replace] only, leaving pkg check and the index able to speak the name. Smallest change; keeps the resolution argument intact.
  3. Drop it and let a project pin [deps] mc = "…". This needs an answer to "which mc/… 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:

3.3 § 4.2's check.mc cannot include every files entry #

check.mc includes <name> (the package's lib) 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:

whatthe two filesthe message
the lib needs a host layer chosen first<mcpkg> before src/host_macos.mcsrc/sandbox_profiles.mc:36: initializer must be constant
two host layerssrc/host_macos.mc + src/host_linux.mcsrc/host_linux.mc:14: duplicate #define (O_CREAT is 0x200 on macOS and 0x40 on Linux)
two system layerslib/sys.mc + lib/sys_linux.mclib/sys_linux.mc:32: duplicate #define
a files entry that is not mc sourcetests/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:

  1. An optional [package].check array: the entries a package declares as translation units, each compiled ON ITS OWN, one mc build per entry. Absent, it defaults to ["<the lib>"]. It costs the compiler nothing — nothing in src/ 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's check.mc.
  2. If a new key is unwanted: the lib alone, and accept that a package whose lib does 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 #

packagefiles todaybecomeslines
floatlib/float.mc, lib/float_rt.mc, lib/machine_arm64_float.mc, lib/machine_x86_64_float.mc, lib/user_float.mc, lib/mc_float.mclib/float/*.mc + lib/float/mc.toml2 006
i128lib/i128.mc, lib/mc_i128.mclib/i128/505
f16lib/f16.mc, lib/mc_f16.mclib/f16/ (depends on float)206
preludelib/prelude.mclib/prelude/37
iolib/io.mclib/io/34
sys_linuxlib/sys_linux.mc, lib/sys_linux_aarch64.mc, lib/sys_linux_x86_64.mclib/sys_linux/291
sys_windowslib/sys_windows.mc, lib/sys_windows_host.mc, lib/sys_windows_start.mclib/sys_windows/545
rt, http, sqliteexamples/api/lib/*.mc — not bundled at alla new lib/rt/ etc., or left in the example629

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 #

4.3 What breaks, and what it costs #

riskwhyfix
check-lex, check-ast, check-asmall three iterate tests/*.mc tests/lib/*.mc lib/*.mc src/*.mc; lib/*.mc does not descendone more glob per script, lib/*/*.mc, and a re-count of the three "N/N files" numbers
check-float, check-wide, check-surfacethey name lib/float.mc, lib/mc_float.mc, lib/mc_f16.mc, lib/mc_i128.mc by patha path each
check-inertcompares artefacts, not pathsnothing, if 4.2 holds
docs/bundle.md, build.md, machine.md, guide/96-a-new-primitive.md, three specs/ files name the pathsmake check-docs catches a broken relative link; a stale path inside prose it does not
the root mc.tomlfiles is generated from tools/bundle.listregenerated by the same one-liner check-pkg § 30a asserts
a package inside a packagelib/float/ sits under the mc package's root, and its files are in mc's files toosee 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 manifestbuild/mc1 build check
this branch'scompile 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.

Edit this page