Crafting Interpreters in Rust
Giving https://craftinginterpreters.com/ a try, while learning Rust at the same time.
Just getting started with both :)
Examples
Consider fib.lox
fun fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
var before = clock();
print fib(25);
var after = clock();
print after - before;
We can run this in the treewalk interpreter using
cargo run --release --quiet -- fib.lox --treewalk
On my laptop, this prints a timing of 1755 milliseconds. We can run the same thing in the bytecode interpreter using
cargo run --release --quiet -- fib.lox
On the same laptop, this shows a timing of 401 milliseconds.
For comparison, on the same laptop, the tiger compiler
computes the same answer in 0.00s user 0.00s system 4% cpu 0.077 total
(not counting compilation :)). A C compiler,
or tigerc using the llvm backend :), computes this in 0.00s user 0.00s system 65% cpu 0.004 total
.
Now consider hello_world.lox
print "hello world!";
We can tokenize this with
cargo run --release --quiet -- hello_world.lox --show-tokens
Which gives output
[
Token { ty: Print, lexeme: "print", literal: None, line: 1, col: 4},
Token { ty: String, lexeme: ""hello world!"", literal: Some(Str("hello world!")), line: 1, col: 19},
Token { ty: Semicolon, lexeme: ";", literal: None, line: 1, col: 20},
Token { ty: Eof, lexeme: "", literal: None, line: 1, col: 20},
]
We can show the AST with
cargo run --release --quiet -- hello_world.lox --show-ast
Which gives
[
Print(
Literal(
String(
"hello world!",
),
),
),
]
Finally, we can show compiled bytecode with
cargo run --release --quiet -- hello_world.lox --disassemble
Giving
============ hello_world.lox ============
------------ constants -----------
0 "hello world!"
------------ code -----------------
0000 OP_CONSTANT "hello world!" (idx=0) line 1
0001 OP_PRINT line 1
0002 OP_NIL line 1
0003 OP_RETURN line 1
Debugger
This project includes a basic (in-progress, possibly never to progress further) debugger.
For example, consider f.lox
fun a() { b(); }
fun b() { c(); }
fun c() {
c("too", "many");
}
a();
We can explore this in the debugger with
$ cargo run --release --quiet -- f.lox --debug
(loxdb) b 4
inserted breakpoint at line 4
(loxdb) g
reached breakpoint at line 4
(loxdb) list
2 fun b() { c(); }
3 fun c() {
==> 4 c("too", "many");
5 }
6
7 a();
==> 0000 OP_GET_GLOBAL String("c") (idx=0) line 4
0001 OP_CONSTANT "too" (idx=1) line 4
0002 OP_CONSTANT "many" (idx=2) line 4
0003 OP_CALL 2 line 4
(loxdb) bt
[line 7] in script
[line 1] in a()
[line 2] in b()
[line 4] in c()
(loxdb) g
Lox runtime error: Expected 0 arguments but found 2..
Traceback:
[line 7] in script
[line 1] in a()
[line 2] in b()
[line 4] in c()
REPL
A REPL for interactive development is also available, which uses the slower treewalk interpreter. Launch with
cargo run --release --quiet
Here's an example session:
$ cargo run --release --quiet
============================================
Welcome to lox! using tree-walk interpreter.
============================================
>>> var x = 42;
>>> fun f(n) { return n + 1; }
>>> f(x);
43
Extensions
Using the --Xlists
command line switch (eg cargo run --release --quiet -- --Xlists
), we can enable lists
===================================================
Welcome to lox 0.1.0! Using tree-walk interpreter.
Authors: Thomas Peters <[email protected]>
===================================================
>>> var xs = [1,2,3]
>>> xs
[1, 2, 3]
Lists don't have much functionality yet, but they have lengths
>>> len(xs)
3
can be concatenated
>>> var ys = xs + xs
>>> ys
[1, 2, 3, 1, 2, 3]
can be mapped over
>>> fun square(x) { return x * x; }
>>> map(square, xs)
[1, 4, 9]
>>>
can be iterated
>>> fun printFun(elt) { print elt; }
>>> forEach(xs, printFun)
1
2
3
and also have expected indexing operators
>>> xs[0] = -xs[0]
-1
>>> xs
[-1, 2, 3]