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/// Built-in operations, grouped by domain.
71///
72/// Each operation has a fixed arity enforced at evaluation time.
73/// All operations are pure — no IO, no mutation, deterministic.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
75pub enum BuiltinOp {
76    // --- Arithmetic (7) ---
77    /// `add(a: int|float, b: int|float) → int|float`
78    Add,
79    /// `sub(a: int|float, b: int|float) → int|float`
80    Sub,
81    /// `mul(a: int|float, b: int|float) → int|float`
82    Mul,
83    /// `div(a: int|float, b: int|float) → int|float` (truncating for ints)
84    Div,
85    /// `mod_(a: int, b: int) → int`
86    Mod,
87    /// `neg(a: int|float) → int|float`
88    Neg,
89    /// `abs(a: int|float) → int|float`
90    Abs,
91
92    // --- Rounding (2) ---
93    /// `floor(a: float) → int`
94    Floor,
95    /// `ceil(a: float) → int`
96    Ceil,
97
98    // --- Comparison (6) ---
99    /// `eq(a, b) → bool`
100    Eq,
101    /// `neq(a, b) → bool`
102    Neq,
103    /// `lt(a, b) → bool`
104    Lt,
105    /// `lte(a, b) → bool`
106    Lte,
107    /// `gt(a, b) → bool`
108    Gt,
109    /// `gte(a, b) → bool`
110    Gte,
111
112    // --- Boolean (3) ---
113    /// `and(a: bool, b: bool) → bool`
114    And,
115    /// `or(a: bool, b: bool) → bool`
116    Or,
117    /// `not(a: bool) → bool`
118    Not,
119
120    // --- String (10) ---
121    /// `concat(a: string, b: string) → string`
122    Concat,
123    /// `len(s: string) → int` (byte length)
124    Len,
125    /// `slice(s: string, start: int, end: int) → string`
126    Slice,
127    /// `upper(s: string) → string`
128    Upper,
129    /// `lower(s: string) → string`
130    Lower,
131    /// `trim(s: string) → string`
132    Trim,
133    /// `split(s: string, delim: string) → [string]`
134    Split,
135    /// `join(parts: [string], delim: string) → string`
136    Join,
137    /// `replace(s: string, from: string, to: string) → string`
138    Replace,
139    /// `contains(s: string, substr: string) → bool`
140    Contains,
141
142    // --- List (9) ---
143    /// `map(list: [a], f: a → b) → [b]`
144    Map,
145    /// `filter(list: [a], pred: a → bool) → [a]`
146    Filter,
147    /// `fold(list: [a], init: b, f: (b, a) → b) → b`
148    Fold,
149    /// `append(list: [a], item: a) → [a]`
150    Append,
151    /// `head(list: [a]) → a`
152    Head,
153    /// `tail(list: [a]) → [a]`
154    Tail,
155    /// `reverse(list: [a]) → [a]`
156    Reverse,
157    /// `flat_map(list: [a], f: a → [b]) → [b]`
158    FlatMap,
159    /// `length(list: [a]) → int` (list length, distinct from string Len)
160    Length,
161
162    // --- Record (4) ---
163    /// `merge(a: record, b: record) → record` (b fields override a)
164    MergeRecords,
165    /// `keys(r: record) → [string]`
166    Keys,
167    /// `values(r: record) → [any]`
168    Values,
169    /// `has_field(r: record, name: string) → bool`
170    HasField,
171
172    // --- Type coercions (6) ---
173    /// `int_to_float(n: int) → float`
174    IntToFloat,
175    /// `float_to_int(f: float) → int` (truncates)
176    FloatToInt,
177    /// `int_to_str(n: int) → string`
178    IntToStr,
179    /// `float_to_str(f: float) → string`
180    FloatToStr,
181    /// `str_to_int(s: string) → int` (fails on non-numeric)
182    StrToInt,
183    /// `str_to_float(s: string) → float` (fails on non-numeric)
184    StrToFloat,
185
186    // --- Type inspection (3) ---
187    /// `type_of(v) → string` (returns type name)
188    TypeOf,
189    /// `is_null(v) → bool`
190    IsNull,
191    /// `is_list(v) → bool`
192    IsList,
193
194    // --- Graph traversal (5) ---
195    // These builtins require an instance context (`InstanceEnv` in
196    // panproto-inst) and are evaluated by `eval_with_instance`, not
197    // the standard `eval`. In the standard evaluator they return Null.
198    /// `edge(node_ref: string, edge_kind: string) → value`
199    /// Follow a named edge from a node in the instance tree.
200    Edge,
201    /// `children(node_ref: string) → [value]`
202    /// Get all children of a node in the instance tree.
203    Children,
204    /// `has_edge(node_ref: string, edge_kind: string) → bool`
205    /// Check if a node has a specific outgoing edge.
206    HasEdge,
207    /// `edge_count(node_ref: string) → int`
208    /// Count outgoing edges from a node.
209    EdgeCount,
210    /// `anchor(node_ref: string) → string`
211    /// Get the schema anchor (sort/kind) of a node.
212    Anchor,
213}
214
215impl BuiltinOp {
216    /// Returns the expected number of arguments for this builtin.
217    #[must_use]
218    pub const fn arity(self) -> usize {
219        match self {
220            // Unary
221            Self::Neg
222            | Self::Abs
223            | Self::Floor
224            | Self::Ceil
225            | Self::Not
226            | Self::Upper
227            | Self::Lower
228            | Self::Trim
229            | Self::Head
230            | Self::Tail
231            | Self::Reverse
232            | Self::Keys
233            | Self::Values
234            | Self::IntToFloat
235            | Self::FloatToInt
236            | Self::IntToStr
237            | Self::FloatToStr
238            | Self::StrToInt
239            | Self::StrToFloat
240            | Self::TypeOf
241            | Self::IsNull
242            | Self::IsList
243            | Self::Len
244            | Self::Length
245            | Self::Children
246            | Self::EdgeCount
247            | Self::Anchor => 1,
248            // Binary
249            Self::Add
250            | Self::Sub
251            | Self::Mul
252            | Self::Div
253            | Self::Mod
254            | Self::Eq
255            | Self::Neq
256            | Self::Lt
257            | Self::Lte
258            | Self::Gt
259            | Self::Gte
260            | Self::And
261            | Self::Or
262            | Self::Concat
263            | Self::Split
264            | Self::Join
265            | Self::Append
266            | Self::Map
267            | Self::Filter
268            | Self::HasField
269            | Self::MergeRecords
270            | Self::Contains
271            | Self::FlatMap
272            | Self::Edge
273            | Self::HasEdge => 2,
274            // Ternary
275            Self::Slice | Self::Replace | Self::Fold => 3,
276        }
277    }
278}
279
280impl Expr {
281    /// Create a variable expression.
282    #[must_use]
283    pub fn var(name: impl Into<Arc<str>>) -> Self {
284        Self::Var(name.into())
285    }
286
287    /// Create a lambda expression.
288    #[must_use]
289    pub fn lam(param: impl Into<Arc<str>>, body: Self) -> Self {
290        Self::Lam(param.into(), Box::new(body))
291    }
292
293    /// Create an application expression.
294    #[must_use]
295    pub fn app(func: Self, arg: Self) -> Self {
296        Self::App(Box::new(func), Box::new(arg))
297    }
298
299    /// Create a let-binding expression.
300    #[must_use]
301    pub fn let_in(name: impl Into<Arc<str>>, value: Self, body: Self) -> Self {
302        Self::Let {
303            name: name.into(),
304            value: Box::new(value),
305            body: Box::new(body),
306        }
307    }
308
309    /// Create a field access expression.
310    #[must_use]
311    pub fn field(expr: Self, name: impl Into<Arc<str>>) -> Self {
312        Self::Field(Box::new(expr), name.into())
313    }
314
315    /// Create a builtin operation applied to arguments.
316    #[must_use]
317    pub const fn builtin(op: BuiltinOp, args: Vec<Self>) -> Self {
318        Self::Builtin(op, args)
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn builtin_arities() {
328        assert_eq!(BuiltinOp::Add.arity(), 2);
329        assert_eq!(BuiltinOp::Not.arity(), 1);
330        assert_eq!(BuiltinOp::Fold.arity(), 3);
331        assert_eq!(BuiltinOp::Slice.arity(), 3);
332    }
333
334    #[test]
335    fn expr_constructors() {
336        let e = Expr::let_in(
337            "x",
338            Expr::Lit(Literal::Int(42)),
339            Expr::builtin(
340                BuiltinOp::Add,
341                vec![Expr::var("x"), Expr::Lit(Literal::Int(1))],
342            ),
343        );
344        assert!(matches!(e, Expr::Let { .. }));
345    }
346}