Skip to main content

sim_lib_dispatch/
generic.rs

1use std::cmp::Ordering;
2
3use sim_kernel::{Cx, Diagnostic, Error, HintMetadata, Result, Symbol, Value};
4
5use crate::{
6    DispatchMethod, MethodRole, MethodSpecificity,
7    method::{ambiguous_primary_error, compare_specificity},
8};
9
10/// A generic function under its multimethod alias.
11///
12/// `Multimethod` and [`GenericFunction`] are the same dispatch entity; the
13/// alias lets multimethod-flavored language profiles name it familiarly.
14pub type Multimethod = GenericFunction;
15
16/// A named generic function: a set of [`DispatchMethod`]s sharing one name.
17///
18/// This is the dispatch organ's central object. Methods are added with
19/// [`add_method`](GenericFunction::add_method); a call selects the single most
20/// specific applicable primary method (plus around/before/after methods) by
21/// matching arguments through the kernel `Shape` protocol. The kernel defines
22/// the callable/operation contracts; this type supplies the concrete dispatch.
23///
24/// # Examples
25///
26/// ```
27/// use std::sync::Arc;
28/// use sim_kernel::{Cx, DefaultFactory, ExprKind, NoopEvalPolicy, Shape, Symbol};
29/// use sim_lib_dispatch::{DispatchMethod, GenericFunction, MethodRole};
30/// use sim_shape::{AnyShape, ExprKindShape};
31///
32/// let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
33/// let mut generic = GenericFunction::new(Symbol::qualified("demo", "describe"));
34///
35/// // A broad fallback and a more specific string method.
36/// generic.add_method(DispatchMethod::new(
37///     Symbol::qualified("method", "any"),
38///     MethodRole::Primary,
39///     vec![Arc::new(AnyShape) as Arc<dyn Shape>],
40///     Arc::new(|cx: &mut Cx, _| cx.factory().string("any".to_owned())),
41/// )).unwrap();
42/// generic.add_method(DispatchMethod::new(
43///     Symbol::qualified("method", "string"),
44///     MethodRole::Primary,
45///     vec![Arc::new(ExprKindShape::new(ExprKind::String)) as Arc<dyn Shape>],
46///     Arc::new(|cx: &mut Cx, _| cx.factory().string("string".to_owned())),
47/// )).unwrap();
48///
49/// let args = [cx.factory().string("hi".to_owned()).unwrap()];
50/// // The string method is more specific, so it is selected.
51/// let selected = generic.select_primary(&mut cx, &args).unwrap();
52/// assert_eq!(selected.method(), &Symbol::qualified("method", "string"));
53/// ```
54pub struct GenericFunction {
55    name: Symbol,
56    methods: Vec<DispatchMethod>,
57}
58
59impl GenericFunction {
60    /// Creates an empty generic function with the given name.
61    pub fn new(name: Symbol) -> Self {
62        Self {
63            name,
64            methods: Vec::new(),
65        }
66    }
67
68    /// Returns the generic function's name.
69    pub fn name(&self) -> &Symbol {
70        &self.name
71    }
72
73    /// Returns the methods added to this generic function, in insertion order.
74    pub fn methods(&self) -> &[DispatchMethod] {
75        &self.methods
76    }
77
78    /// Returns agent-consumable operation hints from the generic and methods.
79    pub fn operation_hints(&self) -> Vec<HintMetadata> {
80        let mut hints = vec![
81            HintMetadata::new(
82                Symbol::qualified("runtime-hint", "operation"),
83                format!("operation {}", self.name),
84            )
85            .with_tag(Symbol::qualified("runtime", "operation"))
86            .with_argument(self.name.clone()),
87        ];
88        for method in &self.methods {
89            hints.push(
90                HintMetadata::new(
91                    Symbol::qualified("runtime-hint", "method"),
92                    format!("method {}", method.id()),
93                )
94                .with_detail(format!(
95                    "{} method with {} argument shapes",
96                    method.role().as_symbol(),
97                    method.arity()
98                ))
99                .with_tag(Symbol::qualified("runtime", "method"))
100                .with_argument(method.id().clone()),
101            );
102            hints.extend(method.hints().iter().cloned());
103        }
104        hints
105    }
106
107    /// Adds a method, erroring if one with the same id, role, and arity exists.
108    pub fn add_method(&mut self, method: DispatchMethod) -> Result<()> {
109        if let Some(existing) = self.methods.iter().find(|existing| {
110            existing.id() == method.id()
111                && existing.role() == method.role()
112                && existing.arity() == method.arity()
113        }) {
114            return Err(Error::Eval(format!(
115                "generic function {} already has method {} for role {:?}",
116                self.name,
117                existing.id(),
118                existing.role()
119            )));
120        }
121        self.methods.push(method);
122        Ok(())
123    }
124
125    /// Returns the specificity of the most specific applicable primary method.
126    ///
127    /// Errors if no primary method applies, or if the two most specific tie
128    /// (an ambiguous primary).
129    pub fn select_primary(&self, cx: &mut Cx, args: &[Value]) -> Result<MethodSpecificity> {
130        let selected = self.selected_primary(cx, args)?;
131        Ok(selected.specificity)
132    }
133
134    /// Returns the method ids in the order [`call`](GenericFunction::call) runs them.
135    ///
136    /// The plan is around (most specific first), before (most specific first),
137    /// the selected primary, then after (least specific first).
138    pub fn dispatch_order(&self, cx: &mut Cx, args: &[Value]) -> Result<Vec<Symbol>> {
139        Ok(self
140            .execution_plan(cx, args)?
141            .into_iter()
142            .map(|index| self.methods[index].id().clone())
143            .collect())
144    }
145
146    /// Returns every applicable method's specificity, ordered for display.
147    ///
148    /// Sorts by combination role, then most specific first, then method id;
149    /// backs the dispatch `inspect` operation.
150    pub fn inspect_specificity(
151        &self,
152        cx: &mut Cx,
153        args: &[Value],
154    ) -> Result<Vec<MethodSpecificity>> {
155        let mut accepted = Vec::new();
156        for method in &self.methods {
157            if let Some(specificity) = method.match_args(cx, args)? {
158                accepted.push(specificity);
159            }
160        }
161        accepted.sort_by(|left, right| {
162            left.role()
163                .combination_rank()
164                .cmp(&right.role().combination_rank())
165                .then_with(|| compare_specificity(right, left))
166                .then_with(|| left.method().cmp(right.method()))
167        });
168        Ok(accepted)
169    }
170
171    /// Dispatches the call: runs the combination and returns the primary result.
172    ///
173    /// Executes around, before, the selected primary, and after methods in
174    /// order; errors if no applicable primary method exists.
175    pub fn call(&self, cx: &mut Cx, args: &[Value]) -> Result<Value> {
176        let plan = self.execution_plan(cx, args)?;
177        let mut primary_result = None;
178        for index in plan {
179            let method = &self.methods[index];
180            let result = method.invoke(cx, args)?;
181            if method.role() == MethodRole::Primary {
182                primary_result = Some(result);
183            }
184        }
185        primary_result.ok_or_else(|| {
186            Error::Eval(format!(
187                "generic function {} has no applicable primary method",
188                self.name
189            ))
190        })
191    }
192
193    /// Dispatches the call for a named language profile.
194    ///
195    /// One generic function serves every profile: dispatch is profile-neutral,
196    /// so this delegates to [`call`](GenericFunction::call). The `profile`
197    /// argument lets profile-specific surfaces share a single generic.
198    pub fn call_for_profile(
199        &self,
200        cx: &mut Cx,
201        _profile: &Symbol,
202        args: &[Value],
203    ) -> Result<Value> {
204        self.call(cx, args)
205    }
206
207    fn execution_plan(&self, cx: &mut Cx, args: &[Value]) -> Result<Vec<usize>> {
208        let mut around = self.applicable_for_role(cx, args, MethodRole::Around)?;
209        let mut before = self.applicable_for_role(cx, args, MethodRole::Before)?;
210        let mut after = self.applicable_for_role(cx, args, MethodRole::After)?;
211        sort_most_specific_first(&mut around);
212        sort_most_specific_first(&mut before);
213        sort_least_specific_first(&mut after);
214        let primary = self.selected_primary(cx, args)?;
215
216        let mut plan = Vec::with_capacity(around.len() + before.len() + 1 + after.len());
217        plan.extend(around.into_iter().map(|method| method.index));
218        plan.extend(before.into_iter().map(|method| method.index));
219        plan.push(primary.index);
220        plan.extend(after.into_iter().map(|method| method.index));
221        Ok(plan)
222    }
223
224    fn selected_primary(&self, cx: &mut Cx, args: &[Value]) -> Result<ApplicableMethod> {
225        let mut primary = self.applicable_for_role(cx, args, MethodRole::Primary)?;
226        if primary.is_empty() {
227            cx.push_diagnostic(self.selection_diagnostic("no applicable primary method"));
228            return Err(Error::Eval(format!(
229                "generic function {} has no applicable primary method",
230                self.name
231            )));
232        }
233        sort_most_specific_first(&mut primary);
234        if primary.len() > 1
235            && compare_specificity(&primary[0].specificity, &primary[1].specificity)
236                == Ordering::Equal
237        {
238            cx.push_diagnostic(self.selection_diagnostic("ambiguous primary methods"));
239            return Err(ambiguous_primary_error(
240                &self.name,
241                primary[0].specificity.method(),
242                primary[1].specificity.method(),
243            ));
244        }
245        Ok(primary.remove(0))
246    }
247
248    fn applicable_for_role(
249        &self,
250        cx: &mut Cx,
251        args: &[Value],
252        role: MethodRole,
253    ) -> Result<Vec<ApplicableMethod>> {
254        let mut accepted = Vec::new();
255        for (index, method) in self.methods.iter().enumerate() {
256            if method.role() != role {
257                continue;
258            }
259            if let Some(specificity) = method.match_args(cx, args)? {
260                accepted.push(ApplicableMethod { index, specificity });
261            }
262        }
263        Ok(accepted)
264    }
265
266    fn selection_diagnostic(&self, reason: &'static str) -> Diagnostic {
267        let message = format!("generic function {}: {reason}", self.name);
268        let mut diagnostic = HintMetadata::new(
269            Symbol::qualified("runtime-hint", "overload-selection"),
270            "dispatch selection",
271        )
272        .with_detail(message.clone())
273        .with_tag(Symbol::qualified("runtime", "dispatch"))
274        .with_argument(self.name.clone())
275        .attach_to(Diagnostic::error(message).with_code(Symbol::qualified("runtime", "dispatch")));
276        for hint in self.operation_hints() {
277            diagnostic = hint.attach_to(diagnostic);
278        }
279        diagnostic
280    }
281}
282
283struct ApplicableMethod {
284    index: usize,
285    specificity: MethodSpecificity,
286}
287
288fn sort_most_specific_first(methods: &mut [ApplicableMethod]) {
289    methods.sort_by(|left, right| {
290        compare_specificity(&right.specificity, &left.specificity)
291            .then_with(|| left.specificity.method().cmp(right.specificity.method()))
292    });
293}
294
295fn sort_least_specific_first(methods: &mut [ApplicableMethod]) {
296    methods.sort_by(|left, right| {
297        compare_specificity(&left.specificity, &right.specificity)
298            .then_with(|| left.specificity.method().cmp(right.specificity.method()))
299    });
300}