Skip to main content

panproto_expr/
expr.rs

1//! Expression AST, pattern, and builtin operation types.
2//!
3//! The expression language is a pure functional language: lambda calculus
4//! with pattern matching, algebraic data types, and built-in operations on
5//! strings, numbers, records, and lists. Comparable to a pure subset of ML.
6
7use std::sync::Arc;
8
9use crate::Literal;
10
11/// An expression in the pure functional language.
12///
13/// All variants are serializable, content-addressable, and evaluate
14/// deterministically on any platform (including WASM).
15#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
16pub enum Expr {
17    /// Variable reference.
18    Var(Arc<str>),
19    /// Lambda abstraction: `λparam. body`.
20    Lam(Arc<str>, Box<Self>),
21    /// Function application: `func(arg)`.
22    App(Box<Self>, Box<Self>),
23    /// Literal value.
24    Lit(Literal),
25    /// Record construction: `{ name: expr, ... }`.
26    Record(Vec<(Arc<str>, Self)>),
27    /// List construction: `[expr, ...]`.
28    List(Vec<Self>),
29    /// Field access: `expr.field`.
30    Field(Box<Self>, Arc<str>),
31    /// Index access: `expr[index]`.
32    Index(Box<Self>, Box<Self>),
33    /// Pattern matching: `match scrutinee { pat => body, ... }`.
34    Match {
35        /// The value being matched against.
36        scrutinee: Box<Self>,
37        /// Arms: (pattern, body) pairs tried in order.
38        arms: Vec<(Pattern, Self)>,
39    },
40    /// Let binding: `let name = value in body`.
41    Let {
42        /// The bound variable name.
43        name: Arc<str>,
44        /// The value to bind.
45        value: Box<Self>,
46        /// The body where the binding is visible.
47        body: Box<Self>,
48    },
49    /// Built-in operation applied to arguments.
50    Builtin(BuiltinOp, Vec<Self>),
51}
52
53/// A destructuring pattern for match expressions.
54#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
55pub enum Pattern {
56    /// Matches anything, binds nothing.
57    Wildcard,
58    /// Matches anything, binds the value to a name.
59    Var(Arc<str>),
60    /// Matches a specific literal value.
61    Lit(Literal),
62    /// Matches a record with specific field patterns.
63    Record(Vec<(Arc<str>, Self)>),
64    /// Matches a list with element patterns.
65    List(Vec<Self>),
66    /// Matches a tagged constructor with argument patterns.
67    Constructor(Arc<str>, Vec<Self>),
68}
69
70/// Simple type classification for expressions.
71///
72/// This is a lightweight type system for the expression language,
73/// independent of the GAT type system in `panproto-gat`. Used for
74/// type inference and coercion validation within expressions.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
76pub enum ExprType {
77    /// 64-bit signed integer.
78    Int,
79    /// 64-bit IEEE 754 float.
80    Float,
81    /// UTF-8 string.
82    Str,
83    /// Boolean.
84    Bool,
85    /// Homogeneous list.
86    List,
87    /// Record (ordered map of fields to values).
88    Record,
89    /// Unknown or polymorphic type.
90    Any,
91}
92
93/// Built-in operations, grouped by domain.
94///
95/// Each operation has a fixed arity enforced at evaluation time.
96/// All operations are pure: no IO, no mutation, deterministic.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
98pub enum BuiltinOp {
99    // --- Arithmetic (7) ---
100    /// `add(a: int|float, b: int|float) → int|float`
101    Add,
102    /// `sub(a: int|float, b: int|float) → int|float`
103    Sub,
104    /// `mul(a: int|float, b: int|float) → int|float`
105    Mul,
106    /// `div(a: int|float, b: int|float) → int|float` (truncating for ints)
107    Div,
108    /// `mod_(a: int, b: int) → int`
109    Mod,
110    /// `neg(a: int|float) → int|float`
111    Neg,
112    /// `abs(a: int|float) → int|float`
113    Abs,
114
115    // --- Rounding (3) ---
116    /// `floor(a: float) → int`
117    Floor,
118    /// `ceil(a: float) → int`
119    Ceil,
120    /// `round(a: float) → int` (rounds to nearest, ties to even)
121    Round,
122
123    // --- Comparison (6) ---
124    /// `eq(a, b) → bool`
125    Eq,
126    /// `neq(a, b) → bool`
127    Neq,
128    /// `lt(a, b) → bool`
129    Lt,
130    /// `lte(a, b) → bool`
131    Lte,
132    /// `gt(a, b) → bool`
133    Gt,
134    /// `gte(a, b) → bool`
135    Gte,
136
137    // --- Boolean (3) ---
138    /// `and(a: bool, b: bool) → bool`
139    And,
140    /// `or(a: bool, b: bool) → bool`
141    Or,
142    /// `not(a: bool) → bool`
143    Not,
144
145    // --- String (10) ---
146    /// `concat(a: string, b: string) → string`
147    Concat,
148    /// `len(s: string) → int` (byte length)
149    Len,
150    /// `slice(s: string, start: int, end: int) → string`
151    Slice,
152    /// `upper(s: string) → string`
153    Upper,
154    /// `lower(s: string) → string`
155    Lower,
156    /// `trim(s: string) → string`
157    Trim,
158    /// `split(s: string, delim: string) → [string]`
159    Split,
160    /// `join(parts: [string], delim: string) → string`
161    Join,
162    /// `replace(s: string, from: string, to: string) → string`
163    Replace,
164    /// `contains(s: string, substr: string) → bool`
165    Contains,
166
167    // --- List (10) ---
168    /// `map(list: [a], f: a → b) → [b]`
169    Map,
170    /// `filter(list: [a], pred: a → bool) → [a]`
171    Filter,
172    /// `fold(list: [a], init: b, f: (b, a) → b) → b`
173    Fold,
174    /// `append(list: [a], item: a) → [a]`
175    Append,
176    /// `head(list: [a]) → a`
177    Head,
178    /// `tail(list: [a]) → [a]`
179    Tail,
180    /// `reverse(list: [a]) → [a]`
181    Reverse,
182    /// `flat_map(list: [a], f: a → [b]) → [b]`
183    FlatMap,
184    /// `length(list: [a]) → int` (list length, distinct from string Len)
185    Length,
186    /// `range(start: int, stop: int) → [int]` (inclusive of both bounds;
187    /// empty when `stop < start`)
188    Range,
189
190    // --- Record (4) ---
191    /// `merge(a: record, b: record) → record` (b fields override a)
192    MergeRecords,
193    /// `keys(r: record) → [string]`
194    Keys,
195    /// `values(r: record) → [any]`
196    Values,
197    /// `has_field(r: record, name: string) → bool`
198    HasField,
199
200    // --- Utility (3) ---
201    /// `default(x, fallback)`: returns fallback if x is null, else x.
202    DefaultVal,
203    /// `clamp(x, min, max)`: clamp a numeric value to the range [min, max].
204    Clamp,
205    /// `truncate_str(s, max_len)`: truncate a string to at most `max_len` bytes
206    /// (char-boundary safe).
207    TruncateStr,
208
209    // --- Type coercions (6) ---
210    /// `int_to_float(n: int) → float`
211    IntToFloat,
212    /// `float_to_int(f: float) → int` (truncates)
213    FloatToInt,
214    /// `int_to_str(n: int) → string`
215    IntToStr,
216    /// `float_to_str(f: float) → string`
217    FloatToStr,
218    /// `str_to_int(s: string) → int` (fails on non-numeric)
219    StrToInt,
220    /// `str_to_float(s: string) → float` (fails on non-numeric)
221    StrToFloat,
222
223    // --- Type inspection (3) ---
224    /// `type_of(v) → string` (returns type name)
225    TypeOf,
226    /// `is_null(v) → bool`
227    IsNull,
228    /// `is_list(v) → bool`
229    IsList,
230
231    // --- Graph traversal (5) ---
232    // These builtins require an instance context (`InstanceEnv` in
233    // panproto-inst) and are evaluated by `eval_with_instance`, not
234    // the standard `eval`. In the standard evaluator they return Null.
235    /// `edge(node_ref: string, edge_kind: string) → value`
236    /// Follow a named edge from a node in the instance tree.
237    Edge,
238    /// `children(node_ref: string) → [value]`
239    /// Get all children of a node in the instance tree.
240    Children,
241    /// `has_edge(node_ref: string, edge_kind: string) → bool`
242    /// Check if a node has a specific outgoing edge.
243    HasEdge,
244    /// `edge_count(node_ref: string) → int`
245    /// Count outgoing edges from a node.
246    EdgeCount,
247    /// `anchor(node_ref: string) → string`
248    /// Get the schema anchor (sort/kind) of a node.
249    Anchor,
250}
251
252impl BuiltinOp {
253    /// Resolve a surface identifier to the builtin it names.
254    ///
255    /// Both the `snake_case` and `camelCase` spellings are accepted where the
256    /// surface syntax offers both.
257    #[must_use]
258    pub fn from_name(name: &str) -> Option<Self> {
259        match name {
260            "add" => Some(Self::Add),
261            "sub" => Some(Self::Sub),
262            "mul" => Some(Self::Mul),
263            "abs" => Some(Self::Abs),
264            "floor" => Some(Self::Floor),
265            "ceil" => Some(Self::Ceil),
266            "round" => Some(Self::Round),
267            "concat" => Some(Self::Concat),
268            "len" => Some(Self::Len),
269            "slice" => Some(Self::Slice),
270            "upper" => Some(Self::Upper),
271            "lower" => Some(Self::Lower),
272            "trim" => Some(Self::Trim),
273            "split" => Some(Self::Split),
274            "join" => Some(Self::Join),
275            "replace" => Some(Self::Replace),
276            "contains" => Some(Self::Contains),
277            "map" => Some(Self::Map),
278            "filter" => Some(Self::Filter),
279            "fold" => Some(Self::Fold),
280            "append" => Some(Self::Append),
281            "head" => Some(Self::Head),
282            "tail" => Some(Self::Tail),
283            "reverse" => Some(Self::Reverse),
284            "flat_map" | "flatMap" => Some(Self::FlatMap),
285            "length" => Some(Self::Length),
286            "range" => Some(Self::Range),
287            "merge" | "merge_records" => Some(Self::MergeRecords),
288            "keys" => Some(Self::Keys),
289            "values" => Some(Self::Values),
290            "has_field" | "hasField" => Some(Self::HasField),
291            "default" | "default_val" | "defaultVal" => Some(Self::DefaultVal),
292            "clamp" => Some(Self::Clamp),
293            "truncate_str" | "truncateStr" => Some(Self::TruncateStr),
294            "int_to_float" | "intToFloat" => Some(Self::IntToFloat),
295            "float_to_int" | "floatToInt" => Some(Self::FloatToInt),
296            "int_to_str" | "intToStr" => Some(Self::IntToStr),
297            "float_to_str" | "floatToStr" => Some(Self::FloatToStr),
298            "str_to_int" | "strToInt" => Some(Self::StrToInt),
299            "str_to_float" | "strToFloat" => Some(Self::StrToFloat),
300            "type_of" | "typeOf" => Some(Self::TypeOf),
301            "is_null" | "isNull" => Some(Self::IsNull),
302            "is_list" | "isList" => Some(Self::IsList),
303            "edge" => Some(Self::Edge),
304            "children" => Some(Self::Children),
305            "has_edge" | "hasEdge" => Some(Self::HasEdge),
306            "edge_count" | "edgeCount" => Some(Self::EdgeCount),
307            "anchor" => Some(Self::Anchor),
308            _ => None,
309        }
310    }
311
312    /// Permute surface-syntax arguments into the order [`Expr::Builtin`] holds.
313    ///
314    /// The surface syntax follows the usual functional convention of naming the
315    /// function first (`map f xs`, `fold f z xs`), while [`Expr::Builtin`] takes
316    /// the list first and the function last. The two orders are deliberately
317    /// distinct: `Expr` is serialized into stored lens documents, so its
318    /// argument order is the compatibility-bearing one and the surface syntax
319    /// lowers into it.
320    ///
321    /// The permutation applies only to a saturated call, since a partial
322    /// application has no complete order to permute. Builtins outside this set
323    /// take their arguments in the same order at both layers and pass through
324    /// untouched.
325    #[must_use]
326    pub fn surface_args_to_expr_args(self, mut args: Vec<Expr>) -> Vec<Expr> {
327        match (self, args.len()) {
328            // `map f xs` / `filter p xs` / `flat_map f xs` -> [xs, f]
329            (Self::Map | Self::Filter | Self::FlatMap, 2) => {
330                args.swap(0, 1);
331                args
332            }
333            // `fold f z xs` -> [xs, z, f]
334            (Self::Fold, 3) => {
335                args.swap(0, 2);
336                args
337            }
338            _ => args,
339        }
340    }
341
342    /// Returns the expected number of arguments for this builtin.
343    #[must_use]
344    pub const fn arity(self) -> usize {
345        match self {
346            // Unary
347            Self::Neg
348            | Self::Abs
349            | Self::Floor
350            | Self::Ceil
351            | Self::Round
352            | Self::Not
353            | Self::Upper
354            | Self::Lower
355            | Self::Trim
356            | Self::Head
357            | Self::Tail
358            | Self::Reverse
359            | Self::Keys
360            | Self::Values
361            | Self::IntToFloat
362            | Self::FloatToInt
363            | Self::IntToStr
364            | Self::FloatToStr
365            | Self::StrToInt
366            | Self::StrToFloat
367            | Self::TypeOf
368            | Self::IsNull
369            | Self::IsList
370            | Self::Len
371            | Self::Length
372            | Self::Children
373            | Self::EdgeCount
374            | Self::Anchor => 1,
375            // Binary
376            Self::Add
377            | Self::Sub
378            | Self::Mul
379            | Self::Div
380            | Self::Mod
381            | Self::Eq
382            | Self::Neq
383            | Self::Lt
384            | Self::Lte
385            | Self::Gt
386            | Self::Gte
387            | Self::And
388            | Self::Or
389            | Self::Concat
390            | Self::Split
391            | Self::Join
392            | Self::Append
393            | Self::Map
394            | Self::Filter
395            | Self::HasField
396            | Self::MergeRecords
397            | Self::Contains
398            | Self::FlatMap
399            | Self::Edge
400            | Self::HasEdge
401            | Self::DefaultVal
402            | Self::Range
403            | Self::TruncateStr => 2,
404            // Ternary
405            Self::Slice | Self::Replace | Self::Fold | Self::Clamp => 3,
406        }
407    }
408
409    /// Returns the type signature `(input_types, output_type)` for builtins
410    /// with a known, monomorphic signature. Polymorphic builtins (e.g., `Add`
411    /// works on both int and float) return `None`.
412    #[must_use]
413    pub const fn signature(self) -> Option<(&'static [ExprType], ExprType)> {
414        match self {
415            // Coercions: precise source→target signatures.
416            Self::IntToFloat => Some((&[ExprType::Int], ExprType::Float)),
417            Self::FloatToInt | Self::Floor | Self::Ceil | Self::Round => {
418                Some((&[ExprType::Float], ExprType::Int))
419            }
420            Self::IntToStr => Some((&[ExprType::Int], ExprType::Str)),
421            Self::FloatToStr => Some((&[ExprType::Float], ExprType::Str)),
422            Self::StrToInt | Self::Len => Some((&[ExprType::Str], ExprType::Int)),
423            Self::StrToFloat => Some((&[ExprType::Str], ExprType::Float)),
424
425            // Boolean operations.
426            Self::And | Self::Or => Some((&[ExprType::Bool, ExprType::Bool], ExprType::Bool)),
427            Self::Not => Some((&[ExprType::Bool], ExprType::Bool)),
428
429            // Comparison: polymorphic inputs, bool output.
430            Self::Eq | Self::Neq | Self::Lt | Self::Lte | Self::Gt | Self::Gte => {
431                Some((&[ExprType::Any, ExprType::Any], ExprType::Bool))
432            }
433
434            // String operations.
435            Self::Concat => Some((&[ExprType::Str, ExprType::Str], ExprType::Str)),
436            Self::Slice => Some((
437                &[ExprType::Str, ExprType::Int, ExprType::Int],
438                ExprType::Str,
439            )),
440            Self::Upper | Self::Lower | Self::Trim => Some((&[ExprType::Str], ExprType::Str)),
441            Self::Split => Some((&[ExprType::Str, ExprType::Str], ExprType::List)),
442            Self::Join => Some((&[ExprType::List, ExprType::Str], ExprType::Str)),
443            Self::Replace => Some((
444                &[ExprType::Str, ExprType::Str, ExprType::Str],
445                ExprType::Str,
446            )),
447            // Overloaded on the first argument: substring containment on a
448            // string, element membership on a list. Inputs are `Any`; only
449            // the `Bool` result is fixed.
450            Self::Contains => Some((&[ExprType::Any, ExprType::Any], ExprType::Bool)),
451            Self::TruncateStr => Some((&[ExprType::Str, ExprType::Int], ExprType::Str)),
452
453            // List operations.
454            Self::Length => Some((&[ExprType::List], ExprType::Int)),
455            Self::Range => Some((&[ExprType::Int, ExprType::Int], ExprType::List)),
456            Self::Reverse => Some((&[ExprType::List], ExprType::List)),
457
458            // Record operations.
459            Self::MergeRecords => Some((&[ExprType::Record, ExprType::Record], ExprType::Record)),
460            Self::Keys | Self::Values => Some((&[ExprType::Record], ExprType::List)),
461            Self::HasField => Some((&[ExprType::Record, ExprType::Str], ExprType::Bool)),
462
463            // Type inspection.
464            Self::TypeOf => Some((&[ExprType::Any], ExprType::Str)),
465            Self::IsNull | Self::IsList => Some((&[ExprType::Any], ExprType::Bool)),
466
467            // Polymorphic builtins: return None.
468            Self::Add
469            | Self::Sub
470            | Self::Mul
471            | Self::Div
472            | Self::Mod
473            | Self::Neg
474            | Self::Abs
475            | Self::Map
476            | Self::Filter
477            | Self::Fold
478            | Self::FlatMap
479            | Self::Append
480            | Self::Head
481            | Self::Tail
482            | Self::DefaultVal
483            | Self::Clamp
484            | Self::Edge
485            | Self::Children
486            | Self::HasEdge
487            | Self::EdgeCount
488            | Self::Anchor => None,
489        }
490    }
491}
492
493impl Expr {
494    /// Create a variable expression.
495    #[must_use]
496    pub fn var(name: impl Into<Arc<str>>) -> Self {
497        Self::Var(name.into())
498    }
499
500    /// Create a lambda expression.
501    #[must_use]
502    pub fn lam(param: impl Into<Arc<str>>, body: Self) -> Self {
503        Self::Lam(param.into(), Box::new(body))
504    }
505
506    /// Create an application expression.
507    #[must_use]
508    pub fn app(func: Self, arg: Self) -> Self {
509        Self::App(Box::new(func), Box::new(arg))
510    }
511
512    /// Create a let-binding expression.
513    #[must_use]
514    pub fn let_in(name: impl Into<Arc<str>>, value: Self, body: Self) -> Self {
515        Self::Let {
516            name: name.into(),
517            value: Box::new(value),
518            body: Box::new(body),
519        }
520    }
521
522    /// Create a field access expression.
523    #[must_use]
524    pub fn field(expr: Self, name: impl Into<Arc<str>>) -> Self {
525        Self::Field(Box::new(expr), name.into())
526    }
527
528    /// Create a builtin operation applied to arguments.
529    #[must_use]
530    pub const fn builtin(op: BuiltinOp, args: Vec<Self>) -> Self {
531        Self::Builtin(op, args)
532    }
533
534    /// Coerce an integer to a float.
535    #[must_use]
536    pub fn int_to_float(arg: Self) -> Self {
537        Self::Builtin(BuiltinOp::IntToFloat, vec![arg])
538    }
539
540    /// Coerce a float to an integer (truncates toward zero).
541    #[must_use]
542    pub fn float_to_int(arg: Self) -> Self {
543        Self::Builtin(BuiltinOp::FloatToInt, vec![arg])
544    }
545
546    /// Coerce an integer to a string.
547    #[must_use]
548    pub fn int_to_str(arg: Self) -> Self {
549        Self::Builtin(BuiltinOp::IntToStr, vec![arg])
550    }
551
552    /// Coerce a float to a string.
553    #[must_use]
554    pub fn float_to_str(arg: Self) -> Self {
555        Self::Builtin(BuiltinOp::FloatToStr, vec![arg])
556    }
557
558    /// Parse a string as an integer.
559    #[must_use]
560    pub fn str_to_int(arg: Self) -> Self {
561        Self::Builtin(BuiltinOp::StrToInt, vec![arg])
562    }
563
564    /// Parse a string as a float.
565    #[must_use]
566    pub fn str_to_float(arg: Self) -> Self {
567        Self::Builtin(BuiltinOp::StrToFloat, vec![arg])
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574
575    #[test]
576    fn builtin_arities() {
577        assert_eq!(BuiltinOp::Add.arity(), 2);
578        assert_eq!(BuiltinOp::Not.arity(), 1);
579        assert_eq!(BuiltinOp::Fold.arity(), 3);
580        assert_eq!(BuiltinOp::Slice.arity(), 3);
581    }
582
583    #[test]
584    fn expr_constructors() {
585        let e = Expr::let_in(
586            "x",
587            Expr::Lit(Literal::Int(42)),
588            Expr::builtin(
589                BuiltinOp::Add,
590                vec![Expr::var("x"), Expr::Lit(Literal::Int(1))],
591            ),
592        );
593        assert!(matches!(e, Expr::Let { .. }));
594    }
595}