on the plane back from :flag_netherlands: I got started on the interpreter
I think we concluded that we wanted to essentially have it output exactly the bytes the host expects, right @Brendan Hansknecht?
Hey i was just talking about how much I'd like to see a v0.1 Hello World running...
and then have the inferred types off to the side (like we do today in the repl) so we can tell what their layout is
Yeah, that sounds roughly right
this seems like what we need for debug builds, since the host has the same abi for debug and release builds
The shim function that the host calls would generate the type spec (cause it knows this from the roc types at compile time). Then it would call into the interepretter passing in raw data and types.
And yeah, same abi for debug and release is the important part.
yeah makes sense!
and then when interpreting constants at compile time, we can do what the repl does
namely traverse the data and use the types to know where to put everything
yep
this is a snippet of the direction I'm going:
switch (op_stack.pop()) {
.branch => {
const then_idx = idx_stack.pop();
const else_idx = idx_stack.pop();
const cond = try val_stack.pop().asBool();
idx_stack.push(if (cond) then_idx, else_idx);
op_stack.push(Op.eval);
},
.assign => {
scope.insert(ident_stack.pop(), val_stack.pop());
},
.begin_scope => {
// TODO
},
.end_scope => {
// TODO
},
.crash => {
// TODO return a crash error along with the string.
},
.eval => {
const idx = idx_stack.pop();
switch (self.exprAt(idx)) {
.if_then_else => |if_then_else| {
// first, eval the condition
op_stack.push(Op.eval);
idx_stack.push(if_then_else.condIdx());
// next, do an if/else branch
op_stack.push(Op.branch);
idx_stack.push(if_then_else.thenIdx());
idx_stack.push(if_then_else.elseIdx());
},
.block => |block| {
// blocks get their own scope for their assignments
op_stack.push(Op.begin_scope);
// run all the statements preceding the expr
for (block.stmts) |stmt| {
switch (stmt) {
.assign => |name, idx| {
// eval the expr that's being assigned to name
op_stack.push(Op.eval);
idx_stack.push(idx);
// assign it to name
op_stack.push(Op.assign);
ident_stack.push(name);
},
}
}
// eval the expr at the end of the block
op_stack.push(Op.eval);
idx_stack.push(block.expr);
op_stack.push(Op.end_scope);
},
}
},
}
that's all in a loop, where all the stack
s are in scope
not using recursion because we don't want the interpreter to blow the stack
That seems reasonable for a base implementation. I'm sure we'll eventually have a lot a tricks for perf, but those aren't too important now.
yeah my thinking is that for now the interpreter needs to be:
Yeah, that seems like great priorities
and then yeah we can see how the perf is in practice and evaluate things accordingly
I want to basically just get it off the ground and then let others pick it up
Very excited to see something minimal working...
Two dumb questions:
Context: I'm working on a project where I need an embedded interpreter where I can place cpu/memory limits (thus, somehow limit the bytecode interpreter). I am not looking for the Roc -> wasm compiler. I am looking to see if there is a Roc interpreter written in Rust somewhere -- and stumbled across this thread.
the Roc compiler as a whole is being rewritten in Zig, and that's where the interpreter will be as well
is the Rust compiler getting abandoned ? is there a thread where this Rust -> Zig switch was debated ? I am mainly curious to understand the reasoning.
Yeah, rust implementation is being abandoned
@tk I wrote up this explanation back at the time: https://gist.github.com/rtfeldman/505bc158ad6965a71ff2777ad8451209
Zig's x86-64 backend can reportedly scratch-build (without anything cached) the entire 300K-line Zig compiler code base in a second or two. It takes us longer than that to build one of our medium-sized Rust crates, and that's even with the benefit of caching.
Yeah, I guess that is pretty relevant. I have a 100 kloc project with another 200 kloc forked crates, and I'd kill for a 2-second non-cached build time.
it's worth noting that the numbers there are unfortunately overly optimistic right now, and aiui were based on a miscommunication between richand and andrew
yeah, although as I understand it, incremental zig with non-llvm backend should outperform Rust by a lot once we get there
and Zig is very close to that, whereas at least at the moment nothing like that is anywhere on Rust's roadmap (although I still hope that changes!)
for the zig compiler, getting compile errors for a core subset of the pipeline is the number in question of 1-2 seconds (it's actually like 5 on my system but my system is currently a laptop lol), actually properly building just those core components is maybe around 50-100% more time all in all, building the whole compiler (llvm and package manager and all) is 25 seconds on my system and probably 15-20 seconds on a reasonable desktop. these are all scratch builds. we're aiming to improve these numbers over time ofc, compiler performance remains a high priority.
and, yeah, incremental builds are the real killer feature for development. idk if anyone on the roc team has had a reason to try out incremental compilation yet, but when it works (there are still occasional bugs, although for some use cases it's fairly solid now) it's amazing. my main use for it right now is after i've done a big local patch, and it's time to start fixing compile errors, i will spin up (approximately) zig build -Dno-bin -fincremental --watch --prominent-compile-errors
in a new terminal, and it will just do semantic analysis on my code and show me all the compile errors; i fix them one at a time, and every time i save a file, i typically get new error output in somewhere between [idk, too fast to see] and (absolute worst case, because of some pathological cases the zig compiler handles poorly at the moment) 2 seconds
well, i will be absolutely fair and say if you get real unlucky with how you hit a pathological case you might get close to scratch build time for one update. there is basically one design flaw in the current implementation of incremental compilation which causes these pathological cases where the compiler does way more work than it should need to, it's sooooomewhere on my priority list to solve
i'm not trying to evangelise btw, i do apologise if it comes across that way, just trying to clarify the current real-world performance with the most realistic numbers i can
oh, and final note: all of the numbers above are with the self-hosted x86_64 backend. right now that's the only self-hosted backend which is generally usable (it's likely to become the default on x86_64-linux somewhat soon); for other targets you usually still have to go through LLVM, which comes with huge compilation speed losses because LLVM is (imo) wholly unreasonably slow at producing debug-mode binaries
okay, i'm done, sorry for wall of text :D
Always a good sign to have honest engineers giving real world number and caveats
yeah I haven't tried it yet, but that's mostly because our Zig compiler code base is still small enough that even cold compile times aren't long enough to be a pain point for me yet :smile:
I'm hoping that there will be an aarch64 backend by the time I want to reach for it :fingers_crossed:
@Matthew Lugg
okay, i'm done, sorry for wall of text :D
I found the wall of text really insightful. Thank you for taking your time to write out your experience. I do agree with you, alot of the time, what I really care about is 'time to first compile error', and less full on compile time.
Richard Feldman said:
I'm hoping that there will be an aarch64 backend by the time I want to reach for it :fingers_crossed:
ah yes, i always forget that people actually run aarch64 now lol
tk said:
Matthew Lugg
okay, i'm done, sorry for wall of text :D
I found the wall of text really insightful. Thank you for taking your time to write out your experience. I do agree with you, alot of the time, what I really care about is 'time to first compile error', and less full on compile time.
no worries, do feel free to ping me with questions about this for the record, i tend to kind of enjoy discussing the compiler internals and how to get the most out of them
and indeed, in cases where i need fast compiles, usually what i actually need is fast response about compile errors. full-on incremental compilation is still very valuable ofc, it's still very actively being worked towards, but even without the codegen i find the feature incredibly useful. one interesting thing it makes feasible is a language server (as in, LSP) communicating with the compiler to quickly get up-to-date information about the compilation
Matthew Lugg said:
Richard Feldman said:
I'm hoping that there will be an aarch64 backend by the time I want to reach for it :fingers_crossed:
ah yes, i always forget that people actually run aarch64 now lol
everyone who has a Mac, yes :smile:
what's really fun is that in principle you could just use the x86_64 backend and Rosetta 2, but iirc our linker (or maybe our actual codegen?) manages to emit something that completely breaks Rosetta
Last updated: Jul 05 2025 at 12:14 UTC