1use pest::Parser;
7use pest_derive::Parser;
8
9use crate::ast::*;
10
11#[derive(Parser)]
14#[grammar = "src/intent.pest"]
15pub struct IntentParser;
16
17#[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
36fn 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 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
62fn humanize_expected_rules(rules: &[Rule]) -> (String, String, Option<String>) {
64 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 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
126pub 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
133fn 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 Rule::expr | Rule::implies_expr => {
345 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
405fn 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
474fn 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 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 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 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
811fn 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()); 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
888fn 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 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 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 assert_eq!(file.items.len(), 7);
1141 }
1142}