diff --git a/crates/arf-console/src/completion/meta.rs b/crates/arf-console/src/completion/meta.rs index b26021e..a38f935 100644 --- a/crates/arf-console/src/completion/meta.rs +++ b/crates/arf-console/src/completion/meta.rs @@ -127,6 +127,16 @@ const META_COMMANDS: &[MetaCommandDef] = &[ description: "IPC server management (start, stop, status)", takes_argument: true, }, + MetaCommandDef { + name: "objects", + description: "Browse .GlobalEnv objects", + takes_argument: false, + }, + MetaCommandDef { + name: "objs", + description: "Browse .GlobalEnv objects (alias)", + takes_argument: false, + }, MetaCommandDef { name: "quit", description: "Quit arf", diff --git a/crates/arf-console/src/pager/mod.rs b/crates/arf-console/src/pager/mod.rs index 0907565..87e3c4b 100644 --- a/crates/arf-console/src/pager/mod.rs +++ b/crates/arf-console/src/pager/mod.rs @@ -8,6 +8,7 @@ mod help; pub mod history_browser; pub mod history_schema; pub(crate) mod markdown; +pub mod objects_browser; pub mod session_info; pub(crate) mod style_convert; pub(crate) mod text_utils; @@ -15,6 +16,7 @@ pub(crate) mod text_utils; pub use changelog::display_changelog; pub use help::run_help_browser; pub use history_browser::{HistoryBrowserResult, HistoryDbMode, run_history_browser}; +pub use objects_browser::run_objects_browser; pub use session_info::display_session_info; use base64::{Engine, engine::general_purpose}; diff --git a/crates/arf-console/src/pager/objects_browser.rs b/crates/arf-console/src/pager/objects_browser.rs new file mode 100644 index 0000000..768faab --- /dev/null +++ b/crates/arf-console/src/pager/objects_browser.rs @@ -0,0 +1,420 @@ +//! Interactive browser for .GlobalEnv objects (`:objects` command). + +use super::text_utils::{display_width, pad_to_width, truncate_to_width}; +use super::{MinimumSize, check_terminal_too_small, render_size_warning, with_alternate_screen}; +use crate::fuzzy::fuzzy_match; +use arf_harp::{EnvEntry, workspace_snapshot}; +use crossterm::{ + ExecutableCommand, cursor, + event::{self, Event, KeyCode, KeyEventKind, KeyModifiers, MouseEventKind}, + queue, + style::Stylize, + terminal::{self, BeginSynchronizedUpdate, ClearType, EndSynchronizedUpdate}, +}; +use std::io::{self, Write}; +use std::time::Duration; + +/// Minimum terminal size for the objects browser. +/// Must be at least FIXED_COLS + NAME_MIN = 51 + 15 = 66. +const MIN_SIZE: MinimumSize = MinimumSize { cols: 66, rows: 8 }; + +/// Fixed column widths (name is dynamic). +const CLASS_WIDTH: usize = 18; +const TYPE_WIDTH: usize = 12; +const SIZE_WIDTH: usize = 8; +/// Columns beyond name: cursor(1) + space(1) + " │ "(3)×3 + class + type + size + marker(2) +const FIXED_COLS: usize = 2 + 3 + CLASS_WIDTH + 3 + TYPE_WIDTH + 3 + SIZE_WIDTH + 2; +const NAME_MIN: usize = 15; + +/// Result of running the objects browser. +#[derive(Debug, Clone)] +pub enum ObjectsBrowserResult { + /// User exited without action. + Cancelled, +} + +struct ObjectsBrowser { + entries: Vec, + filtered: Vec, + cursor: usize, + scroll_offset: usize, + filter_text: String, + filter_active: bool, + /// When true, dot-prefixed names are included (like ls(all.names=TRUE)). + show_hidden: bool, + feedback: Option, +} + +impl ObjectsBrowser { + fn new(entries: Vec) -> Self { + let n = entries.len(); + Self { + entries, + filtered: (0..n).collect(), + cursor: 0, + scroll_offset: 0, + filter_text: String::new(), + filter_active: false, + show_hidden: false, + feedback: None, + } + } + + fn reload(&mut self) { + match workspace_snapshot(self.show_hidden) { + Ok(entries) => { + self.entries = entries; + self.update_filter(); + self.feedback = Some(format!("{} objects", self.filtered.len())); + } + Err(e) => { + self.feedback = Some(format!("Error: {e}")); + } + } + } + + fn update_filter(&mut self) { + if self.filter_text.is_empty() { + self.filtered = (0..self.entries.len()).collect(); + } else { + let query = &self.filter_text; + self.filtered = self + .entries + .iter() + .enumerate() + .filter(|(_, e)| { + fuzzy_match(query, &e.name).is_some() + || fuzzy_match(query, &e.class_label).is_some() + }) + .map(|(i, _)| i) + .collect(); + } + if self.filtered.is_empty() { + self.cursor = 0; + self.scroll_offset = 0; + } else { + self.cursor = self.cursor.min(self.filtered.len() - 1); + // Also clamp scroll_offset so the cursor stays in the visible window. + // We don't know visible_rows here, so just ensure offset <= cursor. + self.scroll_offset = self.scroll_offset.min(self.cursor); + } + } + + fn visible_rows(term_height: u16) -> usize { + // Chrome: header(1) + filter(1) + sep(1) + col-header(1) + sep(1) + footer(1) = 6 + (term_height as usize).saturating_sub(6) + } + + fn name_width(term_width: u16) -> usize { + (term_width as usize) + .saturating_sub(FIXED_COLS) + .max(NAME_MIN) + } + + fn move_up(&mut self) { + if self.cursor > 0 { + self.cursor -= 1; + if self.cursor < self.scroll_offset { + self.scroll_offset = self.cursor; + } + } + } + + fn move_down(&mut self, visible: usize) { + if !self.filtered.is_empty() && self.cursor + 1 < self.filtered.len() { + self.cursor += 1; + if self.cursor >= self.scroll_offset + visible { + self.scroll_offset = self.cursor - visible + 1; + } + } + } + + fn move_page_up(&mut self, visible: usize) { + self.cursor = self.cursor.saturating_sub(visible); + if self.cursor < self.scroll_offset { + self.scroll_offset = self.cursor; + } + } + + fn move_page_down(&mut self, visible: usize) { + if self.filtered.is_empty() { + return; + } + self.cursor = (self.cursor + visible).min(self.filtered.len() - 1); + if self.cursor >= self.scroll_offset + visible { + self.scroll_offset = self.cursor - visible + 1; + } + } + + fn move_to_top(&mut self) { + self.cursor = 0; + self.scroll_offset = 0; + } + + fn move_to_bottom(&mut self, visible: usize) { + if self.filtered.is_empty() { + return; + } + self.cursor = self.filtered.len() - 1; + self.scroll_offset = self.cursor.saturating_sub(visible - 1); + } + + fn render(&self, stdout: &mut io::Stdout) -> io::Result<()> { + let (cols, rows) = terminal::size().unwrap_or((80, 24)); + + if let Some((c, r)) = check_terminal_too_small(&MIN_SIZE) { + return render_size_warning(stdout, c, r, &MIN_SIZE); + } + + let width = cols as usize; + let visible = Self::visible_rows(rows); + let name_w = Self::name_width(cols); + + queue!( + stdout, + BeginSynchronizedUpdate, + cursor::MoveTo(0, 0), + cursor::Hide + )?; + + // ── Header ────────────────────────────────────────────────────────── + let hidden_marker = if self.show_hidden { " [all]" } else { "" }; + let header = format!( + "─ Objects [{}/{}]{} ─", + self.filtered.len(), + self.entries.len(), + hidden_marker, + ); + let padded = format!("{:─" } else { " " }; + let cursor_mark = if is_current { ">" } else { " " }; + + let content = format!( + "{} {} │ {} │ {} │ {}{}", + cursor_mark, + pad_to_width(&name, name_w), + pad_to_width(&class, CLASS_WIDTH), + pad_to_width(&typ, TYPE_WIDTH), + pad_to_width(&size_str, SIZE_WIDTH), + marker, + ); + let line = pad_to_width(&content, width); + + if is_current { + println!("\r{}", line.reverse()); + } else if entry.is_active_binding || entry.is_promise { + println!("\r{}", line.dark_grey()); + } else { + println!("\r{line}"); + } + } else { + println!("\r{}", " ".repeat(width)); + } + } + + // ── Footer ─────────────────────────────────────────────────────────── + stdout.execute(terminal::Clear(ClearType::CurrentLine))?; + let footer_text = if let Some(msg) = &self.feedback { + format!("─ {} ─", msg) + } else { + "─ ↑↓/jk navigate │ / filter │ a toggle hidden │ r refresh │ q exit ─".to_string() + }; + let footer_display = truncate_to_width(&footer_text, width); + let used = display_width(&footer_display); + let padded_footer = format!( + "{}{}", + footer_display, + "─".repeat(width.saturating_sub(used)) + ); + print!("\r{}", padded_footer.dark_grey()); + queue!(stdout, EndSynchronizedUpdate)?; + stdout.flush()?; + + Ok(()) + } + + fn run(mut self, stdout: &mut io::Stdout) -> io::Result { + let mut needs_redraw = true; + + loop { + let (_, rows) = terminal::size().unwrap_or((80, 24)); + let visible = Self::visible_rows(rows); + + if needs_redraw { + self.render(stdout)?; + needs_redraw = false; + } + + if event::poll(Duration::from_millis(100))? { + self.feedback = None; + + match event::read()? { + Event::Key(key) => { + if key.kind != KeyEventKind::Press { + continue; + } + needs_redraw = true; + + if self.filter_active { + match (key.code, key.modifiers) { + (KeyCode::Esc, _) | (KeyCode::Enter, _) => { + self.filter_active = false; + } + (KeyCode::Backspace, _) => { + self.filter_text.pop(); + self.update_filter(); + } + (KeyCode::Char('u'), KeyModifiers::CONTROL) => { + self.filter_text.clear(); + self.update_filter(); + } + (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => { + self.filter_text.push(c); + self.update_filter(); + } + _ => {} + } + } else { + match (key.code, key.modifiers) { + (KeyCode::Esc, _) + | (KeyCode::Char('q'), KeyModifiers::NONE) + | (KeyCode::Char('c'), KeyModifiers::CONTROL) + | (KeyCode::Char('d'), KeyModifiers::CONTROL) => { + return Ok(ObjectsBrowserResult::Cancelled); + } + (KeyCode::Char('/'), KeyModifiers::NONE) => { + self.filter_active = true; + } + (KeyCode::Up, _) + | (KeyCode::Char('k'), KeyModifiers::NONE) + | (KeyCode::Char('p'), KeyModifiers::CONTROL) => { + self.move_up(); + } + (KeyCode::Down, _) + | (KeyCode::Char('j'), KeyModifiers::NONE) + | (KeyCode::Char('n'), KeyModifiers::CONTROL) => { + self.move_down(visible); + } + (KeyCode::PageUp, _) + | (KeyCode::Char('b'), KeyModifiers::CONTROL) => { + self.move_page_up(visible); + } + (KeyCode::PageDown, _) + | (KeyCode::Char('f'), KeyModifiers::CONTROL) => { + self.move_page_down(visible); + } + (KeyCode::Home, _) | (KeyCode::Char('g'), KeyModifiers::NONE) => { + self.move_to_top(); + } + (KeyCode::End, _) | (KeyCode::Char('G'), KeyModifiers::SHIFT) => { + self.move_to_bottom(visible); + } + (KeyCode::Char('a'), KeyModifiers::NONE) => { + self.show_hidden = !self.show_hidden; + self.reload(); + } + (KeyCode::Char('r'), KeyModifiers::NONE) => { + self.reload(); + } + _ => { + needs_redraw = false; + } + } + } + } + Event::Mouse(mouse) => match mouse.kind { + MouseEventKind::ScrollUp => { + needs_redraw = true; + self.move_up(); + } + MouseEventKind::ScrollDown => { + needs_redraw = true; + self.move_down(visible); + } + _ => {} + }, + Event::Resize(_, _) => { + needs_redraw = true; + } + _ => {} + } + } + } + } +} + +fn format_size(size: Option) -> String { + match size { + None => String::new(), + Some(n) => { + if n >= 1_000_000 { + format!("{:.1}M", n as f64 / 1_000_000.0) + } else if n >= 1_000 { + format!("{:.1}k", n as f64 / 1_000.0) + } else { + n.to_string() + } + } + } +} + +/// Run the objects browser, displaying .GlobalEnv contents. +pub fn run_objects_browser() -> io::Result { + // Default: hide dot-prefixed names (matches R's ls() default) + let entries = workspace_snapshot(false).unwrap_or_default(); + let browser = ObjectsBrowser::new(entries); + with_alternate_screen(|| { + let mut stdout = io::stdout(); + browser.run(&mut stdout) + }) +} diff --git a/crates/arf-console/src/repl/meta_command.rs b/crates/arf-console/src/repl/meta_command.rs index 4102795..88ea539 100644 --- a/crates/arf-console/src/repl/meta_command.rs +++ b/crates/arf-console/src/repl/meta_command.rs @@ -33,6 +33,8 @@ pub enum MetaCommandResult { ShowHistoryBrowser { path: PathBuf, mode: HistoryDbMode }, /// Display history schema (caller runs pager) ShowHistorySchema, + /// Open the objects browser (caller runs pager) + ShowObjectsBrowser, } /// Process a meta command (starting with `:`) and return the result. @@ -268,9 +270,11 @@ pub fn process_meta_command( } Some(MetaCommandResult::Handled) } + "objects" | "objs" => Some(MetaCommandResult::ShowObjectsBrowser), "commands" | "cmds" => { arf_println!("Available commands:"); println!("# :help - Search R help"); + println!("# :objects - Browse .GlobalEnv objects"); println!("# :info - Show session information"); println!("# :shell - Enter shell mode (input goes to system shell)"); println!("# :r - Return to R mode (from shell mode)"); diff --git a/crates/arf-console/src/repl/mod.rs b/crates/arf-console/src/repl/mod.rs index 70ed453..8e1c3f1 100644 --- a/crates/arf-console/src/repl/mod.rs +++ b/crates/arf-console/src/repl/mod.rs @@ -1327,6 +1327,12 @@ fn handle_meta_command_result( } MetaAction::Continue } + MetaCommandResult::ShowObjectsBrowser => { + if let Err(e) = with_ipc_alternate_guard(crate::pager::run_objects_browser) { + arf_println!("Error: {}", e); + } + MetaAction::Continue + } } } diff --git a/crates/arf-harp/src/lib.rs b/crates/arf-harp/src/lib.rs index 9fb4fff..9beff4e 100644 --- a/crates/arf-harp/src/lib.rs +++ b/crates/arf-harp/src/lib.rs @@ -9,6 +9,7 @@ pub mod help; mod object; mod protect; pub mod startup; +pub mod workspace; pub use error::*; pub use help::*; @@ -20,3 +21,4 @@ pub use startup::{ call_dot_first, call_dot_first_sys, should_ignore_site_r_profile, should_ignore_user_r_profile, source_site_r_profile, source_user_r_profile, }; +pub use workspace::{EnvEntry, workspace_snapshot}; diff --git a/crates/arf-harp/src/workspace.rs b/crates/arf-harp/src/workspace.rs new file mode 100644 index 0000000..028afdc --- /dev/null +++ b/crates/arf-harp/src/workspace.rs @@ -0,0 +1,204 @@ +//! Workspace snapshot — read .GlobalEnv bindings via R's C API. +//! +//! Active bindings are detected *before* value access to avoid forcing them. +//! Unforced promises are labelled without being evaluated. + +use std::ffi::CString; + +use arf_libr::{R_FALSE, R_TRUE, r_class_symbol, r_library, r_nil_value, r_unbound_value}; + +use crate::error::HarpResult; +use crate::protect::RProtect; + +/// A single binding in .GlobalEnv. +#[derive(Debug, Clone)] +pub struct EnvEntry { + /// Binding name. + pub name: String, + /// R typeof string (e.g. "integer", "closure"). + pub type_label: String, + /// First class from class() or implied class. + pub class_label: String, + /// Rf_xlength of the object; None for active bindings and promises. + pub size: Option, + /// True for list/env/pairlist/S4 — drillable in Phase 2. + pub has_children: bool, + pub is_active_binding: bool, + pub is_promise: bool, +} + +/// Snapshot the current .GlobalEnv bindings. +/// +/// When `all_names` is false (default), names starting with `.` are excluded, +/// matching R's default `ls()` behavior. +/// +/// Must be called from the R main thread. +pub fn workspace_snapshot(all_names: bool) -> HarpResult> { + let lib = r_library()?; + let nil = r_nil_value()?; + let unbound = r_unbound_value()?; + let class_sym = r_class_symbol()?; + + let mut protect = RProtect::new(); + + unsafe { + let global_env = *lib.r_globalenv; + + // R_lsInternal3(env, all_names, sorted=TRUE) — returns a STRSXP + let all = if all_names { R_TRUE } else { R_FALSE }; + let names_vec = protect.protect((lib.r_lsinternal3)(global_env, all, R_TRUE)); + + let n = (lib.rf_length)(names_vec) as isize; + let mut entries = Vec::with_capacity(n as usize); + + for i in 0..n { + let name_sexp = (lib.string_elt)(names_vec, i); + let name_cstr = (lib.r_charsxp)(name_sexp); + if name_cstr.is_null() { + continue; + } + let name = std::ffi::CStr::from_ptr(name_cstr) + .to_string_lossy() + .into_owned(); + + let name_bytes = match CString::new(name.as_bytes()) { + Ok(s) => s, + Err(_) => continue, + }; + // Rf_install interns the symbol permanently — no GC triggered + let sym = (lib.rf_install)(name_bytes.as_ptr()); + + // Existence check (sanity — symbol came from R_lsInternal3) + if (lib.r_existsvarinframe)(global_env, sym) == R_FALSE { + continue; + } + + // Check active binding BEFORE fetching the value. + // R_BindingIsActive(sym, rho): sym is the first arg, env is second. + let is_active = (lib.r_bindingisactive)(sym, global_env) != R_FALSE; + + if is_active { + entries.push(EnvEntry { + name, + type_label: "function".to_string(), + class_label: "(active binding)".to_string(), + size: None, + has_children: false, + is_active_binding: true, + is_promise: false, + }); + continue; + } + + // Fetch value — safe now that we confirmed it is not an active binding + let value = (lib.rf_findvarinframe)(global_env, sym); + + if value == unbound { + continue; + } + + // PROMSXP = 5: unforced lazy value — don't force it + let type_int = (lib.rf_typeof)(value) as u32; + + if type_int == 5 { + entries.push(EnvEntry { + name, + type_label: "promise".to_string(), + class_label: "(promise)".to_string(), + size: None, + has_children: false, + is_active_binding: false, + is_promise: true, + }); + continue; + } + + // Get explicit class attribute. + // value is rooted through global_env; no alloc between here and string read. + let class_sexp = (lib.rf_getattrib)(value, class_sym); + let explicit_class: Option = if class_sexp != nil { + // class attribute must be a non-empty STRSXP (16) + if (lib.rf_typeof)(class_sexp) as u32 == 16 && (lib.rf_xlength)(class_sexp) > 0 { + let cc = (lib.r_charsxp)((lib.string_elt)(class_sexp, 0)); + if cc.is_null() { + None + } else { + Some(std::ffi::CStr::from_ptr(cc).to_string_lossy().into_owned()) + } + } else { + None + } + } else { + None + }; + + let is_s4 = (lib.rf_iss4)(value) != R_FALSE; + let type_label = typeof_to_type_label(type_int).to_string(); + let class_label = explicit_class + .unwrap_or_else(|| typeof_to_implicit_class(type_int, is_s4).to_string()); + let size = Some((lib.rf_xlength)(value) as i64); + // LISTSXP=2, ENVSXP=4, VECSXP=19 have drillable children + let has_children = matches!(type_int, 2 | 4 | 19) || is_s4; + + entries.push(EnvEntry { + name, + type_label, + class_label, + size, + has_children, + is_active_binding: false, + is_promise: false, + }); + } + + Ok(entries) + } +} + +fn typeof_to_type_label(type_int: u32) -> &'static str { + match type_int { + 0 => "NULL", + 1 => "symbol", + 2 => "pairlist", + 3 => "closure", + 4 => "environment", + 5 => "promise", + 6 => "language", + 7 => "special", + 8 => "builtin", + 10 => "logical", + 13 => "integer", + 14 => "double", + 15 => "complex", + 16 => "character", + 17 => "...", + 19 => "list", + 20 => "expression", + 24 => "raw", + 25 => "S4", + _ => "unknown", + } +} + +fn typeof_to_implicit_class(type_int: u32, is_s4: bool) -> &'static str { + if is_s4 { + return "S4"; + } + match type_int { + 0 => "NULL", + 1 => "name", + 2 => "pairlist", + 3 | 7 | 8 => "function", + 4 => "environment", + 6 => "call", + 10 => "logical", + 13 => "integer", + 14 => "numeric", + 15 => "complex", + 16 => "character", + 19 => "list", + 20 => "expression", + 24 => "raw", + _ => "unknown", + } +} diff --git a/crates/arf-libr/src/functions.rs b/crates/arf-libr/src/functions.rs index ef0828d..f6ce675 100644 --- a/crates/arf-libr/src/functions.rs +++ b/crates/arf-libr/src/functions.rs @@ -131,6 +131,22 @@ pub struct RLibrary { #[cfg(windows)] pub ga_initapp: Option c_int>, + // Workspace snapshot functions + // Rf_findVarInFrame(rho, sym) — look up sym in env rho only (no parent search) + pub rf_findvarinframe: unsafe extern "C" fn(SEXP, SEXP) -> SEXP, + // R_existsVarInFrame(rho, sym) — check existence without fetching + pub r_existsvarinframe: unsafe extern "C" fn(SEXP, SEXP) -> Rboolean, + // R_BindingIsActive(sym, rho) — NOTE: sym first, env second + pub r_bindingisactive: unsafe extern "C" fn(SEXP, SEXP) -> Rboolean, + // R_lsInternal3(env, all_names, sorted) — list binding names + pub r_lsinternal3: unsafe extern "C" fn(SEXP, Rboolean, Rboolean) -> SEXP, + // Rf_getAttrib(x, sym) — get attribute + pub rf_getattrib: unsafe extern "C" fn(SEXP, SEXP) -> SEXP, + // R_ClassSymbol — the "class" attribute symbol (global pointer) + pub r_classsymbol: *mut SEXP, + // Rf_isS4(x) — check if object is S4 + pub rf_iss4: unsafe extern "C" fn(SEXP) -> Rboolean, + // R state variables pub r_interactive: *mut c_int, pub r_signalhandlers: *mut c_int, @@ -342,6 +358,15 @@ impl RLibrary { load_symbol!(rf_definevar, b"Rf_defineVar\0"); load_symbol!(rf_scalarlogical, b"Rf_ScalarLogical\0"); + // Load workspace snapshot functions + load_symbol!(rf_findvarinframe, b"Rf_findVarInFrame\0"); + load_symbol!(r_existsvarinframe, b"R_existsVarInFrame\0"); + load_symbol!(r_bindingisactive, b"R_BindingIsActive\0"); + load_symbol!(r_lsinternal3, b"R_lsInternal3\0"); + load_symbol!(rf_getattrib, b"Rf_getAttrib\0"); + load_ptr!(r_classsymbol, b"R_ClassSymbol\0", SEXP); + load_symbol!(rf_iss4, b"Rf_isS4\0"); + // Load stack limit pointer load_ptr!(r_cstacklimit, b"R_CStackLimit\0", usize); @@ -587,6 +612,13 @@ impl RLibrary { rf_definevar, rf_scalarlogical, r_cstacklimit, + rf_findvarinframe, + r_existsvarinframe, + r_bindingisactive, + r_lsinternal3, + rf_getattrib, + r_classsymbol, + rf_iss4, #[cfg(unix)] ptr_r_readconsole, #[cfg(unix)] @@ -661,3 +693,15 @@ pub fn r_global_env() -> RResult { let lib = r_library()?; unsafe { Ok(*lib.r_globalenv) } } + +/// Get R_UnboundValue. +pub fn r_unbound_value() -> RResult { + let lib = r_library()?; + unsafe { Ok(*lib.r_unboundvalue) } +} + +/// Get R_ClassSymbol. +pub fn r_class_symbol() -> RResult { + let lib = r_library()?; + unsafe { Ok(*lib.r_classsymbol) } +} diff --git a/crates/arf-libr/src/lib.rs b/crates/arf-libr/src/lib.rs index 3aae062..3f36970 100644 --- a/crates/arf-libr/src/lib.rs +++ b/crates/arf-libr/src/lib.rs @@ -13,7 +13,9 @@ mod sys; pub use error::{RError, RResult}; // functions -pub use functions::{RLibrary, init_r_library, r_global_env, r_library, r_nil_value}; +pub use functions::{ + RLibrary, init_r_library, r_class_symbol, r_global_env, r_library, r_nil_value, r_unbound_value, +}; // types pub use types::{ diff --git a/crates/arf-libr/src/sys.rs b/crates/arf-libr/src/sys.rs index 570c386..2478848 100644 --- a/crates/arf-libr/src/sys.rs +++ b/crates/arf-libr/src/sys.rs @@ -1718,7 +1718,7 @@ local({ .arf_error_state <- new.env(parent = emptyenv()) .arf_error_state$had_error <- FALSE - # Store it in global environment for persistence + # Store in globalenv with dot prefix so ls() hides it by default assign(".arf_error_state", .arf_error_state, envir = globalenv()) # Store the user's previous error handler (if any) so we can chain to it @@ -1745,9 +1745,6 @@ local({ /// Check if the R error state indicates an error occurred. /// -/// This reads `.arf_error_state$had_error` from the global environment. -/// The globalCallingHandlers error handler sets this to TRUE when an error occurs. -/// /// # Safety /// R must be initialized and the global error handler must be set up /// before this function returns meaningful results. @@ -1763,14 +1760,13 @@ fn check_r_error_state() -> bool { }; unsafe { - // Look up .arf_error_state in global environment using Rf_findVar let arf_error_state_sym = { let name = std::ffi::CString::new(".arf_error_state").unwrap(); (lib.rf_install)(name.as_ptr()) }; let global_env = *lib.r_globalenv; - let state_env = (lib.rf_findvar)(arf_error_state_sym, global_env); + let state_env = (lib.rf_findvarinframe)(global_env, arf_error_state_sym); // Check if the environment exists if state_env.is_null() || state_env == *lib.r_unboundvalue { @@ -1819,14 +1815,13 @@ fn reset_r_error_state() { }; unsafe { - // Look up .arf_error_state in global environment let arf_error_state_sym = { let name = std::ffi::CString::new(".arf_error_state").unwrap(); (lib.rf_install)(name.as_ptr()) }; let global_env = *lib.r_globalenv; - let state_env = (lib.rf_findvar)(arf_error_state_sym, global_env); + let state_env = (lib.rf_findvarinframe)(global_env, arf_error_state_sym); // If the environment doesn't exist, nothing to reset if state_env.is_null() || state_env == *lib.r_unboundvalue {