Engineering tour · github.com/mparrett/fmpl
Streaming-first, prototype-based, and built for AI agents — grown in Rust from the grammar of a 1992 MUD-server language, with first-class PEG grammars, an indexed-RPN bytecode VM, and a parser that compiles itself.
Lists, pattern guards, a user-defined grammar, and the parse → IR → bytecode → eval pipeline — in nine lines of REPL. Or skip the transcript: run these yourself in the browser REPL — the same VM compiled to WebAssembly.
The original FMPL (“of Accardi”) came out of UC Berkeley’s Experimental Computing Facility around 1992 — a MUD server language in the LambdaMOO / ColdMUD tradition, with an interpreter written by Jon Blow. Those systems shared a distinctive shape: a live image of prototype-based objects, edited from inside, serving many users at once, persisting across restarts. The program wasn’t a file you ran; it was a world you inhabited.
This FMPL is Norman Nunley’s — a descendant of the original, not a restoration of it. The surviving link to the 1992 language is an EBNF grammar Nunley extracted from its sources back in the late 1990s; decades later, that grammar became the seed of this project. The syntax is only lightly similar to the original, and everything beyond the grammar — the streaming model, the PEG grammar system, the indexed-RPN VM, the metacircular bootstrap — is new design. The MUD lineage is first-hand, too: Nunley co-wrote cool++, a C++ rewrite of Stephen White’s CoolMUD (White also created MOO, the server LambdaMOO grew from).
The project bets that the MUD shape is the right shape for AI agents. A modern agent system wants exactly what a MUD had: long-lived stateful objects, many concurrent actors (now LLMs as well as humans), capability-scoped access between them, and durable state that survives a crash mid-conversation. FMPL adds the modern half: streaming as a first-class concern, OMeta-style PEG grammars as the universal tool for parsing any stream — text, bytes, or structured values — and an async runtime for tool calls.
One more thing worth saying plainly, because it shapes the codebase: most of this implementation was written by an autonomous LLM agent loop that Nunley set up and steered, iterating against a roadmap for months. It was recently rehabilitated for public release by hand — the agent harness sidelined to an archive branch (~30,000 lines), a build that works on a fresh clone, CI, and a documentation pass in which every claim in the tutorial was re-verified against a live REPL. The result is unusually honest about itself: the test suite encodes where the language is and, via 74 deliberately-ignored tests, where it is going.
FMPL is a Cargo workspace of six crates around one core. The execution pipeline is conventional in outline and unconventional in detail:
The whole language: lexer, parser, compiler, bytecode VM, prototype object system, OMeta-style PEG grammar engine (with packrat memoization, streaming input, and an optional trampolined evaluator for bounded stack), pattern matching, tuple space, and Fjall-backed persistence. Sixteen builtin modules cover I/O, HTTP, JSON, and the metaprogramming surface.
The REPL — rustyline, dot-prefixed commands, multiline continuation.
Axum + HTMX web REPL with per-user sessions and an approval queue.
Ratatui terminal UI with DAG-based conversation management and LLM chat.
Stage-0 interpreter that runs the parser generator at build time, breaking the circular dependency.
Data-driven test runner; the build script generates a test suite from a markdown corpus of behavior scenarios.
wasm-bindgen bindings for the browser REPL — the same VM compiled to WebAssembly, rebuilt from source on every deploy.
The VM’s central design choice: instead of a classic operand stack, every instruction writes its result to values[ip] — the slot named by its own instruction index — and operands are references to earlier instruction indices, not popped values.
// A frame is just results-by-instruction-index plus a cursor:
Frame { values: Vec<Value>, ip: usize }
// "1 + 2" compiles to:
// 0: LoadInt(1) → values[0] = 1
// 1: LoadInt(2) → values[1] = 2
// 2: Add { lhs: 0, rhs: 1 } → values[2] = 3
Every intermediate result stays addressable for the frame’s lifetime, which pays off twice: the async runtime can suspend mid-expression (an <- await or a streaming read) and resume with all live values intact by index, and serializing a continuation for durable suspension is a matter of writing out the frame, not reconstructing stack discipline.
The single most characteristic feature is the @ operator: apply the thing on the right to the stream on the left. The right side can be a full OMeta-style PEG grammar rule, an inline pattern block with guards, or a tree-transformation grammar — over strings, byte streams, or lists of structured values. Parsing, destructuring, and data transformation are one mechanism, not three.
The grammar engine is a proper PEG runtime: ordered choice, negative lookahead, packrat memoization, left-recursion handling via the optimizer’s first-set computation, incremental/streaming input, and semantic actions that are ordinary FMPL expressions with :binding captures. A distinctive detail with teeth — action errors are hard errors, not backtrackable failures: if a semantic action raises (say, integer overflow while folding digits), the parse aborts with that error rather than silently trying another alternative. The generated parser and the interpreted engine agree on this by contract.
The object system is prototype-based in the Goblins tradition — spawn, facets for capability attenuation, bcom-style become — with self, parent, and caller as the method-context variables. The north star is image-based: the persistent object image is the source of truth and source files are a bootstrapping convenience. Persistence rides on Fjall, an embedded LSM key-value store, with a versioned zero-copy envelope (rkyv/zerocopy) for every persisted record. A Linda-style tuple space for actor coordination exists in-core but isn’t yet surfaced in the language.
The flagship engineering commitment (DESIGN-001): the Rust parser is a stage-0 mechanism, and the canonical parser is written in FMPL itself. The chain that makes this real runs at build time:
An FMPL grammar describing FMPL is executed by a minimal interpreter, which emits a complete recursive-descent Rust parser that is compiled into the core crate. ast::parse in the hero transcript above runs through this generated parser. Because a stale or silently-substituted parser would make every parity test vacuously true, the build embeds a generator epoch number (currently 9) checked at compile time, and a canonical_pipeline_must_be_active test fails loudly if the fallback parser is in use.
The same discipline extends downstream: lib/core/ast_to_ir.fmpl is a tree-transformation grammar that lowers AST to IR, ir::compile linearizes IR to bytecode, and 21 parity tests compare the FMPL pipeline’s output against the Rust compiler’s, node type by node type. The self-hosting loop is genuinely closed for core expressions and genuinely open for the rest — the ledger in §5 is precise about which.
The first CI run in this repo’s history failed a check no local build had ever exercised: regenerate the parser twice, byte-compare the outputs. They differed — every run. The cause: grammar rules travel through FMPL’s Value::Map, which is a HashMap, so each generator process emitted the parser’s functions in a different random order. Rust doesn’t care about definition order, so 1,390 tests stayed green for months while the build was nondeterministic underneath.
The fix was one sort — emit rules in name order — but the lesson generalizes: byte-stable codegen is a property you must assert, because nothing else will tell you. The previously-ignored determinism test now runs on every build.
Every row below was verified against a fresh REPL during the July 2026 documentation sweep — this is observed behavior, not roadmap.
&& || !; overflow is a clean error, not a panic.map() / .fold(), prototype objects with self\x, \x, y, curried \x \y, lambda (n); recursion via top-level let_:name bindings and when/if guards:binding captures and expression actionsast::parse → transform → ir::compile → code::evalis_int, …, type_name())json::parse, curl.get/post; LLM clients for Claude and Ollama in the stdlib<-, spawn, |> — syntax in, runtime in progress[first | rest]; string interpolationlet; mutable closure capture<: — deliberately deferred (DESIGN-005)**; list .length() / .get()The metaprogramming pipeline deserves one full example, because it is the language’s thesis in miniature — a compiler written as a pattern match:
let ast = ast::parse("1 + 2")
-- => [:Binary, :+, [:Int, 1], [:Int, 2]]
let ir = ast @ {
[:Binary, :+, [:Int, a], [:Int, b]] => [:Add, [:LoadInt, a], [:LoadInt, b]];
[:Binary, :-, [:Int, a], [:Int, b]] => [:Sub, [:LoadInt, a], [:LoadInt, b]]
}
code::eval(ir::compile(ir))
-- => 3
This is exactly the shape of the real bootstrap: ast_to_ir.fmpl is this pattern match, grown to cover the language.
74 tests are #[ignore]d, each with a machine-readable reason, grouped by root cause in docs/known-gaps.md. Read as a ledger, the count is intent rather than neglect — the suite pins behavior for features before they exist:
ast_to_ir.fmpl end to end, and fmpl_parser.fmpl runs as an interpreted grammar in the VM. How it went to zero — five stacked bugs behind one error message — is written up in the field log.let and mutable closure capture need a strategy (Y-combinator, let rec, or cells); yield.Fuzzing the REPL found that typing 99999999999999999999999 aborted the process. Fixing the VM’s arithmetic to checked ops moved the panic rather than removing it: the generated parser’s digit-fold action (acc * 10 + d) was emitted with .unwrap() on the now-fallible ops, and the REPL runs that parser on every keystroke’s completeness check.
The real fix was threading Result through generated grammar actions — fold closures return Result, arithmetic emits ?, and backtracking combinators propagate runtime errors instead of swallowing them, matching the interpreted engine’s semantics. The tempting shortcut, catch_unwind at the eval boundary, was rejected for a concrete reason: FMPL holds Mutexes across evaluation, and a caught panic would poison them.
Five durable invariants govern the project (docs/design-principles.md). They override feature scope when in conflict — several large deletions in the history are these principles being enforced:
[:Tag, child, …]. The former :Tag(args) constructor syntax was deleted from both parsers and is rejected with a migration hint — a second representation would tax every consumer at every seam.:Binary), compared by identity, never strings.ir::compile allocates instruction indices. Transformation grammars never see indices — sharing is expressed with :Let.<: inheritance, and premature helpers would obscure it.The build is two-step because of the bootstrap: a plain cargo build works but silently uses the Rust fallback parser. just build runs both steps and leaves the canonical FMPL-generated parser active (a parity test enforces it).
git clone https://github.com/mparrett/fmpl && cd fmpl
just build # bootstrap the FMPL-generated parser, then build the workspace
just test # 1,519 passing / 74 pinned to future work
just repl # REPL (dot-commands: .help, .quit)
just web # web REPL http://localhost:3000
just tui # terminal UI (Ctrl+L for LLM chat)
Start with the language guide — the web rendering of TUTORIAL.md and DEMO.md, every snippet of which runs as documented, verified line-by-line against the REPL. specs/ holds the implementation specs (VM, grammars, objects, persistence, tuplespace); docs/known-gaps.md is the honest map of the frontier. If you want to contribute, the metacircular-parser bucket is the critical path: pick a file from the ledger, run it with -- --ignored, and land the feature its tests describe.