Skip to main content

r/interpreter/
call_eval.rs

1//! Call evaluation and dispatch helpers used by the evaluator.
2
3use smallvec::SmallVec;
4use tracing::trace;
5
6use crate::interpreter::environment::Environment;
7use crate::interpreter::value::*;
8use crate::interpreter::{BuiltinContext, Interpreter};
9use crate::parser::ast::{Arg, Expr, Param};
10
11type PositionalArgs = SmallVec<[RValue; 4]>;
12
13type NamedArgs = SmallVec<[(String, RValue); 2]>;
14
15type EvaluatedCallArgs = (PositionalArgs, NamedArgs);
16
17struct ClosureCall<'a> {
18    func: &'a RValue,
19    params: &'a [Param],
20    body: &'a Expr,
21    closure_env: &'a Environment,
22    positional: &'a [RValue],
23    named: &'a [(String, RValue)],
24    call_expr: Option<Expr>,
25}
26
27impl Interpreter {
28    pub(super) fn eval_call(
29        &self,
30        func: &Expr,
31        args: &[Arg],
32        span: Option<crate::parser::ast::Span>,
33        env: &Environment,
34    ) -> Result<RValue, RFlow> {
35        eval_call(self, func, args, span, env)
36    }
37
38    pub fn call_function(
39        &self,
40        func: &RValue,
41        positional: &[RValue],
42        named: &[(String, RValue)],
43        env: &Environment,
44    ) -> Result<RValue, RFlow> {
45        call_function(self, func, positional, named, env)
46    }
47
48    pub(crate) fn call_function_with_call(
49        &self,
50        func: &RValue,
51        positional: &[RValue],
52        named: &[(String, RValue)],
53        env: &Environment,
54        call_expr: Option<Expr>,
55    ) -> Result<RValue, RFlow> {
56        call_function_with_call(self, func, positional, named, env, call_expr)
57    }
58}
59
60pub(super) fn eval_call(
61    interp: &Interpreter,
62    func: &Expr,
63    args: &[Arg],
64    span: Option<crate::parser::ast::Span>,
65    env: &Environment,
66) -> Result<RValue, RFlow> {
67    trace!(func = %expr_name(func), nargs = args.len(), "calling function");
68    let function = resolve_callable_for_call(interp, func, env)?;
69    let call_expr = Expr::Call {
70        func: Box::new(func.clone()),
71        args: args.to_vec(),
72        span,
73    };
74
75    if let Some(result) = try_call_special_builtin(interp, &function, args, env)? {
76        return Ok(result);
77    }
78
79    // For closures, create lazy promises instead of eagerly evaluating args.
80    // This implements R's call-by-need semantics: arguments are only evaluated
81    // when their value is first accessed.
82    if let RValue::Function(RFunction::Closure {
83        params,
84        body,
85        env: closure_env,
86    }) = &function
87    {
88        let (positional, named) = create_promise_arguments(args, env);
89        return call_closure_lazy(
90            interp,
91            params,
92            body,
93            closure_env,
94            &function,
95            &positional,
96            &named,
97            call_expr,
98        );
99    }
100
101    let (positional, named) = evaluate_call_arguments(interp, args, env)?;
102    call_function_with_call(interp, &function, &positional, &named, env, Some(call_expr))
103}
104
105pub(super) fn call_function(
106    interp: &Interpreter,
107    func: &RValue,
108    positional: &[RValue],
109    named: &[(String, RValue)],
110    env: &Environment,
111) -> Result<RValue, RFlow> {
112    call_function_with_call(interp, func, positional, named, env, None)
113}
114
115#[tracing::instrument(level = "trace", name = "call_function", skip_all, fields(func_type))]
116pub(crate) fn call_function_with_call(
117    interp: &Interpreter,
118    func: &RValue,
119    positional: &[RValue],
120    named: &[(String, RValue)],
121    env: &Environment,
122    call_expr: Option<Expr>,
123) -> Result<RValue, RFlow> {
124    match func {
125        RValue::Function(RFunction::Builtin {
126            name,
127            implementation,
128            min_args,
129            max_args,
130            formals,
131        }) => {
132            tracing::Span::current().record("func_type", "builtin");
133            trace!(
134                builtin = name.as_str(),
135                positional = positional.len(),
136                named = named.len(),
137                "call builtin"
138            );
139            // Arity checks use the original arg counts (before reordering)
140            // because reordering may duplicate named args into positional slots.
141            let actual_args = positional.len() + named.len();
142            Interpreter::ensure_builtin_min_arity(name, *min_args, actual_args)
143                .map_err(RFlow::from)?;
144            Interpreter::ensure_builtin_max_arity(name, *max_args, actual_args)
145                .map_err(RFlow::from)?;
146
147            // Force all promise arguments at the builtin boundary —
148            // builtins expect concrete values, not promises.
149            let (forced_pos, forced_named) = interp.force_args(positional, named)?;
150
151            // Reorder args so positional slots match formal parameter order,
152            // regardless of whether the user passed args by name or position.
153            let (reordered_pos, remaining_named) =
154                reorder_builtin_args(&forced_pos, &forced_named, formals);
155            call_builtin(
156                interp,
157                name,
158                implementation,
159                0,    // min already checked above
160                None, // max already checked above
161                &reordered_pos,
162                &remaining_named,
163                env,
164            )
165        }
166        RValue::Function(RFunction::Closure {
167            params,
168            body,
169            env: closure_env,
170        }) => {
171            tracing::Span::current().record("func_type", "closure");
172            trace!(
173                params = params.len(),
174                positional = positional.len(),
175                named = named.len(),
176                "call closure"
177            );
178            call_closure(
179                interp,
180                ClosureCall {
181                    func,
182                    params,
183                    body,
184                    closure_env,
185                    positional,
186                    named,
187                    call_expr,
188                },
189            )
190        }
191        _ => Err(RError::new(
192            RErrorKind::Type,
193            "attempt to apply non-function".to_string(),
194        )
195        .into()),
196    }
197}
198
199fn resolve_callable_for_call(
200    interp: &Interpreter,
201    func: &Expr,
202    env: &Environment,
203) -> Result<RValue, RFlow> {
204    let value = interp.eval_in(func, env)?;
205    if matches!(value, RValue::Function(_)) {
206        return Ok(value);
207    }
208
209    if let Expr::Symbol(name) = func {
210        return env
211            .get_function(name)
212            .ok_or_else(|| RError::other("attempt to apply non-function".to_string()).into());
213    }
214
215    Ok(value)
216}
217
218fn try_call_special_builtin(
219    interp: &Interpreter,
220    function: &RValue,
221    args: &[Arg],
222    env: &Environment,
223) -> Result<Option<RValue>, RFlow> {
224    let RValue::Function(RFunction::Builtin {
225        name,
226        implementation,
227        min_args,
228        max_args,
229        ..
230    }) = function
231    else {
232        return Ok(None);
233    };
234
235    if name == "UseMethod" {
236        return interp.eval_use_method(args, env).map(Some);
237    }
238
239    if let BuiltinImplementation::PreEval(handler) = implementation {
240        Interpreter::ensure_builtin_min_arity(name, *min_args, args.len()).map_err(RFlow::from)?;
241        Interpreter::ensure_builtin_max_arity(name, *max_args, args.len()).map_err(RFlow::from)?;
242        let ctx = BuiltinContext::new(interp, env);
243        return handler(args, env, &ctx).map_err(Into::into).map(Some);
244    }
245
246    Ok(None)
247}
248
249fn evaluate_call_arguments(
250    interp: &Interpreter,
251    args: &[Arg],
252    env: &Environment,
253) -> Result<EvaluatedCallArgs, RFlow> {
254    let mut positional = PositionalArgs::new();
255    let mut named = NamedArgs::new();
256
257    for arg in args {
258        if let Some(name) = &arg.name {
259            if let Some(val_expr) = &arg.value {
260                named.push((name.clone(), interp.eval_in(val_expr, env)?));
261            } else {
262                named.push((name.clone(), RValue::Null));
263            }
264            continue;
265        }
266
267        let Some(val_expr) = &arg.value else {
268            continue;
269        };
270
271        if matches!(val_expr, Expr::Dots) {
272            expand_dots_arguments(env, &mut positional, &mut named);
273        } else {
274            positional.push(interp.eval_in(val_expr, env)?);
275        }
276    }
277
278    Ok((positional, named))
279}
280
281/// Create lazy promise arguments for closure calls.
282///
283/// Instead of evaluating arguments eagerly, wraps each argument expression
284/// in an `RValue::Promise` that captures the calling environment. The promise
285/// is only forced (evaluated) when its value is actually needed.
286///
287/// `...` (dots) entries are forwarded as-is — if they already contain promises,
288/// those promises are passed through without forcing.
289fn create_promise_arguments(args: &[Arg], env: &Environment) -> EvaluatedCallArgs {
290    let mut positional = PositionalArgs::new();
291    let mut named = NamedArgs::new();
292
293    for arg in args {
294        if let Some(name) = &arg.name {
295            if let Some(val_expr) = &arg.value {
296                named.push((name.clone(), RValue::promise(val_expr.clone(), env.clone())));
297            } else {
298                named.push((name.clone(), RValue::Null));
299            }
300            continue;
301        }
302
303        let Some(val_expr) = &arg.value else {
304            continue;
305        };
306
307        if matches!(val_expr, Expr::Dots) {
308            expand_dots_arguments(env, &mut positional, &mut named);
309        } else {
310            positional.push(RValue::promise(val_expr.clone(), env.clone()));
311        }
312    }
313
314    (positional, named)
315}
316
317fn expand_dots_arguments(
318    env: &Environment,
319    positional: &mut PositionalArgs,
320    named: &mut NamedArgs,
321) {
322    if let Some(RValue::List(list)) = env.get("...") {
323        for (opt_name, value) in &list.values {
324            if let Some(name) = opt_name {
325                named.push((name.clone(), value.clone()));
326            } else {
327                positional.push(value.clone());
328            }
329        }
330    }
331}
332
333/// Reorder positional and named args to match the builtin's formal parameter order.
334///
335/// When `formals` is non-empty, named args that match a formal are placed at
336/// the corresponding slot, and remaining positional args fill the unmatched
337/// slots left-to-right.  This is the same matching R does for closures, applied
338/// at the builtin dispatch boundary so every builtin benefits automatically.
339///
340/// When `formals` is empty (variadic / dots builtins), args pass through unchanged.
341fn reorder_builtin_args(
342    positional: &[RValue],
343    named: &[(String, RValue)],
344    formals: &[&str],
345) -> (PositionalArgs, NamedArgs) {
346    if formals.is_empty() || named.is_empty() {
347        // No formals declared (variadic) or no named args — nothing to reorder.
348        return (
349            positional.iter().cloned().collect(),
350            named.iter().cloned().collect(),
351        );
352    }
353
354    let mut matched: Vec<Option<RValue>> = vec![None; formals.len()];
355
356    // Phase 1: match named args to formals (exact match, then unique partial match).
357    for (arg_name, value) in named {
358        // Exact match
359        if let Some(idx) = formals.iter().position(|f| *f == arg_name.as_str()) {
360            matched[idx] = Some(value.clone());
361            continue;
362        }
363        // Partial prefix match — only if unambiguous
364        let candidates: Vec<usize> = formals
365            .iter()
366            .enumerate()
367            .filter(|(_, f)| f.starts_with(arg_name.as_str()))
368            .map(|(i, _)| i)
369            .collect();
370        if candidates.len() == 1 && matched[candidates[0]].is_none() {
371            matched[candidates[0]] = Some(value.clone());
372        }
373    }
374
375    // Phase 2: fill unmatched formal slots with positional args, left-to-right.
376    let mut pos_iter = positional.iter();
377    for slot in &mut matched {
378        if slot.is_none() {
379            if let Some(val) = pos_iter.next() {
380                *slot = Some(val.clone());
381            }
382        }
383    }
384
385    // Build result: consecutive matched values from the start.
386    // Stop at the first gap — builtins with gaps rely on named-arg lookup
387    // (which still works because `named` is passed through unchanged).
388    let mut result: SmallVec<[RValue; 4]> = SmallVec::new();
389    for slot in &matched {
390        match slot {
391            Some(val) => result.push(val.clone()),
392            None => break,
393        }
394    }
395
396    // Append any overflow positional args (more positional args than formals).
397    result.extend(pos_iter.cloned());
398
399    // Keep the original `named` slice unchanged so builtins that look up
400    // optional params by name (e.g. `named.find("tolerance")`) still work.
401    (result, named.iter().cloned().collect())
402}
403
404#[allow(clippy::too_many_arguments)]
405fn call_builtin(
406    interp: &Interpreter,
407    name: &str,
408    implementation: &BuiltinImplementation,
409    min_args: usize,
410    max_args: Option<usize>,
411    positional: &[RValue],
412    named: &[(String, RValue)],
413    env: &Environment,
414) -> Result<RValue, RFlow> {
415    let actual_args = positional.len() + named.len();
416    Interpreter::ensure_builtin_min_arity(name, min_args, actual_args).map_err(RFlow::from)?;
417    Interpreter::ensure_builtin_max_arity(name, max_args, actual_args).map_err(RFlow::from)?;
418
419    match implementation {
420        BuiltinImplementation::Eager(func) => func(positional, named).map_err(Into::into),
421        BuiltinImplementation::Interpreter(handler) => {
422            handler(positional, named, &BuiltinContext::new(interp, env)).map_err(Into::into)
423        }
424        BuiltinImplementation::PreEval(_) => Err(RError::other(
425            "internal error: pre-eval builtin reached eager dispatch".to_string(),
426        )
427        .into()),
428    }
429}
430
431fn call_closure(interp: &Interpreter, closure_call: ClosureCall<'_>) -> Result<RValue, RFlow> {
432    let bound = interp.bind_closure_call(
433        closure_call.params,
434        closure_call.positional,
435        closure_call.named,
436        closure_call.closure_env,
437        closure_call.func,
438        closure_call.call_expr,
439    )?;
440    let call_env = bound.env;
441
442    interp.call_stack.borrow_mut().push(bound.frame);
443    let result = match interp.eval_in(closure_call.body, &call_env) {
444        Ok(value) => Ok(value),
445        Err(RFlow::Signal(RSignal::Return(value))) => Ok(value),
446        Err(err) => Err(err),
447    };
448    run_on_exit_handlers(interp, &call_env);
449    if result.is_err() {
450        interp.capture_traceback();
451    }
452    interp.call_stack.borrow_mut().pop();
453
454    result
455}
456
457/// Call a closure with lazy (promise) arguments. Arguments are bound as
458/// `RValue::Promise` values in the call environment, only forced when accessed.
459#[allow(clippy::too_many_arguments)]
460fn call_closure_lazy(
461    interp: &Interpreter,
462    params: &[Param],
463    body: &Expr,
464    closure_env: &Environment,
465    func: &RValue,
466    positional: &[RValue],
467    named: &[(String, RValue)],
468    call_expr: Expr,
469) -> Result<RValue, RFlow> {
470    let bound = interp.bind_closure_call(
471        params,
472        positional,
473        named,
474        closure_env,
475        func,
476        Some(call_expr),
477    )?;
478    let call_env = bound.env;
479
480    interp.call_stack.borrow_mut().push(bound.frame);
481    let result = match interp.eval_in(body, &call_env) {
482        Ok(value) => Ok(value),
483        Err(RFlow::Signal(RSignal::Return(value))) => Ok(value),
484        Err(err) => Err(err),
485    };
486    run_on_exit_handlers(interp, &call_env);
487    if result.is_err() {
488        interp.capture_traceback();
489    }
490    interp.call_stack.borrow_mut().pop();
491
492    result
493}
494
495/// Extract a short display name from a call-position expression for tracing.
496fn expr_name(expr: &Expr) -> &str {
497    match expr {
498        Expr::Symbol(name) => name.as_str(),
499        Expr::NsGet { name, .. } | Expr::NsGetInt { name, .. } => name.as_str(),
500        _ => "<expr>",
501    }
502}
503
504fn run_on_exit_handlers(interp: &Interpreter, call_env: &Environment) {
505    let on_exit_exprs = call_env.take_on_exit();
506    for expr in &on_exit_exprs {
507        // on.exit handlers run for side effects; errors are silently ignored (R semantics).
508        if interp.eval_in(expr, call_env).is_err() {}
509    }
510}