Very Creaky (but still interesting) Debug Step *DEMO*

Though there's been no published news about the debugger in well over a year, I've tried to bear in mind the concerns that a debugger would have.

Stopping everything to work on the debugger for a few months isn't really an option. But it's good to do a bit of a status check for where things stand, if "keeping the debugger in mind" has any credibility. The question to ask is "could a sufficiently motivated individual build a debugger on top of the evaluator infrastructure if they wanted to".

So the last few days I've frittered around with a very-buggy-proof-of-concept for single-stepping. Given:

foo: function [f] [
    print "Entering foo"
    f: bar f
    f: f + 1000
]

bar: function [b] [
     print "Entering bar"
     breakpoint
     print "STEPPING!"
     b: b + 10
     return b + 100
]

The following demo seems to run, but it's really just a thought experiment to inform the design:

>> foo 1
Entering foo
Entering bar
(i) BREAKPOINT hit

!! Entering *EXPERIMENTAL* Debug Console that only barely works for a demo.
Expect crashes and mayhem.  But see BACKTRACE, RESUME, and STEP.

bar:|1|>> backtrace
2 [foo 1 ~~]
1 [...
    f: bar f ~~
    f: f + ...]
0 [
    print "Entering bar"
    breakpoint ~~
    print "STEPPING!"
    b: ...]

bar:|1|>> step
STEPPING!

bar:|1|>> backtrace
2 [foo 1 ~~]
1 [...
    f: bar f ~~
    f: f + ...]
0 [...
    breakpoint
    print "STEPPING!" ~~
    b: b + ...]

bar:|1|>> b
== 1

bar:|1|>> step

bar:|1|>> b
== 11

bar:|1|>> 2
(i) Interpreting integer input as DEBUG

foo:|2|>> f
== 1

foo:|2|>> step

bar:|1|>> backtrace
2 [foo 1 ~~]
1 [...
    f: bar f ~~
    f: f + ...]
0 [... b + 100 ~~]

bar:|1|>> b
== 11

bar:|1|>> step

foo:|1|>> backtrace
1 [foo 1 ~~]
0 [... f + 1000 ~~]

foo:|1|>> f
== 1111

foo:|1|>> step

>> resume  ; !!! If you don't type this, it will start acting weird

(Discourse put a scroll bar on that, so be sure you scrolled through it all, the scroll bar is easy to miss.)

The Good News

A lot of parts have to come together just for that. You're seeing a console written in Rebol being able to kick off a recursive call to offer a nested debugger console with a custom skin (also in Rebol).

Here we see it interpreting raw ENTER as "STEP" and a plain INTEGER! as a request to switch stack levels. This is the direction I wanted to see, and why I held off from the idea of any further development of a C console... this runs in the web console too!

It also adds to that hook the binding what you type to the variables of the stack level you are "focused" on. Above you see it finding the b local in bar, and then switching stack levels and being able to find the f local in foo.

Even though it's all single threaded (no linkage to pthreads in a console build), that nested command session is doing arbitrary variable inspections...but also running a usermode BACKTRACE command...where you only see the stack levels pertaining to the code you were running. So it's not stepping through the console code or seeing its implementation.

Lines are shaping up between the pieces; they can be separated out. You can build with just the evaluator and no console, or add the console in, or add the debugger in. It's something you can mix and match--and the paths are laid down to be able to pull in these elements dynamically as well as in a static build.

The star of the show is really FRAME!, and how that's supporting the whole idea.

What's the Bad News?

It's important to emphasize this is still sticks and glue at this point.

As mentioned at the top of this post: this was really just a temperature check on how things are going in the evaluator. I think it's generally the right direction, but seeing it actually trying to line up with reality exposes a lot of issues. Going to have to go back to the drawing board with some ideas.

Single Threading

What's been the longstanding "big issue" is what it means to be attempting to design a scriptable debugger in a single-threaded system. It's certainly possible to write debuggers on single-threaded platforms (remember Turbo Pascal for DOS?) But here we have something quite different...where scripting for the debug console is running in the same language on the same execution stack.

Industrial-strength debuggers tend to be able to connect from remote machines (e.g. debugging on your phone from the PC). This RPC/messaging runaround can be pretty intimidating, if you look at a sample of how to call V8's debug API from C++.

If we were committed to saying you needed threads and message pumps to do debugging, it would make some things easier. But multithreaded Rebol is still not a real thing...we would need at least something parallel to V8's "isolates" to be able to have interpreter sessions that could not actually share objects. So rendering/molding of anything you wanted to look at in the debugger would have to be done in the "debuggee" isolate anyway.

So trying to abstract the interface so that it can grow into a message-passing solution is a big challenge. Being able to have it work whether or not you're linked to pthreads is actually kind of interesting.

Debugging Dialects

Something I really wanted to see was the debugger generalizing to parse. And at first glance, it seems like it might:

>> count: 0
>> rule: ["a" (count: count + 1 breakpoint)]
>> parse "aaa" [some rule]
(i) BREAKPOINT hit

subparse:|1|>> backtrace
3 [parse "aaa" [some rule] ~~]
2 [some rule ~~]
1 ["a" ~~ (count: count + ...)]
0 [... + 1 breakpoint ~~]

