Luke Gorrie luke@bluetail.com writes:
Peter Seibel peter@javamonkey.com writes:
Here's how I was envisioning it: when I pick a thread to debug from the list of debuggable therads SLIME creates a new buffer for interacting with that thread. I switch between threads by switching between buffers. Thus I can C-x 2 to split my window and put one thread-buffer in each window and switch back and forth with C-x o.
The main issue is for people writing SLIME-based Elisp code. Anyone not interested in how that works can safely skip this explanation :-)
The tricky part is when Emacs has made RPCs to Lisp, and the Lisp threads being debugged are actually computing results to return to Emacs functions.
Initially the Emacs stack looks something like:
((emacs-event-loop))
Then we call an Emacs function that wants to RPC to Lisp to evaluate the expression FOO in thread T1 and do-something-with the result (the top of the stack comes first):
((receive-reply-from T1) (do-something-with (eval-in-thread FOO T1)) (emacs-event-loop))
But FOO causes an error and invokes the debugger, so Emacs enters a recursive edit (i.e. calls the event loop recursively) to do the debugging, hoping to debug the problem and then get its result:
Just to make sure I understand this, it can do that because it actually got back a response from the Common Lisp which it grokked is an event that told it to invoke the debugger. Otherwise it would still be blocked, right. Does that mean if I call (eval-in-thread (sleep 300) t1), my emacs is going to hang for 5 minutes?
((emacs-event-loop) (debug-thread T1) (receive-reply-from T1) (do-something-with (eval-in-thread FOO T1)) (emacs-event-loop))
Now we have one thread available for debugging. But suppose we let it wait a while, and now ask another thread T2 to evaluate an expression BAR and do-something-else with that result, and T2 also enters the debugger:
((emacs-event-loop) (debug-thread T2) (do-something-else-with (eval-in-thread BAR T2)) (emacs-event-loop) (debug-thread T1) (receive-reply-from T1) (do-something-with (eval-in-thread FOO T1)) (emacs-event-loop))
Now we have two Emacs functions on the stack that are expecting a value - `do-something-with' and `do-something-else-with'.
See the problem?
Yeah. But what I don't understand (because, I think, I don't know enough about what a recursive edit really implies in emacs, let alone SLIME) is what other options there are. If there's only one emacs stack, regardless of how many buffers you have--which is what I'm gathering--then I'd say eventually the way to go is to make all interactions with Common Lisp be asynchronous--when you pass an expression to Lisp to be evaluated you have four choices what happens with the result--it gets stuck into a buffer, it gets passed to MESSAGE, it goes to the bit bucket in the sky, or it gets passed to a user-supplied function.
To deliver the result of FOO to do-something-with, we need to have popped everything below off the stack, including do-something-else-with. So either we have to debug BAR first, so that it can deliver its result and unwind the stack for us, or we get the result of FOO and 'throw' up the stack, so that do-something-else-with is discarded and there won't be anything left to use the result of BAR.
Okay, so maybe this is not so exciting to you guys doing pure CL, but it's a big deal for those of us using Emacs as a UI toolkit, and indirectly for people using the UIs we write :-)
There are some solutions available. One is to just do the throw -- we have to be able to handle that anyway (and we can) because the same thing happens if the user calls `abort-recursive-edit' (`C-]').
That seems bad because that means you lose all that state that was on the stack. That's okay for aborts but seems an impovrished environment for writing more sophisticated features.
Alternatively, we could ditch our synchronous-eval elisp funtion and use a purely asynchronous interface based on continuation-passing-style -- then we don't need the Elisp stack or recursive edits, and can deliver results in any order.
That seems like the way to go.
But I don't think the time is ripe yet :-)
Well, you're the one hacking on it, not me. But I'd be worried that if you build an synchronous API and everybody builds on it, you'll soon have a code base that prevents you from switching to an asynch API. And then you'll never be able to provide the good debug-two-threads-in-interleaved-fashion UI.
BTW, Distel has exactly the debugging interface you describe. All its Elisp code is totally asynchronous, based on (macro-assisted) CPS with a trampolined-style scheduler providing a multiprocessing abstraction in Elisp. Make me happy, read all about it :-)
Will do, it just finished printing out.
Distel: Distributed Emacs Lisp (for Erlang) http://www.bluetail.com/~luke/distel/distel-euc.pdf
-Peter