Skip to main content

r/
session.rs

1use std::cell::RefCell;
2use std::fmt;
3use std::fs;
4use std::path::Path;
5use std::sync::atomic::AtomicBool;
6use std::sync::Arc;
7
8use tracing::info;
9
10use crate::interpreter::value::{RFlow, RValue};
11use crate::interpreter::{with_interpreter_state, Interpreter};
12use crate::parser::ast::Expr;
13use crate::parser::{parse_program, ParseError};
14
15#[derive(Debug)]
16pub struct EvalOutput {
17    pub value: RValue,
18    pub visible: bool,
19}
20
21#[derive(Debug)]
22pub enum SessionError {
23    Parse(Box<ParseError>),
24    Runtime(RFlow),
25    CannotRead {
26        path: String,
27        source: std::io::Error,
28    },
29}
30
31impl SessionError {
32    /// Render the error as a string. When the `diagnostics` feature is enabled,
33    /// parse errors are rendered using miette's graphical report handler with
34    /// source spans, colors, and suggestions.
35    pub fn render(&self) -> String {
36        match self {
37            SessionError::Parse(err) => err.render(),
38            other => format!("{}", other),
39        }
40    }
41}
42
43impl fmt::Display for SessionError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            SessionError::Parse(err) => write!(f, "{}", err),
47            SessionError::Runtime(err) => write!(f, "{}", err),
48            SessionError::CannotRead { path, source } => {
49                write!(f, "Error reading file '{}': {}", path, source)
50            }
51        }
52    }
53}
54
55impl std::error::Error for SessionError {
56    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
57        match self {
58            SessionError::CannotRead { source, .. } => Some(source),
59            SessionError::Parse(_) | SessionError::Runtime(_) => None,
60        }
61    }
62}
63
64/// A `Write` adapter backed by a shared `Arc<Mutex<Vec<u8>>>` so that both
65/// the interpreter and the session can access the accumulated bytes.
66struct SharedBuf(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
67
68impl std::io::Write for SharedBuf {
69    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
70        let mut guard = self.0.lock().unwrap_or_else(|e| e.into_inner());
71        guard.extend_from_slice(buf);
72        Ok(buf.len())
73    }
74    fn flush(&mut self) -> std::io::Result<()> {
75        Ok(())
76    }
77}
78
79pub struct Session {
80    interpreter: Interpreter,
81    /// Shared stdout capture buffer (only set for captured-output sessions).
82    captured_stdout: Option<std::sync::Arc<std::sync::Mutex<Vec<u8>>>>,
83    /// Shared stderr capture buffer (only set for captured-output sessions).
84    captured_stderr: Option<std::sync::Arc<std::sync::Mutex<Vec<u8>>>>,
85}
86
87impl Default for Session {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl Session {
94    pub fn new() -> Self {
95        Session {
96            interpreter: Interpreter::new(),
97            captured_stdout: None,
98            captured_stderr: None,
99        }
100    }
101
102    /// Create a session that captures stdout and stderr into in-memory buffers
103    /// instead of writing to the process streams. Use `captured_stdout()` and
104    /// `captured_stderr()` to retrieve the accumulated output.
105    pub fn new_with_captured_output() -> Self {
106        let mut interp = Interpreter::new();
107        let stdout_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
108        let stderr_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
109        interp.stdout = RefCell::new(Box::new(SharedBuf(stdout_buf.clone())));
110        interp.stderr = RefCell::new(Box::new(SharedBuf(stderr_buf.clone())));
111        interp.set_color_stderr(false);
112        Session {
113            interpreter: interp,
114            captured_stdout: Some(stdout_buf),
115            captured_stderr: Some(stderr_buf),
116        }
117    }
118
119    /// Return all output written to the interpreter's stdout writer so far.
120    ///
121    /// Only meaningful when the session was created with `new_with_captured_output()`.
122    /// For sessions using real stdio this will return an empty string.
123    pub fn captured_stdout(&self) -> String {
124        match &self.captured_stdout {
125            Some(buf) => {
126                let guard = buf.lock().unwrap_or_else(|e| e.into_inner());
127                String::from_utf8_lossy(&guard).into_owned()
128            }
129            None => String::new(),
130        }
131    }
132
133    /// Return all output written to the interpreter's stderr writer so far.
134    ///
135    /// Only meaningful when the session was created with `new_with_captured_output()`.
136    pub fn captured_stderr(&self) -> String {
137        match &self.captured_stderr {
138            Some(buf) => {
139                let guard = buf.lock().unwrap_or_else(|e| e.into_inner());
140                String::from_utf8_lossy(&guard).into_owned()
141            }
142            None => String::new(),
143        }
144    }
145
146    pub fn eval_expr(&mut self, expr: &Expr) -> Result<EvalOutput, SessionError> {
147        // Reset the invisible flag before evaluation so we can detect
148        // whether invisible() was called during this eval.
149        self.interpreter.last_value_invisible.set(false);
150        let value = with_interpreter_state(&mut self.interpreter, |interp| interp.eval(expr))
151            .map_err(SessionError::Runtime)?;
152        // Check both the runtime flag (set by invisible()) and the syntactic test
153        let runtime_invisible = self.interpreter.take_invisible();
154        let syntactic_invisible = is_invisible_result(expr);
155        Ok(EvalOutput {
156            visible: !runtime_invisible && !syntactic_invisible,
157            value,
158        })
159    }
160
161    pub fn eval_source(&mut self, source: &str) -> Result<EvalOutput, SessionError> {
162        let ast = parse_program(source).map_err(SessionError::Parse)?;
163        self.eval_expr(&ast)
164    }
165
166    /// Auto-print a value by calling print() through the interpreter.
167    /// This dispatches to S3 methods (print.matrix, print.data.frame, etc.)
168    /// unlike Display which just formats as a flat vector.
169    pub fn auto_print(&mut self, value: &crate::interpreter::value::RValue) {
170        use crate::interpreter::value::RValue;
171        if matches!(value, RValue::Null) {
172            // Print "NULL" like GNU R does for visible NULL results
173            self.interpreter.write_stdout("NULL\n");
174            return;
175        }
176        let print_code = "print(.miniR.auto_print_value)";
177        // Temporarily bind the value in the global env so print() can access it
178        self.interpreter
179            .global_env
180            .set(".miniR.auto_print_value".to_string(), value.clone());
181        self.eval_source(print_code).ok();
182        self.interpreter
183            .global_env
184            .remove(".miniR.auto_print_value");
185    }
186
187    pub fn eval_file(&mut self, path: impl AsRef<Path>) -> Result<EvalOutput, SessionError> {
188        let path = path.as_ref();
189        info!(path = %path.display(), "loading source file");
190        let source = read_source(path)?;
191        let ast = match parse_program(&source) {
192            Ok(ast) => ast,
193            Err(mut err) => {
194                err.filename = Some(path.display().to_string());
195                return Err(SessionError::Parse(err));
196            }
197        };
198        self.interpreter
199            .source_stack
200            .borrow_mut()
201            .push((path.display().to_string(), source));
202        let result = self.eval_expr(&ast);
203        self.interpreter.source_stack.borrow_mut().pop();
204        result
205    }
206
207    pub fn interpreter(&self) -> &Interpreter {
208        &self.interpreter
209    }
210
211    /// Format the last error's traceback for display, or `None` if there is none.
212    pub fn format_last_traceback(&self) -> Option<String> {
213        self.interpreter.format_traceback()
214    }
215
216    /// Render an error with its traceback (if any).
217    pub fn render_error(&self, err: &SessionError) -> String {
218        let base = err.render();
219        if matches!(err, SessionError::Runtime(_)) {
220            if let Some(tb) = self.interpreter.format_traceback() {
221                return format!("{}\nTraceback (most recent call last):\n{}\n", base, tb);
222            }
223        }
224        base
225    }
226
227    /// Install a plot sender channel so builtins can send plots to the GUI thread.
228    #[cfg(feature = "plot")]
229    pub fn set_plot_sender(&self, tx: crate::interpreter::graphics::egui_device::PlotSender) {
230        *self.interpreter.plot_tx.borrow_mut() = Some(tx);
231    }
232
233    /// Set a per-interpreter R option (same effect as `options(name = value)` in R).
234    pub fn set_option(&self, name: &str, value: RValue) {
235        self.interpreter
236            .options
237            .borrow_mut()
238            .insert(name.to_string(), value);
239    }
240
241    /// Update `getOption("width")` to match the current terminal width.
242    ///
243    /// Uses crossterm when the `repl` feature is enabled (most accurate, since
244    /// crossterm is already linked for the REPL). Falls back to the lighter
245    /// `terminal_size` crate when only the `terminal-size` feature is enabled.
246    /// Returns 80 columns when neither is available.
247    pub fn sync_terminal_width(&self) {
248        let cols = detect_terminal_width();
249        self.set_option(
250            "width",
251            RValue::vec(crate::interpreter::value::Vector::Integer(
252                vec![Some(cols)].into(),
253            )),
254        );
255    }
256
257    /// Generate `.Rd` documentation files for all documented builtins.
258    ///
259    /// Creates the output directory if it doesn't exist, then writes one `.Rd`
260    /// file per primary builtin name. Returns the number of files written.
261    pub fn generate_rd_docs(dir: &Path) -> Result<usize, std::io::Error> {
262        crate::interpreter::builtins::generate_rd_docs(dir)
263    }
264
265    /// Return a clone of the interpreter's interrupt flag.
266    /// The caller (or a signal handler) can set it to `true` to interrupt
267    /// the current computation.
268    pub fn interrupt_flag(&self) -> Arc<AtomicBool> {
269        self.interpreter.interrupt_flag()
270    }
271
272    /// Register a SIGINT handler that sets the interpreter's interrupt flag
273    /// instead of killing the process. Returns `Ok(())` on success.
274    ///
275    /// This should be called once at startup (e.g. before entering the REPL).
276    /// On platforms where SIGINT is not available this is a no-op.
277    #[cfg(feature = "signal")]
278    pub fn install_signal_handler(&self) -> std::io::Result<()> {
279        #[cfg(unix)]
280        {
281            use signal_hook::consts::SIGINT;
282            signal_hook::flag::register(SIGINT, self.interrupt_flag())?;
283        }
284        Ok(())
285    }
286
287    /// No-op stub when signal-hook is not available.
288    #[cfg(not(feature = "signal"))]
289    pub fn install_signal_handler(&self) -> std::io::Result<()> {
290        Ok(())
291    }
292}
293
294pub fn is_invisible_result(ast: &Expr) -> bool {
295    match ast {
296        Expr::Assign { .. } => true,
297        Expr::For { .. } => true,
298        Expr::While { .. } => true,
299        Expr::Repeat { .. } => true,
300        Expr::Call { func, .. } => {
301            matches!(func.as_ref(), Expr::Symbol(name) if name == "invisible")
302        }
303        Expr::Program(exprs) => exprs.last().is_some_and(is_invisible_result),
304        Expr::Block(exprs) => exprs.last().is_some_and(is_invisible_result),
305        _ => false,
306    }
307}
308
309/// Detect the terminal width using the best available backend.
310///
311/// Priority: crossterm (repl feature) > terminal_size crate > fallback to 80.
312fn detect_terminal_width() -> i64 {
313    // When the repl feature is on, crossterm is already linked — prefer it.
314    #[cfg(feature = "repl")]
315    {
316        if let Ok((cols, _)) = crossterm::terminal::size() {
317            return i64::from(cols).clamp(10, 10000);
318        }
319    }
320
321    // Fallback: the lightweight terminal_size crate (available via diagnostics/miette).
322    #[cfg(feature = "terminal-size")]
323    {
324        if let Some((terminal_size::Width(w), _)) = terminal_size::terminal_size() {
325            return i64::from(w).clamp(10, 10000);
326        }
327    }
328
329    // No terminal detection available — use a sensible default.
330    80
331}
332
333fn read_source(path: &Path) -> Result<String, SessionError> {
334    match fs::read_to_string(path) {
335        Ok(source) => Ok(source),
336        Err(source_err) if source_err.kind() == std::io::ErrorKind::InvalidData => fs::read(path)
337            .map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
338            .map_err(|source| SessionError::CannotRead {
339                path: path.display().to_string(),
340                source,
341            }),
342        Err(source) => Err(SessionError::CannotRead {
343            path: path.display().to_string(),
344            source,
345        }),
346    }
347}