Ship Your Interpreter Researchers at Basis, including Kiran Gopinathan, Nikhil Pimpalkhare, Yiyun Liu and Zenna Tavares, reported an experiment in which LLMs wrote a formal semantics for a small language and proved in Lean that it matches the language's real interpreter on every program, after compiling that interpreter to RISC-V, whose meaning is already formally defined. The team published the full semantics and proof on GitHub, arguing that specification, not theorem proving, is now the bottleneck for scaling verified software. The work follows Basis agents' April JavaScript-to-WebAssembly compiler and later verification of Linux's nftables compiler, and MIT researchers Frans Kaashoek and Nickolai Zeldovich's September verification of the xv6 kernel down to machine code. Ship Your Interpreter Published: Article: Kiran Gopinathan https://www.basis.ai/about/people/kiran-gopinathan/ , Nikhil Pimpalkhare https://www.basis.ai/about/people/nikhil-pimpalkhare/ , Yiyun Liu https://www.basis.ai/about/people/yiyun-liu/ , Zenna Tavares https://www.basis.ai/about/people/zenna-tavares/ Kiran Gopinathan https://www.basis.ai/about/people/kiran-gopinathan/ LLMs are growing alarmingly capable at finding and exploiting bugs in production software, with increasing reports of AI-assisted hacks in the wild. Fortunately, LLMs are advancing quickly in their ability to write verified software : programs that come with mathematical proofs, checked by a computer, that rule out entire classes of bugs. In April, our agents built a JavaScript-to-WebAssembly compiler https://www.basis.ai/blog/verified-compiler/ but could not prove that it preserved program behaviour. Months later, our agents verified Linux’s nftables compiler https://www.basis.ai/blog/verified-nftables/ , which translates firewall rules into instructions the kernel executes. In September, MIT researchers Frans Kaashoek and Nickolai Zeldovich used AI agents to verify the xv6 operating-system kernel https://arxiv.org/abs/2609.04043 down to its machine code. Our goal at Basis is to develop verification methods that could plausibly scale to all software, so that the systems powering critical infrastructure are secure by design and keep pace with attackers’ growing capabilities. As LLMs advance in capability, the bottleneck is shifting away from theorem proving and towards the problem of specification: how can we be sure that the statements we prove capture the behaviour we actually care about? To prove anything about a program, we first need a mathematical description of what it does. This starts with describing the programming language it is written in: what every statement, loop, and function call means when it runs. These descriptions are called a formal semantics , and can be written with pen and paper, but are increasingly expressed in a proof assistant such as Lean. A handful of languages have one Standard ML, WebAssembly, and C via CompCert , as does the RISC-V instruction set that a growing number of processors execute. Developing each of these semantics took years of expert effort. Most programs, however, are written in languages that do not have a formal semantics, presenting a major bottleneck for deploying verified software at scale: without a semantics, there is nothing precise to prove against. A natural solution is to use LLMs to write semantics, but LLMs make mistakes, and a broken semantics undermines every proof built on it. Testing helps, but as we show below, a semantics can pass every test and still be wrong. What is needed is a means to verify the semantics itself. Languages without a formal semantics still have interpreters or compilers, which in practice define what their programs mean. Here we consider a radical idea: make the LLM prove that its semantics agrees with the real interpreter on every program, treating the interpreter as ground truth. To do this, we compile the interpreter to RISC-V, whose meaning is already formally defined. That gives an exact, if unreadable, description of what the interpreter does with any program. The LLM then writes readable rules for the language and proves in Lean that, for every possible program, those rules predict exactly what the compiled interpreter prints. If a rule is wrong, the proof gets stuck. We report on an experiment where we had LLMs develop a semantics and its proof together for a small language. We used Lean to prove that the interpreter, the program that runs code written in that language, follows those rules even after being compiled into low-level machine instructions. The whole development, semantics and proof, is on GitHub https://github.com/BasisResearch/ship-your-interpreter . The rest of this post walks through this experiment, explaining first why semantics are the bottleneck for larger verification efforts and why tests aren’t sufficient to build trust in them. We then show how the proof process refined the semantics, how we handled the memory reasoning, and what tools we built on the result. Every verified program starts from a semantics Before you can prove a program does anything, you have to define how each operation transforms its state, that is, define its semantics . Most languages describe their semantics informally in prose, in a reference manual, and leave the concrete details to the compiler or interpreter implementation. A formal semantics writes the rules down in mathematics, precisely enough for a proof checker like Lean 4 to use them. Notable examples include CompCert’s semantics for C, CakeML’s for ML, and JSCert’s for JavaScript. Each one took years of expert effort, and often covers only a restricted subset of its language. For this experiment we used an extended version of WHILE, the small imperative language used in textbooks such as Nielson and Nielson’s Semantics with Applications https://orbit.dtu.dk/en/publications/semantics-with-applications-a-formal-introduction/ . Our version has integers, strings, first-class functions with closures, if , while and for loops, and runtime errors. Its interpreter is 1,855 lines of C, and a WHILE program looks like this it prints 55 : js var i = 0; var sum = 0; while i < 10 { i = i + 1; sum = sum + i; } println sum ; The semantics an LLM wrote for WHILE is a set of rules, one for each kind of expression and statement. These rules describe how to evaluate an expression in terms of its subexpressions. For integer addition, the rule is: st, env ⊢ l ⇓ a, st' st', env ⊢ r ⇓ b, st'' a + b = c ────────────────────────────────────────────────────────── st, env ⊢ l + r ⇓ c, st'' A rule reads from the bottom up. The line at the bottom is its conclusion. Starting from state st in environment env , the expression l + r evaluates ⇓ to c and leaves state st'' . The state holds variable values, closures and everything printed so far; the environment says which variables are in scope. The premises above the line explain how the result follows from the subexpressions. First l evaluates to a , leaving state st' . Then r evaluates to b in that state, leaving st'' . The final premise, a + b = c , adds the two integer values using WHILE’s 64-bit wrapping arithmetic. Take 1 + 1 + x , with state x ↦ 5 and environment {x} : x holds 5 and is in scope. Applying the rule gives: x ↦ 5 , {x} ⊢ 1 + 1 ⇓ 2, x ↦ 5 x ↦ 5 , {x} ⊢ x ⇓ 5, x ↦ 5 2 + 5 = 7 ───────────────────────────────────────────────────────────────────── x ↦ 5 , {x} ⊢ 1 + 1 + x ⇓ 7, x ↦ 5 That is, to evaluate 1 + 1 + x , we first evaluate 1 + 1 to get 2, then look up x to get 5, and add the two values to get 7. Neither subexpression changes the state, so x ↦ 5 remains unchanged. To justify the first premise, we apply the addition rule again: x ↦ 5 , {x} ⊢ 1 ⇓ 1, x ↦ 5 x ↦ 5 , {x} ⊢ 1 ⇓ 1, x ↦ 5 1 + 1 = 2 ─────────────────────────────────────────────────────────────── x ↦ 5 , {x} ⊢ 1 + 1 ⇓ 2, x ↦ 5 These rule applications form a tree that derives the value of the whole expression from the values of its parts. The LLM wrote about 400 lines of rules like these, in Lean the full semantics https://github.com/BasisResearch/ship-your-interpreter/blob/fb07094cff916318c34b07494a7d739f1e53989b/Vsa/While/Semantics.lean . Each rule looks reasonable on its own, but an end user has no rigorous way to tell whether they faithfully describe how WHILE programs behave when they are actually run. A semantics can pass every test and still be wrong The usual way to check a semantics is to run it. Take a test suite, run each program under the semantics and under the real interpreter, and compare what they print. Unfortunately, in practice, this process is insufficient to build trust in the generated semantics. For example, an early version of our semantics passed the test suite generated for the WHILE interpreter, but disagreed with the binary when a function’s name reached 59 characters: fn aaa…aaa {} // a name of 59 a's println "" + aaa…aaa ; semantics: