Skip to main content

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}