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>.

flagmeaning
-o OUToutput path. Default out.o. -o with nothing after it is mc: -o requires an argument.
--exewrite 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|muslwhich 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|staticthe 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=PATHthe 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=NAMEpick 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=DIRadd 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).
--hostprint what this binary is and exit 0 — three lines, no source needed.
--versionprint mc <version> and exit 0 — one line, no source needed.
--opt=Nthe 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.
-Oan 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=NAMEpick 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 linethe refusal
mc prog.mc -o prog.o --libc=gnu, mc --backend=elf-obj --libc=gnu … — an objectmc: --libc applies to an executable: use --exe
mc --dump-asm --libc=gnu … — any of the six dumps, with or without --exemc: --libc applies to an executable: a --dump-* mode writes none
mc --exe --libc=gnu … on a host that is not Linuxmc: --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.

flagprintsstops after
--dump-tokensline id lexeme, one token per linethe lexer
--dump-astthe tree, two spaces per level, after #rule expansion and after every registered pass()the parse
--dump-rulesthe #rule table, then every infix and prefix operator with precedence and associativitythe parse
--dump-asmone function per label, one instruction per line, gen_lower only (nothing is encoded)lowering
--dump-symsone line per section, then one line per symbolencoding
--dump-machineevery registered machine, one line per task, with the origin of the slotuser_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.

flagmeaning
--config FILEuse FILE instead of DIR/mc.toml. Missing argument: mc: --config requires an argument.
--entry-onlyskip 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-onlybuild 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.
--syncrun 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.
--yeswith --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.
--limitsafter the build, print the table report and return a verdict (see below).
--fix-limitsthe same report, and rewrite only the [limits] section of mc.toml with the smallest tolerance that would have avoided grew and tight.
--libs-dir DIRwhere 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 DIRDIR 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.

mc sysroot list
mc sysroot path <os>-<arch>
mc sysroot fetch <os>-<arch> [--yes] [--sysroot-dir DIR]
mc sysroot stub [DIR] [--config FILE]
subcommandwhat it does
listone 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
flagmeaning
--yesactually 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 DIRDIR 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.

subcommandGo analoguewhat it does
syncgo mod tidy + go mod downloadread [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@vwrite one [deps] line — the newest non-yanked version when there is no @ — then sync
listgo list -m allone 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
vendorgo mod vendorcopy each locked tree into deps/<name>/ — mc.toml plus [package].files — then verify
verifygo mod verifyrehash every locked tree and check it against mc.lock; exit 0 or 2
hash DIRdirhash.Hash1print 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 -uraise the [deps] minimum(s) to the newest non-yanked version of the same major, then sync
flagmeaning
--yesactually 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
--longmc pkg list only: under each row, the reason each permission was declared for, read out of the package's own tree
--registry URL|DIRwhere 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 DIRwhere installed packages live, instead of ~/.mc/libs. mc build takes it too, so no CI job depends on HOME
--config FILEthe 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 readswhen
the registrythe mc package's own index row for exactly VERSION — the tag archive, strip = 1, the [package].files list hashed like any other packagea released binary
--from-tree DIRDIR/mc.toml and the files it declares, copieda 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 .
flagmeaning
--from-tree DIRinstall from a checkout instead of the registry: DIR/mc.toml must be the mc package
--forcereinstall over a tree that is already blessed
--yesactually download (the registry road only; --from-tree copies and needs none)
--registry URL|DIRas for mc pkg
--libs-dir DIRas 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.

flagmeaning
--yesactually download and replace; without it, only the plan
--no-installreplace the binary and stop: do not install the matching library tree
--to PATHwrite the new compiler to PATH instead of over the running binary
--registry URL|DIRas for mc pkg
--libs-dir DIRas 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.

flagmeaning
--yesaccept the plan and the permissions; without it, nothing is fetched or staged
--registry URL|DIRwhere the index is read, as for mc pkg
--libs-dir DIRwhere 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 DIRwhere the launcher is written, instead of <mc home>/bin
--workspace DIRthe 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.

flagmeaning
--time SCPU seconds (RLIMIT_CPU), default 2, at most 86400
--wall Swall-clock seconds before the box is killed, default 5, at most 86400
--mem MiBaddress space (RLIMIT_AS), default 256, at most 1048576 (1 TiB)
--out MiBhow much the program may write (the box tmpfs and RLIMIT_FSIZE), default 64, at most 65536 (64 GiB)
--allow=threadslet 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=netkeep 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|gnuwhich family the program compiled inside the box is linked against; by default, whichever loader this host has on disk
--stdin FILEthe program's standard input; without it, immediate EOF
--ro DIRone more directory the box may read (repeatable, at most 16)
--rw DIRone 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-pathbind every --ro DIR at its own absolute path instead of at /ro0, /ro1, …
--tmpa writable /tmp inside the box, on the box tmpfs, counted against --out and gone when the box is
--bin PROGa 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 NAMEcopy 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 DIRthe working directory inside the box: an absolute path is a path in the box (/ro0), a relative one is under /src
--root DIRthe tree that becomes /src, instead of the source's own directory. PATH must be inside it
--config NAMEfor run DIR: the project file to build, relative to DIR. [project].out stays relative to that file's directory
--report FILEwrite the report to this file as well as to stderr
--verboseadd 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 #

codemeaning
0success — and, under --limits, verdict ok
1any diagnostic: a compile error, a TOML error, a spawned tool that failed
2the 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)
3verdict tight or grew (--limits / mc limits only)
124mc sandbox: a cap stopped the program (CPU or wall clock)
125mc 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)
126mc 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 #

Edit this page