mc.toml, every key
mc build [DIR] reads DIR/mc.toml (or --config FILE). Every path in the file is relative
to the directory of the config, never to the working directory, so mc build examples/api from
the repository root does exactly what mc build does from inside examples/api.
The parser is src/toml.mc; the driver that reads these keys is src/driver.mc.
Every key on this page is in the recorded public surface (the toml kind), compared on every
make check; what a PATCH, a MINOR and a MAJOR may do to one is hooks.md § 8.
The whole file #
[project]
name = "api" # only used to default [compiler].out
entry = "main.mc" # required
out = "build/api" # required
kind = "exe" # exe (default) | obj
[target]
os = "macos" # macos (default) | linux
arch = "aarch64" # aarch64 only
[compiler] # optional: build a taught compiler first
core = "../../src/core.mc"
modules = ["mc-api.mc"]
out = "build/mc-api"
[linker] # optional on macOS; REQUIRED when os = "linux"
cmd = "ld"
args = ["-arch", "arm64", "-syslibroot", "{sdk}", "-lSystem",
"-o", "{out}", "{obj}", "{libs}"]
[sysroot]
path = "build/sysroot/linux-aarch64"
[libs]
sqlite3 = "/usr/lib/libsqlite3.dylib"
[externs]
"sqlite3_*" = "sqlite3"
[include]
paths = ["lib"]
[deps] # optional: packages, resolved through mc.lock
geo = "1.2.0" # a version constraint (bare = minimum); the lock pins the exact one
plot = "^1.4.0" # ^/~/=/* narrow the range; see § [deps]
[replace] # optional: point a name at a local tree
geo = "../geo"
[registry] # optional: where `mc pkg` looks a package up
url = "https://raw.githubusercontent.com/minicompiler/mc-registry/main"
[limits]
tolerance = 0.25
[project] #
| key | type | default | meaning |
|---|---|---|---|
project.name | string | — | only used to default [compiler].out to build/mc-<name> |
project.entry | string | required | the source handed to the compiler |
project.out | string | required | the artifact. Parent directories are created; the file is unlinked before being written, because overwriting a signed executable on the same inode makes the kernel SIGKILL its next run |
project.kind | string | "exe" | exe or obj. obj stops at the object file |
project.opt | integer | 0 | the optimization level of the ENTRY, 0 or 1 — the same two values --opt=N takes (cli.md). 1 turns on the register allocator (M49). It applies to [project].entry and to nothing else: a taught compiler is a TOOL this build runs, not the artefact it was asked for, and it is always built on the plain road, so the compiler mc build writes is reproducible whatever this key says. A --opt=/-O on the mc build command line wins over the key and is forwarded to the child process that compiles the entry. Anything but 0 or 1 is <file>: must be 0 or 1: project.opt |
A missing entry or out is <file>: missing key: project.entry. A kind that is neither is
must be exe or obj, reported at the offending value's line and column.
[target] #
| key | type | default | accepted |
|---|---|---|---|
target.os | string | the host's | a pair the compiler or one of its modules registered |
target.arch | string | the host's | likewise |
target.libc | string | "musl" | "gnu" or "musl" — the C library family a dynamic Linux executable is linked against |
target.link | string | "dynamic" | "dynamic" or "static" |
target.interp | string | the family's | the program interpreter a dynamic ELF executable asks for (PT_INTERP), when it is neither standard path |
The accepted set is the (os, arch) pairs the target() registry holds
(hooks.md) — macos/aarch64, linux/aarch64, linux/x86_64,
windows/aarch64 and windows/x86_64 out of the box — and a pair a module registered counts.
The registry is consulted after user_init() has run (M39.5), so a taught compiler that calls
target("none", "riscv64", "rv-image", "rv-image") can be driven by mc build through its own
mc.toml; examples/kernel is the worked example. The consequence to know is that the entry
source is opened and lexed before an unknown pair is reported, so the diagnostic comes after the
compile x -> y step line. Anything the registry does not hold is refused at the value's
position, with a message built from the registry:
$ mc build tests/proj --config /tmp/haiku.toml
/tmp/haiku.toml:6:6: only macos, linux and windows (see docs/build.md): target.os
$ mc build tests/proj --config /tmp/riscv.toml
/tmp/riscv.toml:7:8: only aarch64 and x86_64 (see docs/build.md): target.arch
os = "linux" changes one thing: the file format. The object comes out as an ELF64 ET_REL (the
elf-obj / elf-obj-x86_64 backends) instead of a Mach-O, and kind = "exe" writes a dynamic
ELF64 ET_EXEC (elf-exe / elf-exe-x86_64, M42) with no [linker] and no sysroot. arch
then decides the instruction set inside either one. [linker] is still accepted and is still the
only route to a static libc link — when it is present the driver writes the object and hands it
over, exactly as before.
target.libc and target.link — the two axes of a Linux target #
A dynamic executable needs no libc files, only names: the interpreter path, the DT_NEEDED
soname and the symbol names. Two of those differ between the C libraries, and libc is one word
that decides both — a family, never a soname:
libc | PT_INTERP (aarch64) | PT_INTERP (x86_64) | DT_NEEDED |
|---|---|---|---|
"musl" (the default) | /lib/ld-musl-aarch64.so.1 | /lib/ld-musl-x86_64.so.1 | libc.so |
"gnu" | /lib/ld-linux-aarch64.so.1 | /lib64/ld-linux-x86-64.so.2 | libc.so.6 |
[target]
os = "linux"
arch = "aarch64"
libc = "gnu"
link = "dynamic"
Anything else is refused at the value's own position, with the migration in the message —
libc = "libc.so.6" was the M42 spelling and is not a value any more:
mc.toml:8:8: libc must be gnu or musl (a soname is not a value: gnu is libc.so.6, musl is libc.so): target.libc
mc.toml:9:8: link must be dynamic or static: target.link
link = "static" is an assertion, not a switch. The executable writer takes the static path by
counting imports — a program on <sys_linux_aarch64> imports nothing and gets an image with no
PT_INTERP, no PT_DYNAMIC and no PLT — and the key makes that a requirement: with an import in
the set the build stops rather than quietly producing a dynamic binary.
mc.toml:10:8: static link with imports needs [linker]: see docs/build.md -- static linking (M46): target.link
A static link against a real libc.a is the [linker] road (../build.md):
with that table present the driver writes the object and hands it over, and link never reaches
the executable writer at all.
interp overrides the loader path alone, for a system whose loader is at neither standard place;
the soname still comes from libc.
All three describe a Linux dynamic image, so all three are refused when this build writes none
(interp applies to a linux target) instead of being read and ignored, and every object backend
ignores them: only elf-exe reads
them. "This build" and not "this [target]", because a taught compiler is a binary for the
host: on a Linux host the three keys still describe the compiler mc build writes, even when
[target] is macOS. The single-file CLI says the same three things with
--libc=, --link= and --interp=, in the same words.
os = "windows" still says both things: the object is an
IMAGE_FILE_MACHINE_ARM64 .obj (coff-obj-arm64) and [linker] is required
(windows requires [linker]: there is no direct executable) — the PE executable writer is a
separate milestone, so Windows is where the message M42 removed for Linux still lives.
See ../guide/50-cross-compile.md.
A registered pair may be missing the other half instead: target(os, arch, 0, exe) is a target
with no separable object step, which is what a board writing a flat image registers. Then
kind = "obj" — and kind = "exe" with a [linker], which goes through the object first — is
<os>/<arch> has no object backend: use kind = "exe", at the same position and with the same
exit 1. kind = "exe" and no [linker] is the shape such a target is for.
[compiler] — build the compiler that will compile the entry #
| key | type | default | meaning |
|---|---|---|---|
compiler.modules | array of strings | — | files #included in order, after the core. Its presence is what turns on this whole step |
compiler.out | string | build/mc-<project.name> | where the taught compiler is written. Must be relative and contain no ... Its directory also receives lib/mc/v<version>/, the library root the product resolves <name> through when it is run standalone (M52 step D) |
compiler.core | string | <mc/core> from the bundle | pin a checkout of src/core.mc instead of the copy inside the binary |
mc build writes <compiler.out>.mc — a generated file, next to the compiler — containing the
core include plus one include per module:
// generated by `mc build` from examples/api/mc.toml
#include <mc/core>
#include "../mc-api.mc"
compiles it with the built-in macho-exe backend, and then spawns the binary that came out
as <out> build DIR --config FILE --entry-only. The spawn is not decoration: the compiler's
tables are process globals built once, so two compilations never fit in one run. --entry-only
re-reads the same TOML, which is why [include], [libs] and [externs] apply to the entry
either way.
The generated file lives inside build/, so each #include gets one ../ per directory level
between the config and [compiler].out — counted on the normalised path, so ./build/x,
build/./x and build//x all count as one level. The .. and leading-/ checks run on the
string as written, because normalising first would swallow a ...
Beside the binary it writes, mc build also stages lib/mc/v<version>/ — bundle.list plus the
library files, the same partial tree a release tarball carries beside mc. A [compiler] product
is a second binary in the project's own build/, so the roots the parent reads
(packages.md § 2) cannot be reached from it; with the tree beside it, the product
resolves #include <sys> run standalone, by anyone, with no --libs-dir and no $HOME. Staging
is idempotent (a byte-identical file is left alone) and there is no flag for it.
Errors: missing key: compiler.modules, must be a relative path: compiler.out,
must not contain ..: compiler.out, missing key: compiler.out (when there is no
project.name to default from).
[linker] — hand off to an external linker #
| key | type | meaning |
|---|---|---|
linker.cmd | string | the program to run. Its presence is what turns on the link step — for the entry and, since 0.16.1, for the compiler a [compiler] section builds |
linker.args | array of strings | the argument list, with placeholders |
Without [linker], kind = "exe" uses the built-in macho-exe backend: no ld, ad-hoc
signature, dylibs bound by ordinal. With it, mc writes <out>.o and spawns the tool, which
inherits stdin/stdout/stderr so its diagnostics reach you unchanged; a non-zero exit stops the
build with exit 1 — silently when it exited 1 (the tool printed its own error), and with
mc: <cmd> exited <N> otherwise.
A declared [linker] wins over the host's direct executable backend, and it does so for the
taught compiler too: a config that says how to link is the developer's decision, and the taught
compiler is linked by the same section the entry is (build.md § [compiler]). The
linker it names is therefore expected to produce a binary this host can run, since mc build
spawns the compiler it just wrote.
| placeholder | expands to |
|---|---|
{out} | [project].out, resolved against the config's directory |
{obj} | the object just written, <out>.o |
{sdk} | the output of xcrun --show-sdk-path, run lazily — only if some argument mentions it, at most once per build |
{sysroot} | the sysroot for [target], found by the resolution chain of sysroot.md: [sysroot].path (checked), then the running system, then the cache |
{stubs} | the directory of the import stubs mc writes from the program's own externs — <dirname of [project].out>/stubs. Written lazily, like {sdk}: only if some argument mentions it, and at most once per build (sysroot.md § 9) |
{libs} | one argument per [libs] entry, in key order |
{out}, {obj}, {sdk}, {sysroot} and {stubs} are substituted inside an argument, so
-L{sdk}/usr/lib and {sysroot}/crt1.o both work. {libs} is the one that must be a whole
argument, since it expands to several; each expanded value then goes through the same
substitution, so a library may be written "{sdk}/usr/lib/libsqlite3.tbd".
Errors: too many arguments in [linker].args (the argv cap is 64),
mc: cannot run: <cmd>, mc: <cmd> exited <N>, mc: <cmd> killed by signal <N>,
xcrun --show-sdk-path failed.
[sysroot] #
| key | type | meaning |
|---|---|---|
sysroot.path | string | the sysroot itself, resolved against the config's directory. Step 1 of the chain, and the one that wins |
sysroot.cache | string | the ROOT of a cache of sysroots, resolved the same way: the one for this target is <cache>/<os>-<arch>. Step 3 |
Neither key is required. {sysroot} is resolved by the chain of
sysroot.md — [sysroot].path, then the running system when the host is the
target, then --sysroot-dir / [sysroot].cache / ~/.mc/sysroots/<os>-<arch> — and the chain
runs at most once per build, and only when some [linker].args argument actually mentions the
placeholder.
sysroot.path is now checked: a directory that is there but does not hold the target's
marker files (crt1.o and libc.a for Linux, kernel32.lib for Windows,
usr/lib/libSystem.tbd for macOS) stops the build with the no sysroot message and exit 2,
instead of being handed to the linker to fail on. An explicit path that is wrong is a mistake to
report, not a reason to go looking somewhere else, so the probes and the cache are not tried
after it.
[libs] and [externs] — #dylib said from outside the source #
[libs]
sqlite3 = "/usr/lib/libsqlite3.dylib"
[externs]
"sqlite3_*" = "sqlite3"
libsfeeds the very same table#dylibdoes. Ordinals are handed out in the order the keys appear, starting at 2, because ordinal 1 is always libSystem.externsmaps a symbol name, or a prefix ending in*, to a[libs]key. The pattern table is consulted after the exact table, so a#dylibin the source always wins for its ownexterns. Anexternno pattern claims stays on libSystem.- A key containing
*has to be quoted — bare TOML keys areA-Za-z0-9_-. - On a
[linker]build the ordinals do not matter (the linker resolves symbols);[libs]is then just the list{libs}expands to.
Errors: library not declared in [libs] — an [externs] value naming a key [libs] does not
have.
[include] #
| key | type | meaning |
|---|---|---|
include.paths | array of strings | extra roots for #include "x", tried after the includer's own directory, in the order written |
A project never shadows a relative include that already resolved; the roots only catch what would
otherwise fail. With no root registered the lexer behaves exactly as it did before this key
existed — not even an extra open happens.
[deps], [tools], [replace] and [registry] — packages #
| key | type | meaning |
|---|---|---|
deps.<name> | string | a version constraint for a LIBRARY this project needs (a bare version is a minimum) |
tools.<name> | string | a version constraint for a TOOL this project needs (a bare version is a minimum) |
replace.<name> | string | a directory to use for <name> instead of a locked tree, relative to the config |
registry.url | string | where mc pkg looks a package up; a URL or a directory |
[deps] keys are package names: [a-z][a-z0-9_]*, at most 32 bytes, and mc, deps and
build are reserved. A name that breaks either rule is refused at the key's own position
(reserved package name: deps.mc, invalid package name: deps.Geo), exit 1.
The value is a version constraint built on a X.Y.Z version, optionally with the
-pre.release and +build parts of SemVer 2.0 (mathx = "2.1.0-rc1"). A bare version is a
minimum, in Go's sense, not a pin, and that is the form every existing manifest uses; the
other operators add an upper bound:
| form | meaning | range |
|---|---|---|
1.2.3 | bare = minimum, unchanged | >= 1.2.3 |
>=1.2.3 | minimum, spelled explicitly (a space after >= is allowed) | >= 1.2.3 |
=1.2.3 | exact: the version must equal 1.2.3 | = 1.2.3 |
~1.2.3 | patch may rise | >= 1.2.3 < 1.3.0 |
^1.2.3 | minor and patch may rise | >= 1.2.3 < 2.0.0 |
* | any version | — |
The caret narrows on a 0.x version (the SemVer/npm convention): for a 0.x a minor bump is
breaking, so ^0.2.3 is >= 0.2.3 < 0.3.0 and ^0.0.3 is >= 0.0.3 < 0.0.4.
There is no >, < or <=: the resolver is Minimal Version Selection, whose model is a
lower bound and a caller-chosen upper bound, so a ceiling is always derived from ~/^/= and
never spelled with a bare <. >1.2.3, <1.2.3, <=1.2.3, ^1.2.x and ~banana are malformed
and refused at the key's own position (a version constraint must be X.Y.Z, =X.Y.Z, ~X.Y.Z,
^X.Y.Z, >=X.Y.Z, or *), exit 2.
What is actually compiled is the row mc.lock carries — beside mc.toml, machine-written, one
[[package]] per package with an exact, concrete version and a content hash — which
mc build re-checks on every build: the constraint syntax lives in mc.toml, the lock stays
concrete. A [deps] constraint the lock cannot satisfy is mc.lock is stale, exit 2 (re-run
mc pkg sync --yes).
[deps] is what turns on #include <pack/file.mc>; with neither [deps] nor [tools]
mc build reads no lock at all and every <name> resolves exactly as it did before packages
existed.
[tools] has exactly [deps]' shape and is resolved in exactly the same MVS graph — a tool's own
[deps] are libraries — but what it names is a PROGRAM, not a tree the compiler reads:
mc pkg sync writes it into the lock with kind = "tool", and mc build registers no include
root for it, opens nothing under it and emits the same bytes it would emit with the table deleted.
What it does read is the lock, which is the answer to both tables: a project whose only
package table is [tools] needs one, and its constraint is checked at its own position like a
[deps] one. A name may be in one table or the other and never in both, and the registry says
which it is: a library named under [tools] is net: is a library: name it under [deps], a tool
named under [deps] the mirror of that, both exit 1. Installing and running one is mc tool
(tools.md), and [replace] does not apply to it.
[registry].url is where mc pkg looks a name up, and it takes a URL or a directory: a
private registry is a git clone of a tap plus one line here, at no cost in code. --registry on
the command line wins over it, and with neither the default is
https://pkg.minicompiler.dev. Nothing in mc build ever reads it — the network side is
mc pkg's alone.
A replaced package is not pinned and not hashed, and the build says so on stdout
(replaced geo: ../geo -- not pinned by mc.lock).
A pre-release minimum is never written for you: mc pkg add <name> with no @ and mc update
skip every pre-release row, so a candidate only lands in this table when it is asked for by name
(mc pkg add mathx@2.1.0-rc1) or when the minimum already there is one. The ordering rule is in
packages.md.
Everything else about packages — the resolution order, the lock format, the tree hash, the closure rule — is in packages.md.
[package] — this tree IS a package #
| key | type | meaning |
|---|---|---|
package.name | string | the package's registry name |
package.version | string | optional: which version this tree is. Read only out of a VENDORED tree, deps/<name>/, to refuse a checkout at another version by name instead of by hash |
package.mc | string | optional: a version constraint on the mc that builds it, normally a minimum |
package.files | array of strings | every file the package ships, in the order that fixes the hash |
package.lib | string | optional: the file a bare #include <name> means |
package.module | string | optional: the file a COMPILER includes; it exports <name>_init() |
package.bin | string | optional: the name a TOOL takes in ~/.mc/bin; default the basename of project.out |
package.licence | string | optional: an SPDX identifier. The compiler reads it only to compare a registry row against the archive it describes |
package.repo | string | read out of a REGISTRY INDEX file, not out of a package: where the source lives |
The first keys are read out of a DEPENDENCY's mc.toml, not out of your own — a project's mc.toml
without [package] is not a package, and a package that is also a program simply carries both
tables. files is the hash's input, the vendor-copy list and the boundary a package may not read
outside of; see packages.md § 3.
bin is the one name in this file that may carry a hyphen ([a-z][a-z0-9_-]*, at most 32 bytes):
it is a FILE name, not an identifier. mc build ignores it.
version resolves nothing: the registry index picks a version and mc.lock pins it, and a fetched
tree carries its version in the manifest beside it. Its one use is the tree a developer maintains
by hand, deps/<name>/ — a checkout at another version is then
mc: deps/<name> is 1.0.0, [deps] wants 1.2.0: update the checkout or remove deps/<name> instead
of a checksum mismatch on bytes that are not corrupt. A value that is not a version
([0-9A-Za-z.+_-], no ..) is a file:line:col error, exit 2. See
packages.md § 2.
mc is a MINIMUM mc version, checked when the compiler builds or resolves a package — the entry's
own mc.toml and each dependency's, whether it comes from [deps], [replace] or a vendored
tree. It takes the same version-constraint grammar as [deps] (§ [deps] above). A bare 1.2.3
(an optional -suffix allowed) or a leading >= 1.2.3 both mean "at least this", which is the
normal form: the API is frozen at 1.0.0 and a newer mc keeps working. A ^/~/= is allowed
but unusual — it CAPS the compiler version, refusing a newer mc as well as an older one. A
malformed value (banana, 1.0 - 2.0) is a file:line:col error, exit 2. When the running
compiler does not satisfy the constraint the build is
refused with <pkg> <ver> needs mc >= <min> (this is mc <cur>): upgrade the compiler (or, for a
capped constraint, >= <min> and < <max> / = <exact>), exit 2, at
the key's own position. A working-tree build reports the 0.0.0-dev sentinel, which is treated
as newest and skips the check entirely — otherwise every local build of a pinned tree would
fail. See packages.md § The minimum mc version.
A package's kind is [project]'s and is written nowhere else: no [project] at all — every
package published so far — or kind = "obj" is a library, kind = "exe" is a tool, and a
[project] with no kind line is refused with a package's [project] must say kind = "obj" or
"exe" at its own position, because mc build defaults that key to exe and a library carrying a
[project] for its own tests would otherwise be classified as a program to install.
[[permission]] — what a package asks to be allowed to do #
| key | type | meaning |
|---|---|---|
kind | string | fs.read, fs.write, net, exec or env |
path | string | fs.* only: workspace, tmp, workspace/<rel> or home/<rel> |
name | string | exec and env only: a program's basename, or a variable's name |
reason | string | optional, at most 120 bytes: shown by mc pkg list --long and on the package's page |
An array of tables, at most 32 rows, in a PACKAGE's mc.toml — a project's own rows mean nothing
to anybody. No rows at all means stdio only. Each row becomes one canonical line —
fs.read workspace, fs.write workspace/build, net, exec mc, env HOME — and the set,
duplicates collapsed and sorted bytewise, is what a registry row carries, what mc pkg sync prints
before it downloads anything and what mc.lock records as accepted.
No absolute path is ever accepted: the registry cannot verify one, it leaks a host's layout
into a published manifest, and a sandbox cannot promise what it would map to. <rel> obeys the one
containment rule every path in every manifest obeys (no .., no empty component, no backslash, no
control byte, none of the characters Windows reserves). A path on net, a name on fs.* and a
kind outside the five are each refused at the offending key's own file:line:col.
What enforces them is the sandbox around a tool (mc tool run, tools.md); a library is
compiled into your program, so what it declares is its author's statement and the install table says
so in those words. See packages.md § Permissions.
[tool] — a tool install manifest #
mc tool install writes one manifest per installed version, ~/.mc/tools/<name>/v<ver>.toml, as
its last step (the claim). It is not written by a person and is not part of a project; mc tool run
and mc tool list read it back. Its keys are under [tool]:
| key | type | meaning |
|---|---|---|
tool.name | string | the package name |
tool.version | string | the installed version |
tool.bin | string | the launcher's name in the bin directory |
tool.out | string | the built binary, relative to the staged tree (project.out) |
tool.sha256 | string | the tree hash of the staged copy |
tool.kind | string | tool |
tool.sandbox | bool | whether the host it was installed on has a sandbox — a fact mc tool list shows; mc tool run decides afresh at run time |
tool.permissions | array | the accepted set, canonical and sorted, the same shape mc.lock carries |
[limits] #
| key | type | default | range |
|---|---|---|---|
limits.tolerance | float | 0.25 | 0.0 to 1.0 |
How much slack mc build reserves on top of its own estimate of what the build needs. mc limits
reports how good the guess was, and mc build --fix-limits is what raises this number. Out of
range is refused at the value's position: tolerance must be between 0 and 1.
There is no float type in the language, so there is none here either: the parser reads the
decimal text into an i64 of basis points. 0.25 is 2500, 1.0 is 10000, 0.0001 is
1, and a fifth fraction digit is float with more than 4 fraction digits.
The TOML subset #
src/toml.mc accepts exactly this:
# comment, to the end of the line
[table] # sets the prefix for the keys that follow
[[array.of.tables]] # same, with an occurrence index in the prefix
key = "value" # bare key, quoted key, or dotted: a.b.c
"quoted key" = 1 # a key containing * has to be quoted
n = -42 # decimal integer; + - and _ separators allowed
t = 0.25 # decimal float, kept as basis points (2500)
flag = true # true | false
list = ["a", "b"] # array of strings or of integers, multi-line,
# trailing comma allowed, no nesting
String escapes are \" \\ \n \t \r; anything else is unknown escape. Every error carries
line and column:
$ build/tomldump tests/toml/bad-string.toml
tests/toml/bad-string.toml:2:8: unterminated string
$ build/tomldump tests/toml/bad-equals.toml
tests/toml/bad-equals.toml:2:6: expected = after the key
$ build/tomldump tests/toml/bad-header.toml
tests/toml/bad-header.toml:1:9: expected ] in the table header
A quoted key may not contain a . (quoted key must not contain .). The result table is
flat and its paths are segments joined with ., so "b.c" under [a] and c under [a.b]
would both land on a.b.c; rather than pick a winner, the parser refuses the ambiguous one. *
is unaffected, which is why [externs] "sqlite3_*" keeps working.
The result is a flat table, not a tree #
Deliberately — it is the same shape as every other table in this compiler (docs/determinism.md
rule 1: nothing hashed, nothing iterated but an array in insertion order). An entry is
(path, value, type, index) and entries stay in source order. Array elements share a path and
carry an index; [[x]] puts the occurrence number in the path (server.0.host). Values are
always stored as text.
$ build/tomldump tests/toml/full.toml
str project.name = "api"
str project.entry = "main.mc"
...
str compiler.modules[0] = "mc-api.mc"
str libs.sqlite3 = "/usr/lib/libsqlite3.dylib"
str externs.sqlite3_* = "sqlite3"
str include.paths[0] = "lib"
That flat walk is what makes [libs] and [externs] work: the driver needs the keys of a
table, not just a value, and walking in order is also what fixes the dylib ordinals.
src/tomldump.mc is a small driver that prints the table; scripts/check-toml.sh compares its
output against tests/toml/*.expect, well-formed and malformed files alike.