Skip to main content

intent_parser/
parser.rs

1//! Pest-based parser that converts `.intent` source text into a typed AST.
2//!
3//! The grammar is defined in `grammar/intent.pest`. This module wraps the
4//! generated pest parser and transforms pest `Pairs` into [`ast`] nodes.
5
6use pest::Parser;
7use pest_derive::Parser;
8
9use crate::ast::*;
10
11/// The pest-generated parser. Grammar is loaded at compile time from the
12/// workspace-relative path.
13#[derive(Parser)]
14#[grammar = "src/intent.pest"]
15pub struct IntentParser;
16
17/// Parse error with human-readable message and source location.
18#[derive(Debug, thiserror::Error, miette::Diagnostic, Clone)]
19#[error("{message}")]
20#[diagnostic(code(intent::parse::syntax_error))]
21pub struct ParseError {
22    pub message: String,
23    #[label("{label}")]
24    pub span: miette::SourceSpan,
25    pub label: String,
26    #[help]
27    pub help: Option<String>,
28}
29
30impl From<pest::error::Error<Rule>> for ParseError {
31    fn from(err: pest::error::Error<Rule>) -> Self {
32        humanize_pest_error(err)
33    }
34}
35
36/// Convert a pest error into a human-readable ParseError with helpful messages.
37fn humanize_pest_error(err: pest::error::Error<Rule>) -> ParseError {
38    let (offset, len) = match err.location {
39        pest::error::InputLocation::Pos(p) => (p, 1),
40        pest::error::InputLocation::Span((s, e)) => (s, e - s),
41    };
42    let span: miette::SourceSpan = (offset, len).into();
43
44    // Extract the expected rules from the pest error variant
45    let (message, label, help) = match &err.variant {
46        pest::error::ErrorVariant::ParsingError { positives, .. } => {
47            humanize_expected_rules(positives)
48        }
49        pest::error::ErrorVariant::CustomError { message } => {
50            (message.clone(), "here".to_string(), None)
51        }
52    };
53
54    ParseError {
55        message,
56        span,
57        label,
58        help,
59    }
60}
61
62/// Map pest rule names to human-readable error messages.
63fn humanize_expected_rules(rules: &[Rule]) -> (String, String, Option<String>) {
64    // Check for common patterns in what was expected
65    let rule_set: std::collections::HashSet<&Rule> = rules.iter().collect();
66
67    if rule_set.contains(&Rule::module_decl) {
68        return (
69            "missing module declaration".to_string(),
70            "expected `module ModuleName`".to_string(),
71            Some("every .intent file must start with `module ModuleName`".to_string()),
72        );
73    }
74
75    if rule_set.contains(&Rule::union_type) || rule_set.contains(&Rule::simple_type) {
76        return (
77            "invalid type".to_string(),
78            "expected a type".to_string(),
79            Some(
80                "types must start with an uppercase letter (e.g., String, UUID, MyEntity)"
81                    .to_string(),
82            ),
83        );
84    }
85
86    if rule_set.contains(&Rule::optional_marker) && rule_set.contains(&Rule::ident) {
87        return (
88            "unexpected end of block".to_string(),
89            "expected a field declaration or `}`".to_string(),
90            Some("check for unclosed braces or missing field declarations".to_string()),
91        );
92    }
93
94    if rule_set.contains(&Rule::field_decl) || rule_set.contains(&Rule::param_decl) {
95        return (
96            "expected a field or parameter declaration".to_string(),
97            "expected `name: Type`".to_string(),
98            Some("fields are declared as `name: Type` (e.g., `email: String`)".to_string()),
99        );
100    }
101
102    if rule_set.contains(&Rule::EOI) {
103        return (
104            "unexpected content after end of file".to_string(),
105            "unexpected token".to_string(),
106            Some("check for extra text or unclosed blocks".to_string()),
107        );
108    }
109
110    // Fallback: format the rule names
111    let names: Vec<String> = rules
112        .iter()
113        .filter(|r| !matches!(r, Rule::WHITESPACE | Rule::COMMENT | Rule::EOI))
114        .map(|r| format!("`{:?}`", r))
115        .collect();
116
117    let msg = if names.is_empty() {
118        "syntax error".to_string()
119    } else {
120        format!("expected {}", names.join(" or "))
121    };
122
123    ("syntax error".to_string(), msg, None)
124}
125
126/// Parse a complete `.intent` source string into an AST [`File`].
127pub fn parse_file(source: &str) -> Result<File, ParseError> {
128    let pairs = IntentParser::parse(Rule::file, source)?;
129    let pair = pairs.into_iter().next().unwrap();
130    Ok(build_file(pair))
131}
132
133// ── Builders ─────────────────────────────────────────────────
134// Each `build_*` function consumes a pest `Pair` and returns an AST node.
135
136fn span_of(pair: &pest::iterators::Pair<'_, Rule>) -> Span {
137    let s = pair.as_span();
138    Span {
139        start: s.start(),
140        end: s.end(),
141    }
142}
143
144fn build_file(pair: pest::iterators::Pair<'_, Rule>) -> File {
145    let span = span_of(&pair);
146    let mut inner = pair.into_inner();
147
148    let module = build_module_decl(inner.next().unwrap());
149
150    let mut doc = None;
151    let mut imports = Vec::new();
152    let mut items = Vec::new();
153
154    for p in inner {
155        match p.as_rule() {
156            Rule::doc_block => doc = Some(build_doc_block(p)),
157            Rule::use_decl => imports.push(build_use_decl(p)),
158            Rule::entity_decl => items.push(TopLevelItem::Entity(build_entity_decl(p))),
159            Rule::action_decl => items.push(TopLevelItem::Action(build_action_decl(p))),
160            Rule::invariant_decl => items.push(TopLevelItem::Invariant(build_invariant_decl(p))),
161            Rule::edge_cases_decl => items.push(TopLevelItem::EdgeCases(build_edge_cases_decl(p))),
162            Rule::test_decl => items.push(TopLevelItem::Test(build_test_decl(p))),
163            Rule::EOI => {}
164            _ => {}
165        }
166    }
167
168    File {
169        module,
170        doc,
171        imports,
172        items,
173        span,
174    }
175}
176
177fn build_use_decl(pair: pest::iterators::Pair<'_, Rule>) -> UseDecl {
178    let span = span_of(&pair);
179    let mut inner = pair.into_inner();
180    let module_name = inner.next().unwrap().as_str().to_string();
181    let item = inner.next().map(|p| p.as_str().to_string());
182    UseDecl {
183        module_name,
184        item,
185        span,
186    }
187}
188
189fn build_module_decl(pair: pest::iterators::Pair<'_, Rule>) -> ModuleDecl {
190    let span = span_of(&pair);
191    let name = pair.into_inner().next().unwrap().as_str().to_string();
192    ModuleDecl { name, span }
193}
194
195fn build_doc_block(pair: pest::iterators::Pair<'_, Rule>) -> DocBlock {
196    let span = span_of(&pair);
197    let lines = pair
198        .into_inner()
199        .map(|p| {
200            let text = p.as_str();
201            let content = text
202                .strip_prefix("---")
203                .unwrap_or(text)
204                .trim_end_matches('\n');
205            content.strip_prefix(' ').unwrap_or(content).to_string()
206        })
207        .collect();
208    DocBlock { lines, span }
209}
210
211fn build_entity_decl(pair: pest::iterators::Pair<'_, Rule>) -> EntityDecl {
212    let span = span_of(&pair);
213    let mut doc = None;
214    let mut name = String::new();
215    let mut fields = Vec::new();
216
217    for p in pair.into_inner() {
218        match p.as_rule() {
219            Rule::doc_block => doc = Some(build_doc_block(p)),
220            Rule::type_ident => name = p.as_str().to_string(),
221            Rule::field_decl => fields.push(build_field_decl(p)),
222            _ => {}
223        }
224    }
225
226    EntityDecl {
227        doc,
228        name,
229        fields,
230        span,
231    }
232}
233
234fn build_field_decl(pair: pest::iterators::Pair<'_, Rule>) -> FieldDecl {
235    let span = span_of(&pair);
236    let mut inner = pair.into_inner();
237    let name = inner.next().unwrap().as_str().to_string();
238    let ty = build_type_expr(inner.next().unwrap());
239    FieldDecl { name, ty, span }
240}
241
242fn build_action_decl(pair: pest::iterators::Pair<'_, Rule>) -> ActionDecl {
243    let span = span_of(&pair);
244    let mut doc = None;
245    let mut name = String::new();
246    let mut params = Vec::new();
247    let mut requires = None;
248    let mut ensures = None;
249    let mut properties = None;
250
251    for p in pair.into_inner() {
252        match p.as_rule() {
253            Rule::doc_block => doc = Some(build_doc_block(p)),
254            Rule::type_ident => name = p.as_str().to_string(),
255            Rule::param_decl => params.push(build_field_decl(p)),
256            Rule::requires_block => requires = Some(build_requires_block(p)),
257            Rule::ensures_block => ensures = Some(build_ensures_block(p)),
258            Rule::properties_block => properties = Some(build_properties_block(p)),
259            _ => {}
260        }
261    }
262
263    ActionDecl {
264        doc,
265        name,
266        params,
267        requires,
268        ensures,
269        properties,
270        span,
271    }
272}
273
274fn build_requires_block(pair: pest::iterators::Pair<'_, Rule>) -> RequiresBlock {
275    let span = span_of(&pair);
276    let conditions = pair.into_inner().map(build_expr).collect();
277    RequiresBlock { conditions, span }
278}
279
280fn build_ensures_block(pair: pest::iterators::Pair<'_, Rule>) -> EnsuresBlock {
281    let span = span_of(&pair);
282    let items = pair
283        .into_inner()
284        .map(|p| match p.as_rule() {
285            Rule::when_clause => EnsuresItem::When(build_when_clause(p)),
286            _ => EnsuresItem::Expr(build_expr(p)),
287        })
288        .collect();
289    EnsuresBlock { items, span }
290}
291
292fn build_when_clause(pair: pest::iterators::Pair<'_, Rule>) -> WhenClause {
293    let span = span_of(&pair);
294    let mut inner = pair.into_inner();
295    let condition = build_or_expr(inner.next().unwrap());
296    let consequence = build_expr(inner.next().unwrap());
297    WhenClause {
298        condition,
299        consequence,
300        span,
301    }
302}
303
304fn build_properties_block(pair: pest::iterators::Pair<'_, Rule>) -> PropertiesBlock {
305    let span = span_of(&pair);
306    let entries = pair.into_inner().map(build_prop_entry).collect();
307    PropertiesBlock { entries, span }
308}
309
310fn build_prop_entry(pair: pest::iterators::Pair<'_, Rule>) -> PropEntry {
311    let span = span_of(&pair);
312    let mut inner = pair.into_inner();
313    let key = inner.next().unwrap().as_str().to_string();
314    let value = build_prop_value(inner.next().unwrap());
315    PropEntry { key, value, span }
316}
317
318fn build_prop_value(pair: pest::iterators::Pair<'_, Rule>) -> PropValue {
319    match pair.as_rule() {
320        Rule::obj_literal => {
321            let fields = pair
322                .into_inner()
323                .map(|f| {
324                    let mut inner = f.into_inner();
325                    let key = inner.next().unwrap().as_str().to_string();
326                    let value = build_prop_value(inner.next().unwrap());
327                    (key, value)
328                })
329                .collect();
330            PropValue::Object(fields)
331        }
332        Rule::list_literal => {
333            let items = pair.into_inner().map(build_prop_value).collect();
334            PropValue::List(items)
335        }
336        Rule::string_literal => {
337            let s = extract_string(pair);
338            PropValue::Literal(Literal::String(s))
339        }
340        Rule::number_literal => PropValue::Literal(parse_number_literal(pair.as_str())),
341        Rule::bool_literal => PropValue::Literal(Literal::Bool(pair.as_str() == "true")),
342        Rule::ident => PropValue::Ident(pair.as_str().to_string()),
343        // For expressions nested in prop value contexts, try to extract
344        Rule::expr | Rule::implies_expr => {
345            // Recurse into inner pairs
346            let inner = pair.into_inner().next().unwrap();
347            build_prop_value(inner)
348        }
349        _ => PropValue::Ident(pair.as_str().to_string()),
350    }
351}
352
353fn build_invariant_decl(pair: pest::iterators::Pair<'_, Rule>) -> InvariantDecl {
354    let span = span_of(&pair);
355    let mut doc = None;
356    let mut name = String::new();
357    let mut body = None;
358
359    for p in pair.into_inner() {
360        match p.as_rule() {
361            Rule::doc_block => doc = Some(build_doc_block(p)),
362            Rule::type_ident => name = p.as_str().to_string(),
363            Rule::expr => body = Some(build_expr(p)),
364            _ => {}
365        }
366    }
367
368    InvariantDecl {
369        doc,
370        name,
371        body: body.expect("invariant must have a body expression"),
372        span,
373    }
374}
375
376fn build_edge_cases_decl(pair: pest::iterators::Pair<'_, Rule>) -> EdgeCasesDecl {
377    let span = span_of(&pair);
378    let rules = pair.into_inner().map(build_edge_rule).collect();
379    EdgeCasesDecl { rules, span }
380}
381
382fn build_edge_rule(pair: pest::iterators::Pair<'_, Rule>) -> EdgeRule {
383    let span = span_of(&pair);
384    let mut inner = pair.into_inner();
385    let condition = build_or_expr(inner.next().unwrap());
386    let action = build_action_call(inner.next().unwrap());
387    EdgeRule {
388        condition,
389        action,
390        span,
391    }
392}
393
394fn build_action_call(pair: pest::iterators::Pair<'_, Rule>) -> ActionCall {
395    let span = span_of(&pair);
396    let mut inner = pair.into_inner();
397    let name = inner.next().unwrap().as_str().to_string();
398    let args = inner
399        .next()
400        .map(|p| p.into_inner().map(build_call_arg).collect())
401        .unwrap_or_default();
402    ActionCall { name, args, span }
403}
404
405// ── Type expression builders ─────────────────────────────────
406
407fn build_type_expr(pair: pest::iterators::Pair<'_, Rule>) -> TypeExpr {
408    let span = span_of(&pair);
409    let mut optional = false;
410    let mut ty_kind = None;
411
412    for p in pair.into_inner() {
413        match p.as_rule() {
414            Rule::union_type => ty_kind = Some(build_union_type(p)),
415            Rule::optional_marker => optional = true,
416            _ => {}
417        }
418    }
419
420    TypeExpr {
421        ty: ty_kind.unwrap(),
422        optional,
423        span,
424    }
425}
426
427fn build_union_type(pair: pest::iterators::Pair<'_, Rule>) -> TypeKind {
428    let variants: Vec<TypeKind> = pair.into_inner().map(build_base_type).collect();
429    if variants.len() == 1 {
430        variants.into_iter().next().unwrap()
431    } else {
432        TypeKind::Union(variants)
433    }
434}
435
436fn build_base_type(pair: pest::iterators::Pair<'_, Rule>) -> TypeKind {
437    match pair.as_rule() {
438        Rule::list_type => {
439            let inner = pair.into_inner().next().unwrap();
440            TypeKind::List(Box::new(build_type_expr(inner)))
441        }
442        Rule::set_type => {
443            let inner = pair.into_inner().next().unwrap();
444            TypeKind::Set(Box::new(build_type_expr(inner)))
445        }
446        Rule::map_type => {
447            let mut inner = pair.into_inner();
448            let key = build_type_expr(inner.next().unwrap());
449            let value = build_type_expr(inner.next().unwrap());
450            TypeKind::Map(Box::new(key), Box::new(value))
451        }
452        Rule::parameterized_type => {
453            let mut inner = pair.into_inner();
454            let name = inner.next().unwrap().as_str().to_string();
455            let params = inner.map(build_type_param).collect();
456            TypeKind::Parameterized { name, params }
457        }
458        Rule::simple_type => {
459            let name = pair.into_inner().next().unwrap().as_str().to_string();
460            TypeKind::Simple(name)
461        }
462        _ => TypeKind::Simple(pair.as_str().to_string()),
463    }
464}
465
466fn build_type_param(pair: pest::iterators::Pair<'_, Rule>) -> TypeParam {
467    let span = span_of(&pair);
468    let mut inner = pair.into_inner();
469    let name = inner.next().unwrap().as_str().to_string();
470    let value = parse_number_literal(inner.next().unwrap().as_str());
471    TypeParam { name, value, span }
472}
473
474// ── Expression builders ──────────────────────────────────────
475
476fn build_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
477    let span = span_of(&pair);
478    let inner = pair.into_inner().next().unwrap();
479    match inner.as_rule() {
480        Rule::implies_expr => build_implies_expr(inner),
481        _ => {
482            let kind = build_expr_kind(inner);
483            Expr { kind, span }
484        }
485    }
486}
487
488fn build_implies_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
489    let mut parts: Vec<pest::iterators::Pair<'_, Rule>> = Vec::new();
490
491    for p in pair.into_inner() {
492        match p.as_rule() {
493            Rule::implies_op => {}
494            _ => parts.push(p),
495        }
496    }
497
498    let mut result = build_or_expr(parts.remove(0));
499    for part in parts {
500        let right = build_or_expr(part);
501        let new_span = Span {
502            start: result.span.start,
503            end: right.span.end,
504        };
505        result = Expr {
506            kind: ExprKind::Implies(Box::new(result), Box::new(right)),
507            span: new_span,
508        };
509    }
510    result
511}
512
513fn build_or_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
514    let span = span_of(&pair);
515    let mut parts: Vec<pest::iterators::Pair<'_, Rule>> = Vec::new();
516
517    for p in pair.into_inner() {
518        match p.as_rule() {
519            Rule::or_op => {}
520            _ => parts.push(p),
521        }
522    }
523
524    if parts.is_empty() {
525        return Expr {
526            kind: ExprKind::Literal(Literal::Null),
527            span,
528        };
529    }
530
531    let mut result = build_and_expr(parts.remove(0));
532    for part in parts {
533        let right = build_and_expr(part);
534        let new_span = Span {
535            start: result.span.start,
536            end: right.span.end,
537        };
538        result = Expr {
539            kind: ExprKind::Or(Box::new(result), Box::new(right)),
540            span: new_span,
541        };
542    }
543    result
544}
545
546fn build_and_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
547    let span = span_of(&pair);
548    let mut parts: Vec<pest::iterators::Pair<'_, Rule>> = Vec::new();
549
550    for p in pair.into_inner() {
551        match p.as_rule() {
552            Rule::and_op => {}
553            _ => parts.push(p),
554        }
555    }
556
557    if parts.is_empty() {
558        return Expr {
559            kind: ExprKind::Literal(Literal::Null),
560            span,
561        };
562    }
563
564    let mut result = build_not_expr(parts.remove(0));
565    for part in parts {
566        let right = build_not_expr(part);
567        let new_span = Span {
568            start: result.span.start,
569            end: right.span.end,
570        };
571        result = Expr {
572            kind: ExprKind::And(Box::new(result), Box::new(right)),
573            span: new_span,
574        };
575    }
576    result
577}
578
579fn build_not_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
580    let span = span_of(&pair);
581    let mut inner = pair.into_inner();
582    let first = inner.next().unwrap();
583
584    match first.as_rule() {
585        Rule::not_op => {
586            let operand = build_not_expr(inner.next().unwrap());
587            Expr {
588                kind: ExprKind::Not(Box::new(operand)),
589                span,
590            }
591        }
592        Rule::cmp_expr => build_cmp_expr(first),
593        _ => {
594            let kind = build_expr_kind(first);
595            Expr { kind, span }
596        }
597    }
598}
599
600fn build_cmp_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
601    let span = span_of(&pair);
602    let mut inner = pair.into_inner();
603    let left = build_add_expr(inner.next().unwrap());
604
605    if let Some(op_pair) = inner.next() {
606        let op = match op_pair.as_str() {
607            "==" => CmpOp::Eq,
608            "!=" => CmpOp::Ne,
609            "<" => CmpOp::Lt,
610            ">" => CmpOp::Gt,
611            "<=" => CmpOp::Le,
612            ">=" => CmpOp::Ge,
613            _ => unreachable!("unknown cmp op: {}", op_pair.as_str()),
614        };
615        let right = build_add_expr(inner.next().unwrap());
616        Expr {
617            kind: ExprKind::Compare {
618                left: Box::new(left),
619                op,
620                right: Box::new(right),
621            },
622            span,
623        }
624    } else {
625        Expr {
626            kind: left.kind,
627            span,
628        }
629    }
630}
631
632fn build_add_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
633    let mut children: Vec<pest::iterators::Pair<'_, Rule>> = pair.into_inner().collect();
634
635    if children.len() == 1 {
636        return build_primary(children.remove(0));
637    }
638
639    // Interleaved: primary, op, primary, op, primary, ...
640    let mut iter = children.into_iter();
641    let mut result = build_primary(iter.next().unwrap());
642
643    while let Some(op_pair) = iter.next() {
644        let op = match op_pair.as_str() {
645            "+" => ArithOp::Add,
646            "-" => ArithOp::Sub,
647            _ => unreachable!("unknown add op"),
648        };
649        let right = build_primary(iter.next().unwrap());
650        let new_span = Span {
651            start: result.span.start,
652            end: right.span.end,
653        };
654        result = Expr {
655            kind: ExprKind::Arithmetic {
656                left: Box::new(result),
657                op,
658                right: Box::new(right),
659            },
660            span: new_span,
661        };
662    }
663
664    result
665}
666
667fn build_primary(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
668    let span = span_of(&pair);
669    let mut inner: Vec<pest::iterators::Pair<'_, Rule>> = pair.into_inner().collect();
670
671    // First child is the atom, rest are `.ident` field accesses
672    let atom_pair = inner.remove(0);
673    let base = build_atom(atom_pair);
674
675    if inner.is_empty() {
676        return Expr {
677            kind: base.kind,
678            span,
679        };
680    }
681
682    let fields: Vec<String> = inner.into_iter().map(|p| p.as_str().to_string()).collect();
683
684    Expr {
685        kind: ExprKind::FieldAccess {
686            root: Box::new(base),
687            fields,
688        },
689        span,
690    }
691}
692
693fn build_atom(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
694    let span = span_of(&pair);
695    match pair.as_rule() {
696        Rule::old_expr => {
697            let inner = pair.into_inner().next().unwrap();
698            let expr = build_expr(inner);
699            Expr {
700                kind: ExprKind::Old(Box::new(expr)),
701                span,
702            }
703        }
704        Rule::quantifier_expr => {
705            let mut inner = pair.into_inner();
706            let kw = inner.next().unwrap();
707            let kind = match kw.as_str() {
708                "forall" => QuantifierKind::Forall,
709                "exists" => QuantifierKind::Exists,
710                _ => unreachable!(),
711            };
712            let binding = inner.next().unwrap().as_str().to_string();
713            let ty = inner.next().unwrap().as_str().to_string();
714            let body = build_expr(inner.next().unwrap());
715            Expr {
716                kind: ExprKind::Quantifier {
717                    kind,
718                    binding,
719                    ty,
720                    body: Box::new(body),
721                },
722                span,
723            }
724        }
725        Rule::null_literal => Expr {
726            kind: ExprKind::Literal(Literal::Null),
727            span,
728        },
729        Rule::bool_literal => Expr {
730            kind: ExprKind::Literal(Literal::Bool(pair.as_str() == "true")),
731            span,
732        },
733        Rule::number_literal => Expr {
734            kind: ExprKind::Literal(parse_number_literal(pair.as_str())),
735            span,
736        },
737        Rule::string_literal => Expr {
738            kind: ExprKind::Literal(Literal::String(extract_string(pair))),
739            span,
740        },
741        Rule::list_literal => {
742            let items = pair.into_inner().map(build_expr).collect();
743            Expr {
744                kind: ExprKind::List(items),
745                span,
746            }
747        }
748        Rule::paren_expr => {
749            let inner = pair.into_inner().next().unwrap();
750            build_expr(inner)
751        }
752        Rule::call_or_ident => {
753            // When call_args is empty (e.g., `now()`), pest produces no inner
754            // pairs for the parens. Check the raw text for `(` to distinguish
755            // zero-arg calls from plain identifiers.
756            let text = pair.as_str();
757            let mut inner = pair.into_inner();
758            let name = inner.next().unwrap().as_str().to_string();
759            if text.contains('(') {
760                let args = inner
761                    .next()
762                    .map(|args_pair| args_pair.into_inner().map(build_call_arg).collect())
763                    .unwrap_or_default();
764                Expr {
765                    kind: ExprKind::Call { name, args },
766                    span,
767                }
768            } else {
769                Expr {
770                    kind: ExprKind::Ident(name),
771                    span,
772                }
773            }
774        }
775        _ => Expr {
776            kind: ExprKind::Ident(pair.as_str().to_string()),
777            span,
778        },
779    }
780}
781
782fn build_call_arg(pair: pest::iterators::Pair<'_, Rule>) -> CallArg {
783    let mut inner = pair.into_inner();
784    let first = inner.next().unwrap();
785
786    match first.as_rule() {
787        Rule::named_arg => {
788            let span = span_of(&first);
789            let mut named_inner = first.into_inner();
790            let key = named_inner.next().unwrap().as_str().to_string();
791            let value = build_expr(named_inner.next().unwrap());
792            CallArg::Named { key, value, span }
793        }
794        _ => CallArg::Positional(build_expr(first)),
795    }
796}
797
798fn build_expr_kind(pair: pest::iterators::Pair<'_, Rule>) -> ExprKind {
799    match pair.as_rule() {
800        Rule::implies_expr => build_implies_expr(pair).kind,
801        Rule::or_expr => build_or_expr(pair).kind,
802        Rule::and_expr => build_and_expr(pair).kind,
803        Rule::not_expr => build_not_expr(pair).kind,
804        Rule::cmp_expr => build_cmp_expr(pair).kind,
805        Rule::add_expr => build_add_expr(pair).kind,
806        Rule::primary => build_primary(pair).kind,
807        _ => build_atom(pair).kind,
808    }
809}
810
811// ── Test declaration builders ─────────────────────────────────
812
813fn build_test_decl(pair: pest::iterators::Pair<'_, Rule>) -> TestDecl {
814    let span = span_of(&pair);
815    let mut inner = pair.into_inner();
816
817    let name = extract_string(inner.next().unwrap()); // string_literal
818    let given = build_given_block(inner.next().unwrap());
819    let when_action = build_when_block(inner.next().unwrap());
820    let then = build_then_block(inner.next().unwrap());
821
822    TestDecl {
823        name,
824        given,
825        when_action,
826        then,
827        span,
828    }
829}
830
831fn build_given_block(pair: pest::iterators::Pair<'_, Rule>) -> Vec<GivenBinding> {
832    pair.into_inner().map(build_given_binding).collect()
833}
834
835fn build_given_binding(pair: pest::iterators::Pair<'_, Rule>) -> GivenBinding {
836    let span = span_of(&pair);
837    let mut inner = pair.into_inner();
838    let name = inner.next().unwrap().as_str().to_string();
839    let value_pair = inner.next().unwrap();
840    let value = match value_pair.as_rule() {
841        Rule::entity_constructor => {
842            let mut ci = value_pair.into_inner();
843            let type_name = ci.next().unwrap().as_str().to_string();
844            let fields = ci.map(build_constructor_field).collect();
845            GivenValue::EntityConstructor { type_name, fields }
846        }
847        _ => GivenValue::Expr(build_expr(value_pair)),
848    };
849    GivenBinding { name, value, span }
850}
851
852fn build_constructor_field(pair: pest::iterators::Pair<'_, Rule>) -> ConstructorField {
853    let span = span_of(&pair);
854    let mut inner = pair.into_inner();
855    let name = inner.next().unwrap().as_str().to_string();
856    let value = build_expr(inner.next().unwrap());
857    ConstructorField { name, value, span }
858}
859
860fn build_when_block(pair: pest::iterators::Pair<'_, Rule>) -> WhenAction {
861    let span = span_of(&pair);
862    let mut inner = pair.into_inner();
863    let action_name = inner.next().unwrap().as_str().to_string();
864    let args = inner.map(build_constructor_field).collect();
865    WhenAction {
866        action_name,
867        args,
868        span,
869    }
870}
871
872fn build_then_block(pair: pest::iterators::Pair<'_, Rule>) -> ThenClause {
873    let inner = pair.into_inner().next().unwrap();
874    let span = span_of(&inner);
875    match inner.as_rule() {
876        Rule::then_fails => {
877            let kind = inner.into_inner().next().map(|p| p.as_str().to_string());
878            ThenClause::Fails(kind, span)
879        }
880        Rule::then_asserts => {
881            let exprs = inner.into_inner().map(build_expr).collect();
882            ThenClause::Asserts(exprs, span)
883        }
884        _ => unreachable!("then_block must contain then_fails or then_asserts"),
885    }
886}
887
888// ── Helpers ──────────────────────────────────────────────────
889
890fn parse_number_literal(s: &str) -> Literal {
891    if s.contains('.') {
892        Literal::Decimal(s.to_string())
893    } else {
894        Literal::Int(s.parse().unwrap_or(0))
895    }
896}
897
898fn extract_string(pair: pest::iterators::Pair<'_, Rule>) -> String {
899    pair.into_inner()
900        .next()
901        .map(|p| p.as_str().to_string())
902        .unwrap_or_default()
903}
904
905#[cfg(test)]
906mod tests {
907    use super::*;
908
909    #[test]
910    fn parse_minimal_module() {
911        let src = "module Foo\n";
912        let file = parse_file(src).unwrap();
913        assert_eq!(file.module.name, "Foo");
914        assert!(file.items.is_empty());
915    }
916
917    #[test]
918    fn parse_entity() {
919        let src = r#"module Test
920
921entity Account {
922  id: UUID
923  balance: Decimal(precision: 2)
924  status: Active | Frozen | Closed
925  notes: String?
926}
927"#;
928        let file = parse_file(src).unwrap();
929        assert_eq!(file.items.len(), 1);
930        if let TopLevelItem::Entity(e) = &file.items[0] {
931            assert_eq!(e.name, "Account");
932            assert_eq!(e.fields.len(), 4);
933            assert_eq!(e.fields[0].name, "id");
934            assert!(e.fields[2].ty.optional == false);
935            assert!(e.fields[3].ty.optional == true);
936        } else {
937            panic!("expected entity");
938        }
939    }
940
941    #[test]
942    fn parse_action_with_requires_ensures() {
943        let src = r#"module Test
944
945action Transfer {
946  from: Account
947  amount: Decimal(precision: 2)
948
949  requires {
950    from.status == Active
951    amount > 0
952  }
953
954  ensures {
955    from.balance == old(from.balance) - amount
956  }
957}
958"#;
959        let file = parse_file(src).unwrap();
960        assert_eq!(file.items.len(), 1);
961        if let TopLevelItem::Action(a) = &file.items[0] {
962            assert_eq!(a.name, "Transfer");
963            assert_eq!(a.params.len(), 2);
964            assert_eq!(a.requires.as_ref().unwrap().conditions.len(), 2);
965            assert_eq!(a.ensures.as_ref().unwrap().items.len(), 1);
966        } else {
967            panic!("expected action");
968        }
969    }
970
971    #[test]
972    fn parse_invariant() {
973        let src = r#"module Test
974
975invariant NoNegativeBalances {
976  forall a: Account => a.balance >= 0
977}
978"#;
979        let file = parse_file(src).unwrap();
980        if let TopLevelItem::Invariant(inv) = &file.items[0] {
981            assert_eq!(inv.name, "NoNegativeBalances");
982            assert!(matches!(inv.body.kind, ExprKind::Quantifier { .. }));
983        } else {
984            panic!("expected invariant");
985        }
986    }
987
988    #[test]
989    fn parse_edge_cases() {
990        let src = r#"module Test
991
992edge_cases {
993  when amount > 10000.00 => require_approval(level: "manager")
994  when from == to => reject("Cannot transfer to same account")
995}
996"#;
997        let file = parse_file(src).unwrap();
998        if let TopLevelItem::EdgeCases(ec) = &file.items[0] {
999            assert_eq!(ec.rules.len(), 2);
1000            assert_eq!(ec.rules[0].action.name, "require_approval");
1001            assert_eq!(ec.rules[1].action.name, "reject");
1002        } else {
1003            panic!("expected edge_cases");
1004        }
1005    }
1006
1007    #[test]
1008    fn parse_list_literal() {
1009        let src = r#"module Test
1010
1011action SetTags {
1012  item: Item
1013
1014  ensures {
1015    item.tags == [1, 2, 3]
1016  }
1017}
1018"#;
1019        let file = parse_file(src).unwrap();
1020        if let TopLevelItem::Action(a) = &file.items[0] {
1021            let ensures = a.ensures.as_ref().unwrap();
1022            // The ensures condition is: item.tags == [1, 2, 3]
1023            if let EnsuresItem::Expr(expr) = &ensures.items[0] {
1024                if let ExprKind::Compare { right, .. } = &expr.kind {
1025                    if let ExprKind::List(items) = &right.kind {
1026                        assert_eq!(items.len(), 3);
1027                        assert!(matches!(items[0].kind, ExprKind::Literal(Literal::Int(1))));
1028                        assert!(matches!(items[1].kind, ExprKind::Literal(Literal::Int(2))));
1029                        assert!(matches!(items[2].kind, ExprKind::Literal(Literal::Int(3))));
1030                    } else {
1031                        panic!("expected list literal on right side");
1032                    }
1033                } else {
1034                    panic!("expected compare expr");
1035                }
1036            } else {
1037                panic!("expected ensures expr");
1038            }
1039        } else {
1040            panic!("expected action");
1041        }
1042    }
1043
1044    #[test]
1045    fn parse_empty_list_literal() {
1046        let src = r#"module Test
1047
1048action Clear {
1049  item: Item
1050
1051  ensures {
1052    item.tags == []
1053  }
1054}
1055"#;
1056        let file = parse_file(src).unwrap();
1057        if let TopLevelItem::Action(a) = &file.items[0] {
1058            let ensures = a.ensures.as_ref().unwrap();
1059            if let EnsuresItem::Expr(expr) = &ensures.items[0] {
1060                if let ExprKind::Compare { right, .. } = &expr.kind {
1061                    if let ExprKind::List(items) = &right.kind {
1062                        assert!(items.is_empty());
1063                    } else {
1064                        panic!("expected empty list literal on right side");
1065                    }
1066                } else {
1067                    panic!("expected compare expr");
1068                }
1069            } else {
1070                panic!("expected ensures expr");
1071            }
1072        } else {
1073            panic!("expected action");
1074        }
1075    }
1076
1077    #[test]
1078    fn parse_use_whole_module() {
1079        let src = "module Foo\n\nuse Bar\n";
1080        let file = parse_file(src).unwrap();
1081        assert_eq!(file.imports.len(), 1);
1082        assert_eq!(file.imports[0].module_name, "Bar");
1083        assert_eq!(file.imports[0].item, None);
1084    }
1085
1086    #[test]
1087    fn parse_use_specific_item() {
1088        let src = "module Foo\n\nuse Bar.Account\n";
1089        let file = parse_file(src).unwrap();
1090        assert_eq!(file.imports.len(), 1);
1091        assert_eq!(file.imports[0].module_name, "Bar");
1092        assert_eq!(file.imports[0].item.as_deref(), Some("Account"));
1093    }
1094
1095    #[test]
1096    fn parse_multiple_imports() {
1097        let src = "module Foo\n\nuse Bar\nuse Baz.Entity\nuse Qux.Action\n";
1098        let file = parse_file(src).unwrap();
1099        assert_eq!(file.imports.len(), 3);
1100        assert_eq!(file.imports[0].module_name, "Bar");
1101        assert_eq!(file.imports[0].item, None);
1102        assert_eq!(file.imports[1].module_name, "Baz");
1103        assert_eq!(file.imports[1].item.as_deref(), Some("Entity"));
1104        assert_eq!(file.imports[2].module_name, "Qux");
1105        assert_eq!(file.imports[2].item.as_deref(), Some("Action"));
1106    }
1107
1108    #[test]
1109    fn parse_imports_with_doc_block() {
1110        let src = "module Foo\n\n--- A module that imports things.\n\nuse Bar\n\nentity Thing {\n  id: UUID\n}\n";
1111        let file = parse_file(src).unwrap();
1112        assert!(file.doc.is_some());
1113        assert_eq!(file.imports.len(), 1);
1114        assert_eq!(file.items.len(), 1);
1115    }
1116
1117    #[test]
1118    fn parse_no_imports() {
1119        let src = "module Foo\n\nentity Bar {\n  id: UUID\n}\n";
1120        let file = parse_file(src).unwrap();
1121        assert!(file.imports.is_empty());
1122        assert_eq!(file.items.len(), 1);
1123    }
1124
1125    #[test]
1126    fn parse_transfer_example() {
1127        let src = include_str!("../../../examples/transfer.intent");
1128        let file = parse_file(src).unwrap();
1129        assert_eq!(file.module.name, "TransferFunds");
1130        // 2 entities + 2 actions + 2 invariants + 1 edge_cases + 3 tests = 10 items
1131        assert_eq!(file.items.len(), 10);
1132    }
1133
1134    #[test]
1135    fn parse_auth_example() {
1136        let src = include_str!("../../../examples/auth.intent");
1137        let file = parse_file(src).unwrap();
1138        assert_eq!(file.module.name, "Authentication");
1139        // 2 entities + 2 actions + 2 invariants + 1 edge_cases = 7 items
1140        assert_eq!(file.items.len(), 7);
1141    }
1142}