Every message the compiler emits

Every diagnostic mc can print, with what causes it and what to do about it. The list is extracted from the die/die2/err_at/err_at2/err_node/expect/toml_err* call sites in src/*.mc; nothing here is invented, and scripts/check-docs.sh compiles a sample for the ones marked with an example.

Every diagnostic exits 1. There are no warnings: the compiler either produces the artifact or says why it did not.

The four shapes #

shapewhoexample
mc: MESSAGEdie — no position availablemc: arena exhausted
mc: MESSAGE: DETAILdie2mc: cannot open: sys.mc
FILE:LINE: MESSAGEerr_at / err_node — the position of the token or node at faultprog.mc:7: type expected
FILE:LINE: MESSAGE: DETAILerr_at2prog.mc:3: the rule expected: )
FILE:LINE:COL: MESSAGE[: KEY]toml_err*mc.toml:6:6: only macos, linux and windows (see docs/build.md): target.os

FILE is whatever the token's source is called: a real path, or a bundled name when the code came from inside the binary (syntax_demo_test:10: type expected at top level). A missing file is printed as ?.

// expect-error: type expected at top level
notatype main() { return 0; }

1. Lexer #

messagecausefix
unexpected charactera byte that starts no token and no lexeme in the tableremove it; if it is meant to be an operator, register it with #token first
unterminated commenta /* with no */ before end of fileclose it — block comments do not nest
unterminated stringa " with no closing " on the same sourceclose it, or split with two literals
unterminated char literala ' with no closing 'close it
unterminated literala literal that ends at end of filethe file is truncated
unterminated escapea \ at the very end of a literalcomplete the escape
unknown escapean escape that is not \n \t \r \0 \\ \' \"use one of those, or write the byte with a char literal and st8
\0 not allowed in stringan explicit NUL inside a string literal__cstring is S_CSTRING_LITERALS and the linker merges literals at the first NUL, so "a\0b" and "a" would share an address. Build the bytes into a u8 array instead
invalid hexadecimal0x with no hex digit after itwrite at least one digit
empty lexeme#token ""give the lexeme at least one byte
unknown directivea #name that is not one of the tencheck the spelling; the list is in directives.md. #include <name> and #embed do not exist in the C seed
invalid holea $ that is not followed by a hole name$name and $$name are only meaningful inside a #rule template
unknown bundled include#include <name> with a name the bundle does not carrythe catalogue is bundle.md. There is no filesystem fallback for <...>
path with too many segmentsa path with more than 64 components after normalisationshorten it, or add an [include].paths root and include by a short name
too many substitutionsmore than 16 p_subst_* entries pending for one pushed sourcea module bug: batch fewer substitutions per push

2. Directives #

