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}