Moggi documentation

Tests

One entry point: test.php, wrapped by the dev shell as runtest. There is no test registry — the tree decides what runs, and tests/suite/ decides how. Where a test goes is tests/README.md; this file is the contract of running them.

Run

runtest                        # php backend (default), whole suite
runtest --backend all          # php → jvm → dotnet, one aggregate verdict
runtest --group syntax/parser  # one subject, or any group: --group semantics
runtest examples/twice         # one case, by logical name or by path
runtest --list                 # discover everything, run nothing
runtest --native --backend jvm # also build and run the native-executable smoke
MOGGI_MICRO_SFX=/path/micro.sfx runtest --native   # …on php, which needs the runtime named
Flag Meaning
--backend php\|jvm\|dotnet Repeatable; all runs php, then jvm, then dotnet, with one aggregate verdict. Default php.
--group <name> Group or a subject path below one, repeatable (semantics, syntax/parser, backend/runtime).
<path\|name>... A path (file or directory subtree) or a logical case name. Repeatable, and freely mixed with --group.
--list Discover the selection, print the case names grouped as the report groups them, run nothing.
--json One machine-readable document instead of the report (--list --json prints the same names plus group/backends).
--log <path> Also write the human report to a file.
--stop-on-failure Stop at the first failure (default: keep going). Never pooled automatically; with an explicit --jobs it warns that dispatched work finishes.
--progress, --no-progress Force the live per-group block on (--progress) or off (--no-progress). Default: on when stderr is a terminal.
--jobs N, -j N Run N worker processes. Identical counts, ordering and report.
--native Also build one example natively and run it. See below.
--help, -h Usage.

A selector that matches nothing is an error (exit 2), never an empty selection — a typo must not select nothing, or worse, everything.

What a run prints

One report, and no verbosity dial in front of it:

--no-progress is the same report without the block: every case is named as it finishes (OK backend/codegen/Opt-Lambda 153ms), printed the moment it is known — in a --jobs N run that is when its worker reports it, not when the pool drains — and each backend closes with its group rows (optimize 24 passed) before the recap and the summary. A group row carries counts, not a time: the only time a group has is the sum of its cases, and under --jobs N those cases overlap, so the sum can exceed the whole run. Per-case times are on the case lines, a group’s wall clock is in the live block, and the run’s own time is the summary line’s. In a run that covers several backends the streamed lines name theirs (… 153ms jvm) and the ===== JVM ===== banners are left out, since a pool’s three backends arrive interleaved and a banner over the first block would name the wrong one; the per-backend summary still says which backend it closes. A case that is about other backends is counted as n/a in its group’s row and gets no case line either way — there is nothing this run could say about it — and is left out of the backend summary.

Under --jobs N the first line states the worker count. Every failing case’s message is written to .moggi/test/logs/<group>-<case>.log, which is the file the recap names. --log always gets the --no-progress form, so a log never carries terminal escapes.

Backends

php, jvm and dotnet. Which backend a case runs on is decided by its goldens, never by a list somewhere:

For a project, each golden is named after the module it describes, and the one for the backend being compiled wins: lib/Platform.jvm.err.expected is compared on jvm even when the shared lib/Platform.err.expected is there for php.

A case that is not about the selected backend is still selected and reported as not applicable with its reason (case is about jvm, dotnet) — never silently dropped, and never called a skip: a skip would claim something was deferred. A skip is the other case: the case applies to this backend but the machine could not run it (a missing toolchain).

--backend all keeps going when one backend fails, reports each backend separately, adds an ALL BACKENDS: line, and exits 1 if any of them failed.

The native smoke (--native)

One example (examples/twice) is additionally built into a native executable — the micro PHP runtime on php, GraalVM native-image on jvm, .NET Native AOT on dotnet — and run, comparing stdout with the same golden the managed run uses. It is a mode of that example’s case, not a case of its own: the example already compiles and runs on every backend, and the flag only adds “…and also as an executable”.

It is opt-in because the build takes minutes. If the toolchain is not at hand, the case is reported as skipped with its name (GraalVM native-image not available, micro PHP runtime not available); a build or run that fails once the toolchain is present is a real failure. The three toolchains are found differently: native-image on PATH or in JAVA_HOME, dotnet on PATH or in DOTNET_ROOT, and the micro runtime in MOGGI_MICRO_SFX or bundled by the installation — never on PATH, since it is a file the compiler reads rather than a command it runs.

Discovery and naming

A case is identified by its files — <dir>/Main.mog is a project, Foo.mog with a golden beside it is a fixture, *_test.php is a script. The name a case prints and is selected by is its path without the input extension (backend/codegen/Alias). Names are Kebab-Cased, directories included, and the examples keep their tree (examples/twice); tests/README.md lists which kind of test goes where.