subparse:|1|>> count
== 1

Seems promising. But from there on out, the granularity of STEP is not what you want. In order for STEP to intelligently STEP over a parse rule there has to be a common protocol spoken by PARSE and the evaluator beyond just what the "stack" looks like.

But this also shows just how many meanings of STEP you might have. How do dialects get involved in debugging? What does the "hello world" of making a dialect that offers its own idea of what steps are look like?

Red has a /TRACE option to parse which will call a callback on "parse events" described in the "Introducing Parse" post. But at the end of the day, I think what this is aiming for is more what people are going to want...a unified stack concept which fits dialects into a holistic model. It's got to have a design, though.

Rendering

As these snippets show, it's a challenge to know how much to show. And you're flattening a data structure with nested levels and pointers and (potentially) cycles.

Even if we had a GUI environment to throw up an annotated source file in...debugging Rebol is like debugging C where everything is a macro. :-/ There's not always a clear place in something the user thought of "source code" to tie generated blocks to.

Inheriting Console Behaviors

The debugger spawns a nested console, and wants to augment it with its own debugger-specific methods. Right away this ran up against the problem of SYSTEM/CONSOLE being thought of as a global object. If you want to change the prompt, you say system/console/prompt: "whatever>> ". How do multiple console objects fit into this picture?

Not only that, but what if you want to keep some of the customizations you have for your usual console while debugging? How do you keep your shortcuts or your dialect hooks?

Compared to the other issues this is kind of a fringe/luxury issue. So for now, the debugger console is a console unto itself. Your customizations don't apply when the debugger is running. My point is just that these aren't questions just magically answer themselves.

I Could Go On, But...

...the point is just that there's a lot to think about. We haven't seen single-stepping of this type in Redbol before this...and it's been decades. So it would be unrealistic to expect that the first try to be anything but an extremely rough draft.

But I'd like it to evolve to the point of where we start thinking we can push a pause button and get some kind of debugger up. So I've gone ahead and included it in the web build--for experimentation and prototyping only. Do not file bugs against the debugger yet, it doesn't exist. :ghost:

3 Likes

In 2019, I made a small debugging demo:

The "bad" (actually good) news is that those sticks and glue are all gone. Switching to stackless removed the hooks for writing code that intercepted the evaluator.

The "good" (no, actually good) news is that I've accomplished this using modern methods:

ReplPad Visual PARSE Debugger

The methods that accomplish this are legitimate, and not likely to be overturned in the design.

However, they aren't part of a generalized notion of debugging. This debugger is tailored to PARSE, and parse only.

Not only that...it works only on a single parse call. You make a specific call to parse parameterized with a hook, e.g. PARSE-DEBUG. Only that call should be hooked. So if the debugger uses PARSE in its own implementation (which it does) it needs to use the non-DEBUG form to do so, otherwise it would break the in-progress debug session by interfering with its interface.

Could The Evaluator Use The Same Methodology?

PARSE-DEBUG is based on the idea of hooking a frame... using ENCLOSE. All parser combinators are ENCLOSE'd when they are built, with the ability to be hooked.

I've discussed the idea of the main evaluation process being driven by EVALUATORs, much like PARSE uses COMBINATORs. So you'd have a SET-WORD! evaluator function that took the SET-WORD! it was invoked with, and a variadic feed of code coming after it.

Under this idea, you could actually create something like REDBOL-DO, which had a different EVALUATOR for PATH! to do variable picking (while Ren-C's default evaluator would do variables with TUPLE!).

I think it could work. But it does mean that variadics are needed by the language, because the FRAME! built for evaluators can't see to the end of how much that evaluator is going to consume. (I'd been mulling over killing variadics as a "it's too much" feature now and again, but this kind of proves that's a bad idea...and they actually just need to be fixed up to be less of a mess.)

It would be a pretty gnarly performance penalty to turn every SET-WORD! or GET-TUPLE! into a function call. But I have an inkling on how to make it cheaper. As it happens, there's a stack frame already for the right hand side of the evaluation... and what this could do would be to morph that stack frame on-demand so it looked like a function call...but only when the debugger looked at it. It just might work.

Could PARSE-Debug and EVALUATOR-Debug Unify?

You run into some interesting questions in terms of whether you want to debug the combinators of a PARSE, or if you want to debug the evaluator implementing parse. Or if there's some way of switching modes.

Being able to switch modes on a whim would be something like being able to switch into an assembly view of an already-running C function in a C debugger.

Right now the PARSE debug demo is dependent on the hooked parse building up and tearing down its own stack level list in the combinator hook. That's already a problem, because it means that you can't trigger a breakpoint at an arbitrary point in the parse...because it only maintained this level list if it was tracing.

So the unified model would depend on some way to enumerate the stack API, and be able to make decisions about who to ask about rendering a stack level. Because a combinator is both an evaluator level and a parse level.

Prototyping EVAL Debugging in ReplPad is Probably Best

After having put together the PARSE stepping demo, I think that it makes the most sense to build debugger prototypes in the browser.

I continue to think this needs to be pushed on sooner rather than later. (I know it seems quite a bit "later" in the scheme of things, but there's always more later to come. :-P)

1 Like