messagecausefix
#include expects a string#include without "path" or <name>quote the path. In the C seed, <name> itself produces this message
unterminated #include <name>a < with no > before end of fileclose the bracket
directive expects a string#token/#infix/#prefix given something that is not a stringquote the lexeme
directive not yet supporteda directive the parser recognises but does not handlenone: this is reachable only from a partially added directive
#define expects a name#define followed by something that is not an identifiergive it a name
#define expects a constant expressionthe value does not fold to a constant#define is a folded constant, not a textual macro; it cannot mention a variable
duplicate #definethe same name defined twicerename one, or delete the second
name already defined by #definea local, parameter, global or function declared with a name a #define ownsrename either, in whichever order they appear
#token expects a string#token without a quoted lexemequote it
#infix expects the precedence#infix "op" with no integer after the lexemeadd a precedence, 1..100
#infix expects left or rightthe associativity word missing or misspelledwrite left or right
precedence out of 1..100the precedence is outside the range (from #infix or syntax_infix)pick a value in 1..100; the core uses 1..10
#dylib expects a string#dylib without a quoted pathquote the path, or write #dylib "" to return to libSystem
#embed expects NAME "path" [lz]the name or the quoted path is missingwrite #embed name "file", optionally followed by lz
#embed file is empty or over 16 MiBthe file is 0 bytes or exceeds the declared ceilingevery byte costs an AST node, so the arena is the real bound long before 16 MiB
#section expects the section name#section with a segment but no section namegive both, or write #section alone to return to the defaults
#section expects constant flagsthe flags argument does not folduse a literal or a #define
#section expects constant alignmentthe alignment argument does not foldsame
section flags out of 32 bitsthe flags value does not fit in u32it is the Mach-O flags word: 0x80000400, 1, 0, …
alignment out of 0..15the alignment is not a log2 in 0..15write 3 for 8 bytes, 4 for 16
#opcode expects a name#opcode not followed by an identifiername the instruction
expected ( in #opcodeno parameter listwrite name(a, b) even when empty: name()
parameter name expected in #opcodea non-identifier in the parameter listparameters are plain names
at most 12 parameters in #opcodemore than 12 parameterssplit the encoding into two opcodes
expected ) in #opcodethe parameter list is not closedclose it
duplicate #opcodetwo #opcodes with the same namerename one

3. #rule #

messagecausefix
#rule expects category stmtno category word after #rulewrite #rule stmt:
#rule expr: reserved, not yet supportedthe category exprreserved. Use #infix/#prefix, or syntax_expr from a module
#rule only knows category stmtany other categoryonly stmt exists
expected : after the #rule categorythe colon is missingwrite #rule stmt: …
empty #rule patternnothing between the colon and =>a pattern needs at least one item
the #rule pattern must open with a literal tokenthe first item is expr/stmt/blockdispatch is by the opening token. Only a leading ident $x may precede the literal
cannot redefine core keywordthe dispatch literal is a type word or a control wordif, loop, return, i64, … are refused because a rule opened by them would hijack the core's parser. Punctuation and new #tokens are free
expected $name in the patternan nt with no $name after itwrite expr $c, block $b, ident $x
duplicate hole in #rulethe same $name twice in one patternrename one
too many holes in #rulemore holes than one rule may carrysplit the rule
too many name holes in #rulemore ident $x/$$t names than one rule may carrysplit the rule
too many items in the #rule patternthe pattern is longer than one rule record allowssplit the rule
nt `type` is out of scope for M9type $t in a patternnot implemented; it is what a real struct would need. Use #define offsets and accessors
#rule without =>the arrow is missingPATTERN => TEMPLATE
hole $name has no rule binding ita $name in the template that the pattern never boundcheck the spelling, or add the item to the pattern
hole out of range in templatean expansion referenced a hole index the rule does not havean internal inconsistency; reduce the rule and re-add items
$$name only works in a #rule templatea gensym outside a template$$t is only meaningful on the right of =>
the rule expectedthe matched rule wanted a literal that is not there — the detail is the lexemeonce a rule is chosen there is no backtracking; write what the pattern says
the rule expected a namethe rule wanted an ident $x and got something elsesupply a plain identifier
the rule expected a name on the lefta compound rule (ident $x += …) whose left side is not a plain namea.b += 1 cannot match; assign explicitly
too many nested rulesmore than 64 levels of rule-inside-rule at definition timeflatten the templates

4. Parser — expressions and statements #

messagecausefix
expression expecteda primary was required and none was thereoften the real cause is a missing #token: without it a compound operator is lexed as two tokens and the Pratt loop stops far from the mistake
expected ) after expressionan unbalanced ( in an expressionclose it
expected ) in cast(type without the closing )write (u32) x
cast to void(void) xthere is no value of type void; drop the cast
expected ) in callan argument list is not closedclose it
call by name onlycalling something that is not a plain namefor an indirect call use callp(p, …)
& expects a name& applied to an expression& takes a local, a global or a function
name expectedp_ident() required an identifiersupply a name
name expected here / variable name expected / parameter name expected / name expected at top levela declaration name is missingsupply it. If the token is a taught word, the message is the next one instead
name reserved by a syntax/type_alias registrationthe name is a word some Tier 3 registration claimed — the detail is the word. Since M41.5 that word may be a core operator, if a module taught + with syntax_infix; since M45 it may be i32, which the core registers for itselfa registration reserves the word for the whole program. Rename the identifier, or choose a different word in the module
type expectedp_type() required a type wordone of the seven core types, or a type_alias
type expected at top levela top-level declaration that starts with neither a type nor a taught wordthis is what the default compiler says about a source written for a taught one
type expected in parametera parameter with no typewrite i64 x, never a bare x
type expected in externextern with no return typeextern i64 name(...)
name expected in externextern with a type but no nameadd the name
parameter of type voida parameter declared voiddrop it
local of type voida local declared voidthere is no value of that type
global of type voida global declared voidsame
at most 12 parametersmore than 12 parametersthe ABI limit (8 in registers, 4 on the stack): pass a pointer to a struct-like block instead
expected ( in the parameter list / expected ) in the parameter listthe parentheses of a declarationclose them
expected ( after if / expected ) after conditionif without its parenthesised conditionwrite if (c) …
expected {a block was requiredloop, a function body and a block $b hole all need braces
unterminated blocka { with no } before end of fileclose it
expected ; after declarationa local declaration without its semicolonadd it
expected ; after assignmentan assignment statement without its semicolonadd it
expected ; after expressionan expression statement without its semicolonadd it
expected ; after returnreturn without its semicolonadd it
expected ; after breakbreak or break N without its semicolonadd it
expected ; after continuecontinue or continue N without its semicolonadd it
expected ; after externan extern declaration without its semicolonadd it
expected ; after the globala global declaration without its semicolonadd it
left side of assignment must be a nameassigning to something that is not a plain nameuse st8/st64 for memory, or a taught syntax_infix for members
break expects a positive levelbreak 0; or a negative levelbreak; is break 1;
continue expects a positive levelcontinue 0; or a negative levelcontinue; is continue 1;
expected ] in the array sizean array declaration's bracketclose it
array size must be a positive constantN in type x[N] does not fold to a positive integeruse a literal or a #define
expected { in the array initializer / expected } in the array initializerthe braces of = { … }close them
empty array initializer= { }give at least one element, or drop the initializer
initializer must be constantan element that does not foldinitializers are constants
initializer with too many elementsmore elements than the declared Nraise N, or write type x[] = { … } and let it be inferred
a string only initializes uptra string element in an array that is not uptra string element becomes a relocated pointer; the element type must be uptr
global initializer must be constanta global's scalar initializer does not foldsame rule
global array too largethe declared array exceeds the addressable sizereduce it
division by zerox / 0 or x % 0 with both sides constantfolding happens at compile time
operator without constant foldingan operator used where a constant is required and the folder has no rule for ituse a simpler constant expression

