Skip to main content

mf2_runtime/
function.rs

1//! Function handlers and the closed-world registry (`plans/03-runtime.md`
2//! §2.4, §3; budget B13).
3
4use mf2_catalog::Catalog;
5use mf2_model::Dir;
6
7use crate::datetime::TimeZone;
8use crate::error::FormatError;
9use crate::host::Host;
10use crate::scratch::Scratch;
11use crate::sink::{ErrorSink, Sink, SubPartSink};
12use crate::unannotated;
13use crate::value::Value;
14
15/// A function handler. `:string`, `:number`, `:integer`, `:offset` are in
16/// [`crate::functions`]; custom functions implement the same trait (the
17/// suite's `:test:*` functions are written against it).
18///
19/// A handler first *resolves* an expression to a [`Value`]; the runtime then
20/// asks the same handler, for that value, whether and how it formats, its
21/// direction, and whether and how it selects.
22pub trait Function: Sync {
23    /// Function resolution (formatting.md): `operand` resolved — a
24    /// [`Value::Fallback`] when it failed to resolve, so the handler decides
25    /// (`plans/03-runtime.md` §2.6) — and `options` resolved with
26    /// `u:id`/`u:dir` removed. `None` makes the expression a fallback value;
27    /// the handler has reported why.
28    fn resolve<'a>(
29        &self,
30        cx: &FnContext<'_>,
31        operand: Option<&Value<'a>>,
32        options: &Options<'_, 'a>,
33        errs: &mut dyn ErrorSink,
34    ) -> Option<Value<'a>>;
35
36    /// Whether `value` can be formatted. `Err(e)`: the placeholder is a
37    /// fallback value and `e` is reported.
38    fn formattable(&self, cx: &FnContext<'_>, value: &Value<'_>) -> Result<(), FormatError> {
39        let _ = (cx, value);
40        Ok(())
41    }
42
43    /// Writes `value`'s formatted text.
44    fn format(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn Sink);
45
46    /// Writes `value`'s formatted sub-parts (none by default).
47    fn format_parts(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn SubPartSink) {
48        let _ = (cx, value, out);
49    }
50
51    /// The `type` of this handler's expression parts.
52    fn part_kind(&self) -> &'static str {
53        "string"
54    }
55
56    /// The direction of `value`'s formatted text (Default Bidi Strategy);
57    /// `Auto` = unknown.
58    fn dir(&self, cx: &FnContext<'_>, value: &Value<'_>) -> Dir {
59        let _ = (cx, value);
60        Dir::Auto
61    }
62
63    /// Whether `value` supports selection (formatting.md, "Resolve
64    /// Selectors"). A value that does not matches only `*`, with *Bad
65    /// Selector*.
66    fn selectable(&self, value: &Value<'_>) -> bool {
67        let _ = value;
68        false
69    }
70
71    /// Match(`value`, `key`); `key` is NFC. Report e.g. *Bad Variant Key*
72    /// through `errs`.
73    fn matches(
74        &self,
75        cx: &FnContext<'_>,
76        value: &Value<'_>,
77        key: &str,
78        errs: &mut dyn ErrorSink,
79    ) -> bool {
80        let _ = (cx, value, key, errs);
81        false
82    }
83
84    /// `BetterThan(value, key1, key2)`, for two keys that both match.
85    fn better_than(&self, cx: &FnContext<'_>, value: &Value<'_>, key1: &str, key2: &str) -> bool {
86        let _ = (cx, value, key1, key2);
87        false
88    }
89}
90
91/// What a handler may see of the formatting context: read-only and minimal
92/// (formatting.md, "Function Handler").
93#[derive(Clone, Copy)]
94pub struct FnContext<'x> {
95    pub(crate) catalog: &'x Catalog,
96    pub(crate) host: &'static dyn Host,
97    pub(crate) dir: Option<Dir>,
98    pub(crate) time_zone: &'x TimeZone,
99}
100
101impl<'x> FnContext<'x> {
102    /// The locale (the catalog's).
103    pub fn locale(&self) -> &'x str {
104        self.catalog.locale()
105    }
106
107    /// The expression's `u:dir` (`Ltr`, `Rtl` or `Auto`), if set.
108    pub fn dir(&self) -> Option<Dir> {
109        self.dir
110    }
111
112    /// The platform services.
113    pub fn host(&self) -> &'static dyn Host {
114        self.host
115    }
116
117    /// The catalog, for its locale data (`Catalog::locale_entry`).
118    #[doc(hidden)]
119    pub fn catalog(&self) -> &'x Catalog {
120        self.catalog
121    }
122
123    /// The formatting context's time zone (the default of `timeZone`).
124    pub fn time_zone(&self) -> &'x TimeZone {
125        self.time_zone
126    }
127}
128
129/// A resolved option value.
130#[derive(Clone, Copy)]
131#[non_exhaustive]
132pub struct OptionValue<'o, 'a> {
133    /// The value.
134    pub value: &'o Value<'a>,
135    /// Whether it was set directly by a literal (formatting.md, "Resolved
136    /// Values": handlers may require literals, like `select`).
137    pub literal: bool,
138}
139
140/// The resolved options of an expression, in source order.
141pub(crate) type OptionList<'o, 'a> = Scratch<(&'a str, OptionValue<'o, 'a>)>;
142
143/// A view of an [`OptionList`] (possibly empty).
144#[derive(Clone, Copy)]
145pub(crate) struct OptionEntries<'o, 'a> {
146    list: Option<&'o OptionList<'o, 'a>>,
147}
148
149impl<'o, 'a> OptionEntries<'o, 'a> {
150    pub(crate) fn new(list: &'o OptionList<'o, 'a>) -> Self {
151        OptionEntries { list: Some(list) }
152    }
153
154    pub(crate) fn len(self) -> usize {
155        self.list.map_or(0, Scratch::len)
156    }
157
158    pub(crate) fn get(self, i: usize) -> Option<&'o (&'a str, OptionValue<'o, 'a>)> {
159        self.list?.get(i)
160    }
161}
162
163/// The resolved options passed to [`Function::resolve`]. Their order is not
164/// significant; a repeated name (a Duplicate Option Name, which the build
165/// rejects) resolves to the last.
166#[derive(Clone, Copy)]
167pub struct Options<'o, 'a> {
168    pub(crate) entries: OptionEntries<'o, 'a>,
169}
170
171impl<'o, 'a> Options<'o, 'a> {
172    /// The option named `name` (NFC).
173    pub fn get(&self, name: &str) -> Option<OptionValue<'o, 'a>> {
174        let mut found = None;
175        for i in 0..self.entries.len() {
176            if let Some((n, v)) = self.entries.get(i)
177                && *n == name
178            {
179                found = Some(*v);
180            }
181        }
182        found
183    }
184
185    /// Every option, in source order.
186    pub fn iter(&self) -> impl Iterator<Item = (&'a str, OptionValue<'o, 'a>)> + 'o {
187        let entries = self.entries;
188        (0..entries.len()).filter_map(move |i| entries.get(i).copied())
189    }
190
191    /// The number of options.
192    pub fn len(&self) -> usize {
193        self.entries.len()
194    }
195
196    /// Whether there are none.
197    pub fn is_empty(&self) -> bool {
198        self.entries.len() == 0
199    }
200}
201
202/// The function handlers an application links: closed world (B13). Build
203/// code generates it from exactly the functions the corpus uses, e.g.
204/// `static REGISTRY: Registry = Registry::new(&[("integer",
205/// &mf2_runtime::functions::INTEGER)]);` — an unused handler is never
206/// referenced, so never linked.
207#[derive(Clone, Copy)]
208pub struct Registry {
209    functions: &'static [(&'static str, &'static dyn Function)],
210    numbers: Option<&'static dyn Function>,
211    dates: Option<&'static dyn Function>,
212}
213
214impl Registry {
215    /// No functions: every annotation is an Unknown Function.
216    pub const EMPTY: Registry = Registry {
217        functions: &[],
218        numbers: None,
219        dates: None,
220    };
221
222    /// The registry of `functions`: `(identifier, handler)`, the identifier
223    /// as the catalog's FUNCS has it (`ns:name`, NFC). The first entry of a
224    /// repeated identifier wins.
225    pub const fn new(functions: &'static [(&'static str, &'static dyn Function)]) -> Registry {
226        Registry {
227            functions,
228            numbers: None,
229            dates: None,
230        }
231    }
232
233    /// This registry, with `f` formatting unannotated numeric values
234    /// (integer, float and decimal arguments): `mf2-fn-number`'s localized
235    /// exact value (`plans/03-runtime.md` §2.7). The evaluator checks such a
236    /// value as any unannotated value (a non-finite float is a Bad Operand)
237    /// and then asks `f` for its direction, text, sub-parts and part kind;
238    /// it still does not select. Without it they format in neutral digits
239    /// (§2.6).
240    #[must_use]
241    pub const fn with_numbers(self, f: &'static dyn Function) -> Registry {
242        Registry {
243            numbers: Some(f),
244            ..self
245        }
246    }
247
248    /// This registry, with `f` formatting unannotated date/time values
249    /// (`Arg::DateTime`, `CustomValue` has no say): `mf2-fn-datetime`, as
250    /// `:datetime` with its defaults (`plans/03-runtime.md` §2.7). The
251    /// evaluator asks `f` whether such a value formats, its direction, text,
252    /// sub-parts and part kind; it does not select. Without it an
253    /// unannotated date/time is a Bad Operand, so no date code is linked.
254    #[must_use]
255    pub const fn with_dates(self, f: &'static dyn Function) -> Registry {
256        Registry {
257            dates: Some(f),
258            ..self
259        }
260    }
261
262    /// The handler for an unannotated value `v` — a number or a date/time —
263    /// if the registry has one.
264    pub(crate) fn unannotated(&self, v: &Value<'_>) -> Option<&'static dyn Function> {
265        if unannotated::is_numeric(v) {
266            self.numbers
267        } else if unannotated::is_date_time(v) {
268            self.dates
269        } else {
270            None
271        }
272    }
273
274    /// The handler for `name`.
275    pub fn get(&self, name: &str) -> Option<&'static dyn Function> {
276        self.functions
277            .iter()
278            .find(|(n, _)| *n == name)
279            .map(|&(_, f)| f)
280    }
281}
282
283impl core::fmt::Debug for Registry {
284    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
285        f.debug_list()
286            .entries(self.functions.iter().map(|(n, _)| n))
287            .finish()
288    }
289}