There are two routes to a working m9c, and both end at the same
place. What they need is deliberately small: the only compiler
required is gcc. M9 compiles to C11, the generated C for the
toolchain itself is checked into the repository as the bootstrap,
and a CI gate builds the whole thing on a machine where every other
compiler has been replaced by a script that fails loudly — so the
claim is tested, not asserted.
For your work that means something concrete: what you build today rebuilds the same way in ten years, with a compiler every machine already has, and no chain of pinned versions to keep alive for the analysis to run again.
It is also an unusually good language to have an AI write for you, and for the same reason it is good to review: the explicitness the compiler insists on — every import named, every width exact, every error a procedure can raise declared, every allocation owned — lets it act as a strict reviewer of whatever a model produces, refusing the quiet mistakes a code generator makes rather than running them. Code you did not write by hand arrives already checked, which is exactly what you need before you trust a number it computed.
The release page of the compiler's repository, m9c, carries one package per distribution, x86-64, each built ON that distribution from the same source tarball (the M9Tutorial repository holds every example in these chapters):
| distribution | package | install |
|---|---|---|
| Ubuntu 24.04 LTS | m9_0.10.0-1_amd64.ubuntu24.04.deb |
sudo apt install ./m9_0.10.0-1_amd64.ubuntu24.04.deb |
| Ubuntu 26.04 LTS | m9_0.10.0-1_amd64.ubuntu26.04.deb |
sudo apt install ./m9_0.10.0-1_amd64.ubuntu26.04.deb |
| Debian 13 | m9_0.10.0-1_amd64.debian13.deb |
sudo apt install ./m9_0.10.0-1_amd64.debian13.deb |
| Fedora 43 | m9-0.10.0-1.fc43.x86_64.rpm |
sudo dnf install ./m9-0.10.0-1.fc43.x86_64.rpm |
| Rocky 9 (RHEL 9, Alma 9) | m9-0.10.0-1.el9.x86_64.rpm |
sudo dnf install ./m9-0.10.0-1.el9.x86_64.rpm |
| Arch | m9-0.10.0-1-x86_64.pkg.tar.zst |
sudo pacman -U ./m9-0.10.0-1-x86_64.pkg.tar.zst |
Each package comes with a .receipt beside it — the distribution it
was built on, its sha256, the tarball it came from, the gcc that
built it, and the line proving the installed compiler compiled and
ran a program with an empty environment. What is on the page is
what came back from the build machine, unchanged.
Any of them puts m9c in /usr/bin, the runtime header in
/usr/include/m9, the runtime archive at /usr/lib/libm9rt.a, and
the standard library — as M9 SOURCE, because a readable library is
part of the point — in /usr/lib/m9, which the compiler searches
without any variable being set. The per-module reference pages land
in /usr/share/doc/m9/modules, the language report beside them, and
man m9c works.
The two debs are different files — a deb names its distribution's library versions — so take the one for your distribution. On any other distribution or release, route 2 is a two-line build.
git clone https://github.com/atverm/m9c && cd m9c
./build.sh # needs gcc, nothing else
Two artifacts appear: out/m9c and out/libm9rt.a. To use the
compiler in place, tell it where the library and runtime sources
live (installed via route 1, neither variable is needed):
export PATH=$PWD/out:$PATH
export M9LIBRARY=$PWD/corpus
export M9RUNTIME=$PWD/runtime
./build.sh DESTDIR also installs the route-1 layout under a
prefix, which is exactly how the package itself is assembled.
m9-0.10.0-windows-x86_64.zip on the same release page is one folder
with everything in it: its own gcc (a subset of the MSYS2 UCRT64
toolchain), the compiler's bootstrap C, the standard library and the
tools as M9 source, this tutorial as pages, and an install.bat.
(unpack the zip anywhere -- your Documents folder is fine)
cd m9-0.10.0-windows-x86_64
install.bat
Run it from a terminal rather than by double-clicking: that is
what keeps Windows from asking whether you trust a file you just
downloaded, and it is where you will read what it says. It opens by
telling you what it will put in the folder and that your machine is
about to compile the compiler from its own C — the same claim route 2
makes, with the gcc in the box — and then asks once. About half a
minute later there is a bin\m9c.exe your machine built.
Two things it asks about separately, because they are the only ones
that write outside the folder: the VS Code extension (into
%USERPROFILE%\.vscode\extensions) and your PATH. Decline
either and it says how to do it by hand. At the end it offers to
start the tutorial locally — the same pages you are reading, served
from that folder, with every example runnable in the browser and
compiled by the m9c you just installed.
Experimental means experimental. The Windows build is the same
compiler as the Linux one — the runtime carries _WIN32 paths beside
the POSIX ones and the generated C never names a platform — but it
is verified under wine and on one real Windows machine, where Linux
is tested across six distributions. Two chapters do not work there
yet: chapter 14, because netCDF resolves paths through its own
Windows converter, and chapter 17, because it runs sort and
uniq and Windows has neither. The rest, zarr over TLS and threads
included, runs. If something else breaks, that is worth reporting —
it is what the label is for.
cat > Hello.m9 <<'M9'
MODULE Hello ;
IMPORT Io ;
BEGIN
Io.WriteLine ('it works')
END Hello.
M9
m9c --make -o hello Hello.m9
./hello
m9c checks, generates C, and drives gcc; --make also builds any
imported module whose object is missing or stale, deepest first.
Without --make, a missing library object is not an error dump —
it is NAMED, with the exact command that produces it, before the C
compiler runs. The compiler supplies the include paths, the module
objects, the runtime and -lm on its own; -v prints the composed
gcc line when you want to see precisely what it did, and anything
you place after -- replaces the supplied flags entirely, because
m9c is not a build system and does not pretend to be one.
The flags you will actually use:
m9c --make -o prog Main.m9 compile and link a program
m9c --make -c Mod compile one module to Mod.o + Mod.h
m9c -g -o prog Main.m9 debuggable build (keeps the C;
gdb breaks by function, walks
file:line)
m9c --doc Mod.m9 render the module's reference page
from its own definition comments
m9c --version say which m9c this is
A refused program prints its diagnostics to stderr, with line and column, and writes nothing — there is no output to half-trust.
The repository ships a VS Code extension with the M9 grammar
(highlighting), HOVER DOCUMENTATION and dot-completion. Hovers and
completion are fed by m9c --doc — the same generated pages you
read in /usr/share/doc/m9/modules — never by a private re-scan of
the source, so what the editor shows you is what the compiler
believes. (The repository refuted the second-scanner design with
its own corpus: a code generator holding eighteen string literals
containing (* defeats any scanner that does not lex strings.)
From the Debian package, the extension is installed at
/usr/share/m9/vscode-m9; VS Code loads per-user extensions, so
link it once:
ln -s /usr/share/m9/vscode-m9 \
~/.vscode/extensions/atverm.m9-lang-0.2.0
From a source tree, link tools/vscode-m9 to the same place.
Restart VS Code; .m9 files get the grammar, and hovering
Io.WriteLine shows its contract. If your modules import things
outside the default search path, the m9.includePaths setting
names the extra directories, and everything documented in chapter 3
— your own definition comments included — appears in your own
hovers.
For editors that speak the Language Server Protocol — Neovim, Helix,
Emacs with eglot, Kate — the repository carries m9lsp, a language
server written in M9 itself (corpus/Lsp.m9). It publishes
diagnostics on open and on save, and it holds the same rule as the
VS Code extension: it never lexes M9 privately. Every diagnostic is
the compiler's own — the server runs m9c --check (check, print,
write nothing) and republishes the messages with their line and
column, so an editor squiggle can never disagree with the build.
The server is not a second binary to install: Lsp.m9 ships in the
standard library, and you build it with the compiler you already
have — one command, anywhere you have write permission:
m9c --make -o m9lsp Lsp
This needs m9c 0.5.0 or newer (m9c --version says): 0.4.1's notes
claimed the library module and its package did not install it, which
is what an installed-package test now checks. From a source build
(route 2), any current checkout works.
Wire it as a stdio server for .m9 files. Helix
(~/.config/helix/languages.toml):
[language-server.m9lsp]
command = "m9lsp"
[[language]]
name = "modula9"
scope = "source.modula9"
file-types = ["m9"]
roots = []
language-servers = ["m9lsp"]
Neovim (0.11+):
vim.lsp.config['m9lsp'] = {
cmd = { 'm9lsp' },
filetypes = { 'modula9' },
}
vim.lsp.enable('m9lsp')
vim.filetype.add({ extension = { m9 = 'modula9' } })
A formatter travels the same way: m9fmt (m9c --make -o m9fmt M9fmt) prints the compiler's own canonical layout with every
comment kept in place — columns, banners and trailing remarks
preserved — and m9fmt --check FILE.m9 exits nonzero when a file is
not already in that layout, which is the CI-shaped half. -w
rewrites in place. Like the language server, it never guesses: a
file that does not parse is refused with the parser's positions.
The server finds m9c on PATH (or $M9LSP_M9C names one), and
the compiler's own search rules apply — $M9LIBRARY, the installed
library, and the checked file's directory — so a file whose imports
sit beside it needs no configuration at all. Checks run against
the file on disk: save to see fresh diagnostics. In VS Code, keep
using the extension above; it already has hovers and completion,
which the server does not yet.
m9c --version m9c 0.10.0
man m9c the reference, options and the
supplied-flags contract
ls /usr/share/doc/m9/modules the standard library, one page
per module