-
Notifications
You must be signed in to change notification settings - Fork 23
Take charge of parsing and evaluating inputs #960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
ae27d23 to
b4839c2
Compare
DavisVaughan
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! I've been running this for a few days and have not had any major issues.
In addition to my comments below, two other things
I still think Option would be useful around srcrefs to show to the reader that they are optional. I confused myself about them again today. It doesn't seem too hard to do this #961
There is one regression I noted with debugonce() when inside a package that you have done load_all() with:
devtools::load_all()
debugonce(vec_count)
vec_count(1:5)If you take a step and then run 1:2 in the Console, then with this PR you get jumped to a fallback view of vec_count, which is pretty jarring and isn't right compared with the current behavior of staying in the deparsed srcref file we created for vctrs.
This seems worth fixing before merging.
This PR:
Screen.Recording.2025-11-11.at.9.15.22.AM.mov
Current main:
Screen.Recording.2025-11-11.at.9.16.28.AM.mov
955733f to
dc8c5cb
Compare
5765c07 to
8441b97
Compare
I couldn't produce any issues with `R_EvalDepth` but it seems more sound to let R reset state before evaluation
Would be hard to get right in the case of nested browser sessions
From R's perspective, the handler runs _after_ the error was emitted. That's why the user is able to see the error message before the recover prompt. From our perspective though, we have to run the handler _before_ emitting the error, because all executed code needs to nested in an execution request so that we can properly match output to a prompt on the frontend side. The Jupyter protocol does not really support orphan side effects (streams, input requests).
The `recover()` functionality is provided by the call stack panes of frontends
8441b97 to
fbd76e2
Compare
Progress towards posit-dev/positron#1766
Progress towards posit-dev/positron#3078
Progress towards posit-dev/positron#9156
Addresses posit-dev/positron#6553
Closes #598
Closes #722
Closes #840
This PR does two things:
Doing parsing and evaluation in Ark is a significant departure of how
read_console(), the heart of the kernel, is currently structured. This handler is hooked to the R frontend method ReadConsole, which takes user inputs as strings. The R REPL gets these strings, parses them, evaluates them, prints the result, then loops back. With this PR, we take control of both parsing and evaluation.There is no functional changes here except for a few bugfixes. In the future though, this will allow us to:
New Features
Selections of code are now parsed in a single pass. A minor consequence is that all functions evaluated as part of the same selection share a common virtual document when step-debugging through them.
recover()is now an alias forbrowser(). The R version ofrecover()was buggy (options(error = recover)seems broken positron#9156) and it was hard to conciliate its behaviour with the requirements of the Jupyter protocol. By aliasing it tobrowser(), we can leverage the IDE's call stack pane to fulfill the same functionality as the originalrecover()function (R: Frame-aware custom debug REPL positron#3078).Fixes
Minor: We no longer have any size limitation on lines of inputs. They were previously limited to 4096 bytes, the maximum size of the ReadConsole input buffer.
Restart during debug sessions now works as expected: Restart the R kernel during debugging session leads to bad state positron#6553
We now interrupt ongoing evaluations before restarting. This shouldn't have any impact on Positron, which sends an interrupt before restarting, but should make restarts behave better with other frontends. This is also a nice defensive measure even with Positron.
We now flush autoprint output when an error is thrown by a print method. I was quite confused by this bug because it also swallowed debugging output!
The set of syntax errors that throw an R error instead of returning an error code no longer cause a leaked backtrace in the console (Sending a pair of empty backticks to the kernel results in an internal error showing up in the console #598, Weird error with
"\s"#722)These fixes (and more) are covered by integration tests.
Approach
We parse inputs into a vector of complete expressions. These expressions are stored in the read-console state in a new
PendingInputsstruct that implements a stack interface, with a pop method. This stack of pending inputs replaces the stack pending lines we implemented in Split inputs by newlines #536.When read-console is called by R for a new input, it is evaluated and the result is stored in
base::.ark_last_value. We store it in the base environment for robust access from (almost) any evaluation environment (global on top level but the debugger can evaluate in any environment). We only require the presence of::so that we can reach into base.Read-console returns to R with the string input
"base::.ark_last_value"if evaluation returned visibly, or"base::invisible(base::.ark_last_value)"if it returned invisibly. The R REPL then parses this expression and evaluates it.Because we are still hooked into the R REPL, it has a chance to do top-level stuff like printing warnings, running task callbacks, updating
.Last.value, etc.While this approach works, I hit two main issues that we need to workaround:
When an error happens, the
R_EvalDepthvariable is not reset before the next evaluation (it's normally reset here: https://github.com/r-devel/r-svn/blob/811080fb/src/main/main.c#L260). This could be bad especially if that error is an exceeded evaluation depth error and we're looping back to evaluate a new expression without having reset it.When a nested debug REPL returns to a parent REPL, the
R_ConsoleIobparse buffer is normally reset after returning fromeval()(here: https://github.com/r-devel/r-svn/blob/811080fb/src/main/main.c#L279). With this approach, it doesn't have a chance to reset before entering read-console again.To solve the first issue, we evaluate a dummy input (
"base::.Last.value"to keep the last value stable, this is tested) after an error occurs. This gives R a chance to fully reset state. To solve the second issue, we send a dummy parse causing aPARSE_NULLeverytime we detect a nested read-console has returned. This causes R to reset the parse buffer here: https://github.com/r-devel/r-svn/blob/811080fb/src/main/main.c#L238.These workarounds are unfortunate but have been mostly contained in the
r_read_console()method, which keeps track of all these details and other state.Reorganisation
Read-console is both an event loop (when we're waiting for input from the frontend) and a state machine (to manage execution of inputs). The event loop part is now extracted in a new method
run_event_loop(). We no longer wait for both input replies (when we're asking the frontend to reply to areadline()ormenu()question) and execution requests (when we're waiting for the frontend to send us the next bit of code to execute) at the same time, it's either one or the other, which simplifies the code. The handler for input requests now invokes the event loop itself, instead of returning to the top level of read-console to fall through the event loop.The state machine part of read-console has been simplified in other ways. We now inspect the states from the top level and pass it down through arguments, which makes it easier to reason about. See in particular the new
take_exception()andtake_result()methods called from top-level and passed down to the handler for replying to an active request when there are no inputs left to evaluate. This allowed simplifying things quite a bit.We now keep track of the number of nested consoles (due to active debug sessions) on the stack. Not used for anything but it seems good information to keep track of. To support this (and other state bookkeeping), we use a new wrapper around
R_ExecWithCleanup().The handling of debugger commands has been consolidated into a single location.
A benefit of parsing expressions ourselves is that it is no longer possible for R to send us an incomplete prompt. This has been completely removed from the possible states read-console can be in, which is another nice simplification.
The
DummyFrontendfor integration tests has now a few more methods likeexecute_request()that capture common patterns of messaging. This removes quite a lot of boilerplate.