Stream: contributing

Topic: interpreter


view this post on Zulip Richard Feldman (May 17 2025 at 19:07):

on the plane back from :flag_netherlands: I got started on the interpreter

view this post on Zulip Richard Feldman (May 17 2025 at 19:08):

I think we concluded that we wanted to essentially have it output exactly the bytes the host expects, right @Brendan Hansknecht?

view this post on Zulip Anthony Bullard (May 17 2025 at 19:09):

Hey i was just talking about how much I'd like to see a v0.1 Hello World running...

view this post on Zulip Richard Feldman (May 17 2025 at 19:09):

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

view this post on Zulip Brendan Hansknecht (May 17 2025 at 19:09):

Yeah, that sounds roughly right

view this post on Zulip Richard Feldman (May 17 2025 at 19:10):

this seems like what we need for debug builds, since the host has the same abi for debug and release builds

view this post on Zulip Brendan Hansknecht (May 17 2025 at 19:10):

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.

view this post on Zulip Brendan Hansknecht (May 17 2025 at 19:11):

And yeah, same abi for debug and release is the important part.

view this post on Zulip Richard Feldman (May 17 2025 at 19:11):

yeah makes sense!

view this post on Zulip Richard Feldman (May 17 2025 at 19:11):

and then when interpreting constants at compile time, we can do what the repl does

view this post on Zulip Richard Feldman (May 17 2025 at 19:11):

namely traverse the data and use the types to know where to put everything

view this post on Zulip Brendan Hansknecht (May 17 2025 at 19:12):

yep

view this post on Zulip Richard Feldman (May 17 2025 at 19:14):

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);
                        },
                    }
                },
            }

view this post on Zulip Richard Feldman (May 17 2025 at 19:14):

that's all in a loop, where all the stacks are in scope

view this post on Zulip Richard Feldman (May 17 2025 at 19:14):

not using recursion because we don't want the interpreter to blow the stack

view this post on Zulip Brendan Hansknecht (May 17 2025 at 19:16):

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.

view this post on Zulip Richard Feldman (May 17 2025 at 19:17):

yeah my thinking is that for now the interpreter needs to be:

view this post on Zulip Brendan Hansknecht (May 17 2025 at 19:17):

Yeah, that seems like great priorities

view this post on Zulip Richard Feldman (May 17 2025 at 19:17):

and then yeah we can see how the perf is in practice and evaluate things accordingly

view this post on Zulip Richard Feldman (May 17 2025 at 19:18):

I want to basically just get it off the ground and then let others pick it up

view this post on Zulip Luke Boswell (May 18 2025 at 23:12):

Very excited to see something minimal working...

view this post on Zulip tk (May 25 2025 at 08:52):

Two dumb questions:

  1. Is this an inprogress Roc interpreter written in Rust ?
  2. Where can I find this code on github ?

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.

view this post on Zulip Anthony Bullard (May 25 2025 at 10:30):

the Roc compiler as a whole is being rewritten in Zig, and that's where the interpreter will be as well

view this post on Zulip tk (May 25 2025 at 15:44):

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.

view this post on Zulip Brendan Hansknecht (May 25 2025 at 15:45):

Yeah, rust implementation is being abandoned

view this post on Zulip Richard Feldman (May 25 2025 at 15:58):

@tk I wrote up this explanation back at the time: https://gist.github.com/rtfeldman/505bc158ad6965a71ff2777ad8451209

view this post on Zulip tk (May 25 2025 at 18:15):

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.

view this post on Zulip Matthew Lugg (May 25 2025 at 19:14):

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

view this post on Zulip Richard Feldman (May 25 2025 at 19:18):

yeah, although as I understand it, incremental zig with non-llvm backend should outperform Rust by a lot once we get there

view this post on Zulip Richard Feldman (May 25 2025 at 19:19):

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!)

view this post on Zulip Matthew Lugg (May 25 2025 at 19:22):

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

view this post on Zulip Matthew Lugg (May 25 2025 at 19:24):

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

view this post on Zulip Matthew Lugg (May 25 2025 at 19:26):

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

view this post on Zulip Brendan Hansknecht (May 25 2025 at 19:36):

Always a good sign to have honest engineers giving real world number and caveats

view this post on Zulip Richard Feldman (May 25 2025 at 19:56):

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:

view this post on Zulip Richard Feldman (May 25 2025 at 19:57):

I'm hoping that there will be an aarch64 backend by the time I want to reach for it :fingers_crossed:

view this post on Zulip tk (May 25 2025 at 19:57):

@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.

view this post on Zulip Matthew Lugg (May 25 2025 at 20:20):

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

view this post on Zulip Matthew Lugg (May 25 2025 at 20:24):

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

view this post on Zulip Richard Feldman (May 25 2025 at 20:39):

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:

view this post on Zulip Matthew Lugg (May 25 2025 at 20:41):

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