March is a statically-typed functional language in the ML/Elixir family. It compiles to native binaries via LLVM.
mod Greet do
fn greet(name : String) : String do
"Hello, " ++ name ++ "!"
end
fn main() : Unit do
println(greet("world"))
end
end
Type system
- Hindley-Milner inference with bidirectional checking at function boundaries — annotations are optional except where inference fails
- Algebraic data types with pattern matching
- Records with functional update syntax (
{ r with field: value }) - Polymorphic functions monomorphized at compile time (no boxing overhead)
- Linear and affine types for ownership and safe mutation (in progress)
Syntax
fn name(x, y) do ... end— named functionsfn x -> x + 1/fn (x, y) -> expr— lambdaslet x = expr— block-scoped bindings, noinmatch expr do Pat -> body end— pattern matchingif cond do e1 else e2 end— conditionalsx |> f |> g— pipe operatormod Name do ... end— modules--line comments,{- -}nested block comments- Multi-head functions (Elixir-style): consecutive
fnclauses grouped automatically whenguards on function heads and match branches
Backend
- Compiles to LLVM IR, linked to native binaries via
clang— or to.wasmvia--target wasm64-wasi - Perceus reference counting — deterministic memory management, no GC pauses
- FBIP (Functional But In-Place) — when the reference count on a pattern-matched value is 1, destructured nodes are reused in-place rather than freed and reallocated (see below)
- Escape analysis promotes allocations to the stack where possible
- Defunctionalization: closures compiled to structs + dispatch, no indirect call overhead
- Tree-walking interpreter available for fast iteration
Concurrency
- Actor model: share-nothing message passing,
spawn,send,kill,is_alive(interpreter only) - Actor state updated via record spread:
{ state with count: state.count + 1 } - Structured concurrency via
Task:Task.async,Task.await,Task.race,Task.any,Task.all_settled,Task.scope - Cancellation tokens:
task_cancel_token_new,task_cancel,task_is_cancelled,task_spawn_with_cancel,task_cancel_by_id
One of March's most distinctive performance features is FBIP (Functional But In-Place), derived from the Perceus reference counting paper. March code that recursively transforms a data structure can automatically run as fast as — or faster than — equivalent imperative C code that mutates in-place.
Every heap-allocated value has a reference count. When the Perceus compiler pass inserts DecRC before a match branch body and finds a downstream allocation of the same constructor shape, it replaces the DecRC + alloc pair with a single conditional EReuse node:
- RC == 1 (unique owner): write the new tag and fields directly into the old allocation. Return the same pointer. Zero allocator calls.
- RC > 1 (shared): decrement the count, allocate fresh. Correct behavior for shared data.
No source-level annotations are needed. The compiler derives this entirely from liveness and shape analysis.
type Tree = Leaf(Int) | Node(Tree, Tree)
fn inc_leaves(t : Tree) : Tree do
match t do
Leaf(n) -> Leaf(n + 1) -- rewrites the Leaf in-place when RC=1
Node(l, r) -> Node(inc_leaves(l), inc_leaves(r)) -- rewrites the Node in-place when RC=1
end
end
With FBIP active, after the initial tree is built, every subsequent pass of inc_leaves does zero heap allocations — it rewrites the tree's nodes in-place.
Depth-20 binary tree (1,048,576 leaves), 100 passes of inc_leaves:
| Implementation | Time | Allocations per pass |
|---|---|---|
| March (FBIP off, pre-fix) | 11.0 s | 2 × 2^20 = 2M |
C (malloc / free) |
8.8 s | 2 × 2^20 = 2M |
Rust (Box) |
9.5 s | 2 × 2^20 = 2M |
| OCaml (GC) | ~3.65 s | 2 × 2^20 = 2M (amortized) |
| March (FBIP on) | 1.3 s | 0 (after pass 1) |
March is 7× faster than C on this benchmark — not because C is slow, but because malloc/free carry real overhead (bookkeeping, potential locks, cache pressure). 200M allocator calls across 100 passes add up. March avoids all of them with in-place reuse.
FBIP fires automatically on any function that:
- Consumes a uniquely-owned value via pattern match (RC == 1)
- Returns a new value of the same constructor as the matched arm
- Does not retain the original value alongside the result
This covers map over lists, any tree traversal/transformation, and most structural recursion patterns. Functions that alias the original correctly take the RC > 1 fallback path, which allocates fresh.
For the full technical description — including how shape_matches works, the TIR EReuse node, and the LLVM codegen for the conditional reuse — see specs/design.md § Perceus Reference Counting and FBIP.
# Interpret a file
march file.march
# Compile to a native binary
march --compile file.march # produces ./file
march --compile -o hello file.march
# Compile to WebAssembly (requires wasi-sdk + wasmtime — see below)
march --compile --target wasm64-wasi file.march # produces file.wasm
wasmtime --wasm memory64 file.wasm
# Emit LLVM IR
march --emit-llvm file.march # produces file.ll
march --emit-llvm --target wasm64-wasi file.march
# Interactive REPL
marchPrebuilt binaries are published as GitHub releases for darwin-arm64, linux-x86_64, and linux-aarch64. Each archive bundles the march compiler, the forge build tool, the standard library, and the C runtime sources.
The binaries are self-contained — the macOS build statically links blake3/zstd/brotli and the Linux builds are statically linked — so running March (interpreting programs) needs no extra packages.
To use
march --compileyou also need a C toolchain installed:clang+ LLVM. On macOS install the Xcode Command Line Tools (xcode-select --install); on Linux install e.g.clang llvm. The compiler shells out toclangto build the bundled runtime.
curl -fsSL https://raw.githubusercontent.com/march-language/march/main/install.sh | shThis downloads the latest release into ~/.march, verifies its checksum, and installs march and forge into ~/.march/bin (add that to your PATH as the script prints). Set MARCH_VERSION=nightly for the latest nightly, or MARCH_VERSION=<tag> to pin a specific release.
Once forge is installed, it manages March versions rustup-style:
forge toolchain install # latest stable (falls back to newest nightly)
forge toolchain install nightly # latest nightly
forge toolchain use <version> # switch the active toolchain
forge toolchain list # show installed versions
forge toolchain uninstall <version>Both the installer and forge toolchain share the ~/.march/versions/<tag> layout and switch the active toolchain via the ~/.march/current symlink.
Note:
forge toolchain, the bundled runtime (native compile), and static linking ship in releases built from the current sources. Nightlies published earlier predate them — installing one still works for interpreting, but those features require a newer release. The manual download below works against any release.
PLATFORM=darwin-arm64 # or: linux-x86_64, linux-aarch64
api="https://api.github.com/repos/march-language/march/releases"
# Download the latest nightly tarball + checksums
url=$(curl -fsSL "$api" | grep browser_download_url | grep "${PLATFORM}.tar.gz" | head -1 | cut -d'"' -f4)
sums=$(curl -fsSL "$api" | grep browser_download_url | grep "checksums.txt" | head -1 | cut -d'"' -f4)
curl -fsSLO "$url"
curl -fsSLO "$sums"
# Verify the checksum (sed strips a legacy "sha256:" prefix if present;
# use shasum on macOS, sha256sum on Linux)
sed 's/^sha256://' march-*-checksums.txt | shasum -a 256 -c --ignore-missing
# Extract and add to PATH
tar xzf march-*-"${PLATFORM}.tar.gz"
toolchain="$PWD/$(ls -d march-*-${PLATFORM}/)"
export PATH="${toolchain}bin:$PATH"Add the final export PATH=... line (with the real path) to your ~/.zshrc or ~/.bashrc to make it permanent. Then:
march hello.march # interpret a program
march --compile -o hello hello.march # compile to a native binary
./helloPrerequisites
- OCaml 5.3.0 (via opam)
clang(for native compilation)
1. Install opam (if needed)
brew install opam # macOS
# or: https://opam.ocaml.org/doc/Install.html
opam init2. Create the OCaml switch
opam switch create march 5.3.0
eval $(opam env --switch=march)3. Clone and build
git clone https://github.com/march-language/march.git
cd march
opam install . --deps-only
dune build4. Run the compiler
dune exec march -- examples/list_lib.march
dune exec march -- --compile examples/list_lib.march
./examples/list_libTo install march into your PATH:
dune installMarch can compile pure functional programs to .wasm via the --target wasm64-wasi flag. Actors, networking, and file I/O are not yet available in WASM builds (Tier 1 — pure compute only).
Install prerequisites
brew install wasi-sdk wasmtime
# or download from:
# https://github.com/WebAssembly/wasi-sdk/releases
# https://wasmtime.devCompile and run
march --compile --target wasm64-wasi file.march # → file.wasm
wasmtime --wasm memory64 file.wasmSet WASI_SDK_PATH if wasi-sdk is installed somewhere other than /opt/wasi-sdk:
export WASI_SDK_PATH=/path/to/wasi-sdk
march --compile --target wasm64-wasi file.marchAvailable targets: native (default), wasm64-wasi, wasm32-wasi, wasm32-unknown-unknown.
dune runtestforge watch reruns a build, test run, or program on every source change — it
watches lib/, src/, test/, config/, and forge.toml:
forge watch # rebuild on change (default)
forge watch test # rerun the test suite on change
forge watch run # rerun the program on change
forge watch --clear --interval 200It never exits on a build/test failure — it reports and keeps watching. Ctrl-C stops it.
forge bench compiles and runs every benchmark under bench/ (each bench/*.march
is a standalone program), built at --opt 2, reporting wall-clock times:
forge bench # run all
forge bench tree # only benchmarks whose name contains "tree"
forge bench --json # machine-readable timings for CI trackingforge version # print the current version
forge version patch # 1.2.3 -> 1.2.4 (rewrites [package].version)
forge version minor --tag # bump, commit, and tag vX.Y.Z (needs a clean tree)
forge release # clean tree -> build -> test -> bump + tagforge fmt --check and forge lint (which exits non-zero on errors) round out the CI checks.
forge licenses # list each dependency and its declared license
forge licenses --strict # non-zero if any dependency has no license
forge build --frozen # CI: fail if forge.lock is out of date (don't re-resolve)let x = 42
let greeting = "hello"
let flag = true
fn add(x : Int, y : Int) : Int do
x + y
end
-- Lambdas
let double = fn x -> x * 2
let add = fn (x, y) -> x + y
type Shape = Circle(Float) | Rect(Float, Float) | Point
fn area(s : Shape) : Float do
match s do
Circle(r) -> 3.14159 *. r *. r
Rect(w, h) -> w *. h
Point -> 0.0
end
end
type Point = { x : Int, y : Int }
let p = { x: 3, y: 4 }
-- Field access
let px = p.x
-- Functional update (returns a new record)
let q = { p with x: 10 }
fn describe(n : Int) : String do
match n do
0 -> "zero"
1 -> "one"
_ -> "many"
end
end
fn map(f : Int -> Int, lst : List(Int)) : List(Int) do
match lst do
Nil -> Nil
Cons(h, t) -> Cons(f(h), map(f, t))
end
end
let doubled = map(fn x -> x * 2, my_list)
fn safe_div(a : Int, b : Int) : Option(Int) do
if b == 0 do None else Some(a / b) end
end
match safe_div(10, 2) do
None -> println("error")
Some(n) -> println(int_to_string(n))
end
actor Counter do
state { value : Int }
init { value: 0 }
on Increment(n : Int) do
{ state with value: state.value + n }
end
on Get() do
println(int_to_string(state.value))
state
end
end
fn main() : Unit do
let c = spawn(Counter)
send(c, Increment(10))
send(c, Increment(5))
send(c, Get())
end
let result =
List.range(1, 10)
|> List.filter(fn x -> x % 2 == 0)
|> List.map(fn x -> x * x)
|> List.sum_int
| Function | Type | Description |
|---|---|---|
println(s) |
String -> Unit |
Print with newline |
print(s) |
String -> Unit |
Print without newline |
int_to_string(n) |
Int -> String |
Format integer |
float_to_string(f) |
Float -> String |
Format float |
bool_to_string(b) |
Bool -> String |
Format boolean |
string_length(s) |
String -> Int |
String length |
++ |
String -> String -> String |
String concatenation |
bin/main.ml compiler entry point
lib/
lexer/lexer.mll ocamllex lexer
parser/parser.mly menhir parser
desugar/desugar.ml pipe desugar, multi-head fn grouping
typecheck/typecheck.ml bidirectional HM type inference
eval/eval.ml tree-walking interpreter
tir/
tir.ml TIR type definitions
lower.ml AST → ANF typed IR
mono.ml monomorphization
defun.ml defunctionalization (closure lifting)
perceus.ml Perceus reference counting analysis
borrow.ml borrow inference (Perceus companion)
escape.ml escape analysis (stack promotion)
fusion.ml loop fusion / FBIP reuse propagation
opt.ml optimization pipeline (cprop, dce, fold, inline, simplify)
llvm_emit.ml TIR → LLVM IR
runtime/
march_runtime.c C runtime (alloc, RC, strings, I/O)
examples/
list_lib.march map, filter, fold, reverse, find
actors.march actor spawning, messaging, kill/restart
specs/ language and compiler design documents
MIT