Skip to main content

bicmath_core/
contract.rs

1//! Module and function contracts, invocation arguments, and outcomes.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use num_bigint::BigInt;
7use num_traits::ToPrimitive;
8use serde::{Deserialize, Serialize};
9
10use crate::context::{Conversion, ExecContext};
11use crate::envelope::Exactness;
12use crate::error::{EngineError, ErrorCode};
13use crate::number::{Decimal, Number, NumericMode};
14use crate::schema::ValueSchema;
15use crate::value::Value;
16
17/// A statically registered module.
18#[derive(Clone)]
19pub struct Module {
20    pub descriptor: ModuleDescriptor,
21    pub functions: Vec<Arc<dyn Function>>,
22}
23
24impl Module {
25    pub fn new(descriptor: ModuleDescriptor, functions: Vec<Arc<dyn Function>>) -> Module {
26        Module {
27            descriptor,
28            functions,
29        }
30    }
31}
32
33/// Metadata describing a module.
34#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
35pub struct ModuleDescriptor {
36    pub id: String,
37    pub title: String,
38    pub version: String,
39    pub description: String,
40    pub capabilities: Vec<String>,
41    pub dependencies: Vec<String>,
42    pub supported_modes: Vec<NumericMode>,
43    pub license: String,
44    pub source: String,
45}
46
47impl ModuleDescriptor {
48    pub fn new(
49        id: impl Into<String>,
50        title: impl Into<String>,
51        version: impl Into<String>,
52        description: impl Into<String>,
53    ) -> ModuleDescriptor {
54        ModuleDescriptor {
55            id: id.into(),
56            title: title.into(),
57            version: version.into(),
58            description: description.into(),
59            capabilities: Vec::new(),
60            dependencies: Vec::new(),
61            supported_modes: vec![NumericMode::Auto],
62            license: "MIT".to_string(),
63            source: String::new(),
64        }
65    }
66
67    pub fn with_capabilities(
68        mut self,
69        capabilities: impl IntoIterator<Item = impl Into<String>>,
70    ) -> Self {
71        self.capabilities = capabilities.into_iter().map(Into::into).collect();
72        self
73    }
74
75    pub fn with_dependencies(
76        mut self,
77        dependencies: impl IntoIterator<Item = impl Into<String>>,
78    ) -> Self {
79        self.dependencies = dependencies.into_iter().map(Into::into).collect();
80        self
81    }
82
83    pub fn with_modes(mut self, modes: impl IntoIterator<Item = NumericMode>) -> Self {
84        self.supported_modes = modes.into_iter().collect();
85        self
86    }
87
88    pub fn with_source(mut self, source: impl Into<String>) -> Self {
89        self.source = source.into();
90        self
91    }
92}
93
94/// Whether a function has side effects.
95#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "snake_case")]
97pub enum Purity {
98    Pure,
99}
100
101/// Determinism class of a function.
102#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(rename_all = "snake_case")]
104pub enum Determinism {
105    Deterministic,
106    SeededRandom,
107}
108
109/// Rough cost class, used for documentation and batch budgeting.
110#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum CostClass {
113    Constant,
114    Linear,
115    Quadratic,
116    Cubic,
117    Iterative,
118}
119
120/// Deprecation metadata.
121#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
122pub struct Deprecation {
123    pub since: String,
124    pub message: String,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub replacement: Option<String>,
127}
128
129/// A function parameter.
130#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
131pub struct ParamDescriptor {
132    pub name: String,
133    pub description: String,
134    pub schema: ValueSchema,
135    pub required: bool,
136    /// Whether a string may be used as a numeric shorthand for this parameter.
137    pub numeric_shorthand: bool,
138    /// Whether the parameter may be supplied positionally in expressions.
139    pub positional: bool,
140}
141
142impl ParamDescriptor {
143    pub fn required(
144        name: impl Into<String>,
145        description: impl Into<String>,
146        schema: ValueSchema,
147    ) -> ParamDescriptor {
148        let numeric = schema.is_numeric();
149        ParamDescriptor {
150            name: name.into(),
151            description: description.into(),
152            schema,
153            required: true,
154            numeric_shorthand: numeric,
155            positional: true,
156        }
157    }
158
159    pub fn optional(
160        name: impl Into<String>,
161        description: impl Into<String>,
162        schema: ValueSchema,
163    ) -> ParamDescriptor {
164        let numeric = schema.is_numeric();
165        ParamDescriptor {
166            name: name.into(),
167            description: description.into(),
168            schema,
169            required: false,
170            numeric_shorthand: numeric,
171            positional: true,
172        }
173    }
174
175    pub fn with_shorthand(mut self, allowed: bool) -> Self {
176        self.numeric_shorthand = allowed;
177        self
178    }
179
180    pub fn with_positional(mut self, allowed: bool) -> Self {
181        self.positional = allowed;
182        self
183    }
184}
185
186/// Expected outcome of a documented example.
187///
188/// Serialized explicitly because an internally tagged enum cannot represent
189/// newtype variants whose payload is not a map (booleans, arrays, strings).
190#[derive(Clone, Debug, PartialEq)]
191pub enum ExampleExpectation {
192    Value(Value),
193    Error(ErrorCode),
194    Contains(String),
195}
196
197impl Serialize for ExampleExpectation {
198    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
199        use serde::ser::SerializeMap;
200        match self {
201            ExampleExpectation::Value(value) => {
202                let mut map = serializer.serialize_map(Some(2))?;
203                map.serialize_entry("type", "value")?;
204                map.serialize_entry("value", value)?;
205                map.end()
206            }
207            ExampleExpectation::Error(code) => {
208                let mut map = serializer.serialize_map(Some(2))?;
209                map.serialize_entry("type", "error")?;
210                map.serialize_entry("code", code)?;
211                map.end()
212            }
213            ExampleExpectation::Contains(text) => {
214                let mut map = serializer.serialize_map(Some(2))?;
215                map.serialize_entry("type", "contains")?;
216                map.serialize_entry("text", text)?;
217                map.end()
218            }
219        }
220    }
221}
222
223impl<'de> Deserialize<'de> for ExampleExpectation {
224    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
225        use serde::de::Error as DeError;
226        let raw = serde_json::Value::deserialize(deserializer)?;
227        let kind = raw
228            .get("type")
229            .and_then(|value| value.as_str())
230            .ok_or_else(|| D::Error::custom("example expectation is missing \"type\""))?;
231        match kind {
232            "value" => {
233                let value = raw
234                    .get("value")
235                    .ok_or_else(|| D::Error::custom("value expectation is missing \"value\""))?;
236                Ok(ExampleExpectation::Value(
237                    serde_json::from_value(value.clone()).map_err(D::Error::custom)?,
238                ))
239            }
240            "error" => {
241                let code = raw
242                    .get("code")
243                    .ok_or_else(|| D::Error::custom("error expectation is missing \"code\""))?;
244                Ok(ExampleExpectation::Error(
245                    serde_json::from_value(code.clone()).map_err(D::Error::custom)?,
246                ))
247            }
248            "contains" => {
249                let text = raw
250                    .get("text")
251                    .and_then(|value| value.as_str())
252                    .ok_or_else(|| D::Error::custom("contains expectation is missing \"text\""))?;
253                Ok(ExampleExpectation::Contains(text.to_string()))
254            }
255            other => Err(D::Error::custom(format!(
256                "unknown example expectation type {other:?}"
257            ))),
258        }
259    }
260}
261
262/// A documented, executable example. CI runs every example and checks the
263/// expectation so documentation cannot drift from behaviour.
264#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
265pub struct Example {
266    pub title: String,
267    pub arguments: BTreeMap<String, Value>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub expected: Option<ExampleExpectation>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub note: Option<String>,
272}
273
274impl Example {
275    pub fn new(title: impl Into<String>, arguments: BTreeMap<String, Value>) -> Example {
276        Example {
277            title: title.into(),
278            arguments,
279            expected: None,
280            note: None,
281        }
282    }
283
284    pub fn with_value(mut self, value: Value) -> Example {
285        self.expected = Some(ExampleExpectation::Value(value));
286        self
287    }
288
289    pub fn with_error(mut self, code: ErrorCode) -> Example {
290        self.expected = Some(ExampleExpectation::Error(code));
291        self
292    }
293
294    pub fn with_contains(mut self, text: impl Into<String>) -> Example {
295        self.expected = Some(ExampleExpectation::Contains(text.into()));
296        self
297    }
298}
299
300/// The authoritative function declaration.
301#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
302pub struct FunctionDescriptor {
303    pub id: String,
304    pub module: String,
305    pub version: String,
306    pub title: String,
307    pub summary: String,
308    pub description: String,
309    pub parameters: Vec<ParamDescriptor>,
310    pub output: ValueSchema,
311    pub output_description: String,
312    pub modes: Vec<NumericMode>,
313    pub purity: Purity,
314    pub determinism: Determinism,
315    pub cost: CostClass,
316    pub units_rule: String,
317    pub method_ref: String,
318    pub examples: Vec<Example>,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub deprecated: Option<Deprecation>,
321    pub tags: Vec<String>,
322}
323
324impl FunctionDescriptor {
325    pub fn new(
326        id: impl Into<String>,
327        module: impl Into<String>,
328        version: impl Into<String>,
329        title: impl Into<String>,
330        summary: impl Into<String>,
331    ) -> FunctionDescriptor {
332        FunctionDescriptor {
333            id: id.into(),
334            module: module.into(),
335            version: version.into(),
336            title: title.into(),
337            summary: summary.into(),
338            description: String::new(),
339            parameters: Vec::new(),
340            output: ValueSchema::Any,
341            output_description: String::new(),
342            modes: vec![NumericMode::Auto],
343            purity: Purity::Pure,
344            determinism: Determinism::Deterministic,
345            cost: CostClass::Constant,
346            units_rule: String::new(),
347            method_ref: String::new(),
348            examples: Vec::new(),
349            deprecated: None,
350            tags: Vec::new(),
351        }
352    }
353
354    pub fn with_description(mut self, description: impl Into<String>) -> Self {
355        self.description = description.into();
356        self
357    }
358
359    pub fn with_parameters(mut self, parameters: Vec<ParamDescriptor>) -> Self {
360        self.parameters = parameters;
361        self
362    }
363
364    pub fn with_output(mut self, output: ValueSchema, description: impl Into<String>) -> Self {
365        self.output = output;
366        self.output_description = description.into();
367        self
368    }
369
370    pub fn with_modes(mut self, modes: impl IntoIterator<Item = NumericMode>) -> Self {
371        self.modes = modes.into_iter().collect();
372        self
373    }
374
375    pub fn with_cost(mut self, cost: CostClass) -> Self {
376        self.cost = cost;
377        self
378    }
379
380    pub fn with_units_rule(mut self, rule: impl Into<String>) -> Self {
381        self.units_rule = rule.into();
382        self
383    }
384
385    pub fn with_method_ref(mut self, method_ref: impl Into<String>) -> Self {
386        self.method_ref = method_ref.into();
387        self
388    }
389
390    pub fn with_examples(mut self, examples: Vec<Example>) -> Self {
391        self.examples = examples;
392        self
393    }
394
395    pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
396        self.tags = tags.into_iter().map(Into::into).collect();
397        self
398    }
399
400    pub fn with_deprecation(mut self, deprecation: Deprecation) -> Self {
401        self.deprecated = Some(deprecation);
402        self
403    }
404
405    pub fn parameter(&self, name: &str) -> Option<&ParamDescriptor> {
406        self.parameters.iter().find(|p| p.name == name)
407    }
408}
409
410/// A reference to a function and its version.
411#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
412pub struct FunctionRef {
413    pub id: String,
414    pub version: String,
415}
416
417/// Warning attached to a result.
418#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
419pub struct Warning {
420    pub code: String,
421    pub message: String,
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub details: Option<Value>,
424}
425
426impl Warning {
427    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Warning {
428        Warning {
429            code: code.into(),
430            message: message.into(),
431            details: None,
432        }
433    }
434
435    pub fn with_details(mut self, details: Value) -> Warning {
436        self.details = Some(details);
437        self
438    }
439}
440
441/// Where an assumption came from.
442#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
443#[serde(rename_all = "snake_case")]
444pub enum AssumptionSource {
445    /// Supplied by the caller and not verified by the engine.
446    UserSupplied,
447    /// Checked by the engine for the supplied data.
448    Checked,
449    /// Required by the method but not verifiable from the supplied inputs.
450    Unverified,
451}
452
453/// An explicit method assumption.
454#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
455pub struct Assumption {
456    pub id: String,
457    pub statement: String,
458    pub source: AssumptionSource,
459}
460
461impl Assumption {
462    pub fn user_supplied(id: impl Into<String>, statement: impl Into<String>) -> Assumption {
463        Assumption {
464            id: id.into(),
465            statement: statement.into(),
466            source: AssumptionSource::UserSupplied,
467        }
468    }
469
470    pub fn checked(id: impl Into<String>, statement: impl Into<String>) -> Assumption {
471        Assumption {
472            id: id.into(),
473            statement: statement.into(),
474            source: AssumptionSource::Checked,
475        }
476    }
477
478    pub fn unverified(id: impl Into<String>, statement: impl Into<String>) -> Assumption {
479        Assumption {
480            id: id.into(),
481            statement: statement.into(),
482            source: AssumptionSource::Unverified,
483        }
484    }
485}
486
487/// A validated numerical error estimate, only present when the method can
488/// actually provide one.
489#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
490pub struct ErrorEstimate {
491    pub kind: String,
492    pub bound: Value,
493    pub method: String,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub notes: Option<String>,
496}
497
498impl ErrorEstimate {
499    pub fn new(kind: impl Into<String>, bound: Value, method: impl Into<String>) -> ErrorEstimate {
500        ErrorEstimate {
501            kind: kind.into(),
502            bound,
503            method: method.into(),
504            notes: None,
505        }
506    }
507
508    pub fn with_notes(mut self, notes: impl Into<String>) -> ErrorEstimate {
509        self.notes = Some(notes.into());
510        self
511    }
512}
513
514/// One bounded trace step.
515#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
516pub struct TraceStep {
517    pub op: String,
518    pub detail: BTreeMap<String, Value>,
519}
520
521/// A bounded execution trace of computational steps. This is not a narrative
522/// explanation and contains no hidden reasoning.
523#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
524pub struct Trace {
525    pub steps: Vec<TraceStep>,
526}
527
528impl Trace {
529    pub fn new() -> Trace {
530        Trace { steps: Vec::new() }
531    }
532
533    pub fn push(&mut self, step: TraceStep) {
534        self.steps.push(step);
535    }
536}
537
538/// A successful invocation outcome.
539#[derive(Clone, Debug)]
540pub struct Outcome {
541    pub value: Value,
542    pub exactness: Exactness,
543    pub warnings: Vec<Warning>,
544    pub assumptions: Vec<Assumption>,
545    pub error_estimate: Option<ErrorEstimate>,
546    pub trace: Option<Trace>,
547    pub conversions: Vec<Conversion>,
548}
549
550impl Outcome {
551    pub fn new(value: Value, exactness: Exactness) -> Outcome {
552        Outcome {
553            value,
554            exactness,
555            warnings: Vec::new(),
556            assumptions: Vec::new(),
557            error_estimate: None,
558            trace: None,
559            conversions: Vec::new(),
560        }
561    }
562
563    pub fn exact(value: Value) -> Outcome {
564        Outcome::new(value, Exactness::Exact)
565    }
566
567    pub fn rounded(value: Value) -> Outcome {
568        Outcome::new(value, Exactness::Rounded)
569    }
570
571    pub fn approximate(value: Value) -> Outcome {
572        Outcome::new(value, Exactness::Approximate)
573    }
574
575    pub fn with_warning(mut self, warning: Warning) -> Outcome {
576        self.warnings.push(warning);
577        self
578    }
579
580    pub fn with_assumption(mut self, assumption: Assumption) -> Outcome {
581        self.assumptions.push(assumption);
582        self
583    }
584
585    pub fn with_error_estimate(mut self, estimate: ErrorEstimate) -> Outcome {
586        self.error_estimate = Some(estimate);
587        self
588    }
589
590    pub fn with_trace(mut self, trace: Trace) -> Outcome {
591        self.trace = Some(trace);
592        self
593    }
594
595    pub fn with_conversion(mut self, conversion: Conversion) -> Outcome {
596        self.conversions.push(conversion);
597        self
598    }
599}
600
601/// Validated arguments for a function invocation.
602#[derive(Clone, Debug, Default)]
603pub struct Args {
604    values: BTreeMap<String, Value>,
605}
606
607impl Args {
608    pub fn new(values: BTreeMap<String, Value>) -> Args {
609        Args { values }
610    }
611
612    pub fn get(&self, name: &str) -> Option<&Value> {
613        self.values.get(name)
614    }
615
616    pub fn contains(&self, name: &str) -> bool {
617        self.values.contains_key(name)
618    }
619
620    pub fn iter(&self) -> impl Iterator<Item = (&String, &Value)> {
621        self.values.iter()
622    }
623
624    pub fn into_map(self) -> BTreeMap<String, Value> {
625        self.values
626    }
627
628    fn expected(name: &str, kind: &str) -> EngineError {
629        EngineError::malformed(format!(
630            "missing or invalid argument {name:?}: expected {kind}"
631        ))
632        .with_path(name.to_string())
633    }
634
635    pub fn require(&self, name: &str) -> Result<&Value, EngineError> {
636        self.values
637            .get(name)
638            .ok_or_else(|| Self::expected(name, "a value"))
639    }
640
641    pub fn number(&self, name: &str) -> Result<&Number, EngineError> {
642        self.require(name)?
643            .as_number()
644            .map_err(|e| e.with_path(name.to_string()))
645    }
646
647    pub fn optional_number(&self, name: &str) -> Result<Option<&Number>, EngineError> {
648        match self.values.get(name) {
649            None | Some(Value::Null) => Ok(None),
650            Some(value) => Ok(Some(
651                value
652                    .as_number()
653                    .map_err(|e| e.with_path(name.to_string()))?,
654            )),
655        }
656    }
657
658    pub fn text(&self, name: &str) -> Result<&str, EngineError> {
659        self.require(name)?
660            .as_text()
661            .map_err(|e| e.with_path(name.to_string()))
662    }
663
664    pub fn optional_text(&self, name: &str) -> Result<Option<&str>, EngineError> {
665        match self.values.get(name) {
666            None | Some(Value::Null) => Ok(None),
667            Some(value) => Ok(Some(
668                value.as_text().map_err(|e| e.with_path(name.to_string()))?,
669            )),
670        }
671    }
672
673    pub fn bool(&self, name: &str) -> Result<bool, EngineError> {
674        self.require(name)?
675            .as_bool()
676            .map_err(|e| e.with_path(name.to_string()))
677    }
678
679    pub fn optional_bool(&self, name: &str) -> Result<Option<bool>, EngineError> {
680        match self.values.get(name) {
681            None | Some(Value::Null) => Ok(None),
682            Some(value) => Ok(Some(
683                value.as_bool().map_err(|e| e.with_path(name.to_string()))?,
684            )),
685        }
686    }
687
688    pub fn array(&self, name: &str) -> Result<&[Value], EngineError> {
689        self.require(name)?
690            .as_array()
691            .map_err(|e| e.with_path(name.to_string()))
692    }
693
694    pub fn record(&self, name: &str) -> Result<&BTreeMap<String, Value>, EngineError> {
695        self.require(name)?
696            .as_record()
697            .map_err(|e| e.with_path(name.to_string()))
698    }
699
700    pub fn money(&self, name: &str) -> Result<(&Number, &str), EngineError> {
701        self.require(name)?
702            .as_money()
703            .map_err(|e| e.with_path(name.to_string()))
704    }
705
706    pub fn quantity(&self, name: &str) -> Result<(&Number, crate::value::Dimension), EngineError> {
707        self.require(name)?
708            .as_quantity()
709            .map_err(|e| e.with_path(name.to_string()))
710    }
711
712    pub fn integer(&self, name: &str) -> Result<BigInt, EngineError> {
713        match self.number(name)? {
714            Number::Integer(value) => Ok(value.clone()),
715            other => Err(Self::expected(
716                name,
717                &format!("an integer, found {}", other.kind_name()),
718            )),
719        }
720    }
721
722    pub fn optional_integer(&self, name: &str) -> Result<Option<BigInt>, EngineError> {
723        match self.optional_number(name)? {
724            None => Ok(None),
725            Some(Number::Integer(value)) => Ok(Some(value.clone())),
726            Some(other) => Err(Self::expected(
727                name,
728                &format!("an integer, found {}", other.kind_name()),
729            )),
730        }
731    }
732
733    /// Convert an integer argument to `usize`, rejecting negatives and overflow.
734    pub fn usize_param(&self, name: &str) -> Result<usize, EngineError> {
735        let value = self.integer(name)?;
736        value
737            .to_usize()
738            .ok_or_else(|| Self::expected(name, "a non-negative machine-sized integer"))
739    }
740
741    pub fn u32_param(&self, name: &str) -> Result<u32, EngineError> {
742        let value = self.integer(name)?;
743        value
744            .to_u32()
745            .ok_or_else(|| Self::expected(name, "a non-negative 32-bit integer"))
746    }
747
748    pub fn u64_param(&self, name: &str) -> Result<u64, EngineError> {
749        let value = self.integer(name)?;
750        value
751            .to_u64()
752            .ok_or_else(|| Self::expected(name, "a non-negative 64-bit integer"))
753    }
754
755    /// Convert a numeric argument to f64. This is an explicit conversion and is
756    /// recorded by callers when it matters; it never happens for exact results.
757    pub fn f64_param(&self, name: &str) -> Result<f64, EngineError> {
758        let value = self.number(name)?;
759        value
760            .to_f64()
761            .ok_or_else(|| Self::expected(name, "a value representable as float64"))
762    }
763
764    pub fn optional_f64(&self, name: &str) -> Result<Option<f64>, EngineError> {
765        match self.optional_number(name)? {
766            None => Ok(None),
767            Some(value) => value
768                .to_f64()
769                .map(Some)
770                .ok_or_else(|| Self::expected(name, "a value representable as float64")),
771        }
772    }
773
774    pub fn decimal(&self, name: &str) -> Result<Decimal, EngineError> {
775        match self.number(name)? {
776            Number::Decimal(value) => Ok(value.clone()),
777            Number::Integer(value) => Ok(Decimal::from_bigint(value.clone())),
778            other => Err(Self::expected(
779                name,
780                &format!("a decimal, found {}", other.kind_name()),
781            )),
782        }
783    }
784
785    pub fn optional_decimal(&self, name: &str) -> Result<Option<Decimal>, EngineError> {
786        match self.optional_number(name)? {
787            None => Ok(None),
788            Some(number) => match number {
789                Number::Decimal(value) => Ok(Some(value.clone())),
790                Number::Integer(value) => Ok(Some(Decimal::from_bigint(value.clone()))),
791                other => Err(Self::expected(
792                    name,
793                    &format!("a decimal, found {}", other.kind_name()),
794                )),
795            },
796        }
797    }
798}
799
800/// The callable interface implemented by every function in every module.
801pub trait Function: Send + Sync {
802    fn descriptor(&self) -> &FunctionDescriptor;
803    fn invoke(&self, args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError>;
804}
805
806/// A function implemented by a plain function pointer plus its descriptor.
807///
808/// This is the explicit registration mechanism used by the numerical modules:
809/// no macro framework, no hidden behaviour. The descriptor is the
810/// authoritative schema; the engine validates arguments against it before
811/// [`Function::invoke`] is called.
812pub struct SimpleFunction {
813    descriptor: FunctionDescriptor,
814    invoke: fn(&Args, &ExecContext) -> Result<Outcome, EngineError>,
815}
816
817impl SimpleFunction {
818    pub fn new(
819        descriptor: FunctionDescriptor,
820        invoke: fn(&Args, &ExecContext) -> Result<Outcome, EngineError>,
821    ) -> SimpleFunction {
822        SimpleFunction { descriptor, invoke }
823    }
824
825    pub fn arc(
826        descriptor: FunctionDescriptor,
827        invoke: fn(&Args, &ExecContext) -> Result<Outcome, EngineError>,
828    ) -> Arc<dyn Function> {
829        Arc::new(SimpleFunction::new(descriptor, invoke))
830    }
831}
832
833impl Function for SimpleFunction {
834    fn descriptor(&self) -> &FunctionDescriptor {
835        &self.descriptor
836    }
837
838    fn invoke(&self, args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
839        (self.invoke)(args, ctx)
840    }
841}
842
843/// Helper to require that the context mode is supported by a function.
844pub fn require_mode(
845    ctx: &ExecContext,
846    modes: &[NumericMode],
847    function_id: &str,
848) -> Result<(), EngineError> {
849    if modes.contains(&ctx.numeric.mode) {
850        Ok(())
851    } else {
852        Err(EngineError::new(
853            ErrorCode::UnsupportedNumericMode,
854            format!(
855                "function {function_id} supports modes {:?}, not {}",
856                modes, ctx.numeric.mode
857            ),
858        ))
859    }
860}