one of the things that's cool about interpreters like Ruby's (which I'll use as an example because I'm the most familiar with it) is that you can do things like:
crash
, immediately halt the program and drop into a step debugger on the command line (assuming the process was spawned from the command line)this seems both awesome and also feasible with our planned interpreter backend, such that when you do roc foo.roc
it just supports all of this right away, as long as you didn't pass --optimize
to roc
however, it's still nontrivial!
for reasons I'd also prefer to keep out of scope of this thread, let's assume that we are interpreting canonicalized IR
if the debugger pauses the interpreter on a particular IR node, and you change the source file on disk...when you resume the interpreter, how does it know that the source file changed? And how can it modify the upcoming instructions, including the very next line of the program, when it's in the middle of interpreting a canonical IR in memory?
regarding how we know the source file changed, at first I thought we could just diff the current source file after you hit resume (or step, etc.) to see if it changed, but then I realized you might have edited another module - possibly one that's not even directly imported by this one, but rather indirectly imported
in that scenario, are we really going to re-check the disk every single time we call a function from another module? That would be way too slow.
instead, the fast way to do this would be that, whenever roc
is running a program with debugging enabled, it sets up a watch on every file that gets imported, and whenever that file changes, we know it's time to rebuild the canonical IR (and potentially report new compilation errors)
at this point I realized if we have that, then we have realtime hot code loading :sweat_smile:
so really, I think the experience I mentioned at the start the thread can be broken down into two projects:
there might be a bit of coordination between the two, but I think the vast majority of each of those projects seem totally independent of one another
e.g. the hot code loading needs to (I think) keep around the source file hashes in memory, along with the dependency graph, so it can efficiently tell which IRs need to be rebuilt in memory when source files change on disk
I think it also needs to have a string-based translation step for "upgrading" all the interned IDs and such when a source file changes
Rather than keeping around hashes in memory, I would instead (or in addition) keep around the stat - mtime, ctime, inode, size - and if none of those have changed, assume the hash hasn't changed either
yep, we can do both! Related: https://apenwarr.ca/log/20181113
although that's more about rebuilds
if we're running and have OS file watches set up, then we get events whenever things change (and if they changed, then certainly mtime changed)
OS file watches are not completely reliable, FWIW
(and if they changed, then certainly mtime changed)
This is very much not true
oh, true
given second resolution etc.
Yeah, also some apps have the annoying proclivity to modify a file and then set the mtime to some earlier time (perhaps the exact same time)
The particular such apps I know of are not text editors tho. Text editors are probably not being that sneaky.
hopefully haha
regardless, I haven't heard of OS file watching APIs being unreliable
do you have any links I could read to learn more?
See https://developer.apple.com/documentation/coreservices/kfseventstreameventflaguserdropped for example
watchman
handles that sort of thing for you
(as probably do many libraries that abstract over file events)
That sort of thing can happen if the system is under heavy filesystem load, for instance
OSes generally prioritize availability over delivering all file events
Switching branches in a large repo can trigger that sort of behavior quite frequently
oh I see
well that's just normal error handling though right?
like the OS still informs us that a full file scan is necessary, it's not like we just silently don't get any info
I believe so, yes
Of course the same caveats for mtime often apply to file events
e.g. memory mapped files can delay file events (and maybe suppress them altogether?)
That said, I would be very much not shocked to learn that there are OSes and configurations thereof that can legitimately drop file events with no notice.
File events are generally not treated with the same level of care as other file system operations
Anyway, popping back up a level, I'd recommend looking at how the git index works
That's the sort of state we'd want to keep in memory
And, popping back to the original discussion, debuggers / hot reloading is very cool! (and complicated)
I'm reminded about https://mun-lang.org/
I think there's going to be some overlap between what we'll want for caching, what we'll want for editor tooling, and what we'll want for hot code loading
This sounds fun. I'm for it
Also, I think general hot code reloading would be much easier than patching mid function
Also, I'm more used to seeing a repl from debugger mid function than patching. I think that also is an easier problem to solve than full mid function patching
I actually suspect it's not as hard as it sounds
basically you do a diff and it's just very conservative
if anything changed about the function you're in the middle of executing while the debugger is stopped, you verify that absolutely nothing changed prior to the current line where you're paused
bc if it did, then you can no longer be confident that the state you're currently in at runtime even exists in the new source file
whereas if the entire function up to the current place where you're paused is unchanged, then you can re-canonicalize, use source regions to find the place in the new canonical iR where you need to pick up, and go from there
and if anything changed in the function after where you paused, it's fine
You also need to know that the parts of the function that have changed either haven't been run, or wouldn't have contributed to the current state of the program - i.e. you need to keep track of code coverage
(maybe that's what you meant)
yeah a crude form of that - just source diff basically
I guess a fancier version could do an IR diff
in case you only added a comment or something
if anything changed about the function you're in the middle of executing while the debugger is stopped, you verify that absolutely nothing changed prior to the current line where you're paused
This sounds less nice than just getting an a REPL, but I guess we can support both
oh sorry, I do actually mean both :big_smile:
like in Ruby you get a repl + debugger at the same time
like you're in a repl, but then also you have access to debugging commands for stepping, resuming, etc.
Yeah, so I guess all 3? repl + debugger + hot reloading (even line by line in the debugger)?
yep! :grinning_face_with_smiling_eyes:
Last updated: Jul 06 2025 at 12:14 UTC