nightjar_lang/language/grammar.rs
1// Copyright 2026 Wayne Hong (h-alice) <contact@halice.art>
2// Nightjar Language Project
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! # Grammar module
17//!
18//! Defines the grammar, operator enums, token types, and the AST shared
19//! by the parser and the executor.
20//!
21//! NOTE: the AST is designed to have two "tiers": (BoolExpr / ValueExpr).
22//!
23//! # Formal grammar definition (EBNF)
24//!
25//! ```ebnf
26//! (* A program is a single expression that must reduce to Boolean. *)
27//! program = bool_expr ;
28//!
29//! bool_expr = bool_literal
30//! | verifier_expr
31//! | connective_expr
32//! | not_expr
33//! | unary_check_expr
34//! | quantifier_expr ;
35//!
36//! verifier_expr = "(" verifier_op value_expr value_expr ")" ;
37//! verifier_op = "EQ" | "NE" | "LT" | "LE" | "GT" | "GE" ;
38//!
39//! connective_expr = "(" connective_op bool_expr bool_expr ")" ;
40//! connective_op = "AND" | "OR" ;
41//! not_expr = "(" "NOT" bool_expr ")" ;
42//!
43//! unary_check_expr = "(" "NonEmpty" value_expr ")" ;
44//!
45//! (* Quantifiers: assert a predicate over a List-typed Entity. *)
46//! quantifier_expr = "(" quantifier_op predicate value_expr ")" ;
47//! quantifier_op = "ForAll" | "Exists" ;
48//!
49//! (* Predicates are only legal inside quantifiers. *)
50//! (* Partial vs. full predicate is disambiguated by operand count *)
51//! (* at parse time, not by a syntactic marker: *)
52//! (* (VerifierOp x) — partial verifier (1 operand) *)
53//! (* (VerifierOp x y) — full bool_expr (2 operands) *)
54//! (* NonEmpty (bare) — unary check *)
55//! (* any other bool_expr — full bool_expr *)
56//! (* The body of a full predicate may use the "@" element-rooted *)
57//! (* symbol form to refer to the current iteration element. *)
58//! predicate = partial_verifier | "NonEmpty" | bool_expr ;
59//! partial_verifier = "(" verifier_op value_expr ")" ;
60//!
61//! (* Value expressions produce an Entity. *)
62//! value_expr = literal
63//! | symbol
64//! | func_expr ;
65//!
66//! (* Arity is enforced at parse time from FuncOp::expected_arity(): *)
67//! (* 1-ary: Neg, Abs, Length, Upper, Lower, *)
68//! (* Head, Tail, Count, GetKeys, GetValues *)
69//! (* 2-ary: Add, Sub, Mul, Div, Mod, Concat, Get *)
70//! (* 3-ary: Substring *)
71//! func_expr = "(" func_op value_expr { value_expr } ")" ;
72//! func_op = arith_op | string_op | collection_op ;
73//! arith_op = "Add" | "Sub" | "Mul" | "Div"
74//! | "Mod" | "Neg" | "Abs" ;
75//! string_op = "Concat" | "Length" | "Substring"
76//! | "Upper" | "Lower" ;
77//! collection_op = "Head" | "Tail" | "Get" | "Count"
78//! | "GetKeys" | "GetValues" ;
79//!
80//! (* Terminals. *)
81//! literal = int_literal | float_literal
82//! | string_literal | bool_literal | null_literal ;
83//!
84//! (* A leading "-" is part of the numeric literal *)
85//! int_literal = [ "-" ] digit { digit } ;
86//! float_literal = [ "-" ] digit { digit } "." digit { digit } ;
87//!
88//! string_literal = '"' { any_char } '"' ;
89//! bool_literal = "True" | "False" ;
90//! null_literal = "Null" ;
91//!
92//! (* Symbols have two namespaces: *)
93//! (* "." — root-rooted (resolved against the whole input). *)
94//! (* "@" — element-rooted (resolved against the current iteration *)
95//! (* element of the nearest enclosing ForAll/Exists). *)
96//! (* Bare "." is the whole input; bare "@" is the current element. *)
97//! (* "@" is only legal inside a quantifier predicate; the parser *)
98//! (* rejects it elsewhere with a ParseError. *)
99//! symbol = ( "." | "@" ) [ segment { "." segment } ] ;
100//!
101//! (* Segment characters are Unicode-aware: *)
102//! (* char::is_alphanumeric() covers Unicode categories L* and N*, *)
103//! (* so keys like ".營收" and ".données.résultat" are valid. *)
104//! segment = ident_start { ident_char } ;
105//! ident_start = unicode_letter | "_" ;
106//! ident_char = unicode_letter | unicode_digit | "_" ;
107//!
108//! digit = "0" | "1" | "2" | "3" | "4"
109//! | "5" | "6" | "7" | "8" | "9" ;
110//! ```
111//!
112//! ### Other implementation notes
113//!
114//! not about grammar itself, but related
115//!
116//! - Parser enforces a configurable nesting-depth limit
117//! (`ParserConfig::max_depth`, default 256) to protect the host from
118//! stack overflow on adversarial input.
119//! - Every AST node carries a byte-offset [`Span`] from Phase 1 so runtime
120//! diagnostics can point into the source string.
121
122use crate::error::Span;
123
124/// Spanned wrapper
125///
126/// Every AST node and token is wrapped in `Spanned<T>` so source positions
127/// are preserved through parsing and into runtime errors.
128#[derive(Debug, Clone, PartialEq)]
129pub struct Spanned<T> {
130 /// The wrapped AST node or token value.
131 pub node: T,
132 /// Source span of the node within the original expression.
133 pub span: Span,
134}
135
136impl<T> Spanned<T> {
137 /// Construct a [`Spanned`] from a node and its source [`Span`].
138 pub const fn new(node: T, span: Span) -> Self {
139 Self { node, span }
140 }
141}
142
143/// Token types
144///
145/// Basic units of the language.
146#[derive(Debug, Clone, PartialEq)]
147pub enum Token {
148 /// Left parenthesis, (
149 LParen,
150 /// Right parenthesis, )
151 RParen,
152 /// Keyword
153 Keyword(Keyword),
154 /// Integer literal
155 IntLiteral(i64),
156 /// Float literal
157 FloatLiteral(f64),
158 /// String literal
159 StringLiteral(String),
160 /// Boolean literal
161 BoolLiteral(bool),
162 /// Null literal
163 NullLiteral,
164 /// A symbol path.
165 ///
166 /// `path` holds the segments joined by `.` *without* the leading sigil
167 /// i.e. bare root/element has an empty path.
168 ///
169 /// `root` records which namespace (`.` or `@`) the token was written in.
170 Symbol {
171 /// The symbol namespace (`.` or `@`).
172 root: SymbolRoot,
173 /// Dot-joined segment path **without** the leading sigil.
174 path: String,
175 },
176}
177
178/// Discriminator for which namespace a symbol path is rooted in.
179///
180/// - `Root` — resolved against the whole input (the `.` sigil).
181/// - `Element` — resolved against the current iteration element of the
182/// nearest enclosing `ForAll`/`Exists` (the `@` sigil).
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum SymbolRoot {
185 /// Root-rooted symbol (`.`), resolved against the whole input.
186 Root,
187 /// Element-rooted symbol (`@`), resolved against the current iteration
188 /// element of the nearest enclosing quantifier.
189 Element,
190}
191
192/// Keywords
193///
194/// All reserved keywords (operators, special names).
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum Keyword {
197 /// Equality verifier `EQ`.
198 EQ,
199 /// Inequality verifier `NE`.
200 NE,
201 /// Less-than verifier `LT`.
202 LT,
203 /// Less-than-or-equal verifier `LE`.
204 LE,
205 /// Greater-than verifier `GT`.
206 GT,
207 /// Greater-than-or-equal verifier `GE`.
208 GE,
209 /// Unary `NonEmpty` check.
210 NonEmpty,
211 /// Logical conjunction `AND`.
212 AND,
213 /// Logical disjunction `OR`.
214 OR,
215 /// Logical negation `NOT`.
216 NOT,
217 /// Universal quantifier `ForAll`.
218 ForAll,
219 /// Existential quantifier `Exists`.
220 Exists,
221 /// Arithmetic addition `Add`.
222 Add,
223 /// Arithmetic subtraction `Sub`.
224 Sub,
225 /// Arithmetic multiplication `Mul`.
226 Mul,
227 /// Arithmetic division `Div`.
228 Div,
229 /// Arithmetic modulo `Mod`.
230 Mod,
231 /// Arithmetic negation `Neg`.
232 Neg,
233 /// Arithmetic absolute value `Abs`.
234 Abs,
235 /// String concatenation `Concat`.
236 Concat,
237 /// String length `Length` (Unicode scalar count).
238 Length,
239 /// String substring `Substring` (char-indexed).
240 Substring,
241 /// String upper-case conversion `Upper`.
242 Upper,
243 /// String lower-case conversion `Lower`.
244 Lower,
245 /// List head accessor `Head`.
246 Head,
247 /// List tail accessor `Tail`.
248 Tail,
249 /// List/map element accessor `Get`.
250 Get,
251 /// Container size accessor `Count`.
252 Count,
253 /// Map keys accessor `GetKeys`.
254 GetKeys,
255 /// Map values accessor `GetValues`.
256 GetValues,
257}
258
259impl Keyword {
260 /// Match an identifier string against the reserved keyword table.
261 ///
262 /// Currently case-sensitive.
263 ///
264 /// NOTE: Consider case-insensitive matching in the future.
265 pub fn from_ident(s: &str) -> Option<Keyword> {
266 Some(match s {
267 "EQ" => Keyword::EQ,
268 "NE" => Keyword::NE,
269 "LT" => Keyword::LT,
270 "LE" => Keyword::LE,
271 "GT" => Keyword::GT,
272 "GE" => Keyword::GE,
273 "NonEmpty" => Keyword::NonEmpty,
274 "AND" => Keyword::AND,
275 "OR" => Keyword::OR,
276 "NOT" => Keyword::NOT,
277 "ForAll" => Keyword::ForAll,
278 "Exists" => Keyword::Exists,
279 "Add" => Keyword::Add,
280 "Sub" => Keyword::Sub,
281 "Mul" => Keyword::Mul,
282 "Div" => Keyword::Div,
283 "Mod" => Keyword::Mod,
284 "Neg" => Keyword::Neg,
285 "Abs" => Keyword::Abs,
286 "Concat" => Keyword::Concat,
287 "Length" => Keyword::Length,
288 "Substring" => Keyword::Substring,
289 "Upper" => Keyword::Upper,
290 "Lower" => Keyword::Lower,
291 "Head" => Keyword::Head,
292 "Tail" => Keyword::Tail,
293 "Get" => Keyword::Get,
294 "Count" => Keyword::Count,
295 "GetKeys" => Keyword::GetKeys,
296 "GetValues" => Keyword::GetValues,
297 _ => return None,
298 })
299 }
300}
301
302// ╭─────────────────────────────────────────────────────────╮
303// ═══════════════ Operator sub-enums (for AST) ═══════════════
304// ╰─────────────────────────────────────────────────────────╯
305
306/// Verifier operators
307///
308/// Verifiers are binary operators that return a boolean.
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub enum VerifierOp {
311 /// Equality.
312 EQ,
313 /// Inequality.
314 NE,
315 /// Less-than.
316 LT,
317 /// Less-than-or-equal.
318 LE,
319 /// Greater-than.
320 GT,
321 /// Greater-than-or-equal.
322 GE,
323}
324
325impl VerifierOp {
326 /// Match a keyword against the verifier operator table.
327 pub fn from_keyword(kw: Keyword) -> Option<Self> {
328 Some(match kw {
329 Keyword::EQ => VerifierOp::EQ,
330 Keyword::NE => VerifierOp::NE,
331 Keyword::LT => VerifierOp::LT,
332 Keyword::LE => VerifierOp::LE,
333 Keyword::GT => VerifierOp::GT,
334 Keyword::GE => VerifierOp::GE,
335 _ => return None,
336 })
337 }
338}
339
340/// Unary check operators
341///
342/// As name suggested, these operators are special verifiers that only take one operand.
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344pub enum UnaryCheckOp {
345 /// `NonEmpty` — false for `Null` and empty `String`/`List`/`Map`,
346 /// true otherwise.
347 NonEmpty,
348}
349
350impl UnaryCheckOp {
351 /// Match a keyword against the unary check operator table.
352 pub fn from_keyword(kw: Keyword) -> Option<Self> {
353 Some(match kw {
354 Keyword::NonEmpty => UnaryCheckOp::NonEmpty,
355 _ => return None,
356 })
357 }
358}
359
360/// Quantifier operators
361///
362/// These operators are used to quantify over a collection.
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364pub enum QuantifierOp {
365 /// Universal quantifier — predicate must hold for every element.
366 ForAll,
367 /// Existential quantifier — predicate must hold for at least one element.
368 Exists,
369}
370
371impl QuantifierOp {
372 /// Match a keyword against the quantifier operator table.
373 pub fn from_keyword(kw: Keyword) -> Option<Self> {
374 Some(match kw {
375 Keyword::ForAll => QuantifierOp::ForAll,
376 Keyword::Exists => QuantifierOp::Exists,
377 _ => return None,
378 })
379 }
380}
381
382/// Function operators
383///
384/// These operators are used to perform some operations on values.
385#[derive(Debug, Clone, Copy, PartialEq, Eq)]
386pub enum FuncOp {
387 /// Arithmetic addition (binary).
388 Add,
389 /// Arithmetic subtraction (binary).
390 Sub,
391 /// Arithmetic multiplication (binary).
392 Mul,
393 /// Arithmetic division (binary); errors on divide-by-zero.
394 Div,
395 /// Arithmetic modulo (binary); errors on divide-by-zero.
396 Mod,
397 /// Arithmetic negation (unary).
398 Neg,
399 /// Arithmetic absolute value (unary).
400 Abs,
401 /// String concatenation (binary).
402 Concat,
403 /// String length in Unicode scalars (unary).
404 Length,
405 /// String substring `(Substring s start len)` (ternary, char-indexed).
406 Substring,
407 /// String upper-case conversion (unary, Unicode-aware).
408 Upper,
409 /// String lower-case conversion (unary, Unicode-aware).
410 Lower,
411 /// First element of a list (unary); errors on empty.
412 Head,
413 /// All but the first element of a list (unary); errors on empty.
414 Tail,
415 /// Indexed accessor: `(Get list Int)` or `(Get map String)` (binary).
416 Get,
417 /// Size of a list or map (unary).
418 Count,
419 /// Sorted list of map keys (unary).
420 GetKeys,
421 /// Map values sorted by key (unary).
422 GetValues,
423}
424
425impl FuncOp {
426 /// Match a keyword against the function operator table.
427 pub fn from_keyword(kw: Keyword) -> Option<Self> {
428 Some(match kw {
429 Keyword::Add => FuncOp::Add,
430 Keyword::Sub => FuncOp::Sub,
431 Keyword::Mul => FuncOp::Mul,
432 Keyword::Div => FuncOp::Div,
433 Keyword::Mod => FuncOp::Mod,
434 Keyword::Neg => FuncOp::Neg,
435 Keyword::Abs => FuncOp::Abs,
436 Keyword::Concat => FuncOp::Concat,
437 Keyword::Length => FuncOp::Length,
438 Keyword::Substring => FuncOp::Substring,
439 Keyword::Upper => FuncOp::Upper,
440 Keyword::Lower => FuncOp::Lower,
441 Keyword::Head => FuncOp::Head,
442 Keyword::Tail => FuncOp::Tail,
443 Keyword::Get => FuncOp::Get,
444 Keyword::Count => FuncOp::Count,
445 Keyword::GetKeys => FuncOp::GetKeys,
446 Keyword::GetValues => FuncOp::GetValues,
447 _ => return None,
448 })
449 }
450
451 /// Return the expected arity of the function operator.
452 pub fn expected_arity(&self) -> usize {
453 match self {
454 FuncOp::Neg
455 | FuncOp::Abs
456 | FuncOp::Length
457 | FuncOp::Upper
458 | FuncOp::Lower
459 | FuncOp::Head
460 | FuncOp::Tail
461 | FuncOp::Count
462 | FuncOp::GetKeys
463 | FuncOp::GetValues => 1,
464 FuncOp::Add
465 | FuncOp::Sub
466 | FuncOp::Mul
467 | FuncOp::Div
468 | FuncOp::Mod
469 | FuncOp::Concat
470 | FuncOp::Get => 2,
471 FuncOp::Substring => 3,
472 }
473 }
474
475 /// Return the name of the function operator.
476 pub fn name(&self) -> &'static str {
477 match self {
478 FuncOp::Add => "Add",
479 FuncOp::Sub => "Sub",
480 FuncOp::Mul => "Mul",
481 FuncOp::Div => "Div",
482 FuncOp::Mod => "Mod",
483 FuncOp::Neg => "Neg",
484 FuncOp::Abs => "Abs",
485 FuncOp::Concat => "Concat",
486 FuncOp::Length => "Length",
487 FuncOp::Substring => "Substring",
488 FuncOp::Upper => "Upper",
489 FuncOp::Lower => "Lower",
490 FuncOp::Head => "Head",
491 FuncOp::Tail => "Tail",
492 FuncOp::Get => "Get",
493 FuncOp::Count => "Count",
494 FuncOp::GetKeys => "GetKeys",
495 FuncOp::GetValues => "GetValues",
496 }
497 }
498}
499
500/// Literals
501///
502/// "Words", the building blocks of the language. (for example, "apple",
503/// 123, 45.6, true, false, null)
504#[derive(Debug, Clone, PartialEq)]
505pub enum Literal {
506 /// 64-bit signed integer literal.
507 Int(i64),
508 /// IEEE-754 double-precision float literal.
509 Float(f64),
510 /// UTF-8 string literal.
511 String(String),
512 /// Boolean literal (`True` / `False`).
513 Bool(bool),
514 /// `Null` literal.
515 Null,
516}
517
518// ╭────────────────────────────────────────────────╮
519// ═════════════════ AST (two-tier) ═════════════════
520// ╰────────────────────────────────────────────────╯
521
522/// The AST type which produces booleans.
523pub type SpannedBoolExpr = Spanned<BoolExpr>;
524
525/// The AST type which produces values.
526pub type SpannedValueExpr = Spanned<ValueExpr>;
527
528/// Top-level program, must evaluate to Boolean.
529#[derive(Debug, Clone, PartialEq)]
530pub struct Program {
531 /// Root boolean expression of the program.
532 pub expr: SpannedBoolExpr,
533}
534
535/// Boolean-producing expressions.
536#[derive(Debug, Clone, PartialEq)]
537pub enum BoolExpr {
538 /// Boolean literal `True` or `False`.
539 Literal(bool),
540
541 /// Verifier takes two values, evaluates, and returns a boolean.
542 Verifier {
543 /// Verifier operator.
544 op: VerifierOp,
545 /// Left-hand value expression.
546 left: Box<SpannedValueExpr>,
547 /// Right-hand value expression.
548 right: Box<SpannedValueExpr>,
549 },
550
551 /// Logical AND.
552 And(Box<SpannedBoolExpr>, Box<SpannedBoolExpr>),
553
554 /// Logical OR.
555 Or(Box<SpannedBoolExpr>, Box<SpannedBoolExpr>),
556
557 /// Logical NOT.
558 Not(Box<SpannedBoolExpr>),
559
560 /// Boolean-producing checks which only take one operand.
561 UnaryCheck {
562 /// Unary check operator.
563 op: UnaryCheckOp,
564 /// Operand value expression.
565 operand: Box<SpannedValueExpr>,
566 },
567
568 /// Quantifier takes a predicate and an operand, and returns a boolean.
569 Quantifier {
570 /// Quantifier operator.
571 op: QuantifierOp,
572 /// Predicate applied to each element.
573 predicate: Spanned<Predicate>,
574 /// Collection-producing operand.
575 operand: Box<SpannedValueExpr>,
576 },
577}
578
579/// Value-producing expressions (Entity).
580///
581/// There are three kinds of value-producing expressions:
582/// - Literals (immediate values): `123`, `45.6`, `"hello"`, `true`, `false`, `null`.
583/// - Symbols (references to Symbol table): `@.a`, `@.b`, `@.c`.
584/// - Results from function calls: `(Add 1 2)`, `(Sub 1 2)`.
585#[derive(Debug, Clone, PartialEq)]
586pub enum ValueExpr {
587 /// Immediate literal value.
588 Literal(Literal),
589 /// A symbol reference.
590 ///
591 /// `path` is the dot-joined segment string without
592 /// the leading sigil (empty string = bare root/element).
593 ///
594 /// `root` tells the executor which namespace to resolve against.
595 Symbol {
596 /// The root type (namespace, *local* `@` or *global* `.``)
597 root: SymbolRoot,
598 /// The dot-joined path segments (empty string = bare root/element).
599 path: String,
600 },
601 /// Function-call expression that reduces to a value.
602 FuncCall {
603 /// Function operator being invoked.
604 op: FuncOp,
605 /// Argument value expressions.
606 args: Vec<SpannedValueExpr>,
607 },
608}
609
610/// Predicate, unary boolean function, **used exclusively in quantifiers**.
611///
612/// You can think of a predicate as *curried* verifier.
613///
614/// `PartialVerifier` and `UnaryCheck` are the ergonomic fast paths whose
615/// bound value is evaluated once at setup time.
616///
617/// `Full` carries a general boolean expression that is re-evaluated per
618/// iteration element with the element bound in scope — this is what enables
619/// `(ForAll (EQ @.a @.b) …)`.
620#[derive(Debug, Clone, PartialEq)]
621pub enum Predicate {
622 /// Partially-fulfilled verifier (one argument known).
623 ///
624 /// For example, `(EQ @.a)`, notice that the second argument is missing.
625 PartialVerifier {
626 /// Verifier operator.
627 op: VerifierOp,
628 /// Pre-supplied operand; the iteration element fills the missing slot.
629 bound: Box<SpannedValueExpr>,
630 },
631 /// Unary check, used in quantifiers.
632 ///
633 /// For example, `(NonEmpty @.a)`.
634 UnaryCheck(UnaryCheckOp),
635 /// Full boolean expression, used in quantifiers.
636 ///
637 /// For example, `(EQ @.a @.b)`.
638 Full(Box<SpannedBoolExpr>),
639}
640
641/// Tests!
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
647 fn keyword_from_ident_matches_reserved_words() {
648 assert_eq!(Keyword::from_ident("EQ"), Some(Keyword::EQ));
649 assert_eq!(Keyword::from_ident("NonEmpty"), Some(Keyword::NonEmpty));
650 assert_eq!(Keyword::from_ident("ForAll"), Some(Keyword::ForAll));
651 assert_eq!(Keyword::from_ident("GetValues"), Some(Keyword::GetValues));
652 }
653
654 #[test]
655 fn keyword_from_ident_rejects_unknown_and_is_case_sensitive() {
656 assert_eq!(Keyword::from_ident("eq"), None);
657 assert_eq!(Keyword::from_ident("foobar"), None);
658 assert_eq!(Keyword::from_ident(""), None);
659 }
660
661 #[test]
662 fn verifier_op_conversion_covers_all_variants() {
663 let pairs = [
664 (Keyword::EQ, VerifierOp::EQ),
665 (Keyword::NE, VerifierOp::NE),
666 (Keyword::LT, VerifierOp::LT),
667 (Keyword::LE, VerifierOp::LE),
668 (Keyword::GT, VerifierOp::GT),
669 (Keyword::GE, VerifierOp::GE),
670 ];
671 for (kw, op) in pairs {
672 assert_eq!(VerifierOp::from_keyword(kw), Some(op));
673 }
674 assert_eq!(VerifierOp::from_keyword(Keyword::AND), None);
675 }
676
677 #[test]
678 fn quantifier_op_conversion() {
679 assert_eq!(
680 QuantifierOp::from_keyword(Keyword::ForAll),
681 Some(QuantifierOp::ForAll)
682 );
683 assert_eq!(
684 QuantifierOp::from_keyword(Keyword::Exists),
685 Some(QuantifierOp::Exists)
686 );
687 assert_eq!(QuantifierOp::from_keyword(Keyword::NonEmpty), None);
688 }
689
690 #[test]
691 fn func_op_arity_table_matches_spec() {
692 let one = [
693 FuncOp::Neg,
694 FuncOp::Abs,
695 FuncOp::Length,
696 FuncOp::Upper,
697 FuncOp::Lower,
698 FuncOp::Head,
699 FuncOp::Tail,
700 FuncOp::Count,
701 FuncOp::GetKeys,
702 FuncOp::GetValues,
703 ];
704 let two = [
705 FuncOp::Add,
706 FuncOp::Sub,
707 FuncOp::Mul,
708 FuncOp::Div,
709 FuncOp::Mod,
710 FuncOp::Concat,
711 FuncOp::Get,
712 ];
713 let three = [FuncOp::Substring];
714
715 for op in one {
716 assert_eq!(op.expected_arity(), 1, "{:?} should have arity 1", op);
717 }
718 for op in two {
719 assert_eq!(op.expected_arity(), 2, "{:?} should have arity 2", op);
720 }
721 for op in three {
722 assert_eq!(op.expected_arity(), 3, "{:?} should have arity 3", op);
723 }
724 }
725
726 #[test]
727 fn func_op_from_keyword_rejects_non_func_keywords() {
728 assert_eq!(FuncOp::from_keyword(Keyword::AND), None);
729 assert_eq!(FuncOp::from_keyword(Keyword::EQ), None);
730 assert_eq!(FuncOp::from_keyword(Keyword::ForAll), None);
731 assert_eq!(FuncOp::from_keyword(Keyword::Add), Some(FuncOp::Add));
732 }
733
734 #[test]
735 fn spanned_wrapper_constructs_and_exposes_fields() {
736 let sp = Spanned::new(Literal::Int(42), Span::new(0, 2));
737 assert_eq!(sp.node, Literal::Int(42));
738 assert_eq!(sp.span, Span::new(0, 2));
739 }
740
741 #[test]
742 fn ast_structural_equality_sanity() {
743 // Build `(GT (Add 1 2) 0)` as an AST and assert round-trip equality.
744 let left = Spanned::new(
745 ValueExpr::FuncCall {
746 op: FuncOp::Add,
747 args: vec![
748 Spanned::new(ValueExpr::Literal(Literal::Int(1)), Span::new(5, 6)),
749 Spanned::new(ValueExpr::Literal(Literal::Int(2)), Span::new(7, 8)),
750 ],
751 },
752 Span::new(1, 9),
753 );
754 let right = Spanned::new(ValueExpr::Literal(Literal::Int(0)), Span::new(10, 11));
755 let expr = Spanned::new(
756 BoolExpr::Verifier {
757 op: VerifierOp::GT,
758 left: Box::new(left.clone()),
759 right: Box::new(right.clone()),
760 },
761 Span::new(0, 12),
762 );
763 let program = Program { expr: expr.clone() };
764 assert_eq!(program.expr, expr);
765 }
766
767 #[test]
768 fn func_op_name_round_trips_through_keyword() {
769 // A quick consistency check: for every FuncOp we can recover its keyword.
770 for op in [
771 FuncOp::Add,
772 FuncOp::Sub,
773 FuncOp::Mul,
774 FuncOp::Div,
775 FuncOp::Mod,
776 FuncOp::Neg,
777 FuncOp::Abs,
778 FuncOp::Concat,
779 FuncOp::Length,
780 FuncOp::Substring,
781 FuncOp::Upper,
782 FuncOp::Lower,
783 FuncOp::Head,
784 FuncOp::Tail,
785 FuncOp::Get,
786 FuncOp::Count,
787 FuncOp::GetKeys,
788 FuncOp::GetValues,
789 ] {
790 let kw = Keyword::from_ident(op.name()).expect("name should be a keyword");
791 assert_eq!(FuncOp::from_keyword(kw), Some(op));
792 }
793 }
794}