A small interpreted language for scalar and vector arithmetic, written in Rust.
Dodolang is a tree-walking interpreter built the classic way — lexer → parser → AST →
interpreter — with an interactive REPL as the only frontend. It is an early
work-in-progress: scalars, vectors, arithmetic and print work; matrices, loops and
error recovery are not finished yet (see Status).
- Rust 2018 edition toolchain (
rustc+cargo), e.g. via rustup. Built and tested against stable 1.97.1.
cargo build # debug build
cargo build --release # optimised build (opt-level 3)
cargo run # start the REPL
cargo test # run the lexer/parser unit testsThe build succeeds but is noisy — around 70 warnings, mostly unused imports and
unused mut/borrows. cargo fix can clear most of them.
cargo run drops you straight into the REPL:
Dodolang!
>>> scalar x
>>> x = 21
>>> print x
Literal(21)
Each line is lexed, parsed and interpreted on its own — the interpreter keeps one
Environment, so variables persist across lines for the life of the session. There is
no script-file runner yet; the test file in the repo root is just a scratch snippet.
Exit with Ctrl-D (end of input) or Ctrl-C. Because the REPL reads from stdin, you can also pipe a script in and it will run to the end and exit:
printf 'scalar x\nx = 21\nprint x\n' | cargo run -qEvery variable must be declared before it is assigned. Declarations are one per line.
scalar x # a single value
vector y[3] # a vector of length 3, zero-filled
matrix z[2,2] # a matrix (partially implemented, see Status)
x = 21
y = {1, 2, 3}
y = {1 2 3} # commas are optional in vector literals
Integer literals (i128), variables, parentheses, unary minus and the four binary
operators, with the usual precedence (* and / bind tighter than + and -):
x = (21 * 5) + 3 + (6 * 4)
Vector arithmetic is dispatched on operand types at evaluation time:
| Expression | Result | Example |
|---|---|---|
scalar + - * / scalar |
scalar | 1 + 2 → 3 |
scalar * vector |
vector, scaled element-wise | 2 * {1,2,3} → {2,4,6} |
vector / scalar |
vector, divided element-wise | |
vector * vector |
scalar — the dot product | {1,2,3} * {4,5,6} → 32 |
print x
print 1 + 2
print y
print writes the evaluated AST node using its Debug representation, so print 1 + 2
prints Literal(3) rather than 3, and printing a vector yields
Vector(Token { token_type: IDENT, val: "y" }, [1, 2, 3]). The evaluator and
environment also emit tracing output of their own while they work — variable names,
operand dumps, and stray markers like wtf and asd2. All of it is development
leftovers rather than intended behaviour.
The grammar the parser actually implements today:
program → declaration* EOF
declaration → scalarDecl | vectorDecl | matrixDecl | statement
scalarDecl → "scalar" IDENT NEWLINE
vectorDecl → "vector" IDENT "[" INT "]" NEWLINE
matrixDecl → "matrix" IDENT "[" INT "," INT "]" NEWLINE
statement → printStmt | exprStmt
printStmt → "print" expression NEWLINE
exprStmt → expression NEWLINE
expression → assignment
assignment → IDENT "=" assignment | addition
addition → multiplication ( ( "+" | "-" ) multiplication )*
multiplication → unary ( ( "*" | "/" ) unary )*
unary → ( "-" | "!" ) unary | primary
primary → INT | IDENT | "(" expression ")" | vectorLiteral
vectorLiteral → "{" INT ( ","? INT )* "}"
Statements are newline-terminated; there are no semicolons and no block syntax.
src/
main.rs entry point — starts the REPL
core/
lexer/
lexer.rs character scanner producing Tokens
helper.rs is_letter / is_digit predicates
token/
token.rs Token, TokenType, keyword lookup
ast/
parser.rs recursive-descent parser
expr.rs Expr node enum
stmt.rs Stmt node enum
ast.rs Program wrapper
dodo/
repl.rs read-lex-parse-interpret loop
interpreter.rs tree-walking evaluator
environment.rs variable storage (HashMap<String, Vec<i128>>)
error_types.rs DodoParseError and error reporting
Values are stored uniformly as Vec<i128> — a scalar is a one-element vector, and a
value's length is what distinguishes the two when it is read back.
Working: scalar and vector declarations, assignment, integer arithmetic with
precedence and grouping, scalar/vector mixed arithmetic, the dot product, print, and
a persistent REPL environment.
Not yet implemented:
- Matrices.
matrix z[2,2]parses and declares storage, but only allocatescolumnselements and no 2-D operations or nested{{...}}literals exist. forloops. The keyword is lexed andStmt::FORis defined, but the parser branch is commented out.- Error handling. Errors are unwrapped rather than reported, so anything invalid
panics and kills the session instead of printing a diagnostic and continuing:
q = = 5panics inparser.rs(primaryreturnsErr,unaryunwraps it), and reading an undeclared variable panics ininterpreter.rs(Environment::getreturnsNone). Assigning to an undeclared variable is silently ignored instead. - Vector
+/-and comparison operators (<,>,==,!=) — the tokens exist but no evaluation path does. - Comments, strings, functions. Tokens and AST variants are sketched in; nothing consumes them.
The ndarray and nalgebra dependencies are declared ahead of the matrix work and
are not meaningfully used yet.
Unit tests live alongside the code in #[cfg(test)] modules — token-level tests in
src/core/lexer/lexer.rs and a statement-parsing test in src/core/ast/parser.rs.
cargo test currently runs 4 tests, all passing (one, ast::tests::basic_statements,
is an empty placeholder). Several interpreter and parser tests are commented out
pending the API changes that outdated them.