Skip to main content

lora_analyzer/analyzer/
builtin_signatures.rs

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