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}