This one is odd; I am working on the Sexp2 module and noticed this behavior, only in this specific case, after whittling down to minimal test case:
rwh@ht seatzero (master *)$ roc spike/test_sexp2.roc; echo $?
dbg: [<tag_union variant=3>, <tag_union variant=2>, <tag_union variant=2>, <tag_union variant=2>, <tag_union variant=1>]
dbg: ["(", "1", "2", "3", ")"]
Sexp2 runtime harness reached
1
rwh@ht seatzero (master *)$ cat spike/test_sexp2.roc
app [main!] { pf: platform "../../basic-cli/platform/main.roc" }
import pf.Stdout
import Sexp2
main! = |_args| {
tokens = Sexp2.tokenize("(1 2 3)")
dbg tokens
names = Sexp2.token_names(tokens)
dbg names
expect Bool.True
Stdout.line!("Sexp2 runtime harness reached")
Ok({})
}
What if your use nvm I'm pretty sure it's using that under the hoodStr.inspect on the tokens?
If you implement a it's called to_str : Secp2 -> Str method does that change anything?to_inspect
I vaguely recall an idea that inspect basically desugars to that method under the hood, but I can't remember
note, this is Sexp (as in s-expression). let me investigate that, though i've moved on from this curiousity
In the meantime, not related to this curiosity, i have:
The stack overflow you saw when running roc test --verbose spike/Sexp3.roc (after the
added equality-based expect cases) wasn’t caused by a logic mistake in the Sexp parser
itself—it was the Roc compiler hitting a stack overflow while evaluating those expect
expressions. The crash message explicitly says the compiler overflowed its stack, which
matches the behavior we observed when we defined is_eq methods and tried to compare
Token/Value instances with ==. Removing those comparisons (and instead comparing the
formatted string or the token names) lets roc test complete successfully, so the parser
never “ran away” at runtime. In short: the compiler’s expect evaluation blew up when
trying to derive equality for those custom types, so it’s a compiler bug triggered by
that pattern, not a user error.
i can pause on this and try to make a minimal repro, but I want to try your to_str trick first
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
# Define an opaque type with a custom to_inspect method
Color := [Red, Green, Blue].{
to_inspect : Color -> Str
to_inspect = |color| match color {
Red => "Color::Red"
Green => "Color::Green"
Blue => "Color::Blue"
}
}
main! = || {
red : Color
red = Red
# Test Str.inspect with custom to_inspect method
result = Str.inspect(red)
Stdout.line!(result)
# Compare with what the default would be
Stdout.line!("Expected: Color::Red")
}
Yeah -- consider roc test somewhat broken... if it works :tada: but it clearly needs some love
here is the strace for exiting 1 on success: https://gist.github.com/rickhull/4247a1dddbc40069ad7f853a47bcb552
it's too late for me now, but I'm not sure about your inspect test; in this case, I am executing with roc and not roc test, and in the strace gist, this is after roc build
gpt-codex-mini says:
The strace capture settles it: the binary prints all the dbg/Stdout lines you care about
and then ends with exit_group(1) — there’s no panic, crash, or expect failure at
runtime, so it’s literally choosing to return status 1. That’s exactly what roc does for
these little test harnesses when you run them with roc spike/... instead of roc test
spike/...: the compiler builds a dedicated test binary and the runtime intentionally
exits 1 (even on success) because it wasn’t invoked through the roc test driver that
reports expect results. In short, the harness is behaving as designed — it’s not a logic
bug in your code, it’s the way Roc signals “don’t treat this as a normal runnable app;
use roc test so the expect blocks run and report success.”
Ah yeah, by default a program with a dbg in it returns a non-zero exit code
Actually I might be getting confused there...
Here's an older discussion -- Richard proposed adding a --release flag that would throw a non-zero code if there were any dbg's
Need to look into that and document it somewhere I guess
Last updated: Feb 20 2026 at 12:27 UTC