Every command, flag and dump
mc has one binary and one entry point per registered subcommand: the single-file compiler, then
build, limits, sysroot, pkg, update, install, upgrade and sandbox. Everything below
is read off src/cli.mc (the single-file CLI) and the file that registers each subcommand —
src/driver.mc, src/sysroot.mc, src/pkg.mc, src/install.mc, src/upgrade.mc,
src/sandbox.mc. The list is the subcommand TABLE, so a taught compiler that registers one more
prints one more line; mc with no argument prints it and exits 1:
usage: mc [--dump-tokens|--dump-ast|--dump-asm|--dump-syms|--dump-rules|--dump-machine] [--backend=NAME|--exe] [--machine=NAME] [--include=DIR] [--opt=N|-O] [--libc=gnu|musl] [--interp=PATH] [--link=dynamic|static] source.mc [-o out]
mc --host
mc --version
usage: mc build [DIR] [--config FILE] [--sync [--yes]] [--compiler-only] [--limits|--fix-limits] [--sysroot-dir DIR] [--libs-dir DIR]
mc limits [DIR|FILE.mc]
mc sysroot list|path <target>|fetch <target> [--yes] [--sysroot-dir DIR]
mc sysroot stub [DIR] [--config FILE]
mc pkg sync|add|list|vendor|verify [DIR] [--yes] [--registry URL|DIR] [--libs-dir DIR]
mc pkg hash DIR | check INDEX.toml [--yes]
mc update [NAME] [DIR] [--yes] [--registry URL|DIR] [--libs-dir DIR]
mc install [VERSION] [--from-tree DIR] [--yes] [--force] [--registry URL|DIR] [--libs-dir DIR]
mc upgrade [VERSION] [--yes] [--no-install] [--to PATH] [--registry URL|DIR] [--libs-dir DIR]
mc tool install NAME[@VER]|[DIR] | list | remove NAME | upgrade [NAME] | run NAME [-- ARGS]
mc sandbox run|exec [OPTS] PATH [--] [ARGS]
mc sandbox check
Every flag on this page is in the recorded public surface, compared on every make check; what a
PATCH, a MINOR and a MAJOR may do to one is hooks.md § 8.
1. The single-file compiler #
mc [MODE] [--backend=NAME | --exe] [--opt=N | -O] [--libc=gnu|musl]
[--link=dynamic|static] [--interp=PATH] SOURCE [-o OUT]
Arguments are read left to right. The first non-flag argument is the source; a second one is
mc: duplicate entry: <arg>. An unknown flag starting with - that is not --backend=… is
mc: unknown option: <arg>.
| flag | meaning |
|---|---|
-o OUT | output path. Default out.o. -o with nothing after it is mc: -o requires an argument. |
--exe | write a direct executable for the HOST, no linker. The backend is the exe slot of the host's target() registration, resolved after user_init() (post-M41 review) — macho-exe on macOS, a signed Mach-O binary, and elf-exe / elf-exe-x86_64 on Linux, a dynamic ELF64 ET_EXEC (M42) whose PT_INTERP names musl's loader unless --libc=gnu says otherwise. A host registered with 0 in that slot has no direct executable at all, and the flag is refused with <os>/<arch> requires a linker: there is no direct executable instead of writing a binary for another operating system. On Windows this is per-target: windows/x86_64 has a direct executable (pe-exe-x86_64), so mc --exe on that host writes a PE; windows/aarch64 has none — an arm64 PE needs DYNAMICBASE + a .reloc base-relocation table validated on real Windows-on-ARM (deferred) — so its exe slot is 0 and mc --exe there is refused with the same message, windows/aarch64 requires a linker: there is no direct executable. A Windows x86_64 PE is cross-built with --backend=pe-exe-x86_64 or by mc build with kind = "exe" and no [linker]; windows/aarch64 goes through [linker] (object + lld-link). --exe and --backend= write the same decision, so the last one on the command line wins. |
--libc=gnu|musl | which C library family a dynamic Linux executable is linked against — the command-line form of [target].libc, and the same two words. It picks the PT_INTERP path and the DT_NEEDED soname together (gnu: /lib/ld-linux-aarch64.so.1 or /lib64/ld-linux-x86-64.so.2 + libc.so.6; musl, the default: /lib/ld-musl-<arch>.so.1 + libc.so). Anything else is mc: --libc must be gnu or musl: <value>. |
--link=dynamic|static | the command-line form of [target].link. static is an assertion: the writer already takes the static path for a program that imports nothing, and the flag makes that a requirement — with an import in the set it refuses with mc: static link with imports needs [linker]: see docs/build.md -- static linking (M46) instead of writing a dynamic binary. Anything else is mc: --link must be dynamic or static: <value>. |
--interp=PATH | the loader path alone, overriding the family's — the command-line form of [target].interp, for a system whose loader is at neither standard place. The soname still comes from --libc=. |
--backend=NAME | pick a registered backend. Built in: macho, macho-exe, elf-obj, elf-obj-x86_64, elf-exe, elf-exe-x86_64, coff-obj-arm64, coff-obj-x86_64, pe-exe-x86_64. The default is the HOST's object backend — the object slot of the host's target() registration, macho on macOS and elf-obj/elf-obj-x86_64 on Linux (M37) — resolved after user_init() like --exe's (post-M41 review), so a module that re-registers the host pair is honoured here too. A host registered with 0 in that slot has no object step at all, and the default is refused with <os>/<arch> has no object backend: use --exe. A taught compiler adds its own with backend("name", &f). An unknown name lists what exists and exits 1. |
--include=DIR | add one #include "…" search root, exactly like a [include].paths entry does for mc build. Repeatable; roots are tried in the order given, after the includer's own directory. It is what lets one source tree carry two platform layers in different directories and pick one without a mc.toml (examples/conc/lib/macos, lib/linux). |
--host | print what this binary is and exit 0 — three lines, no source needed. |
--version | print mc <version> and exit 0 — one line, no source needed. |
--opt=N | the optimization level, 0 or 1. Default 0, the plain road, on every host and in every release — the unoptimized lowering is the reference every determinism gate compares against (determinism.md), so it does not move when the optimizer improves. --opt=1 turns on the register allocator (M49): locals and parameters that qualify live in x19..x28 for the length of the function instead of going through a frame slot on every read. It changes no observable behaviour — scripts/check-opt.sh runs the whole corpus on both roads and compares exit code and stdout — and it changes nothing at all for a machine that answers 0 to MTASK_REG_COUNT (machine.md § 5). The dump modes honour it: --dump-asm --opt=1 is how the optimized lowering is read. Anything but 0 or 1 is mc: --opt must be 0 or 1: <value>. The last of --opt=/-O on the command line wins. |
-O | an alias for --opt=1, and the only short flag mc has besides -o. It is spelled out here rather than derived, because scripts/check-docs.sh enumerates the -- literals src/ compares argv against and a bare -O is not one of them. |
--machine=NAME | pick the machine the --dump-* modes lower with: arm64 (the host's, default), x86_64 (System V) or x86_64-win (Win64 — the same instruction set, the Windows calling convention). A compile does not need it — an object backend names its own machine, because the file records the architecture — so this flag exists for looking at what a machine selects (--dump-asm --machine=x86_64-win). An unknown name is mc: unknown machine: NAME. |
$ mc --host
os macos
arch aarch64
sys sys
os and arch are the [target] pair an mc.toml with no [target] gets, and the pair that
picks the default backend (macho on macOS, elf-obj / elf-obj-x86_64 on Linux); sys is the
bundled system layer a program on this host includes for its I/O (<sys> or one of the two
Linux layers). The same binary built for a Linux host answers linux, its architecture, and
sys_linux_aarch64 or sys_linux_x86_64 — see ../guide/90-linux-host.md.
$ mc --version
mc 0.0.0-dev
The version is a constant compiled into the binary — mc_version() in src/version.mc,
which <mc/core_min> includes and the bundle carries as mc/version (M44). Anything built from
the working tree says 0.0.0-dev, the sentinel; a released binary says the tag it was built
from, minus the v, because scripts/set-version.sh rewrites that one literal in the release
job and never commits the result. The program name is on the line so that the output can be
pasted into a bug report as it stands.
Because the string is in the bundle and not only in the binary, a taught compiler reports the
version of the binary that built it: mc --exe over <mc/host> + <mc/core> + a module gives
a compiler whose own --version is the same line. See ../ci.md § Versioning and
bundle.md.
Backends are documented in objects.md and in ../guide/40-backends.md.
$ mc --backend=xyz prog.mc -o x.o
unknown backend: xyz
registered: macho macho-exe elf-obj elf-obj-x86_64 elf-exe elf-exe-x86_64 coff-obj-arm64 coff-obj-x86_64 pe-exe-x86_64
--backend=elf-exe and --backend=elf-exe-x86_64 are what --exe resolves to on Linux, and
naming one of them from macOS is how a Linux executable is cross-built with no mc.toml at all:
$ mc --backend=elf-exe hello.mc -o hello
$ llvm-readelf -h hello | grep Type
Type: EXEC (Executable file)
The three flags above say the rest, so a source never has to become a project to name its libc:
$ mc --backend=elf-exe --libc=gnu hello.mc -o hello
$ llvm-readelf -l -d hello | grep -E 'interpreter|NEEDED'
[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
The compiler never probes the host for its libc — one source, one answer on every machine
(../determinism.md) — so the default is the constant musl and --libc=gnu
is how a glibc host says so. The last one on the command line wins, like every other flag here.
All three describe a Linux dynamic executable, and only the executable writer reads them:
PT_INTERP and DT_NEEDED are program-header fields, and an object file has neither. So they are
refused, never read and ignored, on a run that would not write one (post-M42 review):
| the command line | the refusal |
|---|---|
mc prog.mc -o prog.o --libc=gnu, mc --backend=elf-obj --libc=gnu … — an object | mc: --libc applies to an executable: use --exe |
mc --dump-asm --libc=gnu … — any of the six dumps, with or without --exe | mc: --libc applies to an executable: a --dump-* mode writes none |
mc --exe --libc=gnu … on a host that is not Linux | mc: --libc applies to a linux target |
What makes a writer an executable writer is that some target() registration names it in its
exe slot — the same table --exe resolves through — so a target a module registered from
user_init() answers for its own writer, and no backend name is special-cased. The three
questions are asked after user_init() and before every dump, which is why an unreadable entry
file reports cannot open first (diagnostics.md § 10).
Modes: the six dumps #
A dump writes deterministic text to stdout and produces no object. Only one mode is in
effect — the last one on the command line wins. Five of the six exist in the C seed too, which is
what make check-lex, check-ast and check-asm compare across the two compilers;
--dump-machine is the self-hosted compiler's own, because the seed has no machine table.
| flag | prints | stops after |
|---|---|---|
--dump-tokens | line id lexeme, one token per line | the lexer |
--dump-ast | the tree, two spaces per level, after #rule expansion and after every registered pass() | the parse |
--dump-rules | the #rule table, then every infix and prefix operator with precedence and associativity | the parse |
--dump-asm | one function per label, one instruction per line, gen_lower only (nothing is encoded) | lowering |
--dump-syms | one line per section, then one line per symbol | encoding |
--dump-machine | every registered machine, one line per task, with the origin of the slot | user_init() |
$ mc --dump-tokens prog.mc
1 260 i64
1 1 main
1 270 (
1 271 )
1 272 {
1 268 return
1 2 42
1 277 ;
1 273 }
2 0 EOF
The number after the line is the token id: 0..6 are the classes (T_EOF, T_IDENT, T_INT,
T_CHAR, T_STR, T_DIR, T_HOLE) and 256 upward are lexemes in the mutable token table, in
registration order — #token and every Tier 3 registration append there. See
language.md § Tokens.
$ mc --dump-ast prog.mc
FUNC type=i64 name=main
BLOCK
RETURN
INT val=42 type=i64
$ mc --dump-asm prog.mc
_main:
stp x29, x30, [sp, #-16]!
mov x29, sp
movz x9, #42
mov x0, x9
b L1
L1:
ldp x29, x30, [sp], #16
ret
$ mc --dump-syms prog.mc
section __TEXT,__text flags=0x80000400 align=2 size=28 nreloc=0
sym 0 extern sect=1 value=0 _main
--dump-rules on a source that includes <prelude>:
$ mc --dump-rules prog.mc
rule 0: stmt: while ( expr $1 ) block $2 => 7 nodes
rule 1: stmt: for ( stmt $1 expr $2 ; ident $0 = expr $3 ) block $4 => 11 nodes
rule 2: stmt: ident $0 += expr $1 ; => 4 nodes
rule 3: stmt: ident $0 -= expr $1 ; => 4 nodes
rule 4: stmt: ident $0 ++ ; => 4 nodes
rule 5: stmt: ident $0 -- ; => 4 nodes
infix || prec 1 left
infix && prec 2 left
...
prefix -
prefix ~
prefix !
prefix &
The $N in a rule line is the hole's index in the template, and => N nodes is the size of the
parsed template. --dump-rules is the one dump whose .mc version says more than the C seed's:
only the self-hosted compiler has the handler column that syntax_infix fills in.
--dump-machine (M24) is the audit of the other seam. It stops right after user_init() — a
machine table is not a function of the source — and reports, per registered machine, which
implementation is behind each task:
$ mc-badmach --dump-machine prog.mc
machine arm64 (current)
prologue bundled arm64
const bundled arm64
bin taught
...
machine x86_64
...
bundled <name> means the slot's pointer is the one that bundled machine had for that task before
user_init() ran, and taught means a module replaced it. (current) marks the machine the
walker would drive. It is the cheapest test that an override took effect, and it is what makes a
derived machine reviewable — which slots a module actually replaced, and on top of what. There is
no runtime symbol table, so the answer is read from a snapshot of the registry rather than from a
symbol name; the source file is required only because user_init() runs after lex_init.
2. mc build — the project driver #
mc build [DIR] [--config FILE] [--sync [--yes]] [--entry-only] [--compiler-only] [--limits | --fix-limits] [--sysroot-dir DIR] [--libs-dir DIR]
DIR defaults to ., the config to DIR/mc.toml. Every path inside the file is relative to the
directory of the config, so mc build examples/api from the repository root does the same
thing as mc build from inside examples/api. Every key is in toml.md.
| flag | meaning |
|---|---|
--config FILE | use FILE instead of DIR/mc.toml. Missing argument: mc: --config requires an argument. |
--entry-only | skip the [compiler] step and compile [project].entry with the running binary. This is the flag mc build passes to the taught compiler it just built — you rarely type it. |
--compiler-only | build the taught compiler from [compiler].modules, print its path on stdout and stop — no spawn, no entry. This is the flag a test.sh wants when it drives the taught compiler over its own suite, and the one an editor server needs. Without a [compiler].modules it is an error (missing key: compiler.modules), not a silent full build; together with --entry-only it is mc: --entry-only and --compiler-only are exclusive. |
--sync | run the sync step of mc pkg sync first, and then build. This is the only road from a build to the network — without it mc build opens no socket and spawns no downloader, whatever the project says. Without --yes it prints the plan and stops: no lock is written and nothing is built (exit 0). On a project with no [deps] and no [tools] it is sync: no dependencies, an empty lock, and the build. The registry is [registry].url or the default — mc build has no --registry flag. On a compiler assembled without <mc/core_pkg> the flag is mc: --sync needs the package half of this compiler: mc pkg is not in it, exit 1. |
--yes | with --sync: accept the plan and the [[permission]] sets it printed, exactly as for mc pkg sync --yes. On its own it is mc: --yes applies to --sync: mc build downloads nothing without it. |
--limits | after the build, print the table report and return a verdict (see below). |
--fix-limits | the same report, and rewrite only the [limits] section of mc.toml with the smallest tolerance that would have avoided grew and tight. |
--libs-dir DIR | where an installed package lives, instead of $HOME/.mc/libs. It is the second road a #include <pack/file.mc> takes, after a vendored deps/<pack>/, and it is also where the installed mc package is looked for. CI passes it so that no job depends on HOME, exactly as --sysroot-dir does. Missing argument: mc: --libs-dir requires an argument. See packages.md. |
--sysroot-dir DIR | DIR is the sysroot for [target], ahead of [sysroot].cache and of ~/.mc/sysroots in the resolution chain — but behind [sysroot].path, which still wins. CI passes it so that no job depends on HOME. Missing argument: mc: --sysroot-dir requires an argument. See sysroot.md. |
A second bare argument is mc: duplicate directory: <arg>; any other -flag reprints the usage
and exits 1.
$ mc build examples/api
compiler build/mc-api.mc -> build/mc-api
compile main.mc -> build/api
One line per step, always what from -> to. The steps are compiler (a [compiler] section
was present), compile and link (a [linker] section was present).
With --sync the sync's own lines come first, and there is exactly one set of them however many
compilers the build runs: a project with a [compiler] section builds a taught compiler and spawns
it with --entry-only, and --sync is not passed on — the parent synced, and the child compiles
the entry from the lock the parent wrote.
$ mc build --sync --yes
fetch plot 1.0.0 https://.../plot-1.0.0.tar.gz
fetch mathx 1.1.0 https://.../mathx-1.1.0.tar.gz
lock mc.lock (2 packages)
compile main.mc -> build/sync
3. mc limits — the table report #
mc limits [DIR | FILE.mc] [--config FILE]
With a path ending in .mc it runs the whole pipeline over that one file, up to
gen_encode_all(), and writes no object. With anything else it is mc build DIR --limits.
$ mc limits src/mc.mc
limits src/mc.mc
table estimate reserved used grow verdict
tokens 0 512 51 0 ok
includes 22 64 22 0 ok
...
heap 24139550 33554432 23489728 0 ok
tolerance 0.25, verdict ok (heap in bytes, every other table in elements)
Columns: the static estimate, what was reserved (estimate * (1 + tolerance), floored at a cold
start seed), the high-water usage, how many times the block had to double, and the row verdict.
Row verdicts are ok, tight (used over 90 % of reserved) and grew (the block doubled at
least once); the report's verdict is the worst row.
3b. mc sysroot — where a cross link finds its files #
mc sysroot list
mc sysroot path <os>-<arch>
mc sysroot fetch <os>-<arch> [--yes] [--sysroot-dir DIR]
mc sysroot stub [DIR] [--config FILE]
| subcommand | what it does |
|---|---|
list | one line per registered target and the source fetch would use. Walks the target registry and the pinned table and touches no file, so the output is the same on every host (tests/golden/sysroot-list.txt) |
path <target> | run the resolution chain for that target and print the directory on stdout; the no sysroot message and exit 2 when nothing answered |
fetch <target> | print the plan (url, size, sha256, destination). With --yes, download it, verify the sha256 with mc's own SHA-256, extract it and write manifest.toml |
stub [DIR] | read DIR/mc.toml (or --config FILE), parse [project].entry, and write one import stub per library it uses — a .tbd on macOS, a .def plus the .lib llvm-dlltool builds from it on Windows. No object and no link; [linker] is not required |
| flag | meaning |
|---|---|
--yes | actually download. Without it fetch prints the plan, says nothing was downloaded: re-run with --yes and exits 0. There is no prompt: mc has no isatty |
--sysroot-dir DIR | DIR is the destination (fetch) or the candidate (path), instead of ~/.mc/sysroots/<os>-<arch> |
mc sysroot stub is the same thing {stubs} in [linker].args does on its own during a build,
without the build. fetch reaches the network by spawning curl/wget/curl.exe — there is no
HTTP and no TLS in this language — and it and mc pkg are the only two things in mc that do.
mc build never downloads. Everything about the chain, the cache and the pinned rows is in
sysroot.md.
3d. mc pkg and mc update — dependencies #
mc pkg sync [DIR] [--config FILE] [--yes] [--registry URL|DIR] [--libs-dir DIR]
mc pkg add NAME[@VERSION] [DIR] [--config FILE] [--yes] [--registry URL|DIR] [--libs-dir DIR]
mc pkg list [DIR] [--config FILE] [--long] [--libs-dir DIR]
mc pkg vendor [DIR] [--config FILE] [--libs-dir DIR]
mc pkg verify [DIR] [--config FILE] [--libs-dir DIR]
mc pkg hash DIR
mc pkg check INDEX.toml [--yes] [--registry URL|DIR] [--libs-dir DIR]
mc update [NAME] [DIR] [--config FILE] [--yes] [--registry URL|DIR] [--libs-dir DIR]
Registered by <mc/core_pkg> (bundle.md § The parts), which is a part a compiler can
leave out: without it these two subcommands do not exist, and mc build still builds a project
from its mc.lock and its deps/ tree.
| subcommand | Go analogue | what it does |
|---|---|---|
sync | go mod tidy + go mod download | read [deps] and [tools], read the index rows they need, run minimal version selection, print the permissions anything new asks for, fetch the trees that are missing, write mc.lock. Rows nothing requires are dropped |
add NAME[@VERSION] | go get pkg@v | write one [deps] line — the newest non-yanked version when there is no @ — then sync |
list | go list -m all | one line per lock row: name, version, the first 12 characters of the hash, vendored/cache/path/tool, and the permissions the lock records as accepted (stdio when there are none). No absolute path, so it is a golden |
vendor | go mod vendor | copy each locked tree into deps/<name>/ — mc.toml plus [package].files — then verify |
verify | go mod verify | rehash every locked tree and check it against mc.lock; exit 0 or 2 |
hash DIR | dirhash.Hash1 | print the tree hash of a checkout: what a registry row's sha256 has to carry |
check INDEX.toml | — | the registry-side gate: with --yes it downloads every row, re-derives the hash, and compares the row against the archive's own mc.toml |
mc update [NAME] | go get -u | raise the [deps] minimum(s) to the newest non-yanked version of the same major, then sync |
| flag | meaning |
|---|---|
--yes | actually download, and accept the permissions printed with the plan. Without it, anything that would fetch prints the plan — source, expected tree hash, destination — says nothing was downloaded: re-run with --yes and exits 0. There is no prompt: mc has no isatty. When a package in the build list asks for a permission the current mc.lock does not already record as accepted, the plan carries the permission table too, the last line becomes nothing was downloaded: re-run with --yes to fetch and to accept the permissions above, and sync stops there even when there is nothing to download |
--long | mc pkg list only: under each row, the reason each permission was declared for, read out of the package's own tree |
--registry URL|DIR | where the index lives, instead of [registry].url or the default https://pkg.minicompiler.dev. A directory is read in place; a URL is fetched into <libs>/index/<name>.toml, the offline snapshot — refreshed once per package by any command carrying --yes, and read as it is without one (packages.md § 10) |
--libs-dir DIR | where installed packages live, instead of ~/.mc/libs. mc build takes it too, so no CI job depends on HOME |
--config FILE | the project file, instead of DIR/mc.toml |
mc pkg is the only thing that writes mc.lock, the only thing that writes under <libs>, and —
with mc sysroot fetch — one of the two that spawn a downloader. Everything about what a package
is, how #include <pack/file.mc> is resolved and what a build refuses is in
packages.md.
3e. mc install — the compiler's own package #
mc install [VERSION] [--from-tree DIR] [--yes] [--force] [--registry URL|DIR] [--libs-dir DIR]
Puts the mc package — the compiler's own source tree, every name the bundle carries — under
<libs>/mc/v<VERSION>/, which is the third and last step of #include <name> resolution
(packages.md § 2). VERSION defaults to mc --version, and that default is the
only one this binary will ever read: the directory carries the version in its name, so two
compilers on one machine never share a tree.
A full binary answers every bundled name out of its own blob and never needs it. A slim
binary — mc-<ver>-<target>-slim.tar.gz, the same compiler without the blob
(bundle.md § The slim flavour) — has nothing to answer with, and says so:
prog.mc:1: #include <prelude>: not bundled in this compiler and mc 0.16.0 is not installed: run mc install
Two roads, and both write the same layout:
| what it reads | when | |
|---|---|---|
| the registry | the mc package's own index row for exactly VERSION — the tag archive, strip = 1, the [package].files list hashed like any other package | a released binary |
--from-tree DIR | DIR/mc.toml and the files it declares, copied | a checkout, and any air-gapped machine |
The registry road is mc pkg sync's, verbatim: it prints the plan (fetch mc <ver>, the url, the
expected tree hash, the destination) and downloads nothing without --yes; the archive's members
are checked before tar writes a byte; the extracted tree is hashed and compared to the row's
sha256, and a mismatch unlinks what was written and leaves no cache manifest, so the next read
says "not fetched" rather than reading debris. Afterwards tools/bundle.list is copied to the root
of the tree — the NAME<TAB>PATH map packages.md § 2 step 3 reads, which the
repository keeps under tools/ and which is deliberately not part of the hash.
Installing is idempotent: with the cache manifest already there it prints
mc 0.16.0 is installed (<libs>/mc/v0.16.0/) and does nothing, unless --force.
A development build reports 0.0.0-dev, which no registry publishes, so the registry road refuses
it and names the other one:
mc: mc 0.0.0-dev is a development build: the registry publishes no such version
run: mc install --from-tree .
| flag | meaning |
|---|---|
--from-tree DIR | install from a checkout instead of the registry: DIR/mc.toml must be the mc package |
--force | reinstall over a tree that is already blessed |
--yes | actually download (the registry road only; --from-tree copies and needs none) |
--registry URL|DIR | as for mc pkg |
--libs-dir DIR | as for mc pkg. The single-file compiler has no such flag, so a program compiled by mc prog.mc reads ~/.mc/libs and nothing else |
mc is a reserved package name in [deps], in [replace] and for mc pkg add
(packages.md § 1) — a project may not pin the compiler's own source. This road is
the exception that proves it: it installs the package for the binary itself, at the binary's
own version, into a directory no project resolves through.
3f. mc upgrade — the compiler replaces itself #
mc upgrade [VERSION] [--yes] [--no-install] [--to PATH] [--registry URL|DIR] [--libs-dir DIR]
Downloads a released mc for this host, checks it against the checksum the release published
beside it, replaces the binary that is running, and then installs the library tree that matches the
new version. VERSION is the newest non-yanked, non-pre-release row of the mc package's index
when it is not given; a release candidate is never chosen for you and has to be named
(mc upgrade 1.0.0-rc1), exactly as in mc pkg add (packages.md § 5).
$ mc upgrade
upgrade mc 0.15.18 -> 0.16.0
url https://github.com/minicompiler/mc/releases/download/v0.16.0/mc-0.16.0-macos-arm64.tar.gz
sha256 https://github.com/minicompiler/mc/releases/download/v0.16.0/mc-0.16.0-macos-arm64.tar.gz.sha256
into /usr/local/bin/mc
nothing was downloaded: re-run with --yes
Nothing happens without --yes — the M25 rule every verb here follows. With it:
$ mc upgrade --yes
upgrade mc 0.15.18 -> 0.16.0
...
mc 0.15.18 -> 0.16.0 (/usr/local/bin/mc)
package mc 0.16.0 -> /home/you/.mc/libs/mc/v0.16.0/
and a second run says mc 0.16.0 is the newest and exits 0.
Which archive. The registry is a registry of sources: the index row carries the tag archive, not the binaries. The address of the binaries is derived from it, and only for the one forge whose layout is written down:
https://github.com/<owner>/<repo>/archive/refs/tags/v0.16.0.tar.gz
-> https://github.com/<owner>/<repo>/releases/download/v0.16.0/mc-0.16.0-<target>.tar.gz
with the checksum at that name plus .sha256 — what scripts/release-assets.sh writes. Any other
url is refused rather than guessed at:
mc: upgrade: no binaries known for: https://git.example.com/mc/0.16.0.tar.gz
A row whose url is a local path is the second road: the release assets sit in the same
directory. That is the air-gapped upgrade — unpack a release into a directory, write an index file
beside it, and mc upgrade --registry DIR.
<target> is host_os() and host_arch() in the release vocabulary (macos-arm64,
linux-arm64, linux-x86_64, windows-arm64, windows-x86_64 — aarch64 is spelled arm64
there), and a binary that carries no bundle fetches the -slim archive. Neither is a flag: asking
for an archive that cannot run on this machine is not a choice worth offering.
What it checks, and what it does not. The archive is verified against the .sha256 before it
is unpacked, and the compiler that came out of it is run once (mc --version) and must answer the
version that was asked for. Both checks are about integrity and about the release naming what it
packaged. Neither proves who published it: the checksum is served from the same origin as the
archive. See packages.md § 11.
The swap. The new bytes are written to <dest>.new, mode 0755, and renamed over <dest> — an
atomic replacement that also gives the destination a new inode, which is what macOS needs (a signed
binary overwritten in place is killed on its next run by the cached signature). The running process
keeps the old inode and is unaffected. On Windows a running .exe cannot be replaced at all, so
the new compiler is left beside the old one and the command to finish is printed:
the new compiler is C:\tools\mc.exe.new
windows holds a running .exe open; finish with:
move /y "C:\tools\mc.exe.new" "C:\tools\mc.exe"
A downgrade is not an error. There is no --force: an older version is one you named, and the
plan says downgrade mc 0.16.0 -> 0.15.18 instead of upgrade. A development build is the one
refusal — 0.0.0-dev names no release, so the road that would choose a version for you stops:
mc: mc 0.0.0-dev is a development build: build from the tree
run: make mc1
mc upgrade VERSION still works there, and does what it says.
| flag | meaning |
|---|---|
--yes | actually download and replace; without it, only the plan |
--no-install | replace the binary and stop: do not install the matching library tree |
--to PATH | write the new compiler to PATH instead of over the running binary |
--registry URL|DIR | as for mc pkg |
--libs-dir DIR | as for mc pkg; also where the download goes (<libs>/mc/tmp/<version>/) |
The library tree is installed by spawning the new binary (mc install --yes), because the
directory is named after the version the compiler reports and the process that must ask is the one
that was just written. Its exit status is this command's.
3g. mc tool — install and run a program package #
mc tool install NAME[@VERSION] [--yes] [--registry URL|DIR] [--libs-dir DIR] [--bin-dir DIR]
mc tool install [DIR] [--config FILE] [--yes] ... every [tools] of a project
mc tool list [--libs-dir DIR] [--bin-dir DIR]
mc tool remove NAME [--libs-dir DIR] [--bin-dir DIR]
mc tool upgrade [NAME] [--yes] ...
mc tool run NAME [--workspace DIR] [--] [ARGS]
A tool is a package of kind = "exe" (packages.md § 3, tools.md).
install resolves it in the same graph mc pkg uses — its own [deps] are libraries — prints the
plan and a permission table, and stops without --yes; with --yes it fetches into <libs>,
stages a buildable copy under ~/.mc/tools/<name>/v<ver>/, builds it by spawning this compiler, and
writes a one-line launcher ~/.mc/bin/<bin>. run is what the launcher calls: on a host with a
sandbox it boxes the tool under the permissions it declared (sandbox.md § The
primitives), and on a host without one it runs the binary directly (the M48 amendment — the same
install everywhere, no --unconfined).
box-args <mc.toml> is a hidden verb that prints the derived mc sandbox exec flags one per line;
it is the ONE definition of what each permission means, shared by mc tool run and a registry
validator.
| flag | meaning |
|---|---|
--yes | accept the plan and the permissions; without it, nothing is fetched or staged |
--registry URL|DIR | where the index is read, as for mc pkg |
--libs-dir DIR | where fetched trees live (~/.mc/libs); the tools root is its parent's tools/ and the default bin directory its parent's bin/, so CI can run with no HOME |
--bin-dir DIR | where the launcher is written, instead of <mc home>/bin |
--workspace DIR | the directory the workspace permission grants at run time; the default is the current directory, and $HOME and / are refused without it (tools.md § The workspace) |
3c. mc sandbox — compile and run something you do not trust #
mc sandbox run [OPTS] PATH [--] [ARGS]
mc sandbox exec [OPTS] BIN [--] [ARGS]
mc sandbox check
A Linux-only subcommand, registered by <mc/core_sandbox> (bundle.md § The parts).
run compiles PATH inside the box and then runs what it built; exec runs an
already-built Linux executable; check prints what the running kernel can do. The box, the
report grammar and what is not isolated are in sandbox.md.
| flag | meaning |
|---|---|
--time S | CPU seconds (RLIMIT_CPU), default 2, at most 86400 |
--wall S | wall-clock seconds before the box is killed, default 5, at most 86400 |
--mem MiB | address space (RLIMIT_AS), default 256, at most 1048576 (1 TiB) |
--out MiB | how much the program may write (the box tmpfs and RLIMIT_FSIZE), default 64, at most 65536 (64 GiB) |
--allow=threads | let the filter allow a clone that is a real thread (CLONE_THREAD set, no CLONE_NEW*) and the system calls a thread needs; every other way of making a process is then COUNTED, up to 64 (sandbox.md § The explain channel) |
--allow=net | keep the host's network namespace instead of unsharing an empty one, stop handling the network in the Landlock ruleset, and add the measured net delta to the seccomp profile (sandbox.md § The primitives) |
--libc=musl|gnu | which family the program compiled inside the box is linked against; by default, whichever loader this host has on disk |
--stdin FILE | the program's standard input; without it, immediate EOF |
--ro DIR | one more directory the box may read (repeatable, at most 16) |
--rw DIR | one more directory the box may read AND write, bound at its own absolute path (repeatable, at most 16). --rw / and --rw $HOME are refused, with no flag that lifts it |
--at-path | bind every --ro DIR at its own absolute path instead of at /ro0, /ro1, … |
--tmp | a writable /tmp inside the box, on the box tmpfs, counted against --out and gone when the box is |
--bin PROG | a program from the host's PATH, bound read-only at /bin/<basename> with PATH=/bin in the box (repeatable, at most 8). It also raises the run step's process cap to 16 and its execve cap to 1 + the number of --bin |
--env NAME | copy NAME=<the host's value> into the box's environment (repeatable, at most 8). A variable the host does not have arrives empty, which is not an error |
--cwd DIR | the working directory inside the box: an absolute path is a path in the box (/ro0), a relative one is under /src |
--root DIR | the tree that becomes /src, instead of the source's own directory. PATH must be inside it |
--config NAME | for run DIR: the project file to build, relative to DIR. [project].out stays relative to that file's directory |
--report FILE | write the report to this file as well as to stderr |
--verbose | add one rusage: cpu N ms, maxrss N kb line to the report — not deterministic, and the only line that is not — and one execve N line per step, which is |
The four caps each have a maximum and a minimum of one. The maxima are not the kernel's
limits; they are the largest values that still mean something here, and a number past one of them
is a typo or an attack, never a request: mc: --mem: at most 1048576, exit 2. A value too long to
be a number at all is mc: --mem: number too large — before the post-M43 review the accumulation
was unbounded, so --mem 999999999999999999999999 wrapped and the box ran with a negative cap
(measured: refused: mmap 8192 bytes over the cap (-3541500564788477952)). Zero is refused for
all four: --time 0 and --wall 0 kill the box before it runs, --mem 0 is an RLIMIT_AS
nothing can start under (measured: the compile step died of SIGSEGV), and --out 0 is a tmpfs
the mount refuses.
The six primitives above know nothing about permissions: each one is a mount, a namespace, an
environment entry or a profile row, and what a package's [[permission]] rows map onto them is
somebody else's question. Three of them can refuse before the box exists, each mc: … on stderr
with exit 126 — mc: --rw /: refusing to bind the whole filesystem read-write, mc: --rw
/Users/me: refusing to bind the home directory read-write; name a subdirectory, mc: --rw is not
a directory: PATH and mc: --bin NAME: not on PATH.
mc sandbox check prints six lines on stdout and exits 0 when every one of the five capabilities
the box needs is there, 1 otherwise:
kernel: 7.0.0-30-generic
userns: ok
landlock: abi 8
seccomp: notif ok
overlay: ok
pidfd: ok
userns is ok, restricted (apparmor), EPERM, EACCES or errno N; overlay is ok or
not loaded (modprobe overlay). On macOS and Windows all three verbs print the command to run
instead and exit 126 (sandbox.md § Hosts).
A run prints the program's own stdout and stderr unchanged and then the report, one line per event, on stderr:
$ mc sandbox run --time 2 tests/013-putnum.mc
46368
sandbox: compile: exit 0
sandbox: exit 0
and one line, with exit code 125, when the box refused something:
$ mc sandbox run tests/sandbox/shadow.mc
sandbox: compile: exit 0
sandbox: refused: open /etc/shadow
Exit codes #
| code | meaning |
|---|---|
0 | success — and, under --limits, verdict ok |
1 | any diagnostic: a compile error, a TOML error, a spawned tool that failed |
2 | the environment is not ready: no sysroot for the target (sysroot.md § 5), or a dependency that does not match mc.lock, is not fetched, or a lock that is stale (packages.md § 8) |
3 | verdict tight or grew (--limits / mc limits only) |
124 | mc sandbox: a cap stopped the program (CPU or wall clock) |
125 | mc sandbox: a refusal stopped it — a system call outside the profile, a path outside the box's roots, an mmap over --mem, a process over the cap, an execve too many (sandbox.md § The explain channel) |
126 | mc sandbox: the box could not be set up — including "this host is not Linux" |
--fix-limits exits 0 when it managed to write a tolerance that fits, and 3 when even 1.0
would not have been enough (the file is left alone in that case).
Code 2 is always "your machine is not ready", never "your program does not compile", and that
is why it is a separate code: a script can tell them apart without reading the text. Two families
use it — the no sysroot for <os>-<arch> block of sysroot.md § 5, and the package
refusals of packages.md § 8. Both carry a run: line naming the command that would
fix it.
4. What is not a flag #
{sdk}in[linker].argsmakes the driver runxcrun --show-sdk-path, once per build and only if some argument mentions the placeholder. That is the only external commandmcruns on its own; the others are the linker and the taught compiler, both named inmc.toml.- The subcommands are registrations, not
ifs (M41).build,limitsandsysrootcome from asubcommand()table that<mc/core_build>fills, and the usage textmcprints with no arguments is the two fixed lines plus one entry per registered subcommand name: the listing is what dispatch would run, so a name registered twice is printed once, in the position of its first registration and with the text of the last — the registrationsubcommand_findpicks (hooks.md § 7). A compiler assembled without that part prints two lines and accepts no subcommand — which is the honest answer, since it has none. Same text, byte for byte, formcitself. - The default backend has a second source (M41). With no
--backend=and no--exe,mcasks the target registry for the host's pair, as before; a compiler with no target registry at all uses whateverbackend_default("name")recorded, and with neither saysno backend: use --backend=NAME. Seedocs/guide/98-recreating-the-compiler.md. --exeis the host's, not Mach-O's (post-M41 review). It used to be written insrc/cli.mcas the literal namemacho-exe, so a Linux- or Windows-hostedmc --exewrote a Mach-O binary its own kernel refuses withENOEXEC— the same class of bug M37 fixed for the object backend. It reads the exe slot oftarget(host_os(), host_arch(), …)now, and refuses when that slot is 0. The resolution happens afteruser_init()and after the--dump-*modes have returned, so a target a module registered counts and--exe --dump-asmstill dumps.- Both slots are resolved in the same place (post-M41 review). The DEFAULT object backend used
to be looked up while the flags were being read, which is before
user_init(): a module that re-registered the host pair was honoured bymc build(the driver resolves insidedrv_parse, afteruser_init()— M39.5) and silently ignored bymc x.mc -o x.o. It now sits beside--exe's, afteruser_init()and after the dumps have returned, and a 0 in the object slot — a registration, not an omission: it is what a board whose flat image is the whole artefact writes — is refused with<os>/<arch> has no object backend: use --exeinstead of being handed tobackend_find(), which takes a name. The two refusals are the driver's own messages (drv_backend_for,src/driver.mc) with what a TOML file would do replaced by what a command line can:[linker]becomes "a linker" andkind = "exe"becomes--exe. Because the resolution happens after the--dump-*early returns,mc --dump-ast x.mcnow works on a compiler whose host answers for no backend at all — nothing in a dump needs one. - There is no
--target=: the target is[target]inmc.toml. Cross-compiling is ../guide/50-cross-compile.md. - There is no
--help: any bad invocation prints the usage above. - The C seed (
build/mc0) has fewer flags. It accepts the five dumps,-oand--backend=macho, and nothing else:--exe,--backend=with any other name,mc buildandmc limitsexist only in the self-hosted compiler. See ../guide/70-bootstrap.md.