Skip to main content

lora_builtins_meta/
lib.rs

1//! Metadata table for lora's namespaced builtin functions.
2//!
3//! Owned by this leaf crate so the analyzer (which validates function
4//! references at compile time), the executor (which dispatches on the
5//! `op` strings), and the editor's WASM bridge (which feeds the
6//! autocomplete / signature-hint surface) can all share a single
7//! declaration. Drift-safety tests in lora-executor assert that every
8//! entry here has a dispatch arm.
9//!
10//! This is intentionally more than an arity table: enum-like argument
11//! slots live here too, so analyzer rewrites and executor dispatch
12//! share one declaration for each builtin.
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct BuiltinSpec {
16    pub name: &'static str,
17    pub arity: Arity,
18    pub enum_arg_slots: &'static [usize],
19    pub type_arg_slots: &'static [usize],
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct BuiltinAlias {
24    pub alias: &'static str,
25    pub canonical: &'static str,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Arity {
30    pub min: usize,
31    pub max: Option<usize>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum FunctionId {
36    Builtin(&'static BuiltinSpec),
37    Aggregate(AggregateFunction),
38}
39
40impl FunctionId {
41    #[must_use]
42    pub const fn name(self) -> &'static str {
43        match self {
44            FunctionId::Builtin(spec) => spec.name,
45            FunctionId::Aggregate(function) => function.name(),
46        }
47    }
48
49    #[must_use]
50    pub const fn arity(self) -> Arity {
51        match self {
52            FunctionId::Builtin(spec) => spec.arity,
53            FunctionId::Aggregate(function) => function.arity(),
54        }
55    }
56
57    #[must_use]
58    pub const fn is_aggregate(self) -> bool {
59        matches!(self, FunctionId::Aggregate(_))
60    }
61
62    #[must_use]
63    pub fn eq_ignore_ascii_case(self, other: &str) -> bool {
64        self.name().eq_ignore_ascii_case(other)
65    }
66
67    #[must_use]
68    pub fn to_ascii_lowercase(self) -> String {
69        self.name().to_ascii_lowercase()
70    }
71
72    #[must_use]
73    pub const fn as_aggregate(self) -> Option<AggregateFunction> {
74        match self {
75            FunctionId::Aggregate(function) => Some(function),
76            FunctionId::Builtin(_) => None,
77        }
78    }
79
80    #[must_use]
81    pub fn builtin(name: &str) -> Option<Self> {
82        builtin_spec(name).map(FunctionId::Builtin)
83    }
84}
85
86impl std::fmt::Display for FunctionId {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.write_str(self.name())
89    }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum AggregateFunction {
94    Count,
95    Sum,
96    Avg,
97    Min,
98    Max,
99    Collect,
100    Stdev,
101    Stdevp,
102    PercentileCont,
103    PercentileDisc,
104}
105
106impl AggregateFunction {
107    #[must_use]
108    pub const fn name(self) -> &'static str {
109        match self {
110            AggregateFunction::Count => "count",
111            AggregateFunction::Sum => "sum",
112            AggregateFunction::Avg => "avg",
113            AggregateFunction::Min => "min",
114            AggregateFunction::Max => "max",
115            AggregateFunction::Collect => "collect",
116            AggregateFunction::Stdev => "stdev",
117            AggregateFunction::Stdevp => "stdevp",
118            AggregateFunction::PercentileCont => "percentilecont",
119            AggregateFunction::PercentileDisc => "percentiledisc",
120        }
121    }
122
123    #[must_use]
124    pub const fn arity(self) -> Arity {
125        match self {
126            AggregateFunction::Count => Arity {
127                min: 0,
128                max: Some(1),
129            },
130            AggregateFunction::Sum
131            | AggregateFunction::Avg
132            | AggregateFunction::Min
133            | AggregateFunction::Max
134            | AggregateFunction::Collect
135            | AggregateFunction::Stdev
136            | AggregateFunction::Stdevp => Arity {
137                min: 1,
138                max: Some(1),
139            },
140            AggregateFunction::PercentileCont | AggregateFunction::PercentileDisc => Arity {
141                min: 2,
142                max: Some(2),
143            },
144        }
145    }
146
147    #[must_use]
148    pub fn parse(name: &str) -> Option<Self> {
149        Some(match name {
150            "count" => Self::Count,
151            "sum" => Self::Sum,
152            "avg" => Self::Avg,
153            "min" => Self::Min,
154            "max" => Self::Max,
155            "collect" => Self::Collect,
156            "stdev" => Self::Stdev,
157            "stdevp" => Self::Stdevp,
158            "percentilecont" => Self::PercentileCont,
159            "percentiledisc" => Self::PercentileDisc,
160            _ => return None,
161        })
162    }
163}
164
165const fn spec(name: &'static str, min: usize, max: Option<usize>) -> BuiltinSpec {
166    BuiltinSpec {
167        name,
168        arity: Arity { min, max },
169        enum_arg_slots: &[],
170        type_arg_slots: &[],
171    }
172}
173
174const fn spec_enum(
175    name: &'static str,
176    min: usize,
177    max: Option<usize>,
178    enum_arg_slots: &'static [usize],
179) -> BuiltinSpec {
180    BuiltinSpec {
181        name,
182        arity: Arity { min, max },
183        enum_arg_slots,
184        type_arg_slots: &[],
185    }
186}
187
188const fn spec_type(
189    name: &'static str,
190    min: usize,
191    max: Option<usize>,
192    type_arg_slots: &'static [usize],
193) -> BuiltinSpec {
194    BuiltinSpec {
195        name,
196        arity: Arity { min, max },
197        enum_arg_slots: &[],
198        type_arg_slots,
199    }
200}
201
202const fn alias(alias: &'static str, canonical: &'static str) -> BuiltinAlias {
203    BuiltinAlias { alias, canonical }
204}
205
206pub const BUILTIN_SPECS: &[BuiltinSpec] = &[
207    // -- list.* -------------------------------------------------------------
208    spec("list.sum", 1, Some(1)),
209    spec("list.avg", 1, Some(1)),
210    spec("list.min", 1, Some(1)),
211    spec("list.max", 1, Some(1)),
212    spec("list.product", 1, Some(1)),
213    spec("list.stdev", 1, Some(1)),
214    spec("list.median", 1, Some(1)),
215    spec("list.sort", 1, Some(2)),
216    spec("list.reverse", 1, Some(1)),
217    spec("list.unique", 1, Some(1)),
218    spec("list.first", 1, Some(1)),
219    spec("list.rest", 1, Some(1)),
220    spec("list.init", 1, Some(1)),
221    spec("list.last", 1, Some(1)),
222    spec("list.at", 2, Some(2)),
223    spec("list.slice", 2, Some(3)),
224    spec("list.size", 1, Some(1)),
225    spec("list.range", 2, Some(3)),
226    spec("list.contains", 2, Some(2)),
227    spec("list.contains_all", 2, Some(2)),
228    spec("list.has_duplicates", 1, Some(1)),
229    spec("list.all_distinct", 1, Some(1)),
230    spec("list.equal_unordered", 2, Some(2)),
231    spec("list.is_empty", 1, Some(1)),
232    spec("list.index_of", 2, Some(2)),
233    spec("list.indexes_of", 2, Some(2)),
234    spec("list.find_duplicates", 1, Some(1)),
235    spec("list.count_by", 1, Some(1)),
236    spec("list.union", 2, Some(2)),
237    spec("list.intersect", 2, Some(2)),
238    spec("list.diff", 2, Some(2)),
239    spec("list.symmetric_diff", 2, Some(2)),
240    spec("list.zip", 2, Some(2)),
241    spec("list.chunks", 2, Some(2)),
242    spec("list.split_by", 2, Some(2)),
243    spec("list.windows", 2, Some(3)),
244    spec("list.scan", 2, Some(2)),
245    spec("list.repeat", 2, Some(2)),
246    spec("list.flatten", 1, Some(2)),
247    spec("list.sample", 1, Some(2)),
248    spec("list.shuffle", 1, Some(1)),
249    spec("list.combinations", 2, Some(2)),
250    spec("list.concat", 2, None),
251    spec("list.append", 2, Some(2)),
252    spec("list.prepend", 2, Some(2)),
253    spec("list.take", 2, Some(2)),
254    spec("list.drop", 2, Some(2)),
255    spec("list.take_last", 2, Some(2)),
256    spec("list.drop_last", 2, Some(2)),
257    spec("list.insert", 3, Some(3)),
258    spec("list.remove", 2, Some(2)),
259    spec("list.compact", 1, Some(1)),
260    // -- string.* -----------------------------------------------------------
261    spec("string.upper", 1, Some(1)),
262    spec("string.lower", 1, Some(1)),
263    spec("string.capitalize", 1, Some(2)),
264    spec("string.case", 2, Some(2)),
265    spec("string.replace", 3, Some(4)),
266    spec("string.find", 2, Some(3)),
267    spec("string.count", 2, Some(2)),
268    spec("string.before", 2, Some(2)),
269    spec("string.after", 2, Some(2)),
270    spec("string.split", 2, Some(2)),
271    spec("string.join", 2, Some(2)),
272    spec("string.pad", 3, Some(4)),
273    spec("string.pad_left", 2, Some(3)),
274    spec("string.pad_right", 2, Some(3)),
275    spec("string.repeat", 2, Some(2)),
276    spec("string.slugify", 1, Some(1)),
277    spec("string.escape", 2, Some(2)),
278    spec("string.hex", 1, Some(1)),
279    spec("string.char_at", 2, Some(2)),
280    spec("string.code_at", 2, Some(2)),
281    spec("string.regex_groups", 2, Some(3)),
282    spec("string.matches", 2, Some(2)),
283    spec("string.starts_with", 2, Some(2)),
284    spec("string.ends_with", 2, Some(2)),
285    spec("string.contains", 2, Some(2)),
286    spec("string.words", 1, Some(1)),
287    spec("string.is_blank", 1, Some(1)),
288    spec("string.length", 1, Some(1)),
289    spec("string.url_encode", 1, Some(1)),
290    spec("string.url_decode", 1, Some(1)),
291    spec("string.swap_case", 1, Some(1)),
292    spec("string.trim", 1, Some(2)),
293    spec("string.trim_left", 1, Some(1)),
294    spec("string.trim_right", 1, Some(1)),
295    spec("string.slice", 2, Some(3)),
296    spec("string.prefix", 2, Some(2)),
297    spec("string.suffix", 2, Some(2)),
298    spec("string.reverse", 1, Some(1)),
299    spec("string.normalize", 1, Some(2)),
300    // -- text.* -------------------------------------------------------------
301    spec("text.distance", 3, Some(3)),
302    spec("text.similarity", 3, Some(3)),
303    spec("text.phonetic", 2, Some(2)),
304    spec("text.phonetic_match", 3, Some(3)),
305    // -- map.* --------------------------------------------------------------
306    spec("map.from", 1, Some(2)),
307    spec("map.set", 3, Some(3)),
308    spec("map.remove", 2, Some(2)),
309    spec("map.merge", 2, Some(3)),
310    spec("map.deep_merge", 2, Some(3)),
311    spec("map.compact", 1, Some(1)),
312    spec("map.group_by", 2, Some(2)),
313    spec("map.flatten", 1, Some(2)),
314    spec("map.unflatten", 1, Some(2)),
315    spec("map.get_path", 2, Some(3)),
316    spec("map.set_path", 3, Some(3)),
317    spec("map.remove_path", 2, Some(2)),
318    spec("map.entries", 1, Some(2)),
319    spec("map.values", 1, Some(2)),
320    spec("map.keys", 1, Some(1)),
321    spec("map.has_key", 2, Some(2)),
322    spec("map.pick", 2, Some(2)),
323    spec("map.rename", 3, Some(3)),
324    spec("map.invert", 1, Some(1)),
325    spec("map.get", 2, Some(3)),
326    spec("map.size", 1, Some(1)),
327    spec("map.index_by", 2, Some(2)),
328    // -- number.* -----------------------------------------------------------
329    spec("number.format", 1, Some(3)),
330    spec("number.to_base", 2, Some(2)),
331    spec("number.from_base", 2, Some(2)),
332    spec("number.to_roman", 1, Some(1)),
333    spec("number.from_roman", 1, Some(1)),
334    spec("bits.and", 2, Some(2)),
335    spec("bits.or", 2, Some(2)),
336    spec("bits.xor", 2, Some(2)),
337    spec("bits.shift_left", 2, Some(2)),
338    spec("bits.shift_right", 2, Some(2)),
339    spec("bits.not", 1, Some(1)),
340    spec("number.bitop", 3, Some(3)),
341    spec("number.is_integer", 1, Some(1)),
342    spec("number.is_even", 1, Some(1)),
343    spec("number.is_odd", 1, Some(1)),
344    spec("number.is_positive", 1, Some(1)),
345    spec("number.is_negative", 1, Some(1)),
346    spec("number.is_zero", 1, Some(1)),
347    spec("number.is_nan", 1, Some(1)),
348    spec("number.is_finite", 1, Some(1)),
349    spec("number.is_infinite", 1, Some(1)),
350    // -- math.* -------------------------------------------------------------
351    spec("math.min", 1, None),
352    spec("math.max", 1, None),
353    spec("math.round", 1, Some(3)),
354    spec("math.trunc", 1, Some(1)),
355    spec("math.sigmoid", 1, Some(1)),
356    spec("math.tanh", 1, Some(1)),
357    spec("math.cosh", 1, Some(1)),
358    spec("math.sinh", 1, Some(1)),
359    spec("math.cot", 1, Some(1)),
360    spec("math.coth", 1, Some(1)),
361    spec("math.atan2", 2, Some(2)),
362    spec("math.pow", 2, Some(2)),
363    spec("math.hypot", 2, Some(2)),
364    spec("math.log_base", 2, Some(2)),
365    spec("math.gcd", 2, Some(2)),
366    spec("math.lcm", 2, Some(2)),
367    spec("math.clamp", 3, Some(3)),
368    spec("math.lerp", 3, Some(3)),
369    spec("math.abs", 1, Some(1)),
370    spec("math.ceil", 1, Some(1)),
371    spec("math.floor", 1, Some(1)),
372    spec("math.sqrt", 1, Some(1)),
373    spec("math.sign", 1, Some(1)),
374    spec("math.log", 1, Some(1)),
375    spec("math.ln", 1, Some(1)),
376    spec("math.log10", 1, Some(1)),
377    spec("math.exp", 1, Some(1)),
378    spec("math.sin", 1, Some(1)),
379    spec("math.cos", 1, Some(1)),
380    spec("math.tan", 1, Some(1)),
381    spec("math.asin", 1, Some(1)),
382    spec("math.acos", 1, Some(1)),
383    spec("math.atan", 1, Some(1)),
384    spec("math.degrees", 1, Some(1)),
385    spec("math.radians", 1, Some(1)),
386    spec("math.pi", 0, Some(0)),
387    spec("math.e", 0, Some(0)),
388    spec("math.random", 0, Some(0)),
389    // -- temporal.* ----------------------------------------------------------
390    spec("temporal.now", 0, Some(1)),
391    spec("temporal.today", 0, Some(0)),
392    spec("temporal.timestamp", 0, Some(0)),
393    spec("temporal.timezone", 0, Some(0)),
394    spec("temporal.parse", 1, Some(3)),
395    spec("temporal.format", 1, Some(2)),
396    spec("temporal.reformat", 3, Some(3)),
397    spec("temporal.convert", 3, Some(3)),
398    spec("temporal.add", 2, Some(2)),
399    spec("temporal.get", 2, Some(2)),
400    spec("temporal.fields", 1, Some(1)),
401    spec("temporal.truncate", 2, Some(2)),
402    spec("temporal.between", 2, Some(2)),
403    spec("temporal.in_days", 2, Some(2)),
404    // -- bytes.* ------------------------------------------------------------
405    spec("bytes.size", 1, Some(1)),
406    spec("bytes.from_string", 1, Some(2)),
407    spec("bytes.to_string", 1, Some(2)),
408    spec("bytes.base64_encode", 1, Some(1)),
409    spec("bytes.base64_decode", 1, Some(1)),
410    spec("bytes.hex_encode", 1, Some(1)),
411    spec("bytes.hex_decode", 1, Some(1)),
412    spec("bytes.compress", 1, Some(2)),
413    spec("bytes.decompress", 1, Some(2)),
414    // -- crypto.* -----------------------------------------------------------
415    spec("crypto.blake3", 1, Some(1)),
416    spec("crypto.crc32", 1, Some(1)),
417    // -- uuid.* -------------------------------------------------------------
418    spec("uuid.new", 0, Some(0)),
419    spec("uuid.from_string", 1, Some(1)),
420    spec("uuid.is_valid", 1, Some(1)),
421    // -- json.* -------------------------------------------------------------
422    spec("json.encode", 1, Some(2)),
423    spec("json.decode", 1, Some(1)),
424    spec("json.path", 2, Some(2)),
425    // -- geo.* --------------------------------------------------------------
426    spec("geo.distance", 2, Some(2)),
427    spec("geo.within_bbox", 3, Some(3)),
428    spec("geo.point", 1, Some(1)),
429    // -- vector.* -----------------------------------------------------------
430    spec("vector.dimension", 1, Some(1)),
431    spec_enum("vector.distance", 3, Some(3), &[2]),
432    spec("vector.similarity", 2, Some(3)),
433    spec_enum("vector.norm", 2, Some(2), &[1]),
434    spec_enum("vector.coordinates", 2, Some(2), &[1]),
435    // -- node.* -------------------------------------------------------------
436    spec("node.id", 1, Some(1)),
437    spec("node.labels", 1, Some(1)),
438    spec("node.has_label", 2, Some(2)),
439    spec("node.keys", 1, Some(1)),
440    spec("node.properties", 1, Some(1)),
441    // -- edge.* -------------------------------------------------------------
442    spec("edge.id", 1, Some(1)),
443    spec("edge.type", 1, Some(1)),
444    spec("edge.keys", 1, Some(1)),
445    spec("edge.properties", 1, Some(1)),
446    spec("edge.start", 1, Some(1)),
447    spec("edge.end", 1, Some(1)),
448    // -- path.* -------------------------------------------------------------
449    spec("path.nodes", 1, Some(1)),
450    spec("path.edges", 1, Some(1)),
451    spec("path.length", 1, Some(1)),
452    spec("path.first", 1, Some(1)),
453    spec("path.last", 1, Some(1)),
454    // -- value.* (polymorphic) ----------------------------------------------
455    spec("value.size", 1, Some(1)),
456    spec("value.keys", 1, Some(1)),
457    spec("value.properties", 1, Some(1)),
458    spec("value.reverse", 1, Some(1)),
459    spec("value.coalesce", 1, None),
460    spec("value.is_null", 1, Some(1)),
461    spec("value.is_not_null", 1, Some(1)),
462    spec("value.id", 1, Some(1)),
463    // -- type.* -------------------------------------------------------------
464    spec("type.of", 1, Some(1)),
465    spec_type("type.is", 2, Some(2), &[1]),
466    // -- cast.* -------------------------------------------------------------
467    spec_type("cast.to", 2, Some(2), &[1]),
468    spec_type("cast.try", 2, Some(2), &[1]),
469    spec_type("cast.can", 2, Some(2), &[1]),
470];
471
472pub const BUILTIN_ALIASES: &[BuiltinAlias] = &[
473    // Lora migration aliases.
474    alias("list.find_index", "list.index_of"),
475    alias("list.find_indexes", "list.indexes_of"),
476    alias("vector.dim", "vector.dimension"),
477    alias("value.first_non_null", "value.coalesce"),
478    alias("type.cast", "cast.to"),
479    alias("type.try_cast", "cast.try"),
480    alias("type.can_cast", "cast.can"),
481    alias("now", "temporal.now"),
482    alias("datetime", "temporal.now"),
483    // Cypher temporal constructors. `temporal.now` doubles as a parser
484    // when called with a string argument (see `temporal::now` in the
485    // executor — `date("2025-01-01")` lands in the string-parse branch
486    // that picks DateTime/Date/Duration based on the literal's shape).
487    alias("date", "temporal.now"),
488    alias("time", "temporal.now"),
489    alias("localdatetime", "temporal.now"),
490    alias("localtime", "temporal.now"),
491    alias("duration", "temporal.now"),
492    alias("point", "geo.point"),
493    alias("timestamp", "temporal.timestamp"),
494    alias("timezone", "temporal.timezone"),
495    alias("new", "uuid.new"),
496    alias("random", "math.random"),
497    alias("rand", "math.random"),
498    alias("range", "list.range"),
499    // Cypher / historical compatibility aliases.
500    alias("head", "list.first"),
501    alias("last", "list.last"),
502    alias("coalesce", "value.coalesce"),
503    alias("tolower", "string.lower"),
504    alias("toupper", "string.upper"),
505    alias("left", "string.prefix"),
506    alias("right", "string.suffix"),
507    alias("substring", "string.slice"),
508    alias("reverse", "value.reverse"),
509    alias("size", "value.size"),
510    alias("length", "path.length"),
511    alias("keys", "value.keys"),
512    alias("properties", "value.properties"),
513    alias("id", "value.id"),
514    alias("labels", "node.labels"),
515    alias("type", "edge.type"),
516    alias("randomuuid", "uuid.new"),
517    alias("tostring", "cast.to"),
518    alias("tointeger", "cast.to"),
519    alias("tofloat", "cast.to"),
520    alias("toboolean", "cast.to"),
521    alias("tointegerornull", "cast.try"),
522    alias("tofloatornull", "cast.try"),
523    alias("tobooleanornull", "cast.try"),
524    alias("tostringornull", "cast.try"),
525];
526
527pub fn builtin_spec(name: &str) -> Option<&'static BuiltinSpec> {
528    canonical_builtin_name(name)
529        .and_then(|canonical| BUILTIN_SPECS.iter().find(|spec| spec.name == canonical))
530}
531
532pub fn namespaced_arity(name: &str) -> Option<(usize, Option<usize>)> {
533    builtin_spec(name).map(|spec| (spec.arity.min, spec.arity.max))
534}
535
536pub fn accepts_enum_literal(name: &str, arg_idx: usize) -> bool {
537    builtin_spec(name).is_some_and(|spec| spec.enum_arg_slots.contains(&arg_idx))
538}
539
540pub fn accepts_type_literal(name: &str, arg_idx: usize) -> bool {
541    builtin_spec(name).is_some_and(|spec| spec.type_arg_slots.contains(&arg_idx))
542}
543
544pub fn resolve_function(name: &str) -> Option<FunctionId> {
545    let lower = name.to_ascii_lowercase();
546    builtin_spec(&lower)
547        .map(FunctionId::Builtin)
548        .or_else(|| AggregateFunction::parse(&lower).map(FunctionId::Aggregate))
549}
550
551pub fn canonical_builtin_name(name: &str) -> Option<&'static str> {
552    BUILTIN_SPECS
553        .iter()
554        .find(|spec| spec.name == name)
555        .map(|spec| spec.name)
556        .or_else(|| {
557            BUILTIN_ALIASES
558                .iter()
559                .find(|alias| alias.alias == name)
560                .map(|alias| alias.canonical)
561        })
562}