the other day I was playing with fuzz-canonicalize and found a category of programs which trigger "unreachable code reached"-crashes inside the new zig compiler relating to builtin-function calls inside src/eval/interpreter.zig
for example, running ./zig-out/bin/roc repro.roc on the following file:
# repro.roc
main! = Str.is_eq("a", "b", "c")
results in a compiler crash (reached unreachable code).
essentially, this error (and similar errors in the same category) boils down to fn callLowLevelBuiltin in src/eval/interpreter.zig assuming that it gets the right number of arguments for a builtin function (with std.debug.assert(args.len == 2)).
since I could not constrain my curiosity about this, I am now here to ask:
callLowLevelBuiltin only be called once it was made sure that the op can really execute with args?.str_is_eq assumes args count, but validates args types, .str_concat assumes args count and args types.return error.TypeMismatchin the future?self.env.store.erroneous_exprs.contains check in fn scheduleExprEval seems to be the place where the interpreter would make sure that it only evaluates valid builtin-function callsself.env.store.erroneous_exprs is not really used in such a general fashion (only for "Expressions whose type doesn't match the expected return type in their context" in match/if branch bodies)I appreciate the effort you guys are putting in! Thank you for creating such a delightful language!
Thanks for reaching out @ugi.
For this code we do throw the "TOO MANY ARGS" error:
main! = |_args| {
echo!(Str.inspect(Str.is_eq("a", "b", "c")))
Ok({})
}
So I am going to start my search with checking why that is not the case for your repro.roc
Interesting, it seems that your code does not trigger compile time evaluation (via interpreter) while mine does, probably because the erroneous expression is inside a closure and not a top-level-decl?
Also I think, that in my repro.roc, the type checker does pick up the "TOO MANY ARGS" error, but the interpreter ignores it and tries to run the builtin function anyway, hitting the std.debug.assert(args.len == 2)
I think your analysis is correct @ugi. Two possible changes changes come to mind:
--allow-errors was passed..call_invoke_closure branch in the interpreter and return error.TypeMismatch; similar to ugi's suggestion.What do you think @Richard Feldman?
I would also like to mention, that this category of problems does not only contain args count validation specifically, but function signature validation generally. (but only for builtin functions, not for user-defined functions)
For example, the following roc code would also crash with "reached unreachable code":
main! = Str.concat(5, "b")
This time because the interpreter assumes (and asserts, tough implicitly with the .? operator) that the first arg is a Str.
(NOTE: using Str.is_eq with wrong types would not trigger a crash, because in the .str_is_eq branch of fn callLowLevelBuiltin the argument types are explicitly checked, but they are not explicitly checked in the .str_concat branch)
I haven't looked into this specifically (and I'm neck-deep in some really thorny compiler backend changes right now and I'm trying to stay focused on them!) but I will say that in the long term I definitely want to aim for "no compile-time errors block later stages of the compiler from running" as a general design principle, and that includes compile-time evaluation of constants
that said, I think it's okay if in the short term we don't live up to that goal yet
so my preference regarding this option:
Do not run compile time evaluation if there are type checker errors unless
--allow-errorswas passed.
...would be not to have a CLI flag for it, but rather just block for now and in the future silently transition to nonblocking once we get the bugs ironed out :smile:
With block do you mean bail out and print the errors?
Last updated: May 01 2026 at 12:45 UTC