Skip to main content

sim_lib_function/
bind.rs

1//! Lossless adaptation of kernel call arguments for guest policy.
2
3use sim_kernel::{Args, Symbol, Value};
4
5/// The exact place at which an argument entered the call boundary.
6///
7/// Origins are data rather than formatted diagnostics so a guest can retain
8/// its own source vocabulary while still identifying every occurrence.
9#[derive(Clone, Debug, Eq, Hash, PartialEq)]
10pub enum ArgumentOrigin {
11    /// A zero-based position in the kernel's evaluated [`Args`] sequence.
12    KernelPosition(usize),
13    /// A guest adapter's stable source location or expansion identity.
14    Guest(Symbol),
15}
16
17/// One undecided input supplied to the neutral adaptation boundary.
18///
19/// The variants describe how an upstream adapter observed the value. They do
20/// not imply precedence, legality, parameter assignment, or default behavior.
21#[derive(Clone)]
22pub enum ArgumentInput {
23    /// An ordinary positional input.
24    Positional(Value),
25    /// One named occurrence. Duplicate names remain distinct occurrences.
26    Named {
27        /// The caller-supplied name for this occurrence.
28        name: Symbol,
29        /// The caller-supplied value for this occurrence.
30        value: Value,
31    },
32    /// A call-site receiver offered for guest-policy interpretation.
33    Receiver(Value),
34    /// An already-expanded remainder input offered by an upstream adapter.
35    Remainder(Value),
36    /// An input an upstream adapter could not classify.
37    Unconsumed(Value),
38}
39
40/// An input paired with its exact origin.
41#[derive(Clone)]
42pub struct BoundArgument {
43    input: ArgumentInput,
44    origin: ArgumentOrigin,
45}
46
47impl BoundArgument {
48    /// Creates one explicitly originated input.
49    pub fn new(input: ArgumentInput, origin: ArgumentOrigin) -> Self {
50        Self { input, origin }
51    }
52
53    /// Returns the undecided input classification.
54    pub const fn input(&self) -> &ArgumentInput {
55        &self.input
56    }
57
58    /// Returns the exact input origin.
59    pub const fn origin(&self) -> &ArgumentOrigin {
60        &self.origin
61    }
62}
63
64/// Mutable call input assembled by kernel and guest adapters.
65#[derive(Clone, Default)]
66pub struct CallInput {
67    arguments: Vec<BoundArgument>,
68}
69
70impl CallInput {
71    /// Starts an empty adaptation stream.
72    pub const fn new() -> Self {
73        Self {
74            arguments: Vec::new(),
75        }
76    }
77
78    /// Appends an input without interpreting any earlier occurrence.
79    pub fn push(&mut self, input: ArgumentInput, origin: ArgumentOrigin) {
80        self.arguments.push(BoundArgument::new(input, origin));
81    }
82
83    /// Appends an input and returns the stream for fluent adapter construction.
84    pub fn with(mut self, input: ArgumentInput, origin: ArgumentOrigin) -> Self {
85        self.push(input, origin);
86        self
87    }
88
89    /// Returns the complete input stream in arrival order.
90    pub fn arguments(&self) -> &[BoundArgument] {
91        &self.arguments
92    }
93}
94
95impl From<Args> for CallInput {
96    fn from(args: Args) -> Self {
97        Self {
98            arguments: args
99                .into_vec()
100                .into_iter()
101                .enumerate()
102                .map(|(position, value)| {
103                    BoundArgument::new(
104                        ArgumentInput::Positional(value),
105                        ArgumentOrigin::KernelPosition(position),
106                    )
107                })
108                .collect(),
109        }
110    }
111}
112
113/// A stable, lossless call record awaiting guest-policy decisions.
114#[derive(Clone, Default)]
115pub struct BoundCall {
116    arguments: Vec<BoundArgument>,
117}
118
119impl BoundCall {
120    /// Returns every input in original arrival order.
121    pub fn arguments(&self) -> &[BoundArgument] {
122        &self.arguments
123    }
124
125    /// Returns all positional occurrences in arrival order.
126    pub fn positional(&self) -> impl Iterator<Item = &BoundArgument> {
127        self.select(|input| matches!(input, ArgumentInput::Positional(_)))
128    }
129
130    /// Returns all named occurrences in arrival order, including duplicates.
131    pub fn named(&self) -> impl Iterator<Item = &BoundArgument> {
132        self.select(|input| matches!(input, ArgumentInput::Named { .. }))
133    }
134
135    /// Returns all receiver occurrences in arrival order.
136    pub fn receivers(&self) -> impl Iterator<Item = &BoundArgument> {
137        self.select(|input| matches!(input, ArgumentInput::Receiver(_)))
138    }
139
140    /// Returns all remainder occurrences in arrival order.
141    pub fn remainder(&self) -> impl Iterator<Item = &BoundArgument> {
142        self.select(|input| matches!(input, ArgumentInput::Remainder(_)))
143    }
144
145    /// Returns all unconsumed occurrences in arrival order.
146    pub fn unconsumed(&self) -> impl Iterator<Item = &BoundArgument> {
147        self.select(|input| matches!(input, ArgumentInput::Unconsumed(_)))
148    }
149
150    fn select(
151        &self,
152        predicate: impl Fn(&ArgumentInput) -> bool,
153    ) -> impl Iterator<Item = &BoundArgument> {
154        self.arguments
155            .iter()
156            .filter(move |argument| predicate(&argument.input))
157    }
158}
159
160/// Freezes an assembled input stream without applying a language rule.
161pub fn bind(input: CallInput) -> BoundCall {
162    BoundCall {
163        arguments: input.arguments,
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use sim_kernel::testing::bare_cx;
171
172    fn origin(name: &str) -> ArgumentOrigin {
173        ArgumentOrigin::Guest(Symbol::new(name))
174    }
175
176    #[test]
177    fn duplicate_names_reach_policy_as_distinct_ordered_occurrences() {
178        let cx = bare_cx();
179        let first = cx.factory().symbol(Symbol::new("first")).unwrap();
180        let second = cx.factory().symbol(Symbol::new("second")).unwrap();
181        let name = Symbol::new("option");
182        let input = CallInput::new()
183            .with(
184                ArgumentInput::Named {
185                    name: name.clone(),
186                    value: first,
187                },
188                origin("call:4"),
189            )
190            .with(
191                ArgumentInput::Named {
192                    name,
193                    value: second,
194                },
195                origin("call:9"),
196            );
197
198        let bound = bind(input);
199        let origins = bound.named().map(BoundArgument::origin).collect::<Vec<_>>();
200        assert_eq!(origins, vec![&origin("call:4"), &origin("call:9")]);
201    }
202
203    #[test]
204    fn every_input_class_remains_visible_and_stably_ordered() {
205        let cx = bare_cx();
206        let make = || {
207            let value = |name| cx.factory().symbol(Symbol::new(name)).unwrap();
208            let positional = value("positional");
209            let receiver = value("receiver");
210            let remainder = value("remainder");
211            let unconsumed = value("unconsumed");
212            CallInput::from(Args::new(vec![positional]))
213                .with(ArgumentInput::Receiver(receiver), origin("receiver"))
214                .with(ArgumentInput::Remainder(remainder), origin("spread"))
215                .with(ArgumentInput::Unconsumed(unconsumed), origin("unknown"))
216        };
217
218        let project = |bound: BoundCall| {
219            bound
220                .arguments()
221                .iter()
222                .map(|argument| argument.origin().clone())
223                .collect::<Vec<_>>()
224        };
225        assert_eq!(project(bind(make())), project(bind(make())));
226        let bound = bind(make());
227        assert_eq!(bound.positional().count(), 1);
228        assert_eq!(bound.receivers().count(), 1);
229        assert_eq!(bound.remainder().count(), 1);
230        assert_eq!(bound.unconsumed().count(), 1);
231    }
232}