Every command, flag and dump

mc has one binary and four entry points: the single-file compiler, mc build, mc limits and mc sysroot. Everything below is read off src/cli.mc (the single-file CLI), src/driver.mc (the first two subcommands) and src/sysroot.mc (the third). Running mc with no argument prints exactly this and exits 1:

usage: mc [--dump-tokens|--dump-ast|--dump-asm|--dump-syms|--dump-rules|--dump-machine] [--backend=NAME|--exe] [--machine=NAME] [--include=DIR] source.mc [-o out]
       mc --host
usage: mc build [DIR] [--config FILE] [--compiler-only] [--limits|--fix-limits] [--sysroot-dir DIR]
       mc limits [DIR|FILE.mc]
       mc sysroot list|path <target>|fetch <target> [--yes] [--sysroot-dir DIR]
       mc sysroot stub [DIR] [--config FILE]

1. The single-file compiler #

mc [MODE] [--backend=NAME | --exe] [--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> requires a linker: there is no direct executable instead of writing a binary for another operating system; that is the case on Windows, where the road is an object plus [linker]. --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. 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.
--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 <sys_linux>). The same binary built for a Linux host answers linux, its architecture, and sys_linux — see ../guide/90-linux-host.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

--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] [--entry-only] [--compiler-only] [--limits | --fix-limits] [--sysroot-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.
--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.
--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).

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 is the only thing in mc that reaches the network, and it does it by spawning curl/wget/curl.exe — there is no HTTP and no TLS in this language. mc build never downloads. Everything about the chain, the cache and the pinned rows is in sysroot.md.

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

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)
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 one message and nothing else — the no sysroot for <os>-<arch> block of sysroot.md § 5. It is a separate code so that a script can tell "your machine is missing files" from "your program does not compile" without reading the text.


4. What is not a flag #

Edit this page