Skip to main content

sim_lib_function/
plan.rs

1use std::{collections::BTreeMap, error::Error, fmt};
2
3use sim_kernel::{ShapeId, Symbol};
4
5/// The ways in which a caller may address a parameter.
6#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
7pub struct CallMode {
8    positional: bool,
9    named: bool,
10}
11
12impl CallMode {
13    /// The parameter is addressed only by declaration position.
14    pub const POSITIONAL: Self = Self::new(true, false);
15    /// The parameter is addressed only by name.
16    pub const NAMED: Self = Self::new(false, true);
17    /// The parameter may be addressed by position or name.
18    pub const POSITIONAL_OR_NAMED: Self = Self::new(true, true);
19
20    /// Constructs a call mode from its two independent addressing facets.
21    ///
22    /// A mode with neither facet is contradictory and is refused by
23    /// [`FunctionPlan::new`].
24    pub const fn new(positional: bool, named: bool) -> Self {
25        Self { positional, named }
26    }
27
28    /// Whether positional addressing is admitted.
29    pub const fn is_positional(self) -> bool {
30        self.positional
31    }
32
33    /// Whether named addressing is admitted.
34    pub const fn is_named(self) -> bool {
35        self.named
36    }
37}
38
39/// The declaration role of a parameter.
40#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
41pub enum ParameterKind {
42    /// A value must be supplied by the caller.
43    Required,
44    /// Guest policy may supply a default when the caller omits the value.
45    Optional,
46    /// Remaining arguments in the selected call-mode partition are collected.
47    Remainder,
48}
49
50/// Stable declaration metadata for one parameter.
51#[derive(Clone, Debug, Eq, Hash, PartialEq)]
52pub struct ParameterDescriptor {
53    name: Symbol,
54    kind: ParameterKind,
55    call_mode: CallMode,
56    shape: Option<ShapeId>,
57}
58
59impl ParameterDescriptor {
60    /// Declares a parameter with an optional existing Shape identifier.
61    pub fn new(
62        name: Symbol,
63        kind: ParameterKind,
64        call_mode: CallMode,
65        shape: Option<ShapeId>,
66    ) -> Self {
67        Self {
68            name,
69            kind,
70            call_mode,
71            shape,
72        }
73    }
74
75    /// Returns the binding name.
76    pub fn name(&self) -> &Symbol {
77        &self.name
78    }
79    /// Returns the declaration role.
80    pub const fn kind(&self) -> ParameterKind {
81        self.kind
82    }
83    /// Returns the admitted addressing mode.
84    pub const fn call_mode(&self) -> CallMode {
85        self.call_mode
86    }
87    /// Returns the stable Shape identifier used for browsing, when declared.
88    pub const fn shape(&self) -> Option<ShapeId> {
89        self.shape
90    }
91}
92
93/// Stable metadata for one lexical capture slot.
94#[derive(Clone, Debug, Eq, Hash, PartialEq)]
95pub struct CaptureDescriptor {
96    name: Symbol,
97    shape: Option<ShapeId>,
98}
99
100impl CaptureDescriptor {
101    /// Declares a capture slot with an optional existing Shape identifier.
102    pub fn new(name: Symbol, shape: Option<ShapeId>) -> Self {
103        Self { name, shape }
104    }
105    /// Returns the capture binding name.
106    pub fn name(&self) -> &Symbol {
107        &self.name
108    }
109    /// Returns its stable Shape identifier, when declared.
110    pub const fn shape(&self) -> Option<ShapeId> {
111        self.shape
112    }
113}
114
115/// Canonical, inert projection of a function's browsable Shape metadata.
116#[derive(Clone, Debug, Eq, PartialEq)]
117pub struct BrowseProjection {
118    parameters: Vec<(Symbol, Option<ShapeId>)>,
119    result: Option<ShapeId>,
120}
121
122impl BrowseProjection {
123    /// Returns parameter names and Shape identifiers in declaration order.
124    pub fn parameters(&self) -> &[(Symbol, Option<ShapeId>)] {
125        &self.parameters
126    }
127    /// Returns the declared result Shape identifier.
128    pub const fn result(&self) -> Option<ShapeId> {
129        self.result
130    }
131}
132
133/// A construction failure for an immutable function plan.
134#[derive(Clone, Debug, Eq, PartialEq)]
135pub struct PlanError {
136    message: String,
137}
138
139impl PlanError {
140    fn new(message: impl Into<String>) -> Self {
141        Self {
142            message: message.into(),
143        }
144    }
145}
146
147impl fmt::Display for PlanError {
148    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
149        formatter.write_str(&self.message)
150    }
151}
152
153impl Error for PlanError {}
154
155/// Immutable declaration metadata shared by guest function implementations.
156///
157/// Equality is plan identity: two independently built plans compare equal when
158/// their stable display symbol and complete declarations are equal.
159#[derive(Clone, Debug, Eq, Hash, PartialEq)]
160pub struct FunctionPlan {
161    display_identity: Symbol,
162    parameters: Vec<ParameterDescriptor>,
163    captures: Vec<CaptureDescriptor>,
164    result_shape: Option<ShapeId>,
165}
166
167impl FunctionPlan {
168    /// Validates and freezes one declaration.
169    pub fn new(
170        display_identity: Symbol,
171        parameters: Vec<ParameterDescriptor>,
172        captures: Vec<CaptureDescriptor>,
173        result_shape: Option<ShapeId>,
174    ) -> Result<Self, PlanError> {
175        validate_parameters(&parameters)?;
176        validate_captures(&captures)?;
177        Ok(Self {
178            display_identity,
179            parameters,
180            captures,
181            result_shape,
182        })
183    }
184
185    /// Returns the stable human-facing identity of this declaration.
186    pub fn display_identity(&self) -> &Symbol {
187        &self.display_identity
188    }
189    /// Returns parameter descriptors in declaration order.
190    pub fn parameters(&self) -> &[ParameterDescriptor] {
191        &self.parameters
192    }
193    /// Returns capture slots in stable declaration order.
194    pub fn captures(&self) -> &[CaptureDescriptor] {
195        &self.captures
196    }
197    /// Returns the declared result Shape identifier.
198    pub const fn result_shape(&self) -> Option<ShapeId> {
199        self.result_shape
200    }
201
202    /// Builds the canonical browse projection without resolving or executing a Shape.
203    pub fn browse(&self) -> BrowseProjection {
204        BrowseProjection {
205            parameters: self
206                .parameters
207                .iter()
208                .map(|p| (p.name.clone(), p.shape))
209                .collect(),
210            result: self.result_shape,
211        }
212    }
213}
214
215fn validate_parameters(parameters: &[ParameterDescriptor]) -> Result<(), PlanError> {
216    let mut names = BTreeMap::new();
217    let mut positional_remainder: Option<&Symbol> = None;
218    for parameter in parameters {
219        if let Some(first) = names.insert(parameter.name.clone(), parameter.name.clone()) {
220            return Err(PlanError::new(format!(
221                "duplicate parameter names {first} and {}",
222                parameter.name
223            )));
224        }
225        if !parameter.call_mode.positional && !parameter.call_mode.named {
226            return Err(PlanError::new(format!(
227                "parameter {} has contradictory call modes",
228                parameter.name
229            )));
230        }
231        if let Some(remainder) = positional_remainder
232            && parameter.kind == ParameterKind::Required
233            && parameter.call_mode.positional
234        {
235            return Err(PlanError::new(format!(
236                "positional remainder {remainder} cannot precede required parameter {}",
237                parameter.name
238            )));
239        }
240        if parameter.kind == ParameterKind::Remainder {
241            if parameter.call_mode == CallMode::POSITIONAL_OR_NAMED {
242                return Err(PlanError::new(format!(
243                    "remainder parameter {} has contradictory call modes",
244                    parameter.name
245                )));
246            }
247            if parameter.call_mode.positional {
248                if let Some(first) = positional_remainder {
249                    return Err(PlanError::new(format!(
250                        "positional remainders {first} and {} conflict",
251                        parameter.name
252                    )));
253                }
254                positional_remainder = Some(&parameter.name);
255            }
256        }
257    }
258    Ok(())
259}
260
261fn validate_captures(captures: &[CaptureDescriptor]) -> Result<(), PlanError> {
262    let mut names = BTreeMap::new();
263    for capture in captures {
264        if let Some(first) = names.insert(capture.name.clone(), capture.name.clone()) {
265            return Err(PlanError::new(format!(
266                "duplicate capture names {first} and {}",
267                capture.name
268            )));
269        }
270    }
271    Ok(())
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    fn parameter(name: &str, kind: ParameterKind, mode: CallMode) -> ParameterDescriptor {
279        ParameterDescriptor::new(Symbol::new(name), kind, mode, None)
280    }
281
282    #[test]
283    fn remainder_before_required_names_both_parameters() {
284        let error = FunctionPlan::new(
285            Symbol::new("example"),
286            vec![
287                parameter("rest", ParameterKind::Remainder, CallMode::POSITIONAL),
288                parameter("needed", ParameterKind::Required, CallMode::POSITIONAL),
289            ],
290            vec![],
291            None,
292        )
293        .unwrap_err();
294        assert!(error.to_string().contains("rest"));
295        assert!(error.to_string().contains("needed"));
296    }
297
298    #[test]
299    fn equal_declarations_have_equal_identity() {
300        let build = || {
301            FunctionPlan::new(
302                Symbol::qualified("guest", "work"),
303                vec![parameter(
304                    "value",
305                    ParameterKind::Required,
306                    CallMode::POSITIONAL_OR_NAMED,
307                )],
308                vec![CaptureDescriptor::new(
309                    Symbol::new("scope"),
310                    Some(ShapeId(7)),
311                )],
312                Some(ShapeId(9)),
313            )
314            .unwrap()
315        };
316        assert_eq!(build(), build());
317    }
318
319    #[test]
320    fn construction_rejects_duplicates_and_contradictory_modes() {
321        let duplicate = FunctionPlan::new(
322            Symbol::new("duplicate"),
323            vec![
324                parameter("same", ParameterKind::Required, CallMode::NAMED),
325                parameter("same", ParameterKind::Optional, CallMode::NAMED),
326            ],
327            vec![],
328            None,
329        )
330        .unwrap_err();
331        assert!(duplicate.to_string().contains("same"));
332
333        let contradictory = FunctionPlan::new(
334            Symbol::new("contradictory"),
335            vec![parameter(
336                "lost",
337                ParameterKind::Required,
338                CallMode::new(false, false),
339            )],
340            vec![],
341            None,
342        )
343        .unwrap_err();
344        assert!(contradictory.to_string().contains("lost"));
345    }
346
347    #[test]
348    fn browse_projection_preserves_shape_identifiers() {
349        let plan = FunctionPlan::new(
350            Symbol::new("browse"),
351            vec![ParameterDescriptor::new(
352                Symbol::new("input"),
353                ParameterKind::Required,
354                CallMode::POSITIONAL,
355                Some(ShapeId(3)),
356            )],
357            vec![],
358            Some(ShapeId(4)),
359        )
360        .unwrap();
361        assert_eq!(
362            plan.browse().parameters(),
363            &[(Symbol::new("input"), Some(ShapeId(3)))]
364        );
365        assert_eq!(plan.browse().result(), Some(ShapeId(4)));
366    }
367}