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}