Go into a "debugging mode" when an error happens
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.
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.
I think the option to edit the expression that failed (usually a call) would go a long way.
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.
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"
Consider the change I needed to make here to (the call to) the
makemacro. I don't remember exactly how it failed, or how I realized thatmakewas 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 theThough see below.'underargserror.- 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
makefunction. 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.
- I have no idea, going in, which function or macro emits the
'underargserror.
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 args ⇒ hug 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.
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".
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 tonil. (This is an implicit consequence of the behavior ofcdr.) (Hm, that one probably merits a test, actually.) - In
seta missing value is implicitly taken to bet. - In
parselist, some fairly subtle reasoning reveals thathugcan never be called with an odd-length list. - In both
temandmake, 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 anameand the rest of which are paired up and passed to a function expecting one argument, a list of two elements). Onlytemandmakeare "without guardrails" in this way, as far as I can see. In a sense I understand why; bothwithandsethave natural defaults, buttemandmakedon't.- Much later edit: Passing an odd number of arguments to
temis 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 thetemcombination, 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.
- Much later edit: Passing an odd number of arguments to
Try this in your favorite repl:
Define a function,
foo, that calls some other function,bar, that is not yet defined. Now callfoo. What happens?Obviously, the call to
foobreaks, becausebaris 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
foodrops 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!
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.