in another thread about compile-time evaluations of constants, I wrote:
Richard Feldman said:
in general I don't think top level evaluations should have any restrictions beyond what they'd have if they were evaluated anywhere else
an implication of this is that they can hang your build if there's an infinite loop in them, but I think that's worth accepting as a downside
I had an idea related to this: what if in all dev builds (including when running tests and also when doing top-level evaluation of constants in the future) we had a mechanism for detecting and reporting about suspected infinite loops?
an idea I like for how we could do this:
crash like "hey I recursed 4 billion times on this function which is tail recursive; maybe it doesn't terminate"the idea being that if we only ever emitted those in actual generated loops (which are the only places where pure Roc code can ever get stuck in an infinite loop, since all other forms of recursion will run out of stack space eventually and terminate that way) it seems like it would make infinite loops a lot easier to track down in practice
especially in tests, which would preserve the testing property I like of "only report failures, because successes aren't actionable besides a report of how long they took to run, which can happen at the end"
in optimized builds we'd never generate the counter, on the theory that (a) we don't want to pay the performance cost, and (b) if you actually authored an infinite loop, you almost certainly would have discovered it during a dev build, test run, etc.
thoughts?
whenever we are doing a dev build and the tail recursion optimization kicks in (which is the only scenario in which pure roc code can loop forever), we include a counter in the loop
Not just tail recursive, right? Any recursive could hit it
on each iteration of the loop, we increment the counter and check if it has exceeded some huge value (the value can be configured by a CLI flag, or this check can be disabled altogether if desired)
I think this is essentially what zig does. It limits backwards branches I believe.
non tail recursive functions would stack overflow eventually, so they have a built in reporting mechanics :laughing:
ah
first consuming 128 GB of ram, a few bytes at a time.
haha well stacks tend to normally be limited to some number of kb or maybe mb
true
even Go with its segmented stacks has a cap of 1gb apparently
Also, wouldn't this all happen at compile time, so this extra counter would never appear in the final binary?
not even with dev builds?
the idea would be for the counter to be inserted into the compiled binary during code gen
so you'd see the crash at runtime when running the program
and then when doing compile time evaluation of constants it would work the same way as it would when running an ordinary dev build
so no difference in behavior between a dev build and a compile time evaluation
Interesting. What is the gain to also having this happen in dev builds, I don't quite follow that. Just help debug accidentally giant loops that generate constants?
assuming it's actually successful at detecting infinite loops (as opposed to false positives), it would be nice during tests
I like that roc test only prints failures at the end, and not what test it's currently running, because it keeps the output cleaner and focused on actionable information - but it does mean that if a test gets stuck in an infinite loop, there's no way to tell which test it was (or for that matter what function within the test)
so this would help with that; it would guarantee that tests of pure Roc code would always terminate
(naturally tests of effects in the future couldn't have that guarantee, so we might want timeouts on effectful tests once those exist)
similarly, during dev builds, seeing "this is the function that got stuck in the loop" plus a crash backtrace would be nicer than just having the program hang
of course if it turns out to have false positives, that would be annoying, but my hope is that by setting the counter high enough, that will be rare enough in practice that overall it'll be an improvement to user experience
I love the idea.
The biggest challenge I see is deciding the magic number at language level, in a way that works for all loops. Too low and you start getting false negatives. Too high and it takes too long to report. Not all loops take the same time per iteration. If I have to wait an hour to know where I have an infinite loop I will most likely kill the process before that happens.
yup haha
my hope is that setting it high would work well in practice because it's not going to be doing any I/O if it's in pure roc code, so even if it were like a couple billion I'd usually expect it to crash in a second or two
unless it was doing something pure but also really slow on each iteration of the loop, and the loop turned out to be infinite
which of course is possible, but hopefully wouldn't come up very often :sweat_smile:
Last updated: Jun 16 2026 at 16:19 UTC