liftoff
is a tree-walking interpreter for the toy language Rocket, built as the final project for a CS class.
Prerequisites: Python 3.9 or higher and Git.
-
Clone the repository onto your local machine:
$ git clone https://github.com/jo3-l/liftoff
-
Change your working directory:
$ cd liftoff
-
Run
cli.py
, passing the path of the Rocket file to run:$ py src/cli.py examples/fac.rk
A small collection of representative Rocket programs are available under the examples directory.
Rocket is a toy language, but it should suffice for building simple programs. Below is an overview of the Rocket language syntax:
A statement is a line of code that typically has some side effect, whereas an expression evaluates to something.
Statements must be followed by a semicolon.
Multiple statements can be grouped together using braces, which form a block. Blocks introduce a new scope.
{
let x = 1;
let y = 2;
}
Variables are introduced by the let
keyword:
let x = 1;
Simple integer and float literals are supported (no fancy features like separators or hex), in addition to string, list, and dictionary literals. The null
literal is also supported, being the equivalent of Python's None
.
let int_lit = 1;
let float_lit = 3.14;
let str_lit = "hello,\nworld!";
let list_lit = [[1, 2], 3.4, "five"];
let foo = "foo";
let dict_lit = { foo: 1 };
let null_lit = null;
Conditionals are formed using the if
, else if
, and else
statements.
if (cond) {
// ...
} else if (other_cnd) {
// ...
} else {
// ...
}
There are three forms of loops in Rocket: for loops, iterator-based for loops, and while loops.
for
loops
All elements of the loop are optional, so for (;;) {}
is a valid statement (resulting in an infinite loop.)
for (init_stmt; cond_expr; post_expr) {
// ...
}
Iterator-based for-loops
for (let item in iterable) {
// ...
}
while
loops
while (loop_cond) {
// ...
}
The break
and continue
statements may be used within the body of loops to appropriate effect.
Function definitions can appear anywhere in the global scope. As a special case, functions may call other functions regardless of their position in the source, unlike variables which must be declared before usage.
fn f() {
return 1;
}
print(f());
They may also accept a fixed number of parameters and return values:
fn my_add(a, b) {
return add(a, b);
}
Methods are a kind of function that are called on a receiver. Though they cannot be created in Rocket (which does not support user-defined objects beyond dictionaries), they are accessible via built-ins and are callable using the same syntax as functions:
let nums = [1, 2, 3, 4];
print(nums.index(2));
Function values are first-class, meaning they can be stored in variables for later use. More generally, they are no different from any other value.
let my_print = print;
my_add("hello world!");
Attributes are properties belonging to values; items are key-value pairs of dictionary-like objects.
Again, though Rocket does not support user-defined objects, attributes and items can appear through usage of built-ins and go by syntax akin to Python (a.b
and a[b]
):
let people = ["joe", "bob"];
print(people[0]); // joe
Line comments use the //
token, while multiline comments use /*
and */
. Multiline comments do not nest.
// I'm a line comment
/* and I
am a
multiline comment */
A semi-formal specification of the Rocket language in extended Backus-Naur form is also available in the comments of the parser implementation.
- print: print value(s) to standard output with a trailing newline
- input: take input from standard input with an optional prompt
- range: create an half-closed interval that can be iterated over
- format: perform string interpolation;
format("{} = {}", "x", 5)
yieldsx = 5
.{}
represents a placeholder - lt/le/eq/ne/ge/gt/...: comparison operators (
lt
is less than,le
is less than or equal to, ...) - abs/add/sub/mul/div/pow/...: math operators
- or/and/not: logical operators (no short circuiting!)
- parse_int/parse_float: parse strings into numerical types
Note: Most of these are a consequence of the short time in which this project was written; thus, one may view this as a to-do list of sorts instead.
- No classes
- No operators (unary or otherwise); instead, operators are functions
- No modules / packages
- Errors are not very user-friendly
- Functions interact strangely with their environment in certain edge cases
The lexer and parser are handwritten, using recursive descent. The interpreter is of the tree-walking type, simply descending the tree and evaluating as it goes.
Liftoff is authored and maintained by Joe L. under the MIT License.