Skip to main content

sim_lib_dispatch/
method.rs

1use std::{cmp::Ordering, sync::Arc};
2
3use sim_kernel::{
4    CapabilityName, Cx, Error, HintMetadata, MatchScore, Result, Shape, Symbol, Value,
5};
6
7/// The executable body of a [`DispatchMethod`].
8///
9/// A shared closure invoked with the call [`Cx`] and the matched arguments,
10/// returning the method's result [`Value`].
11pub type MethodBody = Arc<dyn Fn(&mut Cx, &[Value]) -> Result<Value> + Send + Sync>;
12
13/// Role of a method within a generic function's combination.
14///
15/// Roles order the execution plan: `Around` wraps the call, `Before` and
16/// `After` run for effect around the single applicable `Primary`.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
18pub enum MethodRole {
19    /// Wraps the whole call; most specific runs outermost.
20    Around,
21    /// Runs for effect before the primary method; most specific first.
22    Before,
23    /// Provides the call's result; exactly one applies per call.
24    Primary,
25    /// Runs for effect after the primary method; least specific first.
26    After,
27}
28
29impl MethodRole {
30    /// Returns the qualified `method-role:<role>` symbol naming this role.
31    pub fn as_symbol(self) -> Symbol {
32        match self {
33            Self::Around => Symbol::qualified("method-role", "around"),
34            Self::Before => Symbol::qualified("method-role", "before"),
35            Self::Primary => Symbol::qualified("method-role", "primary"),
36            Self::After => Symbol::qualified("method-role", "after"),
37        }
38    }
39
40    /// Returns the role's ordering rank within the combination plan.
41    ///
42    /// Lower ranks run earlier: around (0), before (1), primary (2), after (3).
43    pub fn combination_rank(self) -> u8 {
44        match self {
45            Self::Around => 0,
46            Self::Before => 1,
47            Self::Primary => 2,
48            Self::After => 3,
49        }
50    }
51}
52
53/// A single method belonging to a [`GenericFunction`](crate::GenericFunction).
54///
55/// Pairs a parameter-shape signature with an executable [`MethodBody`] and a
56/// [`MethodRole`]; methods are matched against call arguments through the
57/// kernel [`Shape`] protocol.
58#[derive(Clone)]
59pub struct DispatchMethod {
60    id: Symbol,
61    role: MethodRole,
62    parameter_shapes: Vec<Arc<dyn Shape>>,
63    body: MethodBody,
64    hints: Vec<HintMetadata>,
65}
66
67impl DispatchMethod {
68    /// Builds a method from its id, role, parameter shapes, and body.
69    pub fn new(
70        id: Symbol,
71        role: MethodRole,
72        parameter_shapes: Vec<Arc<dyn Shape>>,
73        body: MethodBody,
74    ) -> Self {
75        Self {
76            id,
77            role,
78            parameter_shapes,
79            body,
80            hints: Vec::new(),
81        }
82    }
83
84    /// Returns the symbol identifying this method.
85    pub fn id(&self) -> &Symbol {
86        &self.id
87    }
88
89    /// Returns the method's role in the combination.
90    pub fn role(&self) -> MethodRole {
91        self.role
92    }
93
94    /// Returns the method's arity (its number of parameter shapes).
95    pub fn arity(&self) -> usize {
96        self.parameter_shapes.len()
97    }
98
99    /// Returns the parameter shapes this method matches against, in order.
100    pub fn parameter_shapes(&self) -> &[Arc<dyn Shape>] {
101        &self.parameter_shapes
102    }
103
104    /// Returns the operation hints attached to this method.
105    pub fn hints(&self) -> &[HintMetadata] {
106        &self.hints
107    }
108
109    /// Adds an operation hint to this method.
110    pub fn with_hint(mut self, hint: HintMetadata) -> Self {
111        self.hints.push(hint);
112        self
113    }
114
115    /// Adds a hint describing one method argument.
116    pub fn with_argument_hint(self, argument: Symbol, detail: impl Into<String>) -> Self {
117        let title = format!("argument {argument}");
118        self.with_hint(
119            HintMetadata::new(Symbol::qualified("runtime-hint", "argument"), title)
120                .with_detail(detail)
121                .with_tag(Symbol::qualified("runtime", "argument"))
122                .with_argument(argument),
123        )
124    }
125
126    /// Adds a hint describing a capability requirement.
127    pub fn with_capability_requirement(self, capability: CapabilityName) -> Self {
128        self.with_hint(
129            HintMetadata::new(
130                Symbol::qualified("runtime-hint", "capability"),
131                format!("requires {capability}"),
132            )
133            .with_tag(Symbol::qualified("runtime", "capability"))
134            .with_capability(capability),
135        )
136    }
137
138    /// Adds a hint describing a codec-safe form.
139    pub fn with_codec_safe_form(self, form: Symbol) -> Self {
140        self.with_hint(
141            HintMetadata::new(
142                Symbol::qualified("runtime-hint", "codec-form"),
143                format!("codec form {form}"),
144            )
145            .with_tag(Symbol::qualified("runtime", "codec"))
146            .with_codec_form(form),
147        )
148    }
149
150    /// Adds a runnable or displayable operation example.
151    pub fn with_example(self, example: impl Into<String>) -> Self {
152        self.with_hint(
153            HintMetadata::new(
154                Symbol::qualified("runtime-hint", "example"),
155                "operation example",
156            )
157            .with_tag(Symbol::qualified("runtime", "example"))
158            .with_example(example),
159        )
160    }
161
162    /// Tests the method against `args`, returning its specificity if applicable.
163    ///
164    /// Returns `None` on arity mismatch or if any parameter shape rejects its
165    /// argument; otherwise returns the accumulated [`MethodSpecificity`].
166    pub fn match_args(&self, cx: &mut Cx, args: &[Value]) -> Result<Option<MethodSpecificity>> {
167        if args.len() != self.parameter_shapes.len() {
168            return Ok(None);
169        }
170
171        let mut total = MatchScore::exact(0);
172        let mut argument_scores = Vec::with_capacity(args.len());
173        for (shape, arg) in self.parameter_shapes.iter().zip(args.iter()) {
174            let matched = shape.check_value(cx, arg.clone())?;
175            if !matched.accepted {
176                return Ok(None);
177            }
178            total += matched.score;
179            argument_scores.push(matched.score);
180        }
181
182        Ok(Some(MethodSpecificity::new(
183            self.id.clone(),
184            self.role,
185            total,
186            argument_scores,
187        )))
188    }
189
190    /// Runs the method body against `args`, returning its result.
191    pub fn invoke(&self, cx: &mut Cx, args: &[Value]) -> Result<Value> {
192        (self.body)(cx, args)
193    }
194}
195
196/// How specifically a method matched a particular call.
197///
198/// Carries the per-argument [`MatchScore`]s plus their total; comparing two
199/// specificities (see [`compare_specificity`]) orders methods most-specific
200/// first for applicable-method selection.
201#[derive(Clone, Debug, PartialEq, Eq)]
202pub struct MethodSpecificity {
203    method: Symbol,
204    role: MethodRole,
205    score: MatchScore,
206    argument_scores: Vec<MatchScore>,
207}
208
209impl MethodSpecificity {
210    /// Builds a specificity record for a matched method.
211    pub fn new(
212        method: Symbol,
213        role: MethodRole,
214        score: MatchScore,
215        argument_scores: Vec<MatchScore>,
216    ) -> Self {
217        Self {
218            method,
219            role,
220            score,
221            argument_scores,
222        }
223    }
224
225    /// Returns the symbol identifying the matched method.
226    pub fn method(&self) -> &Symbol {
227        &self.method
228    }
229
230    /// Returns the matched method's role in the combination.
231    pub fn role(&self) -> MethodRole {
232        self.role
233    }
234
235    /// Returns the total match score across all arguments.
236    pub fn score(&self) -> MatchScore {
237        self.score
238    }
239
240    /// Returns the per-argument match scores, in argument order.
241    pub fn argument_scores(&self) -> &[MatchScore] {
242        &self.argument_scores
243    }
244}
245
246/// Orders two specificities from least to most specific.
247///
248/// Compares argument scores left to right, breaking ties on the total score;
249/// the dispatch organ uses this to rank applicable methods.
250pub fn compare_specificity(left: &MethodSpecificity, right: &MethodSpecificity) -> Ordering {
251    left.argument_scores
252        .cmp(&right.argument_scores)
253        .then_with(|| left.score.cmp(&right.score))
254}
255
256pub(crate) fn ambiguous_primary_error(name: &Symbol, left: &Symbol, right: &Symbol) -> Error {
257    Error::Eval(format!(
258        "generic function {name} has ambiguous primary methods {left} and {right}"
259    ))
260}