Skip to main content

sim_lib_binding/
call.rs

1//! Language-neutral call argument partitioning and binding.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use sim_kernel::{Error, Result, Symbol, Value};
6
7/// One argument at a neutral call boundary.
8#[derive(Clone, Debug)]
9pub enum CallArgument {
10    /// An argument assigned by declaration order.
11    Positional(Value),
12    /// An argument assigned by parameter name.
13    Named(Symbol, Value),
14}
15
16/// Whether unmatched arguments in a partition are rejected or collected.
17#[derive(Clone, Debug, Default, Eq, PartialEq)]
18pub enum Remainder {
19    /// Reject unmatched arguments.
20    #[default]
21    Prohibited,
22    /// Collect unmatched arguments under this binding name.
23    Variadic(Symbol),
24}
25
26/// A declared parameter, optionally carrying its default value.
27#[derive(Clone, Debug)]
28pub struct CallParameter {
29    name: Symbol,
30    default: Option<Value>,
31}
32
33impl CallParameter {
34    /// Declares a required parameter.
35    pub fn required(name: Symbol) -> Self {
36        Self {
37            name,
38            default: None,
39        }
40    }
41
42    /// Declares a parameter whose value is used when the caller omits it.
43    pub fn defaulted(name: Symbol, value: Value) -> Self {
44        Self {
45            name,
46            default: Some(value),
47        }
48    }
49
50    /// Returns the parameter name.
51    pub fn name(&self) -> &Symbol {
52        &self.name
53    }
54}
55
56/// A language-neutral call signature.
57///
58/// Positional parameters are filled in declaration order. Named parameters are
59/// addressable only by name. Each unmatched partition is independently either
60/// prohibited or collected into a variadic binding.
61#[derive(Clone, Debug, Default)]
62pub struct CallSignature {
63    positional: Vec<CallParameter>,
64    named: Vec<CallParameter>,
65    positional_remainder: Remainder,
66    named_remainder: Remainder,
67}
68
69impl CallSignature {
70    /// Creates an empty signature which prohibits all arguments.
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Replaces the ordered positional partition.
76    pub fn with_positional(mut self, parameters: Vec<CallParameter>) -> Self {
77        self.positional = parameters;
78        self
79    }
80
81    /// Replaces the named partition.
82    pub fn with_named(mut self, parameters: Vec<CallParameter>) -> Self {
83        self.named = parameters;
84        self
85    }
86
87    /// Selects how unmatched positional arguments are handled.
88    pub fn with_positional_remainder(mut self, remainder: Remainder) -> Self {
89        self.positional_remainder = remainder;
90        self
91    }
92
93    /// Selects how unmatched named arguments are handled.
94    pub fn with_named_remainder(mut self, remainder: Remainder) -> Self {
95        self.named_remainder = remainder;
96        self
97    }
98
99    /// Validates the declaration and binds a call without invoking its body.
100    pub fn bind(&self, arguments: impl IntoIterator<Item = CallArgument>) -> Result<BoundCall> {
101        self.validate()?;
102        let mut supplied_named = BTreeMap::new();
103        let mut positional_values = Vec::new();
104        let mut saw_named = false;
105
106        for argument in arguments {
107            match argument {
108                CallArgument::Positional(value) => {
109                    if saw_named {
110                        return Err(call_error(
111                            "ordering",
112                            "positional argument follows a named argument",
113                        ));
114                    }
115                    positional_values.push(value);
116                }
117                CallArgument::Named(name, value) => {
118                    saw_named = true;
119                    if supplied_named.insert(name.clone(), value).is_some() {
120                        return Err(call_error("duplicate", format!("named argument {name}")));
121                    }
122                }
123            }
124        }
125
126        let mut bindings = BTreeMap::new();
127        for (index, parameter) in self.positional.iter().enumerate() {
128            let positional = positional_values.get(index).cloned();
129            let named = supplied_named.remove(&parameter.name);
130            let value = match (positional, named, parameter.default.clone()) {
131                (Some(_), Some(_), _) => {
132                    return Err(call_error(
133                        "duplicate",
134                        format!(
135                            "parameter {} supplied positionally and by name",
136                            parameter.name
137                        ),
138                    ));
139                }
140                (Some(value), None, _) | (None, Some(value), _) => value,
141                (None, None, Some(value)) => value,
142                (None, None, None) => {
143                    return Err(call_error(
144                        "missing",
145                        format!("required parameter {}", parameter.name),
146                    ));
147                }
148            };
149            bindings.insert(parameter.name.clone(), value);
150        }
151
152        for parameter in &self.named {
153            let value = match supplied_named.remove(&parameter.name) {
154                Some(value) => value,
155                None => parameter.default.clone().ok_or_else(|| {
156                    call_error(
157                        "missing",
158                        format!("required named parameter {}", parameter.name),
159                    )
160                })?,
161            };
162            bindings.insert(parameter.name.clone(), value);
163        }
164
165        let extra_positional = positional_values
166            .into_iter()
167            .skip(self.positional.len())
168            .collect::<Vec<_>>();
169        if !extra_positional.is_empty() && self.positional_remainder == Remainder::Prohibited {
170            return Err(call_error(
171                "unexpected",
172                format!("{} positional argument(s)", extra_positional.len()),
173            ));
174        }
175        if !supplied_named.is_empty() && self.named_remainder == Remainder::Prohibited {
176            let names = supplied_named
177                .keys()
178                .map(ToString::to_string)
179                .collect::<Vec<_>>()
180                .join(", ");
181            return Err(call_error(
182                "unexpected",
183                format!("named argument(s): {names}"),
184            ));
185        }
186
187        Ok(BoundCall {
188            bindings,
189            positional_remainder: extra_positional,
190            named_remainder: supplied_named,
191        })
192    }
193
194    fn validate(&self) -> Result<()> {
195        let mut names = BTreeSet::new();
196        for parameter in self.positional.iter().chain(&self.named) {
197            if !names.insert(parameter.name.clone()) {
198                return Err(call_error(
199                    "duplicate",
200                    format!("parameter declaration {}", parameter.name),
201                ));
202            }
203        }
204        for remainder in [&self.positional_remainder, &self.named_remainder] {
205            if let Remainder::Variadic(name) = remainder
206                && !names.insert(name.clone())
207            {
208                return Err(call_error(
209                    "duplicate",
210                    format!("variadic declaration {name}"),
211                ));
212            }
213        }
214        Ok(())
215    }
216}
217
218/// The complete result of binding a call signature.
219#[derive(Clone, Debug)]
220pub struct BoundCall {
221    bindings: BTreeMap<Symbol, Value>,
222    positional_remainder: Vec<Value>,
223    named_remainder: BTreeMap<Symbol, Value>,
224}
225
226impl BoundCall {
227    /// Returns the value assigned to a declared parameter.
228    pub fn get(&self, name: &Symbol) -> Option<&Value> {
229        self.bindings.get(name)
230    }
231
232    /// Returns unmatched positional values collected by a variadic partition.
233    pub fn positional_remainder(&self) -> &[Value] {
234        &self.positional_remainder
235    }
236
237    /// Returns unmatched named values in stable name order.
238    pub fn named_remainder(&self) -> &BTreeMap<Symbol, Value> {
239        &self.named_remainder
240    }
241}
242
243fn call_error(category: &str, detail: impl std::fmt::Display) -> Error {
244    Error::Eval(format!("call binding {category}: {detail}"))
245}