I wanted to understand how programming languages actually work - not just “a compiler turns code into machine code” but every single step: how raw text becomes tokens, how tokens become a tree, and how that tree becomes something a CPU can run. So I built one.
Meadows is a compiled language with a C-style syntax. You write .ms files, run the compiler, and get a native binary. No interpreter, no VM - real machine code via LLVM IR.
The pipeline
Source (.ms) → Lexer → Parser → AST → CodeGen → LLVM IR → clang++ → Binary
Each stage is its own module. The lexer tokenises raw source with line/column tracking for error messages. The parser is a hand-written recursive descent parser with 11 levels of operator precedence. The AST covers 11 statement types and 11 expression types. The codegen walks the AST and emits LLVM IR, which clang++ then compiles to a native executable. The Catch2 test suite covers the full parse → codegen pipeline end-to-end, including security edge cases like integer overflow and out-of-bounds access.
What the language looks like
func factorial(n) {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
let result = factorial(5);
print result;
Variables, functions, conditionals, loops, arrays, objects, string escapes - the full set of things you reach for without thinking when you write in any language. Implementing every one of them yourself changes how you see the languages you use every day.
LSP support
The compiler also ships an LSP server with completions and diagnostics for .ms files. Implementing the protocol at the compiler stage means editor integration isn’t an afterthought — the same symbol table the codegen uses drives the completion candidates.
WebAssembly path
LLVM dependencies don’t cross-compile to WebAssembly cleanly via Emscripten, so I made the native LLVM codegen conditional and built a separate WebAssembly code path. The parser and AST run in the browser today; the native backend is skipped when building for the web. The full pipeline in a browser tab is still in progress.
Key decisions
- Recursive descent over a parser generator (yacc/ANTLR) — hand-written parsers give exact control over error recovery and source location tracking; the 11-level precedence table is explicit and readable rather than embedded in a grammar file requiring a separate toolchain to modify
- LLVM IR over direct assembly emission — LLVM handles register allocation, instruction selection, and platform-specific ABI; targeting IR means the compiler works on any architecture LLVM supports without adding architecture-specific codegen
- Conditional LLVM backend with a separate WebAssembly path — LLVM doesn’t cross-compile to WebAssembly cleanly from a browser context; separating the backends lets the parser and AST run in the browser today while the native path runs locally without compromise
- LSP server shipped alongside the compiler — a language without editor support is a language people don’t use; implementing completions and diagnostics at the compiler stage means editor integration shares the same symbol resolution as codegen
Future directions
- Full WebAssembly build with LLVM compiled to wasm
- Type system and static type checking
- Standard library (file I/O, math, strings)
- Better error messages with source spans
- Closures, structs, pattern matching