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/// The catalog, `u:dir` and the time zone; the host is left out.
130impl core::fmt::Debug for FnContext<'_> {
131    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
132        f.debug_struct("FnContext")
133            .field("catalog", self.catalog)
134            .field("dir", &self.dir)
135            .field("time_zone", self.time_zone)
136            .finish_non_exhaustive()
137    }
138}
139
140/// A resolved option value.
141#[derive(Clone, Copy, Debug)]
142#[non_exhaustive]
143pub struct OptionValue<'o, 'a> {
144    /// The value.
145    pub value: &'o Value<'a>,
146    /// Whether it was set directly by a literal (formatting.md, "Resolved
147    /// Values": handlers may require literals, like `select`).
148    pub literal: bool,
149}
150
151/// The resolved options of an expression, in source order.
152pub(crate) type OptionList<'o, 'a> = Scratch<(&'a str, OptionValue<'o, 'a>)>;
153
154/// A view of an [`OptionList`] (possibly empty).
155#[derive(Clone, Copy)]
156pub(crate) struct OptionEntries<'o, 'a> {
157    list: Option<&'o OptionList<'o, 'a>>,
158}
159
160impl<'o, 'a> OptionEntries<'o, 'a> {
161    pub(crate) fn new(list: &'o OptionList<'o, 'a>) -> Self {
162        OptionEntries { list: Some(list) }
163    }
164
165    pub(crate) fn len(self) -> usize {
166        self.list.map_or(0, Scratch::len)
167    }
168
169    pub(crate) fn get(self, i: usize) -> Option<&'o (&'a str, OptionValue<'o, 'a>)> {
170        self.list?.get(i)
171    }
172}
173
174/// The resolved options passed to [`Function::resolve`]. Their order is not
175/// significant; a repeated name (a Duplicate Option Name, which the build
176/// rejects) resolves to the last.
177#[derive(Clone, Copy)]
178pub struct Options<'o, 'a> {
179    pub(crate) entries: OptionEntries<'o, 'a>,
180}
181
182impl<'o, 'a> Options<'o, 'a> {
183    /// The option named `name` (NFC).
184    pub fn get(&self, name: &str) -> Option<OptionValue<'o, 'a>> {
185        let mut found = None;
186        for i in 0..self.entries.len() {
187            if let Some((n, v)) = self.entries.get(i)
188                && *n == name
189            {
190                found = Some(*v);
191            }
192        }
193        found
194    }
195
196    /// Every option, in source order.
197    pub fn iter(&self) -> impl Iterator<Item = (&'a str, OptionValue<'o, 'a>)> + 'o {
198        let entries = self.entries;
199        (0..entries.len()).filter_map(move |i| entries.get(i).copied())
200    }
201
202    /// The number of options.
203    pub fn len(&self) -> usize {
204        self.entries.len()
205    }
206
207    /// Whether there are none.
208    pub fn is_empty(&self) -> bool {
209        self.entries.len() == 0
210    }
211}
212
213/// How many options there are. Written apart from the iterator a handler
214/// uses (`iter`), so that a client's build inlines that as before.
215impl core::fmt::Debug for Options<'_, '_> {
216    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
217        f.debug_struct("Options")
218            .field("len", &self.entries.len())
219            .finish_non_exhaustive()
220    }
221}
222
223/// The function handlers an application links: closed world (B13). Build
224/// code generates it from exactly the functions the corpus uses, e.g.
225/// `static REGISTRY: Registry = Registry::new(&[("integer",
226/// &mf2_runtime::functions::INTEGER)]);` — an unused handler is never
227/// referenced, so never linked.
228#[derive(Clone, Copy)]
229pub struct Registry {
230    functions: &'static [(&'static str, &'static dyn Function)],
231    numbers: Option<&'static dyn Function>,
232    dates: Option<&'static dyn Function>,
233}
234
235impl Registry {
236    /// No functions: every annotation is an Unknown Function.
237    pub const EMPTY: Registry = Registry {
238        functions: &[],
239        numbers: None,
240        dates: None,
241    };
242
243    /// The registry of `functions`: `(identifier, handler)`, the identifier
244    /// as the catalog's FUNCS has it (`ns:name`, NFC). The first entry of a
245    /// repeated identifier wins.
246    pub const fn new(functions: &'static [(&'static str, &'static dyn Function)]) -> Registry {
247        Registry {
248            functions,
249            numbers: None,
250            dates: None,
251        }
252    }
253
254    /// This registry, with `f` formatting unannotated numeric values
255    /// (integer, float and decimal arguments): `mf2-fn-number`'s localized
256    /// exact value (`plans/03-runtime.md` §2.7). The evaluator checks such a
257    /// value as any unannotated value (a non-finite float is a Bad Operand)
258    /// and then asks `f` for its direction, text, sub-parts and part kind;
259    /// it still does not select. Without it they format in neutral digits
260    /// (§2.6).
261    #[must_use]
262    pub const fn with_numbers(self, f: &'static dyn Function) -> Registry {
263        Registry {
264            numbers: Some(f),
265            ..self
266        }
267    }
268
269    /// This registry, with `f` formatting unannotated date/time values
270    /// (`Arg::DateTime`, `CustomValue` has no say): `mf2-fn-datetime`, as
271    /// `:datetime` with its defaults (`plans/03-runtime.md` §2.7). The
272    /// evaluator asks `f` whether such a value formats, its direction, text,
273    /// sub-parts and part kind; it does not select. Without it an
274    /// unannotated date/time is a Bad Operand, so no date code is linked.
275    #[must_use]
276    pub const fn with_dates(self, f: &'static dyn Function) -> Registry {
277        Registry {
278            dates: Some(f),
279            ..self
280        }
281    }
282
283    /// The handler for an unannotated value `v` — a number or a date/time —
284    /// if the registry has one.
285    pub(crate) fn unannotated(&self, v: &Value<'_>) -> Option<&'static dyn Function> {
286        if unannotated::is_numeric(v) {
287            self.numbers
288        } else if unannotated::is_date_time(v) {
289            self.dates
290        } else {
291            None
292        }
293    }
294
295    /// The handler for `name`.
296    pub fn get(&self, name: &str) -> Option<&'static dyn Function> {
297        self.functions
298            .iter()
299            .find(|(n, _)| *n == name)
300            .map(|&(_, f)| f)
301    }
302}
303
304impl core::fmt::Debug for Registry {
305    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
306        f.debug_list()
307            .entries(self.functions.iter().map(|(n, _)| n))
308            .finish()
309    }
310}