5. Tier 3 handler contract #

messagecausefix
syntax handler consumed no tokens: <word>a syntax handler returned without calling p_next — the detail is the word it was registered forthe handler must consume at least that word; the guard exists to stop an infinite loop
syntax_stmt handler consumed no tokens: <word>the same at the statement positionsame
syntax_expr handler consumed no tokens: <word>the same at the expression positionsame
syntax_expr handler produced no expression: <word>the handler returned 0an expression position has no empty node to fall back on; return a node
syntax_param handler consumed no tokens: <word>M41.5: a syntax_param handler returned a node without advancinga handler that claims the parameter has to read it; return 0 to leave it to the core
syntax_param handler consumed tokens and returned 0: <word>M41.5 (review): the handler read part of the parameter and then declined. parse_params would read what was left as a whole parameter — an arity change with no diagnostic0 means "I read nothing"; either claim the parameter and return an N_PARAM, or decline before consuming anything
syntax_param handler did not return a parameterM41.5: what the handler returned is not an N_PARAMbuild it with param_new(ty, name); the node goes straight into a list gen_lower walks by nd_type/nd_name
syntax_type handler consumed tokens and returned 0: <word>a syntax_type handler read a suffix and then declined; the core would read the rest of the declaration from the middle of a type. The detail is the type word the handler was given0 means "I read nothing"; either claim the suffix and return a type id, or decline before consuming anything
syntax_type handler consumed no tokens: <word>a type came back with the cursor unmoved — there is no suffix, so the answer cannot be about this positionreturn 0 to leave the core's type alone
syntax_type handler returned an invalid type: <word>the id is negative or at or past type_count(). It goes straight into type_width/type_align/type_kindanswer 0, a core TY_*, or an id a type_new returned
syntax_lit handler consumed tokens and returned 0: <literal>M41.5 (review): the same rule at M24's literal position — the handler moved the cursor with p_take_lit and then declined, and the core would build an N_INT out of a token whose span no longer covers what was readdecline before calling p_take_lit, or return the node
syntax_infix handler produced no expression: <operator>the same for an operator handlerreturn the resulting node
operator already taughta second syntax_infix on the same token, a core one (+) included since M41.5 — the FIRST registration on a core operator is allowed, because it carries no handler to overridea second registration is a mistake, not an override. A later #infix on the token is allowed: it clears the handler and the template wins
cannot redefine core keywordsyntax/syntax_stmt/syntax_expr/syntax_infix/type_alias on a core wordchoose another word
type_alias with invalid typethe base is not a type id that exists — one of TY_VOID..TY_UPTR, or one a type_new returneduse a core type constant, or the id type_new gave you
type_new with a width below 1M24: a registered type has to occupy at least one bytethe detail is the type's name
type_new with an alignment below 1the same for the alignment
type_new with an unknown kindthe kind is not TK_INT, TK_FLOAT, TK_WIDE, TK_OPAQUE or TK_SINT (M45)
type_set_width only declares the width of uptrM41: type_set_width with any ty but TY_UPTRi64 folds in 64 bits at parse time and u8/u16/u32/u64 are their own names; register a new primitive with type_new() instead
uptr width must be 1, 2, 4 or 8M41: type_set_width(TY_UPTR, w) with any other wa word the relocation length can be the log2 of
type_disable with an unknown typeM41: the id is negative or past type_count()pass a TY_* constant, or an id type_new returned
type_disable beyond the maskM41: a type id above 62 — the disable set is one i64 of bitsdisable it before registering 60 other types, or leave the word in
too many intrinsic_disableM41: more than 32 intrinsic_disable() namesthe ceiling is fixed on purpose, like the subcommand table
too many subcommandsM41: more than 16 subcommand() registrationsthe ceiling is fixed on purpose: the number of subcommands is a property of the compiler
too many on_plan hooksM41: more than 8 on_plan() registrationssame
p_skip_balanced expects the opening tokenthe parse was not sitting on the opening tokenposition the handler on the {/</( before calling
unterminated regionp_skip_balanced reached end of file — reported at the opening tokenclose the region; the position points at what never closed
region crosses a file boundarythe OPENING delimiter of a recorded region was read in one #include frame and the CLOSING one in another — a { in an include and its } in the includer, or the reverse. Reported at the opening token. A region that merely ENDS on the last token of an included file is not this: it is a slice of one buffer and is accepted (M45)a span is a slice of one buffer; keep both delimiters of a recorded region inside one file
p_take_lit outside the source tokenM24: a syntax_lit handler said its literal ends before the cursor, past the end of the source, or on a token that was not just lexed from the source being readsame rule as p_resplit_punct: never a string, never a substituted identifier
p_resplit_punct expects a longer punctuation tokenthe current token is not a punctuation token longer than n, freshly lexed from the sourceit works only on a token just read from the source being lexed — never a string, never a substituted identifier
p_resplit_punct: unknown punctuationthe first n bytes are not a registered lexemesplit at a boundary that is a real token, as >>>

6. Codegen #

messagecausefix
unknown namean identifier that is no local, global, function or externdeclare it, or check the spelling. Codegen looks for a local, then a global, then a signature
call to unknown functiona call to a name with no signaturedeclare a prototype or an extern before the call
wrong number of argumentsthe call's arity does not match the declarationfix one of the two
wrong arity in intrinsicld*, st*, emit, reloc with the wrong countld* takes 1, st* takes 2
callp expects 1 to 12 argumentscallp() empty, or with more than 11 arguments after the pointerthe pointer counts towards the twelve
function declared twicetwo definitions of one namedelete one
declaration does not match prototypethe definition's return type or arity differs from the prototypemake them agree
prototype with no definitiona prototype nobody defines and no externdefine it, or declare it extern
global name declared twicetwo globals with one namerename one
value of type voida void call used where a value is neededvoid produces no value
assignment to arrayassigning to an array namean array name is an address; write through st*
break out of rangebreak N; with N greater than the enclosing loop depthcount the loops again
continue out of rangecontinue N; with N greater than the enclosing loop depthcount the loops again
continue outside loopcontinue; with no enclosing loopremove it, or add the loop
expression too deepmore than 64 levels of expression nestingsplit into statements
frame too largelocals + spill area exceed 4095 bytessub sp, sp, #imm carries only 12 bits; move data into a global
local array too largeone local array exceeds 4095 bytesmake it a global
unary operator with no codegen / binary operator with no codegen / expression with no codegen / statement with no codegena node kind the generator has no rule forreachable when a module builds a node the core does not lower; check the node kind the handler produced
instruction with no encoder / instruction with no dumpan I_* opcode the encoder or the dumper does not handlethe same, one layer down: a backend produced an instruction the core cannot encode
x86 instruction with no encoder / x86 instruction with no dumpthe same two, from the x86-64 machine and its X_* opcodes. There is no third: MTASK_INS_SIZE is the encoder over a scratch buffer, so an opcode with no encoder has no size eithersee machine.md
no machine registeredthe walker was asked to lower with no machine table in effect. Since M41 mc_main says it before gen_lower as well, so a compiler that registered none fails at a defined point instead of mid-loweringmain() registers both and mc_main makes the host's current; a recreated compiler registers its own from user_init()
<word>: removed by this compilerM41: a type word type_disable() removed, at the token that used it, or an intrinsic name intrinsic_disable() removed, at the call. The detail is the word. i32: removed by this compiler is the M45 case a machine with no sign-fill produces (examples/avr)this dialect does not have it. type_disable removes the WORD, not the type: ld32() still yields TY_U32 internally
cannot shadow a core intrinsicM24: intrinsic() was given the name of a built-in one (ld64, st64, emit, reloc, callp, …), which the dispatch would never reachpick another name; the detail is the one asked for
intrinsic with an impossible aritythe arity is negative or above MAXPARAMS
intrinsic with an unknown result typethe result type is not a type id that existsregister the type first
machine_slot outside the task tableM24: the task index handed to machine_slot is not in 0 .. MTASK_COUNT - 1the slot list is machine.md § 2
unknown machine--machine=NAME, or a backend's machine_use, named something not registeredarm64 and x86_64 are built in; the detail is the name
add/sub immediate out of 12 bits / cmp immediate out of 12 bitsa folded immediate does not fit the instructionmaterialise it into a variable first
immediate and mask not supportedan and with an immediate the bitmask encoding cannot expressput the mask in a variable
memory offset out of rangea frame offset outside the scaled ldr/str rangethe frame is too large or too fragmented; reduce the locals
branch too fara branch beyond the 19-bit range the encoder checks uniformly (±1 MiB)split the function
function in a zerofill sectiona function defined while a #section … 1 (S_ZEROFILL) is in effectzerofill sections have no file bytes; switch sections before the function
global with initializer in a zerofill sectionan initialised global in a zerofill sectionsame: an initialiser needs file bytes

7. emit(), reloc() and #opcode #

messagecausefix
emit expects an argument / emit expects a constantemit() empty, or with a non-constantit writes one folded 32-bit word
emitted word does not fit in 32 bitsthe folded word exceeds 0xffffffffmask it
#opcode argument not constantan argument of a #opcode call does not fold#opcode is an encoder, not a function: everything must be known at compile time
wrong number of arguments in #opcodethe call's arity does not match the declarationfix one of the two
reloc expects two argumentsreloc() with a different countreloc(TYPE, "symbol")
relocation type must be constantthe first argument does not folduse BRANCH26, PAGE21, PAGEOFF12 or UNSIGNED
reloc expects the symbol in quotesthe second argument is not a stringquote the symbol name, with its leading _
unknown relocation typea value that is not one of the foursee objects.md
two relocations for the same wordtwo reloc() calls with no emit() between themone relocation per word
reloc without an immediately following emita reloc() with no word after it before the next label or the end of the functionput the emit()/#opcode call right after it
reloc UNSIGNED requires 8 bytes: use a global array initializerreloc(UNSIGNED, …) before a 4-byte wordit is an 8-byte relocation and would overrun the next instruction. Write the address as a global array initializer instead

8. Object writers #

messagecausefix
duplicate symboltwo definitions of one symbol namerename one; the detail is the symbol
no _main: cannot generate an executable--exe on a unit with no mainadd i64 main(), or produce an object instead
UNSIGNED that does not occupy 8 bytesan UNSIGNED relocation of the wrong length reached a backendthe same cause as the reloc UNSIGNED message, seen at write time
relocation not supported in the direct executablea relocation kind macho-exe cannot resolve itselfuse the .o + ld path for that construct
relocation not supported in the ELF object / relocation not supported in the x86-64 ELF objecta relocation kind the ELF writer has no mapping for on that architecturesame, for Linux targets. An AArch64 reloc(BRANCH26, …) compiled for arch = "x86_64" lands here
relocated pointer in __TEXT: the segment is r-x and dyld will not rebase ita global with a relocated pointer placed in a __TEXT sectionput the data in __DATA
pageoff12 misaligned for the access widtha PAGEOFF12 target not aligned for the load/store width it patchesthe address must be aligned to the access width
misaligned bl target / bl too fara call target not 4-byte aligned, or beyond ±128 MiBthe program is too large for one __text; split it
too many segments for the rebase opcode / too many segments for the bind opcodemore distinct segments than a dyld opcode can indexreduce the number of distinct #section segment names
too many dylibs for the bind opcodemore #dylib/[libs] entries than the ordinal encoding allowslink fewer libraries, or use [linker]

9. Command line and driver #

messagecausefix
unknown optiona flag starting with - that is not recognisedthe list is cli.md. --exe and --backend= other than macho do not exist in the C seed
-o requires an argument-o at the end of the command linegive it a path
duplicate entrytwo source files on one command linemc compiles one file at a time; use mc build for a project
--config requires an argument--config at the end of the command linegive it a path
duplicate directorytwo directories after mc build / mc limitspass one
--entry-only and --compiler-only are exclusiveboth halves of a taught build asked for at once--compiler-only builds the compiler and stops; --entry-only compiles the entry with the running binary. Pick one
unknown backend: NAME (then registered: …)--backend= naming something not registeredthe message lists what exists
no backend: use --backend=NAMEM41: no --backend=, no --exe, no target() registered for this host and no backend_default() — a recreated compiler that registered a writer and forgot to name itcall backend_default("name") from user_init(), or pass --backend=
<os> requires a linker: there is no direct executablepost-M41 review: --exe on a host whose target() registration has 0 in the exe slot — windows today, and any pair a module registers that way. linux was on this list until M42 gave ELF a direct executablecompile to an object (mc x.mc -o x.o) and link it, or use mc build with a [linker] section. This is the single-file CLI's wording of the driver's <os> requires [linker]: there is no direct executable (§ 10): same registration, same refusal, and [linker] is a TOML section a command line does not have
<os>/<arch> has no object backend: use --exepost-M41 review: the object backend was left to the host and that pair's target() registration has 0 in the object slot. Only a module can write that 0 — target(os, arch, 0, exe) is what a board whose flat image is the whole artefact registers (examples/kernel) — and it is reached by a plain mc x.mc -o x.o on such a compiler. It used to be handed to backend_find(), whose str_eq dereferenced it: SIGSEGV, exit 139, no messageask for the executable instead (mc --exe x.mc -o x), which is what that registration says the target has. This is the single-file CLI's wording of the driver's <os>/<arch> has no object backend: use kind = "exe" (§ 10): same registration, same refusal, and kind is a TOML key a command line does not have
the host is not a registered targetthe backend was left to the host and the target() registry has no entry for host_os()/host_arch(): with no --backend= when some target is registered but not this one, and always with --exe, which means "a direct executable for the host"register the pair from user_init(), or name the writer with --backend= (or backend_default())
cannot runposix_spawnp could not start the linker or the taught compilercheck the cmd in [linker], and that a relative compiler path starts with ./
cannot spawn: <tool> (error N)a tool mc may legitimately find missing — curl, wget, tar, llvm-dlltool — is there and could not be started anyway. N is the number posix_spawnp returned: 13 EACCES (not executable), 8 ENOEXEC (not a program), 7 E2BIG (on a Windows host, a command line past WH_CMDMAX). Only ENOENT (2) is silent, and means "not on PATH"fix the permissions on the tool, or take the broken copy off the PATH
waitpid failedthe spawned process could not be waited fora system-level failure
posix_spawn_file_actions_init failedthe spawn could not be set upsame
xcrun --show-sdk-path failed{sdk} was used and xcrun failedinstall the command line tools, or write the SDK path literally
too many arguments in [linker].argsmore than 64 arguments after {libs} expansionshorten the list
--libc must be gnu or musl: <value>post-M42 patch: --libc= with anything else. libc names a family, not a sonamegnu or musl (cli.md)
--link must be dynamic or static: <value>post-M42 patch: --link= with anything elsethose are the two values
--libc applies to an executable: use --exe (also --interp, --link)post-M42 review: one of the three Linux flags on a run that writes an object — a plain mc x.mc -o x.o, or a --backend= the target registry names in an object slot. Only the executable writer reads them (PT_INTERP and DT_NEEDED are program-header fields; an object has neither), so before this the flag was accepted and did nothingask for the executable: mc --exe --libc=gnu prog.mc -o prog on a Linux host, or mc --backend=elf-exe --libc=gnu prog.mc -o prog from any host. What counts as an executable writer is the exe slot of a target() registration, so a target a module registered answers for its own writer
--libc applies to an executable: a --dump-* mode writes none (also --interp, --link)post-M42 review: one of the three flags together with --dump-tokens/--dump-ast/--dump-asm/--dump-syms/--dump-rules/--dump-machine. A dump returns before any backend is reached, so the flag can affect nothing — including with --exe, which the dump overridesdrop the flag, or drop the dump
--libc applies to a linux target (also --interp, --link)post-M42 patch: one of the three Linux flags asking for the HOST's executable (--exe) on a host that is not Linux — so the flag would describe an image this command is not going to write. Ignored would be worse than refuseddrop the flag, or name the writer: mc --backend=elf-exe --libc=gnu prog.mc -o prog cross-builds from any host
static link with imports needs [linker]: see docs/build.md -- static linking (M46)post-M42 patch: --link=static (or [target].link = "static", § 10) on a program that imports anything -- a libc symbol or a #dylib one, the import count does not distinguish. mc has no archive linker; the static image it writes is the one for a program that imports nothingeither drop the imports (<sys_linux> is raw syscalls and imports nothing), or take the [linker] road with ld.lld and a sysroot (../build.md). M46 is the milestone that would remove the need for it

10. mc.toml #

All of these carry file:line:col.

messagecausefix
key expecteda line that starts with neither a key nor [check for a stray character
expected = after the keya key with no =add it
value expectednothing, or something unrecognised, after =strings need quotes: entry = main.mc is this error, and the column points at the bare word
unexpected text after the valuetrailing content on the lineone key per line; comments start with #
unterminated stringa string with no closing quoteclose it
unknown escapean escape other than \" \\ \n \t \ruse one of those
expected ] in the table header[table with no ] (or [[table] for an array of tables)close the header
quoted key must not contain .a quoted key containing .the table is flat and paths join with ., so "b.c" under [a] and c under [a.b] would collide. Rename, or use a real sub-table
unterminated arraya [ value with no ]close it; arrays may span lines and allow a trailing comma
expected , or ] in the arraytwo values with no commaadd it
digit expected after .a float ending in .write 0.25, never 0.
float with more than 4 fraction digitsmore precision than basis points can holdthe only float key is [limits].tolerance, kept as basis points: at most four digits

Driver-level key errors, reported at the key's position when it exists and at the file when it does not:

messagekeyfix
missing keyproject.entry, project.out, compiler.modules, compiler.outadd it. compiler.out is only required when there is no project.name to default from. sysroot.path is not on this list since M25: an absent [sysroot] is not an error, it is step 1 of the chain of § 11 falling through to the next step
must be exe or objproject.kindthose are the two values
only macos, linux and windows (see docs/build.md)target.osthe list is built from the target() registry, so a target a module registers appears in it
only aarch64 and x86_64 (see docs/build.md)target.archthe architectures that operating system was registered with (macOS and Windows have only aarch64)
<os> requires [linker]: there is no direct executabletarget.osthe pair is registered and its executable slot is 0, which is a registration saying this target has no direct executable at all. Today that is windows/aarch64 and windows/x86_64 — there is no PE writer — and any pair a module registers with target(os, arch, obj, 0). It was linux until M42 gave ELF one (elf-exe). Add a [linker] section, or kind = "obj" and link the object yourself
<os>/<arch> has no object backend: use kind = "exe"target.osthe mirror of the row above: the pair is registered, its object slot is 0. Only a module can register that — target(os, arch, 0, exe) is what a board whose flat image is the whole artefact writes — so it is reached by asking such a target for kind = "obj", or for an exe through a [linker] (which goes through the object step). Drop the [linker] and use kind = "exe"
libc must be gnu or musl (a soname is not a value: gnu is libc.so.6, musl is libc.so)target.libcpost-M42 patch: the key names a family. libc = "libc.so.6" was the M42 spelling and the message carries the migration
link must be dynamic or statictarget.linkthose are the two values
libc applies to a linux target (also interp, link)target.libc, target.interp, target.linkpost-M42 patch: the three keys describe a Linux dynamic image, which no other target has. Delete the key, or set os = "linux"
static link with imports needs [linker]: see docs/build.md -- static linking (M46)target.linkpost-M42 patch: link = "static" on a program that imports any symbol. Raised by the executable writer, which counts imports, and reported at the key's own position through the reporter the driver installs (dyn_die, src/objmodel.mc). Add a [linker] — with one present the driver writes the object and hands it over, and the key never reaches the writer

| must be a relative path | compiler.out | the generated compiler source lives next to it and includes by relative path | | must not contain .. | compiler.out | same reason; the .. check runs on the string as written | | library not declared in [libs] | an [externs] value | the value must name a [libs] key | | tolerance must be between 0 and 1 | limits.tolerance | a float in [0, 1], at most four fraction digits |

Since M39.5 the four target.* rows are raised after user_init() has run and after the entry source has been opened and lexed — that is what lets a taught compiler register its own (os, arch) pair and be driven by mc build (docs/build.md § M39 / M39.5). They therefore come out after the compile x -> y step line, and after nothing else: the resolution happens before the unit is parsed, so an unknown target is still reported ahead of any error in the program itself. The message, its file, its line and its column are what they always were.

The three --libc/--interp/--link rows of § 9 are raised at the same point and for the same reason (post-M42 review): whether the run writes an executable is a question for the target() registry, and a module may register a target from user_init(). So a command line whose entry file does not exist reports cannot open first, and the flag refusal only after — the resolution still happens before the unit is parsed, and before every dump.

mc sysroot stub (sysroot.md § 7) runs the same resolution at the same point, so the first two rows come out of it byte for byte as they come out of a build. It asks for no backend at all, which is why the two rows about a missing slot cannot be reached from it.

11. The sysroot, and exit code 2 #

One message, printed by sysroot_missing() (src/sysroot.mc) and shared by mc build and by mc sysroot. It is the only thing that exits 2 — "the environment is not ready", as opposed to 1 (a diagnostic) and 3 (the limits verdict), cli.md § Exit codes.

mc: no sysroot for linux-aarch64
  tried: build/sysroot/linux-aarch64 (no crt1.o)
         /Users/me/.mc/sysroots/linux-aarch64 (no crt1.o)
  run:   sh scripts/sysroot-linux.sh --arch aarch64
linewhat it says
no sysroot for <os>-<arch>the target whose {sysroot} could not be resolved
tried:one line per candidate the chain looked at, with the reason it was refused: no <marker>, the first of that target's marker files (sysroot.md § 2) that the directory does not hold. A directory that does not exist at all is refused with the same words, and deliberately — the chain probes the marker FILES and never the directory, because open on a directory is not portable to a Windows host
run:the command that would produce one, per operating system

It is the one diagnostic in this compiler with no file:line:col, on purpose: the chain runs lazily, the first time an [linker].args value asks for {sysroot}, so there is no single key to blame — and mc sysroot path <target> prints the same text with no config open at all. Every tried: line names an absolute directory instead. docs/specs/M25.md § Deviations records it.

mc sysroot fetch fails through the same run:/or: block and the same exit 2, under its own first line:

messagecause
mc: no downloader on this PATH (tried curl)neither host_downloader() nor its alternative is on the PATHENOENT from both spawns, and nothing else: a downloader that is there and refuses to start is the cannot spawn of § 9 instead
mc: the download failed (exit N)the downloader ran and returned non-zero. curl exit 1 here is the --proto '=https' guard refusing a redirect to plaintext
mc: checksum mismatch for <file>the bytes are not the pinned row's. Both digests are printed
mc: wrong size for <file>the length is not the pinned row's
mc: tar could not extract <file>the tar spawn returned non-zero; its own diagnostic came out on stderr just above
mc: the archive did not carry <name>tar returned 0 but a member of the row, or a marker, is not there. The download and the directory's marker files are removed, so the partial directory cannot be mistaken for a sysroot later

Causes, in the order the chain runs them (sysroot.md § 1):

causefix
[sysroot].path names a directory that is not a sysrootfix the path, or populate the directory. An explicit path stops the chain: nothing else is tried after it
the host is not the target, and nothing is cachedmc sysroot fetch <os>-<arch> --yes, or sh scripts/sysroot-linux.sh --arch <arch>, or point --sysroot-dir at a directory you already have
the host is the target but the system has no muslon Debian/Ubuntu apt-get install musl-dev, on Alpine apk add musl-dev
no HOME, no [sysroot].cache and no --sysroot-dirthe tried: line says exactly that; give the chain one of the three

The stub writers ({stubs}, mc sysroot stub, sysroot.md § 9) have three of their own, all exit 1:

messagecausefix
mc: no stub writer for: linux: a static libc is code, not a name list{stubs} or mc sysroot stub on a Linux targeta libc.a cannot be synthesized from a name list; mc sysroot fetch linux-<arch> --yes
mc: cannot run llvm-dlltool for: PATHthe Windows stub writer could not start llvm-dlltoolput it on PATH (Homebrew keeps it in opt/llvm/bin)
mc: llvm-dlltool failed for: PATHit started and returned non-zeroits own diagnostic came out on stderr just above; the .def it was given is the named file

11b. mc sandbox #

Its exit codes are its own — 124 a cap, 125 a refusal, 126 the box could not be set up (cli.md § 3c) — and 2 stays the option errors' code, as everywhere else.

messagecausefix
mc: the sandbox is a Linux feature; on this Mac: limactl shell mc-k7 build/mc-linux-arm64 sandbox run PATH (docs/build.md § Lima)any mc sandbox verb on macOS. Exit 126run the command it prints. There is no macOS sandbox and there will not be one (sandbox.md § Hosts)
mc: the sandbox is a Linux feature; this host is: windowsany mc sandbox verb on Windows. Exit 126the same: it is a Linux feature
mc: unknown sandbox subcommand: WORDthe verb is not run, exec or check. Exit 2, and the usage followsone of the three
mc: unknown sandbox option: --xan option mc sandbox does not take. Exit 2the list is in cli.md § 3c
mc: option requires an argument: --time--time, --wall, --mem, --out, --stdin, --ro, --cwd, --root, --config or --report was last on the line. Exit 2give it its value
mc: not a number: abca non-decimal value for --time/--wall/--mem/--out. Exit 2a non-negative decimal
mc: --mem: number too largethe value has more digits than any cap can have (the accumulation stops at 10^12). Exit 2a real number: the maxima are in cli.md § 3c
mc: --mem: at most 1048576past the option's maximum — 86400 for --time and --wall, 1048576 MiB for --mem, 65536 MiB for --out. Exit 2a value inside the range
mc: --time: must be at least 1zero, for any of the four caps. Exit 2one or more. A cap of zero is a box that cannot start
mc: unknown --allow value: Xonly threads exists today. Exit 2--allow=threads
mc: unknown --libc value: X--libc takes musl or gnu. Exit 2one of the two, or leave it out and let PT_INTERP decide
mc: too many --ro directoriesmore than 16. Exit 2the box mounts one bind per --ro; group them under a common parent
mc: sandbox run needs a source path / mc: sandbox exec needs a programthe verb had options and nothing else. Exit 2give it the path
mc: sandbox run: not in this step / mc: sandbox exec: not in this stepa compiler built before M43 step B, where only mc sandbox check existed. Exit 126gone since step B: run and exec build the box (sandbox.md)
sandbox: cannot install the Landlock ruleset: ERRNO / cannot install the seccomp filter: ERRNO / cannot fetch the seccomp listener: ERRNOthe three step-C ways the box can fail to be built, reported like every other cannot site. Exit 126mc sandbox check first: a kernel below Landlock ABI 4, or without SECCOMP_RET_USER_NOTIF, cannot run this box

The report is not a diagnostic table either: it is a fixed vocabulary written to stderr (and to --report FILE) after the box is gone, and sandbox.md § The report is where it is defined. The five lines that end a box with 125 are the ones a person is most likely to have to read, so they are here too:

linecausefix
sandbox: refused: syscall N (name)the step made a call that is in no profile for its step, architecture and C library. The number is this architecture'sif the call is legitimate for the corpus, re-measure: make sandbox-trace (sandbox.md § The profiles). socket, connect and bind are refused on purpose; clone, clone3, fork and vfork never reach this row at all -- they have two of their own below
sandbox: refused: open PATHan openat/open of a path under none of the box's roots (/src, /out, /mc, /lib, /lib64, /usr/lib, /roN)--ro DIR puts one more tree in the box, at /ro0, /ro1, … A program that probes for a file it does not expect to find is stopped just the same: the box does not answer ENOENT for a path it will not look at
sandbox: refused: mmap N bytes over the cap (M)the running total of what the step has mapped went past --memraise --mem; RLIMIT_AS is the same number and the kernel's own wall behind it
sandbox: refused: process limit (N)the step created one process more than it may: 16 in a compile step, 0 in a run step, 64 with --allow=threads. Every clone, clone3, fork and vfork is counted — no profile allows onewith --allow=threads a real thread is free and never counted. A compile that legitimately needs more processes than sixteen is a project this box will not build
sandbox: refused: clone with namespace flagsa clone or clone3 whose flags carry a CLONE_NEW* bit: a program in the box asking for a namespace of its ownthere is no option for it. The box is a set of namespaces and one made inside it is where an unprivileged process starts collecting capabilities
sandbox: refused: clone3 with unreadable argumentsa clone3 whose struct clone_args could not be read out of the step with process_vm_readv (the flags live there and BPF cannot follow a pointer)a process-creating call whose flags cannot be inspected is not one to let through
sandbox: refused: execvea step exec'd more times than its budget — three for a compile step, one for a run stepa program that re-execs itself is out of scope for the box

mc sandbox check is not a diagnostic: it writes its six lines to stdout and exits 1 when a capability is missing. The one line most likely to need reading is userns: restricted (apparmor)sandbox.md § The AppArmor restriction says what it means and what the two ways out are.

12. Runtime and I/O #

messagecausefix
mc: cannot open: PATHa source, include or config that cannot be readcheck the path. For #include "x" the search is the includer's directory, then each [include].paths root
mc: cannot create: PATHthe output could not be createdcheck the directory and its permissions; mc build creates parent directories, the single-file CLI does not
mc: read error: PATHa short or failed readthe file changed under the compiler, or the medium failed
mc: arena exhausted (R MiB reserved, E MiB estimated, asked N bytes) while parsing FILE:LINE -- raise [limits].tolerance or HEAP_SIZEthe compiler's arena could not growthe arena starts at a static 32 MiB and grows by mmap; this means the kernel refused. The numbers say what was already reserved, what the plan had estimated and what did not fit; while parsing is where the parser was, and appears only while it is parsing: the pre-scan, the passes, the codegen and the object writer report no position rather than the last line the lexer happened to reach. Raise [limits].tolerance so the estimate reserves more up front, or split the translation unit
mc: cannot reserve the arena (…) -- raise [limits].tolerance or HEAP_SIZEthe initial reservation failedsame message, same numbers, at the first mmap

Reproducing them #

Each of these is a real, compiled sample:

// expect-error: cannot redefine core keyword
#rule stmt: if ( expr $c ) block $b => loop { $b }
i64 main() { return 0; }
// expect-error: reloc UNSIGNED requires 8 bytes
i64 main() {
    reloc(UNSIGNED, "_main");
    emit(0x00000000);
}
// expect-error: name already defined by #define
#define LIMIT 10
i64 LIMIT = 5;
i64 main() { return 0; }
// expect-error: \0 not allowed in string
#include <sys>
i64 main() { write(1, "a\0b", 3); return 0; }
// expect-error: at most 12 parameters
i64 f(i64 a, i64 b, i64 c, i64 d, i64 e, i64 g, i64 h, i64 i,
      i64 j, i64 k, i64 l, i64 m, i64 n) { return a; }
i64 main() { return 0; }

Edit this page