Skip to main content

r/interpreter/
call.rs

1//! Call-dispatch state and helpers shared across evaluator and builtin code.
2
3use std::collections::HashSet;
4
5use smallvec::SmallVec;
6
7use crate::interpreter::environment::Environment;
8use crate::interpreter::value::RValue;
9use crate::interpreter::{DiagnosticStyle, Interpreter};
10use crate::parser::ast::Expr;
11
12/// Convert a byte offset in source text to a 1-based line number.
13fn byte_offset_to_line(source: &str, offset: usize) -> usize {
14    source[..offset.min(source.len())]
15        .bytes()
16        .filter(|&b| b == b'\n')
17        .count()
18        + 1
19}
20
21/// Raw native backtrace captured from a C error (Rf_error).
22/// Contains instruction pointer addresses from the C stack at the point
23/// of the error, before longjmp destroyed the frames.
24#[derive(Debug, Clone, Default)]
25pub struct NativeBacktrace {
26    /// Raw instruction pointer addresses from the C stack.
27    pub frames: Vec<usize>,
28}
29
30/// A single entry in a stack trace — a snapshot of one call frame.
31#[derive(Debug, Clone)]
32pub struct TraceEntry {
33    /// The call expression (e.g., `f(x, y)`). None for anonymous calls.
34    pub call: Option<Expr>,
35    /// Native backtrace if this frame was a .Call/.C that errored in C code.
36    pub native_backtrace: Option<NativeBacktrace>,
37    /// True if this is a C→R boundary (C code called Rf_eval back into R).
38    pub is_native_boundary: bool,
39    /// Source file and text for resolving span → file:line (captured at traceback time).
40    pub source_context: Option<(String, String)>,
41}
42
43pub(crate) fn retarget_call_expr(call_expr: Option<Expr>, target: &str) -> Option<Expr> {
44    match call_expr {
45        Some(Expr::Call { args, .. }) => Some(Expr::Call {
46            func: Box::new(Expr::Symbol(target.to_string())),
47            args,
48            span: None,
49        }),
50        _ => None,
51    }
52}
53
54/// Context for S3 method dispatch — tracks which class was dispatched and the
55/// remaining classes in the chain (for NextMethod).
56#[derive(Debug, Clone)]
57pub(crate) struct S3DispatchContext {
58    pub generic: String,
59    pub classes: Vec<String>,
60    pub class_index: usize,
61    pub object: RValue,
62}
63
64#[derive(Debug, Clone)]
65pub(crate) struct CallFrame {
66    pub call: Option<Expr>,
67    pub function: RValue,
68    pub env: Environment,
69    pub formal_args: HashSet<String>,
70    pub supplied_args: HashSet<String>,
71    pub supplied_positional: SmallVec<[RValue; 4]>,
72    pub supplied_named: SmallVec<[(String, RValue); 2]>,
73    pub supplied_arg_count: usize,
74    /// True if this frame is a synthetic boundary marker for a C→R callback
75    /// (e.g., C code called Rf_eval which re-entered the R interpreter).
76    pub is_native_boundary: bool,
77}
78
79#[derive(Clone, Copy)]
80pub struct BuiltinContext<'a> {
81    interpreter: &'a Interpreter,
82    env: &'a Environment,
83}
84
85impl<'a> BuiltinContext<'a> {
86    pub(crate) fn new(interpreter: &'a Interpreter, env: &'a Environment) -> Self {
87        Self { interpreter, env }
88    }
89
90    pub fn env(&self) -> &'a Environment {
91        self.env
92    }
93
94    pub fn interpreter(&self) -> &'a Interpreter {
95        self.interpreter
96    }
97
98    pub fn with_interpreter<F, R>(&self, f: F) -> R
99    where
100        F: FnOnce(&Interpreter) -> R,
101    {
102        f(self.interpreter)
103    }
104
105    /// Write a message to the interpreter's stdout writer.
106    pub fn write(&self, msg: &str) {
107        self.interpreter.write_stdout(msg);
108    }
109
110    /// Write a message to the interpreter's stderr writer.
111    pub fn write_err(&self, msg: &str) {
112        self.interpreter.write_stderr(msg);
113    }
114
115    /// Write a colored diagnostic message to the interpreter's stderr writer.
116    ///
117    /// When the `color` feature is enabled and stderr is a terminal, the
118    /// message is written with the color corresponding to the given style.
119    /// Otherwise, falls back to plain uncolored text.
120    pub fn write_err_colored(&self, msg: &str, style: DiagnosticStyle) {
121        self.interpreter.write_stderr_colored(msg, style);
122    }
123}
124
125impl Interpreter {
126    pub(crate) fn current_call_frame(&self) -> Option<CallFrame> {
127        self.call_stack.borrow().last().cloned()
128    }
129
130    pub(crate) fn call_frame(&self, which: usize) -> Option<CallFrame> {
131        self.call_stack
132            .borrow()
133            .get(which.saturating_sub(1))
134            .cloned()
135    }
136
137    pub(crate) fn call_frames(&self) -> Vec<CallFrame> {
138        self.call_stack.borrow().clone()
139    }
140
141    pub(crate) fn current_call_expr(&self) -> Option<Expr> {
142        self.current_call_frame().and_then(|frame| frame.call)
143    }
144
145    /// Snapshot the current call stack into `last_traceback`.
146    ///
147    /// Within the same top-level `eval()` (same generation), only the deepest
148    /// trace is kept (the first capture wins as the error bubbles up and frames
149    /// are popped). Across different evals, a new error always replaces the old
150    /// traceback, matching R's `traceback()` behavior.
151    pub(crate) fn capture_traceback(&self) {
152        let current_gen = self.traceback_generation.get();
153        let captured_gen = self.traceback_captured_generation.get();
154        let mut tb = self.last_traceback.borrow_mut();
155
156        // Same eval: only keep the deepest (first) capture
157        if current_gen == captured_gen && !tb.is_empty() {
158            return;
159        }
160        self.traceback_captured_generation.set(current_gen);
161        // Consume any pending native backtrace (set by dot_call/dot_c on C error).
162        #[cfg(feature = "native")]
163        let pending_native = self.pending_native_backtrace.borrow_mut().take();
164
165        // Snapshot current source context for span resolution
166        let source_ctx = self.source_stack.borrow().last().cloned();
167
168        let frames = self.call_stack.borrow();
169        let len = frames.len();
170        *tb = frames
171            .iter()
172            .enumerate()
173            .map(|(i, f)| {
174                let native_backtrace = {
175                    #[cfg(feature = "native")]
176                    {
177                        // Attach to the innermost (last) frame
178                        if i == len - 1 {
179                            pending_native.clone()
180                        } else {
181                            None
182                        }
183                    }
184                    #[cfg(not(feature = "native"))]
185                    {
186                        let _ = i;
187                        None
188                    }
189                };
190                TraceEntry {
191                    call: f.call.clone(),
192                    native_backtrace,
193                    is_native_boundary: f.is_native_boundary,
194                    source_context: source_ctx.clone(),
195                }
196            })
197            .collect();
198    }
199
200    /// Format the last traceback for display, R-style (deepest frame first).
201    /// Returns `None` if there is no traceback.
202    pub fn format_traceback(&self) -> Option<String> {
203        use crate::interpreter::value::deparse_expr;
204
205        let tb = self.last_traceback.borrow();
206        if tb.is_empty() {
207            return None;
208        }
209        let mut lines = Vec::with_capacity(tb.len());
210        for (i, entry) in tb.iter().enumerate().rev() {
211            if entry.is_native_boundary {
212                lines.push("   --- entered native code (Rf_eval callback) ---".to_string());
213                continue;
214            }
215            let call_str = match &entry.call {
216                Some(expr) => deparse_expr(expr),
217                None => "<anonymous>".to_string(),
218            };
219
220            // Resolve span to file:line if available
221            let location = entry
222                .call
223                .as_ref()
224                .and_then(|expr| match expr {
225                    Expr::Call {
226                        span: Some(span), ..
227                    } => Some(span),
228                    _ => None,
229                })
230                .and_then(|span| {
231                    let (filename, source_text) = entry.source_context.as_ref()?;
232                    let line = byte_offset_to_line(source_text, span.start as usize);
233                    Some(format!(" at {}:{}", filename, line))
234                })
235                .unwrap_or_default();
236
237            lines.push(format!("{}: {}{}", i + 1, call_str, location));
238
239            // Append resolved native frames if this entry has a C backtrace
240            #[cfg(feature = "native")]
241            if let Some(ref bt) = entry.native_backtrace {
242                if !bt.frames.is_empty() {
243                    let resolved = crate::interpreter::native::stacktrace::resolve_native_backtrace(
244                        &bt.frames,
245                    );
246                    if !resolved.is_empty() {
247                        lines.push(
248                            crate::interpreter::native::stacktrace::format_native_frames(&resolved),
249                        );
250                    }
251                }
252            }
253        }
254        Some(lines.join("\n"))
255    }
256}