The kinds

A golden is <base>[.<backend>].<kind>.expected, beside the input it belongs to (Foo.mog, Foo.ir.php, Foo.script, or a project module Module.mog). The kind says what is compared:

Kind Compares
tokens the lexer’s dump
ast the parse tree
typed-ast the tree after typechecking
ir the lowered IR
opt-ir the optimized IR
emit generated source
stdout, stderr what the program prints, and its uncaught report
err that the compiler rejects the input: message, source location, caret

The first five and err are produced before codegen, so no backend can change them and a backend-qualified one would only repeat the shared file. emit, stdout and stderr can differ per backend and are qualified when they do. A golden whose kind nothing produces is an error at discovery, not a case that never runs.

A verdict is a golden: there is no Foo.exec.php any more (Exec-Exception-HostCause’s was the last, and it became a stderr golden). Asserting in the host language from the harness is not a verdict kind — whatever a case needs to say, it says as output we can compare on all three backends.

A fixture is compiled exactly as a build compiles it: a file without a module header is module Main, so the implicit Prelude is in scope and a fixture that redeclares Maybe, Bool or a class the Prelude exports is rejected as a duplicate. A fixture that needs a Prelude-free environment says so with {-# LANGUAGE NoImplicitPrelude #-} — that is a claim about the fixture, never a way to keep a golden green.

Parallelism

--jobs N splits the selection into N shards, runs each in a worker process, and merges the results in selection order, so every name, count and verdict in the report is the serial one (durations are wall clock, and in a pooled run they are the run’s, not the case’s). Without the flag, a selection large enough to give every worker more than one case uses one worker per physical CPU (6 on a 6-core/12-thread machine — a case is a whole compile, so a core each), clamped to the cgroup CPU quota or cpuset of the process, and to what free memory can hold at 512 MB per worker; a smaller selection keeps to this process, where launching a worker costs more than the cases it would split. The terminal decides only whether the progress block can be drawn, never how the cases run, so a redirected or piped run is pooled by the same rule. The first line says which was chosen, and a requested worker count the selection cannot fill says so instead of claiming a worker nobody launched. --stop-on-failure is never given an automatic pool, and with an explicit --jobs it warns that work already dispatched runs to completion.

Environment

The suite reads exactly six variables, all optional:

Variable Default Effect
MOGGI_CACHE_DIR pinned to <repo>/.moggi Where the compile cache lives. The suite pins it only when it is unset, so a test run never writes into your working directory’s cache and an explicit choice wins.
MOGGI_NO_CACHE unset (cache on) Inherited: disables the cache for the compiler the cases invoke.
MOGGI_FUZZ_DIR <cache>/fuzz-failures Where the frontend fuzzer keeps generated inputs and minimised reproducers.
MOGGI_TEST_STANDALONE_TIMEOUT 300 Watchdog seconds for one script case before it is killed and reported as timeout.
MOGGI_TEST_CPU_COUNT probed Pin the machine’s CPU count, so a run that asserts a worker count gets a machine of exactly that size.
MOGGI_TEST_FORCE_TTY unset 1 makes stderr count as a terminal, so the progress block is on without allocating a pty.
MOGGI_TEST_COLUMNS COLUMNS, the terminal, 80 Progress-block width.
MOGGI_TEST_LINES LINES, the terminal, 24 Progress-block height; a block taller than this folds its earliest rows instead of scrolling.
NUMBER_OF_PROCESSORS probed Window’s CPU count, used only to size an automatic --jobs.
JAVA_HOME probed Locating java and native-image for jvm cases.

MOGGI_PROJECT_ROOT is a PHP constant the harness defines, not an environment variable. Everything else the compiler itself reads is in env-vars.md.

Manual inspection

runtest --list --json | jq '.tests[] | select(.group == "backend")'
moggi compile tests/backend/runtime/Exec-Exception-Report.mog --opt-ir
moggi compile examples/twice --backend jvm -o /tmp/twice.jar

moggi compile <file> --tokens|--ast|--typed-ast|--ir|--opt-ir prints exactly what a golden of that kind holds, and --unpacked -o DIR keeps the generated tree to read the emit output — that is how a golden is produced and reviewed by hand. The flags and the goldens share one producer (the CLI and the suite both call compileFile($file, $stage)), so the two cannot disagree. Two kinds are not reproducible from a single file: an opt-ir golden of a multi-module project (those are compiled as a project — see tests/semantics/modules).

The language server’s own tests are described in lsp.md.


Moggi compiles to PHP, the JVM and .NET. This site is generated from docs/ in the moggi-lang/moggi repository; the same pages are readable there as Markdown.