A programming language that parallelizes your code for you.
You write what you want computed. Mapal works out what can run at the same time, how to split it across cores, how to use the vector and matrix hardware, and which bounds checks it can safely drop. No threads, no locks, no intrinsics, no tuning pragmas - none of it appears in the source.
The same source runs on CPU and GPU, with FPGA planned.
fn main() {
1048576 -> iota -> ts;
ts -> map { t -> t * 2 } -> doubled;
doubled[1048575] -> println;
}
That runs across every core of the machine. Nothing in the source says so.
Fast code is usually fast because a human wrote machine details into it: tile sizes, thread counts, vector widths, memory layouts. Those details are what stop it moving to other hardware.
Mapal keeps them out of your source. The program says what depends on what; the machine details are read off the machine you are compiling on - cache sizes, line sizes, vector widths, whether there is a matrix unit. Change the machine, keep the source.
- What can run at the same time, and how to spread it across cores
- Which loops are matrix-shaped, and how to tile them for the cache and the vector registers
- Which data is read repeatedly, so it gets laid out once instead of re-fetched
- When an access pattern will fight the cache, and how to reorder it - from the machine's own cache geometry, which it detects
- Which bounds checks are provably unnecessary - and only those
How it does that: docs/architecture/.
Method, machine specs, raw logs: docs/performance/.
| N | Mapal | NumPy | |
|---|---|---|---|
| 1024 | 0.72 ms | 0.67 ms | NumPy 1.07× |
| 2048 | 5.03 ms | 5.33 ms | Mapal 1.06× |
| 4096 | 38.7 ms | 44.1 ms | Mapal 1.14× |
3,556 GFLOP/s vs 3,115 at 4096, disjoint. Naive threaded C++: 33,439 ms. Single-threaded NumPy is ~2× ahead (why).
| workload | Mapal FMA off | Mapal FMA on | C++ naive-mt | NumPy 1t |
|---|---|---|---|---|
| FIR filter, 1M | 0.305 ms | 0.289 ms | 1.50 | 6.35 |
| conv2d 3×3, 1024² | 0.084 ms | 0.083 ms | 0.16 | 1.72 |
| saxpy, 1M | 0.070 ms | 0.067 ms | 0.12 | 0.18 |
| sum reduction, 1M | 0.560 ms | 0.562 ms | 0.92 | 0.11 |
| transpose, 1024² | 0.137 ms | 0.124 ms | 0.26 | 0.86 |
gather x[idx[i]], 1M |
0.139 ms | 0.137 ms | 0.16 | 2.19 |
Every row above moved this session, 1.00–1.31×, from scheduling alone — the kernels are unchanged (what changed).
| workload | Mapal FMA off | Mapal FMA on | C++ naive-mt | NumPy 1t |
|---|---|---|---|---|
| FIR filter, 1M | 0.225 ms | 0.187 ms | 1.24 | 5.44 |
| conv2d 3×3, 1024² | 0.118 ms | 0.084 ms | 0.41 | 2.85 |
| saxpy, 1M | 0.137 ms | 0.130 ms | 0.42 | 0.47 |
| sum reduction, 1M | 0.394 ms | 0.391 ms | 7.32 | 0.14 |
| transpose, 1024² | 0.200 ms | 0.176 ms | 0.47 | 2.22 |
gather x[idx[i]], 1M |
0.236 ms | 0.203 ms | 0.48 | 1.33 |
Transpose single-threaded on this box: 2.42 → 1.09 ms, −55%, disjoint (what changed).
| Mapal | NumPy | ||
|---|---|---|---|
| single-threaded | 14.88 ms | 12.21 ms | 1.22× |
| threaded, 8 P-cores | 2.09 ms | 1.74 ms | 1.20× |
| threaded, whole box | 1.52 ms | 1.50 ms | tie |
The whole-box tie is heterogeneity tolerance, not a better kernel - same box, 8 threads per row (detail):
| 8 threads on… | Mapal | NumPy | |
|---|---|---|---|
| 8 E-cores (uniform) | 5.89 ms | 5.59 | NumPy 5% |
| 8 P-cores (uniform) | 2.44 ms | 1.72 | NumPy 41% |
| 4 P + 4 E (mixed) | 3.38 ms | 5.57 | Mapal 65% |
Forward pass only, square (batch = width = d), every layer d×d. Values are
bit-identical to NumPy's on every cell. NumPy is the better of its 1-thread and
multi-thread legs. One run, 41 interleaved cycles
(full table + noise spreads).
| d | 4-layer net | NumPy | 2 nets at once | NumPy | ||
|---|---|---|---|---|---|---|
| 128 | 0.081 ms | 0.023 | NumPy 3.5× | 0.076 ms | 0.023 | NumPy 3.3× |
| 256 | 0.261 ms | 0.092 | NumPy 2.8× | 0.192 ms | 0.091 | NumPy 2.1× |
| 512 | 0.633 ms | 0.645 | tie | 0.473 ms | 0.644 | Mapal 1.36× |
| 1024 | 2.930 ms | 3.764 | Mapal 1.28× | 2.771 ms | 4.064 | Mapal 1.47× |
| 2048 | 20.36 ms | 24.97 | Mapal 1.23× | 19.68 ms | 25.65 | Mapal 1.30× |
Two independent nets are the same total work as one 4-layer net. Nothing in the source says "run these in parallel" (why that is free).
| d | one 4-layer net | two 2-layer nets | gain from the graph alone |
|---|---|---|---|
| 256 | 0.261 ms | 0.192 ms | 1.36× |
| 512 | 0.633 ms | 0.473 ms | 1.34× |
| 1024 | 2.930 ms | 2.771 ms | 1.06× |
Small sizes are now a single-thread kernel gap, not a scheduling one (breakdown).
| build | what you get |
|---|---|
| FMA off (default) | bit-identical to the interpreter, always |
FMA on (--contract) |
lets the hardware fuse multiply and add - faster, last bit moves |
SME (--contract --target=apple-m4-sme) |
same as FMA on, plus the matrix unit; needs an Apple M4 or newer |
| Language core | working - functions, pipelines, parallel fanout, guards, loops, map/fold/zip/enumerate, tuples, fixed arrays, print |
| Interpreter | working - defines the language; every backend is tested against it |
| CPU backend (LLVM) | working - automatic threading, vectorization, cache blocking |
| GPU backend (CUDA) | working - 640 compile-and-runs on an RTX 4090, July 2026; not re-validated on hardware since. No time builtin |
| FPGA backend (Verilog) | not started |
| Command-line tool | not built - mapal prints "not yet implemented" and exits 1 |
| Tests | 1,047; 161 are CUDA's and skip without nvcc. Green |
| CI | cargo fmt + full suite on Linux and macOS, per push. Cannot pass vacuously - a skipped LLVM differential fails the run |
"Byte-identical" means: every compiled program is checked to print exactly what the interpreter prints - 10 examples plus 320 generated programs, at two optimization levels, 1,280 comparisons on every run. Changing the thread count does not change the answer.
Per-component state and test counts: docs/STATUS.md.
Research compiler. No packaged release.
Kernels are the proving ground: byte-identical output is easiest to check against code everyone already has a fast version of.
Not in the language yet: recursion, sum types, pattern matching, strings beyond printing, closures, dynamic arrays, modules. Exactly two effects. General-purpose in shape, not yet in surface.
Where it is going: one program whose parts run on several processors at once - CPU and GPU together - with the data movement between them handled by the compiler rather than by hand. Same answer, byte for byte, however the work gets split.
Needs a recent Rust toolchain and clang.
# the test suite = the correctness argument
cargo test --workspace --release
# run a program on the interpreter
cargo run --release -p mapal-interp --example run -- examples/pipeline.mapal
# compile a program to a native binary
cargo build --release -p mapal-rt # the runtime it links against
cargo run --release -p mapal-backend-llvm --example emit -- \
examples/fir.mapal - --rewrite > fir.ll
clang -O2 fir.ll target/release/libmapal_rt.a -o fir && ./firStart in examples/ - pipeline.mapal for syntax, sepia.mapal for most of the
language, fir.mapal for a loop.
Both editors get highlighting and a file icon. Neither is published to a registry, so both load from disk.
| Neovim | VS Code | |
|---|---|---|
| Highlighting | Vimscript syntax file | TextMate grammar |
| File icon | font glyph | the real SVG logo |
| Binding vs call | resolved by scanning for fn declarations |
lexical only - -> name; reads as a binding |
| Details | editors/nvim/ |
editors/vscode/ |
Neovim - with lazy.nvim:
{ dir = "/path/to/mapal/editors/nvim", ft = "mapal" }or without a plugin manager:
vim.opt.runtimepath:append("/path/to/mapal/editors/nvim")
require("mapal.icon").setup() -- optional: nvim-web-devicons / mini.iconsVS Code / Cursor - build the extension and install it:
python3 editors/vscode/package-vsix.py
code --install-extension editors/vscode/mapal-lang-0.1.0.vsix # or `cursor`Then restart. A .mapal file should show Mapal in the status bar. Copying the folder into
~/.vscode/extensions does not work: VS Code reads extensions.json as its registry and
ignores unregistered directories silently.
The logo as a terminal glyph. The Rust and C++ marks in a file tree are font glyphs, not
images - Nerd Fonts ships those brand logos as characters. So Mapal's mark ships as a
single-glyph font, assets/font/MapalIcons.ttf: install it and add it as a
terminal fallback font, rather than us patching and redistributing someone else's Nerd Font.
Then:
local icon = require("mapal.icon")
icon.setup({ glyph = icon.logo }) -- the real mark at U+F8F0Without it, the closest glyph the Nerd Font already has is used. Neither editor has an LSP yet, so neither resolves names the way the compiler does (ADR-0008).
- Close the single-threaded gap, where NumPy is still ~2× ahead. Threaded is solved; this is a different bottleneck (what we measured).
- Pick the thread count from the program instead of defaulting to every core (plan).
- Intel AMX, so matrix units are not an Apple-only story.
- GPUs in earnest, then running one program across several processors at once.
Done so far, and how each turned out, is in
docs/sessions/ - including the things that were tried and did not work.
Everything is written down, including what turned out wrong:
docs/STATUS.md- what is built, with test countsdocs/decisions/- every significant decision and the alternatives it rejected, indexed (ADR = Architecture Decision Record)docs/performance/- every benchmark: machine, method, failuresdocs/sessions/- dated log of every work session, mistakes included
Contributing: CONTRIBUTING.md - the model-first workflow, the
measurement rules, and the open ADRs anyone can pick up (dynamic
arrays, generics, sum types, an external-backend SDK, co-execution, scan). Recursion, modules
and closures are not in the language yet and have no decision record - writing one is the
contribution that unblocks the code. Forks welcome. One house rule: a change arrives with the evidence of what it
did, and names the published numbers it moves.
Apache License 2.0 with the LLVM exception - the license LLVM and Swift use, for the same reason. Apache-2.0 carries an explicit patent grant; the exception keeps the runtime Mapal links into your binaries from imposing attribution requirements on your program's output.