LM: A Programming Language Where LLMs Write Fewer Bugs
Every programming language is a tradeoff between expressiveness and safety. JavaScript lets you write "1" + 2 and get "12". Python silently returns None from functions that forget to return. C lets you read past array boundaries.
Humans learn to navigate these traps through experience. LLMs don't have experience — they have patterns. And patterns break at the edges.
LM is a programming language designed so that common LLM coding mistakes become compile errors. No mutation, no null, no exceptions, no implicit type conversions, no inheritance. The compiler speaks both human-readable and JSON diagnostic formats, so LLM agents can parse errors and self-correct.
The hypothesis: on a standard set of programming tasks, an LLM writing LM should achieve higher first-pass correctness than the same LLM writing TypeScript or Python.
The Experiment
We tested this with a blind benchmark:
- 30 standard tasks + 10 adversarial tasks designed to trigger common LLM mistakes
- Three languages: LM, TypeScript, Python
- Two models: Claude Opus 4.6 (strongest) and Claude Haiku 4.5 (weakest)
- Blind generation: each model saw only the task description, never the expected output
- Solutions were generated by independent agents with no shared context
The adversarial tasks target areas where LM's design should help:
| Task | Trap | LM's Protection |
|---|---|---|
| String/number concat | JS implicit conversion | ++ for strings only, + for numbers only |
| Nested Option handling | Forgetting null checks | Must match to extract from Option |
| 4-variant pattern match | Missing a case | Exhaustive matching enforced by compiler |
| 3-step error chain | Forgetting to propagate | Must match every Result |
| Int/Float arithmetic | Silent type coercion | Int + Float is a compile error |
| Recursive ADT eval | Wrong case in switch | Exhaustive matching on all variants |
| Pure/IO separation | Side effects in logic | io effect system, compiler-enforced |
| Multi-field validation | Error swallowing | Result propagation required |
Results
Strong Model (Claude Opus 4.6)
On the standard 30 tasks, all three languages performed equally well:
| Language | Standard (30) | Adversarial (10) |
|---|---|---|
| LM | 29/30 | 10/10 |
| TypeScript | 30/30 | 10/10 |
| Python | 29/30 | 10/10 |
No meaningful difference. Opus writes correct code in all three languages.
Weak Model (Claude Haiku 4.5)
This is where it gets interesting:
| Language | Adversarial (10) |
|---|---|
| LM | 10/10 |
| TypeScript | 9/10 |
| Python | 9/10 |
Both TypeScript and Python failed on the same task: recursive expression evaluation with negation. Haiku wrote an Expr type with four variants (Num, Add, Mul, Neg) and got the Neg case wrong in both languages — the evaluator returned 8 instead of -3 for Neg(Num(3)).
In LM, the same model got it right. Why?
Why LM Helped
The failing TypeScript code looked something like this:
function evalExpr(e: Expr): number {
switch (e.type) {
case "Num": return e.value;
case "Add": return evalExpr(e.left) + evalExpr(e.right);
case "Mul": return evalExpr(e.left) * evalExpr(e.right);
case "Neg": return evalExpr(e.expr); // BUG: forgot to negate
}
}
The LM version:
fn eval(e: Expr) -> Int {
match e {
Num(n) -> n,
Add(a, b) -> eval(a) + eval(b),
Mul(a, b) -> eval(a) * eval(b),
Neg(x) -> 0 - eval(x),
}
}
Two things happened:
Exhaustive pattern matching — LM's compiler would reject the code if any variant was missing. The model had to explicitly handle
Neg.Pattern destructuring — In LM,
Neg(x)directly bindsxto the inner expression. The model then had to write an expression usingxthat returns anInt. The structure of the pattern match guided the model toward the correct implementation: you've extractedx, now what do you do with it? Negate it.
In TypeScript, the switch/case pattern doesn't enforce this. The model wrote return evalExpr(e.expr) without the negation, and TypeScript happily accepted it.
The Documentation Lesson
The more surprising finding was how much LM's documentation matters.
Our first blind benchmark with LM scored only 18/30 — worse than TypeScript. Every failure was the same bug: the model defined helper functions inside other functions. LM doesn't support nested function definitions, but the language reference didn't mention this.
After adding one line to the docs — "No nested functions. All fn definitions must be at the top level." — the score jumped to 27/30. Two more documentation fixes brought it to 29/30.
| Documentation version | LM score |
|---|---|
| v1 (original) | 18/30 |
| v2 (+no nested functions) | 27/30 |
| v3 (+list_map is IO, +len vs str_len) | 29/30 |
The language's constraints only work if the LLM knows about them. An undocumented constraint is worse than no constraint — the model assumes the feature exists and writes code that won't compile.
This is a general principle: for LLM-targeted languages, documentation completeness is a feature, not a nicety. Every constraint must be explicit. Every builtin's type signature must be listed. Every common mistake must have a "Wrong → Right" example.
What Didn't Work
The adversarial tasks were designed to trigger LLM mistakes in TypeScript and Python. In practice, only one task actually differentiated. The strong model made zero mistakes in any language. The weak model only failed on one task.
This tells us: LLM coding ability is improving faster than language design can exploit. The bugs LM was designed to prevent — null pointer errors, missing error handling, implicit type coercion — are bugs that today's best models rarely make.
Where LM might matter more:
- Much weaker models — open-source 7B-13B models that make these mistakes frequently
- Longer programs — 100+ line programs where accumulated small errors compound
- Self-correction loops — LM's structured JSON diagnostics let agents parse errors and fix them automatically
The Language
LM is implemented in Rust: hand-written lexer, recursive descent parser, Hindley-Milner type inference, tree-walking interpreter. 182 tests, zero clippy warnings.
| Rule | Rationale |
|---|---|
| Complete immutability | No aliasing bugs, no mutation surprises |
+ for numbers, ++ for strings |
No "1" + 2 = "12" ambiguity |
No null, use Option<T> |
Forces handling of absence |
No exceptions, use Result<T, E> |
Error paths visible in type signatures |
| Exhaustive pattern matching | Forget a case? Compile error |
Effect system (pure / io) |
Pure functions can't do IO |
| No implicit conversion | Int + Float = compile error |
Try It
git clone https://github.com/yingwang/lm
cd lm
cargo build --release
./target/release/lmc run examples/hello.lm
The benchmark suite, all blind-generated solutions, and runner scripts are in benchmark/comparison/.
Conclusion
LM demonstrates that language design can improve LLM code generation quality — but the effect is modest with today's strongest models. The real gains come from compiler constraints that make wrong code uncompilable, complete documentation of every constraint, and structured diagnostics that let agents self-correct.
The strongest result: Claude Haiku writing LM achieved 10/10 on adversarial tasks where the same model writing TypeScript and Python both scored 9/10. The difference was a single bug — but it was exactly the kind of bug LM was designed to prevent.