Skip to main content

r/interpreter/
arguments.rs

1//! Closure argument binding helpers used by the evaluator.
2//!
3//! Implements R's three-pass argument matching algorithm:
4//! 1. Exact name match
5//! 2. Partial (prefix) name match (must be unambiguous)
6//! 3. Positional match for remaining unmatched formals
7//!
8//! After matching, errors on unused arguments when no `...` is present.
9
10use std::collections::{HashMap, HashSet};
11
12use crate::interpreter::call::CallFrame;
13use crate::interpreter::environment::Environment;
14use crate::interpreter::value::{RError, RErrorKind, RFlow, RList, RValue};
15use crate::interpreter::Interpreter;
16use crate::parser::ast::{Expr, Param};
17use itertools::Itertools;
18
19pub(crate) struct BoundClosureCall {
20    pub env: Environment,
21    pub frame: CallFrame,
22}
23
24impl Interpreter {
25    pub(crate) fn bind_closure_call(
26        &self,
27        params: &[Param],
28        positional: &[RValue],
29        named: &[(String, RValue)],
30        closure_env: &Environment,
31        function: &RValue,
32        call: Option<Expr>,
33    ) -> Result<BoundClosureCall, RFlow> {
34        let call_env = Environment::new_child(closure_env);
35        let has_dots = params.iter().any(|p| p.is_dots);
36
37        // Collect formal parameter names (excluding ...)
38        let formal_names: Vec<&str> = params
39            .iter()
40            .filter(|p| !p.is_dots)
41            .map(|p| p.name.as_str())
42            .collect();
43
44        // Maps: named_arg_index → formal_name it matched to
45        let mut named_to_formal: HashMap<usize, &str> = HashMap::new();
46        let mut matched_formals: HashSet<&str> = HashSet::new();
47
48        // Pass 1: Exact name matching
49        for (i, (arg_name, _)) in named.iter().enumerate() {
50            if let Some(&formal) = formal_names.iter().find(|&&f| f == arg_name) {
51                if !matched_formals.contains(formal) {
52                    matched_formals.insert(formal);
53                    named_to_formal.insert(i, formal);
54                }
55            }
56        }
57
58        // Pass 2: Partial (prefix) matching for remaining named args
59        for (i, (arg_name, _)) in named.iter().enumerate() {
60            if named_to_formal.contains_key(&i) {
61                continue;
62            }
63            let candidates: Vec<&str> = formal_names
64                .iter()
65                .filter(|&&f| !matched_formals.contains(f) && f.starts_with(arg_name.as_str()))
66                .copied()
67                .collect();
68            match candidates.len() {
69                1 => {
70                    matched_formals.insert(candidates[0]);
71                    named_to_formal.insert(i, candidates[0]);
72                }
73                n if n > 1 && !has_dots => {
74                    return Err(RError::new(
75                        RErrorKind::Argument,
76                        format!(
77                            "argument '{}' matches multiple formal arguments: {}",
78                            arg_name,
79                            candidates.join(", ")
80                        ),
81                    )
82                    .into());
83                }
84                _ => {} // 0 or ambiguous with dots — handled later
85            }
86        }
87
88        // Build reverse map: formal_name → named_arg_index
89        let formal_to_named: HashMap<&str, usize> = named_to_formal
90            .iter()
91            .map(|(&idx, &formal)| (formal, idx))
92            .collect();
93
94        // Pass 3: Bind matched formals and positional fill
95        let mut pos_idx = 0usize;
96        let mut dots_vals: Vec<(Option<String>, RValue)> = Vec::new();
97        let mut formal_args = HashSet::new();
98        let mut supplied_args = HashSet::new();
99
100        for param in params {
101            if param.is_dots {
102                formal_args.insert("...".to_string());
103                // Collect remaining unmatched positional args
104                while pos_idx < positional.len() {
105                    dots_vals.push((None, positional[pos_idx].clone()));
106                    pos_idx += 1;
107                }
108                // Collect unmatched named args
109                for (i, (name, value)) in named.iter().enumerate() {
110                    if !named_to_formal.contains_key(&i) {
111                        dots_vals.push((Some(name.clone()), value.clone()));
112                    }
113                }
114                continue;
115            }
116
117            formal_args.insert(param.name.clone());
118
119            if let Some(&named_idx) = formal_to_named.get(param.name.as_str()) {
120                // Matched by name (exact or partial)
121                call_env.set(param.name.clone(), named[named_idx].1.clone());
122                supplied_args.insert(param.name.clone());
123            } else if pos_idx < positional.len() {
124                // Positional fill
125                call_env.set(param.name.clone(), positional[pos_idx].clone());
126                supplied_args.insert(param.name.clone());
127                pos_idx += 1;
128            } else if let Some(default) = &param.default {
129                // Default values are lazy too — create a promise evaluated in the
130                // call environment so defaults can reference other parameters.
131                let value = RValue::promise(default.clone(), call_env.clone());
132                call_env.set(param.name.clone(), value);
133            }
134        }
135
136        // Error on unused arguments when no ... is present
137        if !has_dots {
138            if pos_idx < positional.len() {
139                return Err(RError::new(
140                    RErrorKind::Argument,
141                    format!(
142                        "unused argument{}",
143                        if positional.len() - pos_idx == 1 {
144                            ""
145                        } else {
146                            "s"
147                        }
148                    ),
149                )
150                .into());
151            }
152            let unused_named: Vec<&str> = named
153                .iter()
154                .enumerate()
155                .filter(|(i, _)| !named_to_formal.contains_key(i))
156                .map(|(_, (name, _))| name.as_str())
157                .collect();
158            if !unused_named.is_empty() {
159                return Err(RError::new(
160                    RErrorKind::Argument,
161                    format!(
162                        "unused argument{} ({})",
163                        if unused_named.len() == 1 { "" } else { "s" },
164                        unused_named.iter().map(|n| format!("{n} = ")).join(", ")
165                    ),
166                )
167                .into());
168            }
169        }
170
171        if has_dots {
172            call_env.set("...".to_string(), RValue::List(RList::new(dots_vals)));
173        }
174
175        let supplied_arg_count = supplied_args.len();
176        Ok(BoundClosureCall {
177            env: call_env.clone(),
178            frame: CallFrame {
179                call,
180                function: function.clone(),
181                env: call_env,
182                formal_args,
183                supplied_args,
184                supplied_positional: positional.iter().cloned().collect(),
185                supplied_named: named.iter().cloned().collect(),
186                supplied_arg_count,
187                is_native_boundary: false,
188            },
189        })
190    }
191}