Skip to main content

r/interpreter/
s3.rs

1//! S3 method dispatch helpers for UseMethod/NextMethod and class-based lookup.
2
3use tracing::debug;
4
5use crate::interpreter::call::{retarget_call_expr, S3DispatchContext};
6use crate::interpreter::environment::Environment;
7use crate::interpreter::value::{RError, RFlow, RFunction, RSignal, RValue, Vector};
8use crate::interpreter::Interpreter;
9use crate::parser::ast::{Arg, Expr};
10
11struct S3MethodCall<'a> {
12    method: &'a RValue,
13    method_name: &'a str,
14    generic: &'a str,
15    classes: &'a [String],
16    class_index: usize,
17    dispatch_object: RValue,
18    positional: &'a [RValue],
19    named: &'a [(String, RValue)],
20    env: &'a Environment,
21    call_expr: Option<Expr>,
22}
23
24impl Interpreter {
25    pub(crate) fn dispatch_next_method(
26        &self,
27        positional: &[RValue],
28        named: &[(String, RValue)],
29        env: &Environment,
30    ) -> Result<RValue, RFlow> {
31        let ctx = self
32            .s3_dispatch_stack
33            .borrow()
34            .last()
35            .cloned()
36            .ok_or_else(|| {
37                RError::other("NextMethod called outside of a method dispatch".to_string())
38            })?;
39
40        let args: Vec<RValue> = if positional.is_empty() {
41            vec![ctx.object.clone()]
42        } else {
43            positional.to_vec()
44        };
45
46        // Look up in environment chain first, then the S3 method registry
47        for i in (ctx.class_index + 1)..ctx.classes.len() {
48            let method_name = format!("{}.{}", ctx.generic, ctx.classes[i]);
49            let method = env
50                .get(&method_name)
51                .or_else(|| self.lookup_s3_method(&ctx.generic, &ctx.classes[i]));
52            if let Some(method) = method {
53                return self.call_s3_method(S3MethodCall {
54                    method: &method,
55                    method_name: &method_name,
56                    generic: &ctx.generic,
57                    classes: &ctx.classes,
58                    class_index: i,
59                    dispatch_object: args.first().cloned().unwrap_or(RValue::Null),
60                    positional: &args,
61                    named,
62                    env,
63                    call_expr: self.current_call_expr(),
64                });
65            }
66        }
67
68        let default_name = format!("{}.default", ctx.generic);
69        let default_method = env
70            .get(&default_name)
71            .or_else(|| self.lookup_s3_method(&ctx.generic, "default"));
72        if let Some(method) = default_method {
73            return self.call_s3_method(S3MethodCall {
74                method: &method,
75                method_name: &default_name,
76                generic: &ctx.generic,
77                classes: &ctx.classes,
78                class_index: ctx.classes.len(),
79                dispatch_object: args.first().cloned().unwrap_or(RValue::Null),
80                positional: &args,
81                named,
82                env,
83                call_expr: self.current_call_expr(),
84            });
85        }
86
87        Err(RError::other(format!("no more methods to dispatch for '{}'", ctx.generic)).into())
88    }
89
90    pub(crate) fn eval_use_method(&self, args: &[Arg], env: &Environment) -> Result<RValue, RFlow> {
91        let frame = self.current_call_frame().ok_or_else(|| {
92            RError::other("'UseMethod' used in an inappropriate fashion".to_string())
93        })?;
94
95        let generic_expr = match args
96            .iter()
97            .find(|arg| arg.name.as_deref() == Some("generic"))
98            .or_else(|| args.first())
99            .and_then(|arg| arg.value.as_ref())
100        {
101            Some(expr) => expr,
102            None => {
103                return Err(RError::other("there must be a 'generic' argument".to_string()).into());
104            }
105        };
106
107        let generic = self.eval_generic_name(generic_expr, env)?;
108
109        let object_expr = args
110            .iter()
111            .find(|arg| arg.name.as_deref() == Some("object"))
112            .or_else(|| args.get(1))
113            .and_then(|arg| arg.value.as_ref());
114
115        let dispatch_object = match object_expr {
116            Some(expr) => Some(self.eval_in(expr, env)?),
117            None => {
118                // Force the default object (first param) — it may be a promise
119                let obj = self.default_use_method_object(&frame)?;
120                match obj {
121                    Some(val) => Some(self.force_value(val)?),
122                    None => None,
123                }
124            }
125        };
126
127        let value = self.dispatch_s3(
128            &generic,
129            &frame.supplied_positional,
130            &frame.supplied_named,
131            dispatch_object,
132            env,
133            frame.call.clone(),
134        )?;
135
136        Err(RSignal::Return(value).into())
137    }
138
139    fn eval_generic_name(&self, generic_expr: &Expr, env: &Environment) -> Result<String, RFlow> {
140        let generic_value = self.eval_in(generic_expr, env)?;
141        match generic_value {
142            RValue::Vector(rv) => match &rv.inner {
143                Vector::Character(values) if values.len() == 1 => {
144                    values.first().cloned().flatten().ok_or_else(|| {
145                        RError::other("'generic' argument must be a character string".to_string())
146                            .into()
147                    })
148                }
149                _ => Err(RError::other(
150                    "'generic' argument must be a character string".to_string(),
151                )
152                .into()),
153            },
154            _ => Err(
155                RError::other("'generic' argument must be a character string".to_string()).into(),
156            ),
157        }
158    }
159
160    fn default_use_method_object(
161        &self,
162        frame: &crate::interpreter::call::CallFrame,
163    ) -> Result<Option<RValue>, RFlow> {
164        match &frame.function {
165            RValue::Function(RFunction::Closure { params, .. }) => match params.first() {
166                Some(param) if param.is_dots => {
167                    Ok(frame.env.get("...").and_then(|value| match value {
168                        RValue::List(list) => list.values.first().map(|(_, value)| value.clone()),
169                        _ => None,
170                    }))
171                }
172                Some(param) => Ok(frame.env.get(&param.name)),
173                None => Ok(None),
174            },
175            _ => Err(
176                RError::other("'UseMethod' used in an inappropriate fashion".to_string()).into(),
177            ),
178        }
179    }
180
181    /// S3 method dispatch: look up generic.class in the environment chain,
182    /// then fall back to the per-interpreter S3 method registry (populated
183    /// by S3method() directives in NAMESPACE files).
184    #[tracing::instrument(
185        level = "debug",
186        skip(self, positional, named, dispatch_object, env, call_expr)
187    )]
188    fn dispatch_s3(
189        &self,
190        generic: &str,
191        positional: &[RValue],
192        named: &[(String, RValue)],
193        dispatch_object: Option<RValue>,
194        env: &Environment,
195        call_expr: Option<Expr>,
196    ) -> Result<RValue, RFlow> {
197        let raw_dispatch =
198            dispatch_object.unwrap_or_else(|| positional.first().cloned().unwrap_or(RValue::Null));
199        // Force promises so we can inspect the object's class
200        let dispatch_object = self.force_value(raw_dispatch)?;
201        let classes = self.s3_classes_for(&dispatch_object);
202
203        // First pass: look up generic.class in the environment chain
204        for (i, class) in classes.iter().enumerate() {
205            let method_name = format!("{}.{}", generic, class);
206            if let Some(method) = env.get(&method_name) {
207                debug!(
208                    generic,
209                    method = method_name.as_str(),
210                    "S3 dispatch resolved"
211                );
212                return self.call_s3_method(S3MethodCall {
213                    method: &method,
214                    method_name: &method_name,
215                    generic,
216                    classes: &classes,
217                    class_index: i,
218                    dispatch_object: dispatch_object.clone(),
219                    positional,
220                    named,
221                    env,
222                    call_expr: call_expr.clone(),
223                });
224            }
225        }
226
227        // Second pass: check the per-interpreter S3 method registry
228        for (i, class) in classes.iter().enumerate() {
229            if let Some(method) = self.lookup_s3_method(generic, class) {
230                let method_name = format!("{}.{}", generic, class);
231                debug!(
232                    generic,
233                    method = method_name.as_str(),
234                    "S3 dispatch resolved (registry)"
235                );
236                return self.call_s3_method(S3MethodCall {
237                    method: &method,
238                    method_name: &method_name,
239                    generic,
240                    classes: &classes,
241                    class_index: i,
242                    dispatch_object: dispatch_object.clone(),
243                    positional,
244                    named,
245                    env,
246                    call_expr: call_expr.clone(),
247                });
248            }
249        }
250
251        // Fall back to generic.default in the environment chain
252        let default_name = format!("{}.default", generic);
253        if let Some(method) = env.get(&default_name) {
254            debug!(
255                generic,
256                method = default_name.as_str(),
257                "S3 dispatch resolved (default)"
258            );
259            return self.call_s3_method(S3MethodCall {
260                method: &method,
261                method_name: &default_name,
262                generic,
263                classes: &classes,
264                class_index: classes.len(),
265                dispatch_object: dispatch_object.clone(),
266                positional,
267                named,
268                env,
269                call_expr: call_expr.clone(),
270            });
271        }
272
273        // Fall back to generic.default in the registry
274        if let Some(method) = self.lookup_s3_method(generic, "default") {
275            debug!(
276                generic,
277                method = default_name.as_str(),
278                "S3 dispatch resolved (registry default)"
279            );
280            return self.call_s3_method(S3MethodCall {
281                method: &method,
282                method_name: &default_name,
283                generic,
284                classes: &classes,
285                class_index: classes.len(),
286                dispatch_object: dispatch_object.clone(),
287                positional,
288                named,
289                env,
290                call_expr,
291            });
292        }
293
294        debug!(generic, ?classes, "S3 dispatch failed: no method found");
295        Err(RError::other(format!(
296            "no applicable method for '{}' applied to an object of class \"{}\"",
297            generic,
298            classes.first().unwrap_or(&"unknown".to_string())
299        ))
300        .into())
301    }
302
303    pub(crate) fn s3_classes_for(&self, dispatch_object: &RValue) -> Vec<String> {
304        match dispatch_object {
305            RValue::List(list) => {
306                if let Some(RValue::Vector(rv)) = list.get_attr("class") {
307                    if let Vector::Character(classes) = &rv.inner {
308                        classes
309                            .iter()
310                            .filter_map(|class| class.clone())
311                            .collect::<Vec<_>>()
312                    } else {
313                        vec!["list".to_string()]
314                    }
315                } else {
316                    vec!["list".to_string()]
317                }
318            }
319            RValue::Vector(rv) => rv.class().unwrap_or_else(|| match &rv.inner {
320                Vector::Raw(_) => vec!["raw".to_string()],
321                Vector::Logical(_) => vec!["logical".to_string()],
322                Vector::Integer(_) => vec!["integer".to_string()],
323                Vector::Double(_) => vec!["numeric".to_string()],
324                Vector::Complex(_) => vec!["complex".to_string()],
325                Vector::Character(_) => vec!["character".to_string()],
326            }),
327            RValue::Function(_) => vec!["function".to_string()],
328            RValue::Null => vec!["NULL".to_string()],
329            RValue::Language(lang) => lang.class().unwrap_or_default(),
330            _ => vec![],
331        }
332    }
333
334    fn call_s3_method(&self, dispatch: S3MethodCall<'_>) -> Result<RValue, RFlow> {
335        let ctx = S3DispatchContext {
336            generic: dispatch.generic.to_string(),
337            classes: dispatch.classes.to_vec(),
338            class_index: dispatch.class_index,
339            object: dispatch.dispatch_object,
340        };
341        self.s3_dispatch_stack.borrow_mut().push(ctx);
342        let method_call = retarget_call_expr(dispatch.call_expr, dispatch.method_name);
343        let result = self.call_function_with_call(
344            dispatch.method,
345            dispatch.positional,
346            dispatch.named,
347            dispatch.env,
348            method_call,
349        );
350        self.s3_dispatch_stack.borrow_mut().pop();
351        result
352    }
353}