Debugging the Illusion: Function Compositions

I'm trying to get the debugger working again. It got broken back when the console was moved to usermode, and so its getting a rewrite to become more userspace itself.

When you ask for a BACKTRACE, it numbers the function frames in the stack. The numbers are a user convenience, so you can say things about which frame you want to inspect...or where you want to resume running from. debug 3 means switch the binding context of the console to the frame you saw numbered 3 in the backtrace, for instance, so it binds words that are function parameters there when you type in the console.

In a single-threaded world--with a routine like BACKTRACE written in usermode--it has to take into account its own stack frames. Presumably you don't want to see those, nor do you want other debug routines that try to use the numbers to face an issue of "contamination" from their own stack levels if they (like the aforementioned debug) try to turn those numbers back into FRAME! values on the user's behalf as part of their implementation.

A user-friendly approach is to count frames backward from the last breakpoint. So long as all the routines which use BACKTRACE's notion of numbering agree on that convention, they don't have to worry about their own stack frames intervening.

But this raises a curious question: when is a breakpoint on the stack? Let's say I want to make some kind of logging breakpoint, and I use ADAPT to do it:

logging-breakpoint: adapt 'breakpoint [
    write/append %breakpoint-log.txt unspaced [
        "breakpoint hit at" now newline

(Note: you'd more likely want to HIJACK the regular BREAKPOINT with this adaptation than to call it via the LOGGING-BREAKPOINT name explicitly, but for the sake of this argument let's say you just call it directly under this new name.)

When I use LOGGING-BREAKPOINT it logs the time, and falls through to breakpoint as the underlying "phase" of the function. But will BACKTRACE see BREAKPOINT on the stack? Technically it is running BREAKPOINT's body at that moment... it's morally equivalent to:

equivalent-breakpoint: func [return: []] [
    write/append %breakpoint-log.txt unspaced [
        "breakpoint hit at" now newline

This particular issue could be checked by another property...asking if a frame itself is paused?. But it raises a good question. What do you mean when you ask if a particular function is on the stack, in the face of compositions?

Mechanically, there is something called a "phase". So when you call LOGGING-BREAKPOINT it is originally on the stack in the phase of its own adapted body...e.g. its phase is the FUNCTION! of :LOGGING-BREAKPOINT. But when it falls off the end of the adaptation and continues running BREAKPOINT in the same FRAME!, it will be in the :BREAKPOINT phase.

So this raises the question of what you get back when you ask for the FUNCTION-OF a FRAME!. Should the answer be invariant (the top-level function of the frame, whose invocation began it) or should it change depending on how far you've gotten in the execution? Should there be a PHASE-OF which tells you the phase a frame is in, or should it be considered a "black box" and none of your business?

Good question. I see two options:
(1) deepen the execution model, so "falling off the end" is no longer possible and the new frame is pushed on top of the "adaptation phase", and they both exit via popping; or
(2) allow frames to overwrite each other, and once done the debugger will never see the old one.

I see this as the same thing as can be done in bash: you can call a function before exiting, and the caller sticks around, or you can exec a function, and the caller vanishes, never to be seen again.
To me it is clear which one is preferable for debugging, although it could possibly behave in the latter fashion in non-debug builds, provided the behaviour could be proven to be identical -- maybe even a run-time switch to turn it on or off?

1 Like

The reuse of frames in ADAPT (and SPECIALIZE) is intentional. They are as much for runtime efficiency as they are for saving on retyping function interfaces.

Possible. There are other areas where a "debug mode" would need to keep a memory of things that are typically thrown out.

There's a "one-time-cost" of a hook point on calling dispatchers, which could have a slow-ish behavior in a debug build to forward certain function dispatchers to a debug variation. So adapt's debug version could push a new frame for each phase, calling through to the apply mechanics...thus saving the state before the apply.

It would slow things down somewhat, given how pervasive adaptations/specializations/etc. are. You might only want it to apply to certain functions.

It could also be "virtualized" in the sense that it can look at the chain of compositions and make FRAME! values which understand they are proxies. But so long as the frame is actually mechanically reused, they won't save their own copies of the args and locals it would say "--optimized out--" or something euphemistic like that.