bel icon indicating copy to clipboard operation
bel copied to clipboard

Go into a "debugging mode" when an error happens

Open masak opened this issue 6 years ago • 10 comments

Don't know exactly what it would take. Certainly we can stop the code at the point where it fails, allow the user to change parts of the environment, and then let them call a (resume) which we've added (dynamically) when the error occurred.

I'm not sure offhand how much of the already-executed function we need to keep around. The simplest thing imaginable is to keep only the stack element that led to the failure, and to re-run that on (resume). This basically amounts to calling a continuation.

The user should be able to inspect and change both lexical and dynamic bindings in the running code before resuming.

masak avatar Dec 25 '19 06:12 masak

Not to mention being able to change the code itself!

Consider the change I needed to make here to (the call to) the make macro. I don't remember exactly how it failed, or how I realized that make was the problem — need to go back and reproduce the problem — but it would certainly have been nice to be able to fix the problem in the running program (with the possibility to save the changed code back to file, of course) and resume the original run at the appropriate point without having to re-run the code up to there.

I realize that's a bit of a tall order, especially in the face of bytecode compilation. But it's possible.

Also, of course, the situation changes a bit with good-enough static checking (which could probably detect in this case that I'm using make wrong, even before I run the program). That may remove this particular use case, but it doesn't remove the class of use cases.

masak avatar Jun 16 '20 06:06 masak

I think the option to edit the expression that failed (usually a call) would go a long way.

masak avatar Jul 31 '20 03:07 masak

I've concluded that errors are stronger than after semantics, meaning that an error doesn't unwind the stack, it abruptly aborts the evaluation.

Which is good news for this issue, because the evaluator has basically abandoned the evaluation in disgust, and (conceptually) another one could pick it up, prots and all, where the first one left off.

masak avatar Aug 21 '20 17:08 masak

This issue is turning into a general request for debugging, and as such I would like to link to this paper: https://arxiv.org/pdf/1905.06545.pdf "Direct Interpretation of Functional Programs for Debugging"

masak avatar Sep 02 '20 08:09 masak

Consider the change I needed to make here to (the call to) the make macro. I don't remember exactly how it failed, or how I realized that make was the problem — need to go back and reproduce the problem [...]

I reproduced it now. It failed with 'underargs; that is, some function or macro has a (non-optional, non-rest) parameter that it can't match to an argument. This mystery function is not make, since make has the signature (name . args), which we certainly satisfy with the (buggy) combination (make linked-list '(1 2 3 4 5)).

There are several reasons I "like" this error:

  • I have no idea, going in, which function or macro emits the 'underargs error. Though see below.
  • Which one it is matters little — as impartial observers outside of the situation, we already know that the actual problem is in the calling code, with the missing field name to the make function. Where the error triggers is very much a red herring.
  • From an "implementation design" perspective, when this kind of error happens, I would very much like to relate it back from wherever it blew up to the original wrong call. At least in this situation when we're still directly below the wrong call in the call stack, that's theoretically possible.
  • This kind of situation happens a lot in Bel (#18). Or, should I say, it happens a lot in this implementation, and currently there is far too little information/assistance when an error happens.

masak avatar Feb 21 '21 07:02 masak

  • I have no idea, going in, which function or macro emits the 'underargs error.

Cutting right to the chase, it's a little anonymous pair-producing function inside the make macro's body.

(mac make (name . args)
  `(inst ',name
         (list ,@(map (fn ((k v)) `(cons ',k ,v))
                      (hug args)))))

With args having an odd number of elements (1, here), (hug args) is going to produce a final element which is a 1-element list, whereas all the previous elements (0 of them, here) are 2-element lists. These all get sent to the map, which is guaranteed to underargs on that last element, wanting a v where there is none.

So, the chain of causality goes like this: odd number of elements in argshug produces a final element which is a 1-element list ⇒ the anonymous function will fail to bind on this element.

I would like to say something very hopeful about the ability to statically detect this error. There's definitely nothing impossible about it; it's more a question of having tools fine-grained enough to reason through these three steps using some kind of abstract interpretation.

masak avatar Feb 21 '21 11:02 masak

The correct way to think about it is probably some backwards reasoning, "here's a function signature; what would it take for that one to fail to bind? oh, it's part of a map; that means we're looking at elements of the input list(s); oh, that list is produced by hug; that means it'll have this shape, indexed with these conditions; oh, here's a condition that would cause the function signature to fail to match; oh, and the caller's combiner actually looks like that: error".

masak avatar Feb 21 '21 11:02 masak

Interestingly, hug is used in a number of other places, and usually in such a way that the "trailing one-element list element" phenomenon doesn't cause an error:

  • In with, an odd-length parameter list will silently pretend that you wanted to initialize the last unmatched variable name to nil. (This is an implicit consequence of the behavior of cdr.) (Hm, that one probably merits a test, actually.)
  • In set a missing value is implicitly taken to be t.
  • In parselist, some fairly subtle reasoning reveals that hug can never be called with an odd-length list.
  • In both tem and make, the error can and does happen, and it's up to the user of those macros to make sure it doesn't (by passing an odd number of arguments, the first one of which is a name and the rest of which are paired up and passed to a function expecting one argument, a list of two elements). Only tem and make are "without guardrails" in this way, as far as I can see. In a sense I understand why; both with and set have natural defaults, but tem and make don't.
    • Much later edit: Passing an odd number of arguments to tem is a very nice example of an error message that could be "shifted left"; even though it technically happens as late as the runtime which evaluates the tem combination, a clever enough IDE or other tool has all it needs to confidently flag that error early. There's a sense in which that is possible because it can be found using some appropriate abstract interpretation.

masak avatar Feb 21 '21 11:02 masak

This blog post:

Try this in your favorite repl:

Define a function, foo, that calls some other function, bar, that is not yet defined. Now call foo. What happens?

Obviously, the call to foo breaks, because bar is not defined. But what happens when it breaks? What happens next?

If your favorite repl is Python’s or Ruby’s or any of a few dozen other modern repls, the answer is most likely that it prints an error message and returns to its prompt. In some cases, perhaps it crashes.

So what’s my point, right? What else could it do?

The answer to that question is the “differentiating point” of repl-driven programming. In an old-fashioned Lisp or Smalltalk environment, the break in foo drops you into a breakloop.

A breakloop is a full-featured repl, complete with all of the tools of the main repl, but it exists inside the dynamic environment of the broken function. From the breakloop you can roam up and down the suspended call stack, examining all variables that are lexically visible from each stack frame. In fact, you can inspect all live data in the running program.

What’s more, you can edit all live data in the program. If you think that a break was caused by a wrong value in some particular variable or field, you can interactively change it and resume the suspended function. If it now works correctly, then congratulations; you found the problem!

masak avatar Jul 27 '23 05:07 masak

I think the option to edit the expression that failed (usually a call) would go a long way.

Visual Studio has this, for C# and a number of other languages. Demonstrated quite nicely in this video. (Edit: I learned about the feature from this HN comment.)

It's a feature that you have to enable, for some reason. I think I would prefer to have this enabled by default (and maybe not even have a setting) — as far as I can see, there are no drawbacks to that.

I think the way I would do it is to run LCS on the before- and after-versions of the source code, and use the result to map the instruction pointer across from the before-version of the compiled code to the after-version. Now I really feel like trying this out.

masak avatar Oct 11 '23 04:10 masak