@@ -27,13 +27,11 @@ use crate::console::Console;
2727use crate :: console:: ConsoleOutputCapture ;
2828use crate :: fixtures:: r_test_init;
2929
30- /// Task channels for interrupt-time tasks
31- static INTERRUPT_TASKS : LazyLock < TaskChannels > = LazyLock :: new ( TaskChannels :: new) ;
32-
33- /// Task channels for idle-time tasks
30+ /// Task channels for idle-time tasks (top-level only)
3431static IDLE_TASKS : LazyLock < TaskChannels > = LazyLock :: new ( TaskChannels :: new) ;
3532
36- /// Task channels for idle tasks that run at any idle prompt (top-level or browser)
33+ /// Task channels for idle tasks that run at any idle prompt (top-level or browser, but
34+ /// not input)
3735static IDLE_ANY_TASKS : LazyLock < TaskChannels > = LazyLock :: new ( TaskChannels :: new) ;
3836
3937/// Rendezvous channel for try-idle tasks. `bounded(0)` means `try_send`
@@ -87,17 +85,15 @@ impl TaskChannels {
8785 }
8886}
8987
90- /// Returns receivers for interrupt, idle, debug- idle, and try-idle tasks.
88+ /// Returns receivers for idle, idle-any , and try-idle tasks.
9189/// Initializes the task channels if they haven't been initialized yet.
9290/// Can only be called once (intended for `Console` during init).
9391pub ( crate ) fn take_receivers ( ) -> (
94- Receiver < QueuedRTask > ,
9592 Receiver < QueuedRTask > ,
9693 Receiver < QueuedRTask > ,
9794 Receiver < TryIdleTask > ,
9895) {
9996 (
100- INTERRUPT_TASKS . take_rx ( ) ,
10197 IDLE_TASKS . take_rx ( ) ,
10298 IDLE_ANY_TASKS . take_rx ( ) ,
10399 TRY_IDLE . rx . lock ( ) . unwrap ( ) . take ( ) . unwrap ( ) ,
@@ -226,7 +222,7 @@ impl std::task::Wake for RTaskWaker {
226222}
227223
228224impl RTaskStartInfo {
229- pub ( crate ) fn new ( idle : bool ) -> Self {
225+ pub ( crate ) fn new ( ) -> Self {
230226 let thread = std:: thread:: current ( ) ;
231227 let thread_id = thread. id ( ) ;
232228 let thread_name = thread
@@ -236,7 +232,7 @@ impl RTaskStartInfo {
236232 . to_owned ( ) ;
237233
238234 let start_time = std:: time:: Instant :: now ( ) ;
239- let span = tracing:: trace_span!( "R task" , thread = thread_name, interrupt = !idle , ) ;
235+ let span = tracing:: trace_span!( "R task" , thread = thread_name) ;
240236
241237 Self {
242238 thread_id,
@@ -269,6 +265,9 @@ impl RTaskStartInfo {
269265// running, so borrowing is allowed even though we send it to another
270266// thread. See also `Crossbeam::thread::ScopedThreadBuilder` (from which
271267// `r_task()` is adapted) for a similar approach.
268+ //
269+ // `r_task()`s run via `IDLE_ANY_TASKS`, i.e. they run at top level, and at a debugger
270+ // prompt (but notably not the input prompt as a way to reduce risk of reentrancy).
272271
273272pub fn r_task < ' env , F , T > ( f : F ) -> T
274273where
@@ -324,9 +323,9 @@ where
324323 let task = QueuedRTask :: Sync ( RTaskSync {
325324 fun : closure,
326325 status_tx : Some ( status_tx) ,
327- start_info : RTaskStartInfo :: new ( false ) ,
326+ start_info : RTaskStartInfo :: new ( ) ,
328327 } ) ;
329- INTERRUPT_TASKS . tx ( ) . send ( task) . unwrap ( ) ;
328+ IDLE_ANY_TASKS . tx ( ) . send ( task) . unwrap ( ) ;
330329
331330 // Block until we get the signal that the task has started
332331 let status = status_rx. recv ( ) . unwrap ( ) ;
@@ -376,23 +375,18 @@ where
376375
377376/// An async task to be run on the R thread.
378377///
379- /// Construct via ` RTask::interrupt`, `RTask:: idle`, or ` RTask::idle_any_prompt`
380- /// when spawning from the R thread. Use the `Send` variants
381- /// (`RTask::send_interrupt`, etc.) when spawning from other threads.
378+ /// Construct via [ RTask::idle] or [ RTask::idle_any_prompt] when spawning from the R
379+ /// thread. Use the `Send` variants ([RTask::send_idle], etc.) when spawning from other
380+ /// threads.
382381///
383- /// For idle modes, console output is automatically captured during the task's
384- /// execution via a `ConsoleOutputCapture` passed to the closure.
382+ /// Console output is automatically captured during the task's execution via a
383+ /// `ConsoleOutputCapture` passed to the closure.
385384pub ( crate ) enum RTask {
386- /// Run at the next interrupt check. Must be spawned from the R thread.
387- Interrupt ( BoxFuture < ' static , ( ) > ) ,
388385 /// Run when R is at a top-level idle prompt. Must be spawned from the R thread.
389386 Idle ( BoxFuture < ' static , ( ) > ) ,
390387 /// Run when R is at any idle prompt (top-level or browser). Must be spawned
391388 /// from the R thread.
392389 IdleAnyPrompt ( BoxFuture < ' static , ( ) > ) ,
393- /// Like `Interrupt`, but can be spawned from any thread. The constructor
394- /// enforces `Send` on the closure.
395- SendInterrupt ( BoxFuture < ' static , ( ) > ) ,
396390 /// Like `Idle`, but can be spawned from any thread. The constructor
397391 /// enforces `Send` on the closure.
398392 SendIdle ( BoxFuture < ' static , ( ) > ) ,
@@ -402,14 +396,6 @@ pub(crate) enum RTask {
402396}
403397
404398impl RTask {
405- pub ( crate ) fn interrupt < F , Fut > ( fun : F ) -> Self
406- where
407- F : FnOnce ( ) -> Fut + ' static ,
408- Fut : Future < Output = ( ) > + ' static ,
409- {
410- RTask :: Interrupt ( Box :: pin ( fun ( ) ) )
411- }
412-
413399 pub ( crate ) fn idle < F , Fut > ( fun : F ) -> Self
414400 where
415401 F : FnOnce ( ConsoleOutputCapture ) -> Fut + ' static ,
@@ -418,7 +404,7 @@ impl RTask {
418404 RTask :: Idle ( Self :: pin_with_capture ( fun) )
419405 }
420406
421- #[ allow ( unused) ]
407+ #[ expect ( unused) ]
422408 pub ( crate ) fn idle_any_prompt < F , Fut > ( fun : F ) -> Self
423409 where
424410 F : FnOnce ( ConsoleOutputCapture ) -> Fut + ' static ,
@@ -427,38 +413,36 @@ impl RTask {
427413 RTask :: IdleAnyPrompt ( Self :: pin_with_capture ( fun) )
428414 }
429415
430- fn pin_with_capture < F , Fut > ( fun : F ) -> BoxFuture < ' static , ( ) >
416+ /// [Self::idle_any_prompt()], but without capture support
417+ ///
418+ /// Can be useful for spawning long running event loops that don't emit R output to
419+ /// avoid [Self::pin_with_capture()]'s behavior of setting `options(warn = 1)` for the
420+ /// life of `fun`.
421+ pub ( crate ) fn idle_any_prompt_without_capture < F , Fut > ( fun : F ) -> Self
431422 where
432- F : FnOnce ( ConsoleOutputCapture ) -> Fut + ' static ,
423+ F : FnOnce ( ) -> Fut + ' static ,
433424 Fut : Future < Output = ( ) > + ' static ,
434425 {
435- Box :: pin ( async move {
436- let capture = if Console :: is_initialized ( ) {
437- Console :: get_mut ( ) . start_capture ( )
438- } else {
439- // Unit tests run without a Console. The dummy capture is
440- // inert and doesn't interact with Console state.
441- debug_assert ! ( stdext:: IS_TESTING ) ;
442- ConsoleOutputCapture :: dummy ( )
443- } ;
444- fun ( capture) . await
445- } )
426+ RTask :: IdleAnyPrompt ( Box :: pin ( fun ( ) ) )
446427 }
447428
448- pub ( crate ) fn send_interrupt < F , Fut > ( fun : F ) -> Self
429+ pub ( crate ) fn send_idle < F , Fut > ( fun : F ) -> Self
449430 where
450- F : FnOnce ( ) -> Fut + ' static + Send ,
431+ F : FnOnce ( ConsoleOutputCapture ) -> Fut + ' static + Send ,
451432 Fut : Future < Output = ( ) > + ' static ,
452433 {
453- RTask :: SendInterrupt ( Box :: pin ( fun ( ) ) )
434+ RTask :: SendIdle ( Self :: pin_with_capture ( fun) )
454435 }
455436
456- pub ( crate ) fn send_idle < F , Fut > ( fun : F ) -> Self
437+ /// For rare performance sensitive cases where you'd like to avoid the cost of
438+ /// poking R options via [Self::pin_with_capture()] and you know you don't need
439+ /// the safety of capturing output because you aren't running R code
440+ pub ( crate ) fn send_idle_without_capture < F , Fut > ( fun : F ) -> Self
457441 where
458- F : FnOnce ( ConsoleOutputCapture ) -> Fut + ' static + Send ,
442+ F : FnOnce ( ) -> Fut + ' static + Send ,
459443 Fut : Future < Output = ( ) > + ' static ,
460444 {
461- RTask :: SendIdle ( Self :: pin_with_capture ( fun) )
445+ RTask :: SendIdle ( Box :: pin ( fun ( ) ) )
462446 }
463447
464448 pub ( crate ) fn send_idle_any_prompt < F , Fut > ( fun : F ) -> Self
@@ -468,49 +452,62 @@ impl RTask {
468452 {
469453 RTask :: SendIdleAnyPrompt ( Self :: pin_with_capture ( fun) )
470454 }
455+
456+ // Note that `start_capture()` sets `options(warn = 1)` and this persists
457+ // for the life of `f`, so avoid using capturing on long running event loops
458+ // that don't emit R output
459+ fn pin_with_capture < F , Fut > ( fun : F ) -> BoxFuture < ' static , ( ) >
460+ where
461+ F : FnOnce ( ConsoleOutputCapture ) -> Fut + ' static ,
462+ Fut : Future < Output = ( ) > + ' static ,
463+ {
464+ Box :: pin ( async move {
465+ let capture = if Console :: is_initialized ( ) {
466+ Console :: get_mut ( ) . start_capture ( )
467+ } else {
468+ // Unit tests run without a Console. The dummy capture is
469+ // inert and doesn't interact with Console state.
470+ debug_assert ! ( stdext:: IS_TESTING ) ;
471+ ConsoleOutputCapture :: dummy ( )
472+ } ;
473+ fun ( capture) . await
474+ } )
475+ }
471476}
472477
473478/// Spawn an async task on the R thread.
474479///
475- /// For `Send` variants (` RTask::send_interrupt` , etc.) this can be called from
480+ /// For `Send` variants ([ RTask::send_idle] , etc.) this can be called from
476481/// any thread. Non-`Send` variants must be called from the R thread.
477482pub ( crate ) fn spawn ( task : RTask ) {
478483 if stdext:: IS_TESTING && !Console :: is_initialized ( ) {
479484 let _lock = harp:: fixtures:: R_TEST_LOCK . lock ( ) ;
480485 let fut = match task {
481- RTask :: Interrupt ( fut) |
482486 RTask :: Idle ( fut) |
483487 RTask :: IdleAnyPrompt ( fut) |
484- RTask :: SendInterrupt ( fut) |
485488 RTask :: SendIdle ( fut) |
486489 RTask :: SendIdleAnyPrompt ( fut) => fut,
487490 } ;
488491 futures:: executor:: block_on ( fut) ;
489492 return ;
490493 }
491494
492- let needs_r_thread = matches ! (
493- task,
494- RTask :: Interrupt ( _) | RTask :: Idle ( _) | RTask :: IdleAnyPrompt ( _)
495- ) ;
495+ let needs_r_thread = matches ! ( task, RTask :: Idle ( _) | RTask :: IdleAnyPrompt ( _) ) ;
496496 if needs_r_thread && !Console :: on_main_thread ( ) {
497497 let thread = std:: thread:: current ( ) ;
498498 let name = thread. name ( ) . unwrap_or ( "<unnamed>" ) ;
499499 panic ! ( "`spawn()` must be called from the R thread, not thread '{name}'" ) ;
500500 }
501501
502- let ( fut, tasks_tx, only_idle) = match task {
503- RTask :: Interrupt ( fut) | RTask :: SendInterrupt ( fut) => ( fut, INTERRUPT_TASKS . tx ( ) , false ) ,
504- RTask :: Idle ( fut) | RTask :: SendIdle ( fut) => ( fut, IDLE_TASKS . tx ( ) , true ) ,
505- RTask :: IdleAnyPrompt ( fut) | RTask :: SendIdleAnyPrompt ( fut) => {
506- ( fut, IDLE_ANY_TASKS . tx ( ) , true )
507- } ,
502+ let ( fut, tasks_tx) = match task {
503+ RTask :: Idle ( fut) | RTask :: SendIdle ( fut) => ( fut, IDLE_TASKS . tx ( ) ) ,
504+ RTask :: IdleAnyPrompt ( fut) | RTask :: SendIdleAnyPrompt ( fut) => ( fut, IDLE_ANY_TASKS . tx ( ) ) ,
508505 } ;
509506
510507 let task = QueuedRTask :: Async ( RTaskAsync {
511508 fut,
512509 tasks_tx : tasks_tx. clone ( ) ,
513- start_info : RTaskStartInfo :: new ( only_idle ) ,
510+ start_info : RTaskStartInfo :: new ( ) ,
514511 } ) ;
515512
516513 tasks_tx. send ( task) . unwrap ( ) ;
0 commit comments