Compiler Implementation
Keywords of this phase
The keywords that phase 7, Implementation & Tools, adds to the core language (the core keywords are in Syntax). Each one is explained on the page in the last column.
| Keyword | Meaning | Explained in |
|---|---|---|
call | runs a shell command in synchronous mode: call "ls -l"; | Command |
Open Standard
A language is defined by its specification, not by one program that happens to run it. Eve keeps the two apart on purpose: the specification describes what every Eve program means, and any implementation that follows it is a valid Eve compiler.
One Language, Many Compilers
Several independent implementations make a language stronger. Each one finds unclear rules in the specification, and fixing those rules helps every other implementation. For a student, writing a compiler for a language that already has a specification and a test suite is a demanding exercise: it covers lexing, parsing, type systems, code generation and testing in one project.
- An implementation may be an interpreter, a bytecode virtual machine, a transpiler or a native compiler.
- The official implementation is the Eve virtual machine, written in Zig. It is one program: depending on how it is called, it runs a script directly (
eve script.eve) or works in the other modes of the virtual machine (see EVE VM). - It must state which specification version it implements, for example "Eve 0.1".
- It must state the conformance level it passes (see below).
Specification First
The Eve specification is written in the eve-lang repository, in the spec/ folder. It uses Markdown for rules and explanations, and JSON for the tables a compiler loads directly:
| File | Contents | Used by |
|---|---|---|
lexical/keywords.json | Reserved words and their category | Lexer |
lexical/operators.json | Symbols, precedence, associativity | Lexer, expression parser |
syntax/grammar.md | Complete grammar in EBNF | Parser |
semantics/types.json | Types, default values, coercion rules | Type checker |
library/builtins.json | Built-in functions and signatures | Runtime |
The specification is a work in progress (version 0.1-draft). Where it is not complete yet, this tutorial is the reference. When the two disagree, the specification wins.
Conformance Levels
The test/ folder of the repository holds acceptance tests in three levels. Every test is an Eve driver, so it runs as a standalone program.
| Level | File names | What it proves |
|---|---|---|
| 1 | a01_feature.eve | Basic syntax: one script per test, one feature per script |
| 2 | b01_feature.eve | Automation drivers with aspects: several related tests per driver |
| 3 | c01_feature.eve | Advanced features: files, databases, network, libraries |
Status of the Reference Implementation
The first implementation is the Eve virtual machine, written in Zig (evevm/, built to bin/eve.exe). Compiler stage: Level 1 implemented. It parses every script into a syntax tree, checks the whole tree (the static check, below), and only then executes it with a tree-walking interpreter.
| Level | Status | Tests |
|---|---|---|
| 1 | Implemented | 65 of 65 pass (test/level1/), 2026-10-08 |
| 2 | Implemented: functions, procedures, lambdas, closures and aspects | 63 of 63 pass (test/level2/), 2026-10-08 |
| 3 | Implemented: modules, imports, managed and direct modules, classes and methods | 50 of 50 pass (test/level3/), 2026-10-08 |
Level 1 covers drivers and free scripts, new, let and set, operators and precedence, strings with placeholders and formats, if, while, loop, for, match, labels, ordinals, lists, arrays, matrices, slices and views, DataSets, DataMaps, Objects, regular expressions (a small subset), expect and assert, over, exit, panic, raise, defer, recover, finalize, and jobs with retry, resume and stop. Functions, procedures, lambdas and closures (level 2), and classes, methods, Date, Time and the operator as (level 3) are not part of level 1.
Known limitations of the reference implementation
These are the places where bin/eve.exe is simpler than the specification. The tests of level 1 pass with them; a later version removes them. Another implementation may do better, but it must pass the same tests.
- Numbers.
DecimalandFloatare stored as a 64-bit float, so a Decimal is not exact;Hugeis a 64-bit integer, not an arbitrary-size number. The results of arithmetic on a typed number (Byte,Short,Decimal) are anIntegeror aReal: the type suffix is kept only by the literal. - Text. A value is a Text, not a String, only when it comes from a
"""literal: the machine remembers the address of the literal, sotype(x)may say String for a copy made by other means. - Static types. The static check infers the type of a variable only from a literal, a declared type or a constructor call. A rule that needs a type is checked only then: the private methods of a class (
c.inc()is refused whencis known to be a Counter, not when it comes from a parameter) and"a" + 1. When the check does not know, it says nothing and the interpreter raises the error when the line runs. - Types are not enforced. A declared type gives the default value and converts a Real to an Integer or an Integer to a Real; it does not refuse a wrong assignment. A variant type
{Integer | Real}accepts any value. - Regular expressions support literals,
.,^,$, classes,\d \w \s, the quantifiers* + ?, alternatives with|and the flagi; there are no groups. - Ranges. A range with real limits, an open end or a real step is a domain: it answers
inbut can't be iterated. The step of a domain is a grid that starts at 0:4 in (1..8)(2)is true. - Entities. The character references
&name;of a string know the five marked characters and the lower case Greek letters only. - Exit code 5 (unexpected stop: Ctrl+C, a time-out, an outage) is reserved; the machine does not set it yet, and no test checks it.
- Levels 2 to 7 (modules, aspects, imports, parallel groups, databases, the server) are not implemented.
The machine is also driven by command files. eve -x -i level1.vmc runs a .vmc file (Virtual Machine Command): one command per line, run in order from the first line. A workflow file loads one script at a time (load, parse, run, errors, ast, inspect, status), ends with stop, and leaves reports in a folder: the output, the errors, the syntax tree and an introspection report for every script. python script/workflow.py 1 runs a whole level this way and compares it with expect.json. The commands are described in manual/usage.md.
Compiler Architecture
Every Eve implementation follows the same front end. The back end is where implementations differ.
Pipeline
| Stage | Input | Output |
|---|---|---|
| 1. Lexical analysis | Source text (UTF-8) | Token stream |
| 2. Parsing | Tokens | Abstract syntax tree (AST) |
| 3. Semantic analysis | AST | AST with resolved names and types |
| 4. Lowering | Typed AST | Intermediate representation (bytecode, IR or target source) |
| 5. Execution or emission | IR | Program output, or an executable file |
Keep the stages separate, even in a small project. A clean AST lets you add a second back end later without touching the parser.
Lexical Analysis
The lexer turns characters into tokens. For Eve, the difficult part is comments, because each comment marker starts with a character that is also an operator (see Delimiters):
#and##at column 0 start a comment line (a title or a subtitle), while#inside a line is a placeholder or a symbol of a template string.**starts a comment that runs to the end of the line, while*is multiplication and the vararg mark.(** ... **)is an expression comment, which the lexer must tell from(*, a parenthesis followed by a vararg*as in(*args)./* ... */is a block comment (it does not nest), while/is division.
Check comment markers before operators, and follow the longest-match rule. Load keywords and operators from the JSON files instead of hard-coding them, so your lexer follows the specification when it changes. Record the line and column of every token: good error messages depend on it.
For the classic hello world driver, a lexer produces a token stream like this:
# Traditional hello world demo
driver hello_world is
** every driver must have a process
process main is
print ("Hello World!");
return;
end hello_world;
COMMENT "# Traditional hello world demo"
KEYWORD driver
NAME hello_world
COLON :
COMMENT "** every driver must have a process"
KEYWORD process
KEYWORD print
LPAREN (
STRING "Hello World!"
RPAREN )
SEMICOLON ;
KEYWORD return
SEMICOLON ;
EOF
Parsing
Eve has an explicit, keyword-driven syntax: a script is one indented scope (driver … is, declarations, process, end name;) and blocks close with end, return and done keywords. This makes a hand-written recursive descent parser a good fit:
- Write one parsing function per grammar rule; the grammar in
spec/syntax/grammar.mdmaps directly to these functions. - Parse expressions with precedence climbing (Pratt parsing), driven by the precedence table in
operators.json. - On a syntax error, report it, skip to the next
;or region keyword, and continue, so one run reports several errors.
Parser generators (ANTLR, Lark) also work, and they are useful to check the grammar itself. Writing the parser by hand, however, teaches more and gives better error messages.
Semantic Analysis
This stage gives the program its meaning. For Eve it covers:
- Scopes: global and local names, the
@and$sigils, and system variables (see Identifiers). - Types: static checking with type inference, gradual typing, default values and coercion rules (see Data Types).
- Program topology: drivers, aspects and modules, and how imports resolve (see Topology).
- Numeric precision: comparisons that depend on
$epsilon, and exact rational results such as10/3.
Execution Strategies
| Strategy | How it works | Trade-off |
|---|---|---|
| Tree-walking interpreter | Evaluate the AST directly | Quickest to build; slowest to run |
| Bytecode virtual machine | Compile to compact instructions, run them in a loop | Good speed; you design the instruction set |
| Transpiler | Emit source code in another language (C, Python) | Reuses a mature toolchain; errors surface in the target language |
| Native compiler | Emit machine code through LLVM, QBE or Cranelift | Fastest programs; the most work |
Start with a tree-walking interpreter to validate your front end against the tests. Replace the back end later; the tests stay the same.
Recommended Languages
Any general-purpose language can implement Eve. These are the ones we recommend for students, with the reasons.
Comparison
| Language | Why it fits | Natural back end | Effort |
|---|---|---|---|
| Python | Short code; match statements and dataclasses model an AST well | Tree-walking interpreter | Low |
| Zig | Small language, explicit memory control, one static binary, C interoperability; used for the official Eve virtual machine | Bytecode VM | Medium to high |
| Java / Kotlin | ANTLR support; the JVM as a ready-made target | JVM bytecode | Medium |
| OCaml | Algebraic data types and pattern matching make compiler passes short; used by many real compilers | Native, via C or LLVM | Medium to high |
| Rust | Enums and pattern matching for ASTs; memory safety without a garbage collector; Cranelift and LLVM bindings | Bytecode VM, native | High |
| C | Full control over memory and instruction layout; the traditional way to build a VM | Bytecode VM | High |
Choosing by Goal
- First compiler, learn the concepts: Python. You spend your time on the language, not on the tools.
- Performance and systems skills: Rust or C, with a bytecode VM or native code.
- Compiler theory: OCaml. Its type system catches many compiler bugs before they run.
- Interoperability with existing libraries: Java or Kotlin on the JVM, or a C transpiler.
Pick a language you already know well. A compiler is a large program; learning a new language at the same time doubles the difficulty.
Tools and References
- Crafting Interpreters: a free book that builds a tree-walking interpreter and a bytecode VM step by step.
- ANTLR: a parser generator for Java, Python and other languages.
- Lark: a Python parser library that reads EBNF grammars; useful to check the Eve grammar.
- Tree-sitter: incremental parsing for syntax highlighting and editor support.
- LLVM and QBE: back ends for native code.
Student Project
Build the implementation in milestones. Each milestone ends with a set of programs your implementation handles correctly, so you always have working software.
Milestones
| Milestone | Goal | Done when |
|---|---|---|
| M1 Lexer | Tokens with line and column, all comment forms | Every example of the Demos & Examples page and every file in test/level1/ tokenizes without errors |
| M2 Parser | AST for regions, statements and expressions | Every demo parses; an AST printer shows the structure |
| M3 Core runtime | Drivers, process, variables, expressions, print and write | hello_world.eve and the first level 1 tests run |
| M4 Types | Type checking, inference, coercion, default values | Type errors are reported with line and column |
| M5 Control and functions | Conditionals, loops, functions, lambdas, methods | All level 1 and level 2 tests pass; classes (level 3) c10 to c17 |
| M6 Data | Collections, strings, classes and objects | The collection and class demos give the expected output |
| M7 Topology | Modules, aspects, imports, error recovery | Level 2 tests pass |
Where the reference implementation stands: M1, M2, M3, M5 and M6 are done in one tree-walking interpreter (all level 1 tests pass); M4 (type checking) and M7 (topology) are open.
What Level 1 Taught
Rules that the first implementation had to decide while running the level 1 tests. They are now part of the language, and a new implementation must follow them (the decisions are D-058 and D-063 in plan/decision_level1.md):
- Grouping is not a list.
(x)is the value ofx;(x,)is a list of one element. Without this rule(2 + 3) * 4cannot be an expression. - A Rune is one code point.
'one'is a syntax error; use"one". The lexer should reject it with a clear message, not crash later. - One index on a matrix is absolute.
m[5]counts the elements row by row; a row ism[1, *]and a columnm[*, 1]. - Errors carry a code. every error has its own code (
raise4, an index error 10, a missing key 11, a division by zero 12, see Exceptions); the exit code of the process is another thing: 2 for a failedexpect, 3 for a failedassert, 4 for any other unhandled error;recovercatches all of them and reads$error.message,$error.codeand$error.job. An unhandled error runsfinalizeand ends the process with the exit code of the table in Processing. - Recover has three exits.
retryruns the failed job again,resumegoes on after it, and reaching the end ofrecoverends the process as handled.over;skipsrecoverbut runsfinalize;panic;ends at once with code 1 and runs neither. iscompares types and identity.type(x) is Realcompares two types;a is bis true only for the same list or object, while==compares the contents.printandwrite.print (1, 2, 3)joins with a comma,write (1, 2)joins with nothing, and a DataSet prints as{1,2,3}.- Parse and check first. The machine always parses a script and checks the whole tree before it runs it. A syntax error, an undefined name, a name declared twice, a
letof a name that was never declared, a procedure used in an expression and a mandatory parameter given by position after optional ones are compile errors: the script does not start and the exit code is 65. - An error code is not an exit code. The error code identifies the error (
$error.code); the exit code of the process is 0, 1, 2, 3, 4 or 5 (see Processing). exitandover.exit;leaves the subprogram or the process it is written in,over;ends the whole process from any depth; both runfinalizeanddefer.
After M7, choose your direction: a faster back end, level 3 features (files, databases, network), or tooling such as a formatter or a language server.
Common Pitfalls
- Parsing before lexing is solid. Most parser bugs are lexer bugs. Print the token stream first and test it on every demo.
- Hard-coded keyword lists. The specification will add and change keywords. Load them from
keywords.json. - Line endings. Source files may use CRLF or LF. Treat both as one line break, and keep line numbers correct.
- Floating point equality. Eve compares numbers with a precision setting. Do not use the host language
==on floats. - No tests of your own. Keep every program that once broke your implementation as a regression test.
Publishing Your Compiler
- Publish the source code in a public repository with an open source license.
- State the specification version and the conformance level your implementation passes.
- Include the test results: the list of tests that pass and fail.
- Announce the implementation to the Sage-Code community. Compilers verified by the community will be listed in the specification repository.
The approval process for community-verified compilers is not yet defined; it is planned together with the conformance suite.
Read next: Standard Library