Strada Interpreter
A bytecode VM that compiles Strada's AST to bytecode and executes it via a computed goto dispatch loop. 4-5x faster than Perl 5.38.
strada-interp is no longer built by default — run make interpreter in the repo to build it. The everyday strada --repl and strada --script flows now route through strada-jit (real native compilation via tcc/gcc + dlopen) so they support the full Strada language with no semantic gaps. The interpreter described on this page is still useful for embedding, experimentation, and anywhere you genuinely want bytecode execution without invoking gcc.
__C__ blocks, which are JIT-compiled and cached). A tree-walking backend is available as a fallback via --tree-walk.
Overview
Strada offers three execution paths for your code:
| Compiled (stradac) | VM (strada-interp) | Tree-Walk (--tree-walk) | |
|---|---|---|---|
| How it works | Generates C code, compiles to native binary | AST → bytecode → VM dispatch | Walks the AST directly at runtime |
| Requires C compiler | Yes (gcc) | No (except __C__ blocks) |
No |
| Startup time | Slow (compilation step) | Fast (parse + bytecode compile) | Fast (parse and run) |
| Runtime speed | Fastest (native code) | Fast (4-5x faster than Perl 5.38) | Slower (interpreted) |
| Best for | Production, performance | Scripts, file execution | REPL, legacy fallback |
All paths share the same Lexer and Parser front-end, so valid Strada code works with any of them.
Running Programs
# Build the interpreter
make interpreter
# Run a Strada program (uses bytecode VM by default)
./strada-interp program.strada
# Use tree-walking backend instead
./strada-interp --tree-walk program.strada
# With library search paths
./strada-interp -L lib program.strada
# Start the interactive REPL (uses tree-walk)
./strada-interp
Interactive REPL
When run without arguments, the interpreter starts an interactive REPL with readline support (line editing, history):
strada> my int $x = 42;
strada> $x * 2
=> 84
strada> func square(int $n) int {
... return $n * $n;
... }
strada> square(7)
=> 49
Variables and function definitions persist across evaluations. Multi-line input is detected automatically when braces, parentheses, or brackets are unbalanced.
See REPL & Scripting for REPL commands, scripting, and profiling features.
Embedded Eval
The interpreter can be embedded inside compiled Strada programs using the Strada::Interpreter library. This enables runtime code evaluation without needing a C compiler at runtime:
use lib "lib";
use Strada::Interpreter;
# Initialize the interpreter engine
Strada::Interpreter::init();
# Evaluate expressions
my scalar $result = Strada::Interpreter::eval_string("1 + 2");
say($result); # 3
# Variables persist across calls
Strada::Interpreter::eval_string("my int \$x = 10;");
my scalar $val = Strada::Interpreter::eval_string("\$x * 5");
say($val); # 50
# Function definitions persist
Strada::Interpreter::eval_string("func double(int \$n) int { return \$n * 2; }");
say(Strada::Interpreter::eval_string("double(21)")); # 42
# Reset all state
Strada::Interpreter::reset();
Eval API
| Function | Description |
|---|---|
Strada::Interpreter::init() |
Initialize the shared interpreter instance (auto-called by eval_string if needed) |
Strada::Interpreter::eval_string($code) |
Evaluate Strada code, return the result (undef for void statements) |
Strada::Interpreter::reset() |
Clear all persisted state (variables, functions, enums, constants) |
Interpreter vs JIT
Strada also provides Strada::JIT, which evaluates code by compiling to a shared library at runtime. The two approaches serve different needs:
| Strada::Interpreter | Strada::JIT | |
|---|---|---|
| Execution | Bytecode VM (default) or tree-walk | Compile to .so, dlopen, execute |
| Requires C compiler | No | Yes (gcc or tcc) |
| __C__ blocks | JIT-compiled and cached | Supported |
| Async/await | No | Yes |
| Per-eval overhead | Low (parse + bytecode compile) | Higher (compile + link + load) |
| Execution speed | Fast (bytecode VM) | Fastest (native code) |
| Best for | REPL, scripting, no-compiler envs | Plugin systems, perf-sensitive eval |
Architecture
The interpreter reuses the compiler's Lexer and Parser front-end. By default, the AST is compiled to bytecode and executed on a register-based VM. A tree-walking backend is available as a fallback via --tree-walk.
| Component | Source | Purpose |
|---|---|---|
| Lexer | compiler/Lexer.strada |
Tokenizes source (shared with compiler) |
| Parser | compiler/Parser.strada |
Builds AST (shared with compiler) |
| VM Compiler | interpreter/vm_compiler.c |
Compiles AST to bytecode |
| Bytecode VM | interpreter/vm.c |
Register-based VM with JIT for __C__ blocks |
| Tree-walker | lib/Strada/Interpreter.strada |
Fallback AST evaluator (--tree-walk) |
| Driver | interpreter/Main.strada |
File execution and REPL |
Scoping
The interpreter maintains a chain of environment hashes for lexical scoping. Each scope has a vars hash and a parent pointer. Variable lookup walks from innermost to outermost scope. my creates in the current scope; assignments update the nearest scope containing the variable.
Control Flow
Loop control (next, last, redo) and return are implemented as exception-based signals caught by the enclosing loop or function call handler. Labeled loops match signals by label name.
Supported Features
The interpreter supports the full Strada language:
- Types: int, num, str, scalar, array, hash, undef
- Variables:
my,our,local,const,enum - Operators: arithmetic, string (
.,x), comparison, logical, assignment, ternary - Control flow: if/elsif/else, unless, while, until, for, foreach, do-while, loop labels, statement modifiers
- Functions:
func/fn, closures, variadic (...@args), spread operator - OOP: packages,
extends,has ro|rw,before/after/around,isa,can,AUTOLOAD,bless,use overload - Exceptions: try/catch (typed and catch-all), throw, die
- Regex:
=~,!~,s///,tr////y///, captures, named captures,/emodifier - I/O: say, print, printf, sprintf, core::open, core::close, diamond operator, core::slurp, core::spew
- Modules:
use(source inclusion),import_lib(native .so via dlopen) - BEGIN/END blocks, tie/untie/tied, BigInt/BigFloat, DateTime
- Namespaces:
core::,math::,utf8::— the VM exposes the fullcore::/math::libc surface (~235 functions: process, users/groups, file I/O, sockets, extended math) through a generic runtime bridge - Regex:
s///eevaluated replacements,tr///with d/c/s/r flags, named captures, inlinei/s/m/xflags - Recursion limits:
core::set_recursion_limit/core::get_recursion_limit
The bytecode VM runs the entire compiler test suite at parity: 108 pass, 0 fail (12 skips need shared-object compilation or compiled-format stack traces).
Limitations
- Async/await (requires compiled thread pool runtime)
Native shared libraries loaded via import_lib still work — they are called through dlopen/dlsym regardless of execution mode.
Building
# Build the interpreter
make interpreter
# The interpreter binary
./strada-interp --help
# Usage
Strada Interpreter v0.3
Usage:
strada-interp Interactive REPL
strada-interp [OPTIONS] <file.strada> Execute file
strada-interp -e 'code' Execute code string
Options:
-e CODE Execute code (wraps in main() if needed)
-L PATH Add library search path
-h, --help Show this help message
One-Liner Execution
Use -e to evaluate Strada code directly from the command line:
# Simple expression
strada-interp -e 'say("hello world")'
# Multiple -e flags are concatenated
strada-interp -e 'my $x = 6;' -e 'say($x * 7)'
# No func main() boilerplate needed
strada-interp -e 'my @nums = (1, 2, 3); foreach (@nums) { say($_); }'