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}