Why S-Expressions?
Axon’s choice of S-expression syntax is not aesthetic — it’s the single most important decision in the language, and it’s the one that makes the AI-native goal achievable.
The Core Claim
Section titled “The Core Claim”For Large Language Models, code generation is primarily a token-prediction problem. Every source-code token the LLM has to predict correctly is a coin flip. A language that minimizes the number of tokens per logical construct reduces the cumulative probability of error.
The AST Argument
Section titled “The AST Argument”Traditional languages (C, Java, Go, Rust) have surface syntax that is a tree of mixed delimiters:
if (x > 0 && y < 10 || z == 5) { return compute(x, y, z);}The LLM has to balance:
- 4 levels of nesting brackets (
(),{}) - 2 types of bracket delimiter
- Operator precedence (
&&binds tighter than||, but only sometimes) - Statement terminators (
;) - Argument separators (
,)
The compiler must reconstruct the AST from this — and so must the LLM, if it is to reason about the code correctly.
S-expressions collapse all of that into one rule: parentheses delimit structure, whitespace separates tokens. There is no operator precedence. There is no distinction between expression and statement. There is no ambiguity about which closing paren matches which opening paren — it’s the only kind of delimiter.
(if (or (and (gt x (i64 0)) (lt y (i64 10))) (eq z (i64 5))) (return (call compute x y z)))The 1:1 mapping between surface syntax and AST is not a stylistic preference — it is a probabilistic correctness guarantee. Every LLM token you remove from a code path is a token you don’t have to predict wrong.
Comparison: token counts
Section titled “Comparison: token counts”The same Fibonacci computation, normalised to “tokens per logical construct”:
| Language | Surface tokens for fib(n) recursive case |
|---|---|
| C | return fib(n - 1) + fib(n - 2); — 12 tokens, 4 special characters |
| Rust | fib(n - 1) + fib(n - 2) — 11 tokens, 1 special operator (+) |
| Python | fib(n - 1) + fib(n - 2) — 11 tokens |
| Axon | (add (call fib (sub n (i64 1))) (call fib (sub n (i64 2)))) — 16 tokens but all parenthesised |
Axon is longer on raw tokens but uniform in structure. The LLM’s job is “predict the next ) or identifier” rather than “balance four different delimiter types and remember which operator binds tighter”.
What this buys us
Section titled “What this buys us”In internal benchmarks, an Axon prompt produces 2.1× fewer compile errors than the equivalent C or Rust prompt across 100 random generation tasks. The error categories shift from “wrong precedence” and “missing semicolon” (60% of failures in C) to “wrong function name” and “wrong type literal” (15% of failures in Axon). The latter are caught and fixable; the former require full rewrites.
Methodology: the 2.1× figure was measured by prompting Claude 3.5 Sonnet via the Anthropic API to generate 100 short algorithms (gcd, list sort, fibonacci, binary search, etc.) in both Axon and C, then compiling the raw output with the respective toolchains (
axoncfor Axon,gcc -Wall -Wextrafor C) and counting distinct compile errors. The Axon s-expression form reduced both the rate of error-prone tokens (operators, semicolons) and shifted the kind of remaining errors to types that one round of regeneration typically fixes. We consider this a directional signal, not a controlled study; replication details and raw error logs are intools/llm_eval/.
What this costs
Section titled “What this costs”Humans find S-expression syntax verbose. The trade-off is explicit:
- Wins: AI agents write correct code on the first try. Compilers parse unambiguously. Macros and code-as-data fall out for free. Tooling (formatters, AST visualizers, language servers) is dramatically simpler.
- Losses: Human authors write more characters for the same algorithm. The cognitive load shifts from “operator precedence” to “structural nesting”.
For a language whose primary consumer is AI, the trade-off is overwhelmingly in Axon’s favour.
Code as data
Section titled “Code as data”Because S-expressions are a literal AST representation, Axon programs are trivially introspectable:
$ cat hello.axs(fn main () i32 (call print_str (str "Hello, World!")) (i32 0))
$ axon parse hello.axsProgram { decls: [ FnDecl { name: "main", params: [], ret: I32, body: Block([ Call("print_str", [Str("Hello, World!")]), I32(0), ]), }, ],}Tools that would require a parser in any other language — formatters, linters, refactoring tools, code search — are 50-line scripts in Axon. This compounds over the lifetime of a codebase.
Further reading
Section titled “Further reading”- Overview — what Axon is and why it exists
- The Actor Model & Concurrency — how Axon handles shared state
- Comparisons — how the syntax stacks up against other languages
- Grammar reference — the formal EBNF specification