-
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
base: main
Are you sure you want to change the base?
Conversation
3b0cb2f to
d7dbc33
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
We've never supported it, so better be explicit about it. This allows us to simplify some special-casing for that question. Also set `--no-restore-data` by default for consistency.
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
| if stdext::IS_TESTING { | ||
| use libr::ptr_R_CleanUp; | ||
|
|
||
| use crate::interface::r_cleanup_for_tests; | ||
|
|
||
| libr::set(ptr_R_CleanUp, Some(r_cleanup_for_tests)); | ||
| } |
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.
| if stdext::IS_TESTING { | |
| use libr::ptr_R_CleanUp; | |
| use crate::interface::r_cleanup_for_tests; | |
| libr::set(ptr_R_CleanUp, Some(r_cleanup_for_tests)); | |
| } | |
| // TODO!: Comment about why we need this | |
| if stdext::IS_TESTING { | |
| libr::set( | |
| libr::ptr_R_CleanUp, | |
| Some(crate::interface::r_cleanup_for_tests), | |
| ); | |
| } |
I think this needs a big comment like the one below it about why we need this for tests.
I also very much dislike this local use style. I've never seen other Rust programmers use it, and it doesn't feel very ergonomic to me.
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.
IIUC, it is something like
// Install a CleanUp hook for integration tests that test the shutdown process.
// We confirm that shutdown occurs by waiting in the test until `CLEANUP_SIGNAL`'s
// condition variable sends a notification, which occurs in this cleanup method
// that is called during R's shutdown process.
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.
I like that local uses make it very clear what the scope of the usage is, especially in these special paths.
| RObject::new(result) | ||
| } | ||
|
|
||
| pub fn get_block_srcrefs(call: libr::SEXP) -> RObject { |
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.
The name block confuses me. Is this just for calls? Can it be get_call_srcrefs()?
Is this really different from your pub fn srcrefs(&self) helper above? I think we should avoid duplication of work if possible for hygeine reasons.
| // Send a shutdown request with restart = false | ||
| frontend.send_shutdown_request(false); | ||
|
|
||
| // Shutdown requests generate busy/idle status messages on IOPub | ||
| frontend.recv_iopub_busy(); | ||
|
|
||
| // Receive the shutdown reply | ||
| let reply = frontend.recv_control_shutdown_reply(); | ||
| assert_eq!(reply.status, Status::Ok); | ||
| assert_eq!(reply.restart, false); | ||
|
|
||
| frontend.recv_iopub_idle(); | ||
|
|
||
| // Test with restart = true |
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.
I don't think these comments add much
|
|
||
| // Avoids our global calling handler from rlangifying errors. | ||
| // This causes some test instability across configs. |
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.
I don't think I understand this 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.
CI was failing because the error included rlang's ! prefix. I don't understand why this doesn't happen locally.
|
|
||
| // SAFETY: Call this from a POD frame. Inputs must be protected. | ||
| unsafe fn eval(expr: libr::SEXP, srcref: libr::SEXP, buf: *mut c_uchar, buflen: c_int) { | ||
| let frame = harp::r_current_frame(); |
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.
Hmmmmmmmmmmm, big code smell about this being an RObject in a POD frame! Destructor probably won't run if this jumps?
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.
Oh hmm, you've moved ownership into a SEXP and destroyed the RObject in the next line.
That really confused me!
| } | ||
| }, | ||
| Err(err) => ConsoleResult::Error(err), | ||
| Err(err) => ConsoleResult::Error(format!("{err:?}")), |
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.
Why remove the amalthea::Error?
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.
It felt like an odd choice since we're in Ark not Amalthea
| // and are now ready to evaluate the actual input | ||
| if let Some(next_input) = main.read_console_next_input.take() { | ||
| RMain::on_console_input(buf, buflen, next_input).unwrap(); | ||
| return 1; |
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.
| // and are now ready to evaluate the actual input | |
| if let Some(next_input) = main.read_console_next_input.take() { | |
| RMain::on_console_input(buf, buflen, next_input).unwrap(); | |
| return 1; | |
| // and are now ready to evaluate the actual input, which is typically | |
| // just `.ark_last_value`. | |
| if let Some(next_input) = main.read_console_next_input.take() { | |
| RMain::on_console_input(buf, buflen, next_input).unwrap(); | |
| return 1; |
Right? At first I was concerned that we could write something here that would overflow the buffer.
| // Technically this also resets time limits (see `base::setTimeLimit()`) but | ||
| // these aren't supported in Ark because they cause errors when we poll R | ||
| // events. | ||
| if main.last_error.is_some() && main.read_console_threw_error.get() { |
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.
| if main.last_error.is_some() && main.read_console_threw_error.get() { | |
| if main.read_console_threw_error.get() { |
Is one of these enough?
If not, it's mildly confusing that you reset read_console_threw_error but not last_error in the if block
| use std::sync::Condvar; | ||
| pub static CLEANUP_SIGNAL: (Mutex<bool>, Condvar) = (Mutex::new(false), Condvar::new()); | ||
|
|
||
| #[no_mangle] | ||
| pub extern "C-unwind" fn r_cleanup_for_tests(_save_act: i32, _status: i32, _run_last: i32) { |
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.
I think we are likely to get confused about the OS level support for this right now.
Can we move all of this into a unix/ folder's file?
It is only supported by unix/interface.rs, so it is misleading to have CLEANUP_SIGNAL here.
It becomes very confusing when you look at DummyArkFrontend::wait_for_cleanup(), which looks like it can be used on any OS because CLEANUP_SIGNAL is available on any OS. But that will never get a notification on Windows!
So it would be nice if wait_for_cleanup() would be marked with cfg(unix), which requires making everything here cfg(unix) as well.
Progress towards posit-dev/positron#1766
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:
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.