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::EOI => {}
163 _ => {}
164 }
165 }
166
167 File {
168 module,
169 doc,
170 imports,
171 items,
172 span,
173 }
174}
175
176fn build_use_decl(pair: pest::iterators::Pair<'_, Rule>) -> UseDecl {
177 let span = span_of(&pair);
178 let mut inner = pair.into_inner();
179 let module_name = inner.next().unwrap().as_str().to_string();
180 let item = inner.next().map(|p| p.as_str().to_string());
181 UseDecl {
182 module_name,
183 item,
184 span,
185 }
186}
187
188fn build_module_decl(pair: pest::iterators::Pair<'_, Rule>) -> ModuleDecl {
189 let span = span_of(&pair);
190 let name = pair.into_inner().next().unwrap().as_str().to_string();
191 ModuleDecl { name, span }
192}
193
194fn build_doc_block(pair: pest::iterators::Pair<'_, Rule>) -> DocBlock {
195 let span = span_of(&pair);
196 let lines = pair
197 .into_inner()
198 .map(|p| {
199 let text = p.as_str();
200 let content = text
201 .strip_prefix("---")
202 .unwrap_or(text)
203 .trim_end_matches('\n');
204 content.strip_prefix(' ').unwrap_or(content).to_string()
205 })
206 .collect();
207 DocBlock { lines, span }
208}
209
210fn build_entity_decl(pair: pest::iterators::Pair<'_, Rule>) -> EntityDecl {
211 let span = span_of(&pair);
212 let mut doc = None;
213 let mut name = String::new();
214 let mut fields = Vec::new();
215
216 for p in pair.into_inner() {
217 match p.as_rule() {
218 Rule::doc_block => doc = Some(build_doc_block(p)),
219 Rule::type_ident => name = p.as_str().to_string(),
220 Rule::field_decl => fields.push(build_field_decl(p)),
221 _ => {}
222 }
223 }
224
225 EntityDecl {
226 doc,
227 name,
228 fields,
229 span,
230 }
231}
232
233fn build_field_decl(pair: pest::iterators::Pair<'_, Rule>) -> FieldDecl {
234 let span = span_of(&pair);
235 let mut inner = pair.into_inner();
236 let name = inner.next().unwrap().as_str().to_string();
237 let ty = build_type_expr(inner.next().unwrap());
238 FieldDecl { name, ty, span }
239}
240
241fn build_action_decl(pair: pest::iterators::Pair<'_, Rule>) -> ActionDecl {
242 let span = span_of(&pair);
243 let mut doc = None;
244 let mut name = String::new();
245 let mut params = Vec::new();
246 let mut requires = None;
247 let mut ensures = None;
248 let mut properties = None;
249
250 for p in pair.into_inner() {
251 match p.as_rule() {
252 Rule::doc_block => doc = Some(build_doc_block(p)),
253 Rule::type_ident => name = p.as_str().to_string(),
254 Rule::param_decl => params.push(build_field_decl(p)),
255 Rule::requires_block => requires = Some(build_requires_block(p)),
256 Rule::ensures_block => ensures = Some(build_ensures_block(p)),
257 Rule::properties_block => properties = Some(build_properties_block(p)),
258 _ => {}
259 }
260 }
261
262 ActionDecl {
263 doc,
264 name,
265 params,
266 requires,
267 ensures,
268 properties,
269 span,
270 }
271}
272
273fn build_requires_block(pair: pest::iterators::Pair<'_, Rule>) -> RequiresBlock {
274 let span = span_of(&pair);
275 let conditions = pair.into_inner().map(build_expr).collect();
276 RequiresBlock { conditions, span }
277}
278
279fn build_ensures_block(pair: pest::iterators::Pair<'_, Rule>) -> EnsuresBlock {
280 let span = span_of(&pair);
281 let items = pair
282 .into_inner()
283 .map(|p| match p.as_rule() {
284 Rule::when_clause => EnsuresItem::When(build_when_clause(p)),
285 _ => EnsuresItem::Expr(build_expr(p)),
286 })
287 .collect();
288 EnsuresBlock { items, span }
289}
290
291fn build_when_clause(pair: pest::iterators::Pair<'_, Rule>) -> WhenClause {
292 let span = span_of(&pair);
293 let mut inner = pair.into_inner();
294 let condition = build_or_expr(inner.next().unwrap());
295 let consequence = build_expr(inner.next().unwrap());
296 WhenClause {
297 condition,
298 consequence,
299 span,
300 }
301}
302
303fn build_properties_block(pair: pest::iterators::Pair<'_, Rule>) -> PropertiesBlock {
304 let span = span_of(&pair);
305 let entries = pair.into_inner().map(build_prop_entry).collect();
306 PropertiesBlock { entries, span }
307}
308
309fn build_prop_entry(pair: pest::iterators::Pair<'_, Rule>) -> PropEntry {
310 let span = span_of(&pair);
311 let mut inner = pair.into_inner();
312 let key = inner.next().unwrap().as_str().to_string();
313 let value = build_prop_value(inner.next().unwrap());
314 PropEntry { key, value, span }
315}
316
317fn build_prop_value(pair: pest::iterators::Pair<'_, Rule>) -> PropValue {
318 match pair.as_rule() {
319 Rule::obj_literal => {
320 let fields = pair
321 .into_inner()
322 .map(|f| {
323 let mut inner = f.into_inner();
324 let key = inner.next().unwrap().as_str().to_string();
325 let value = build_prop_value(inner.next().unwrap());
326 (key, value)
327 })
328 .collect();
329 PropValue::Object(fields)
330 }
331 Rule::list_literal => {
332 let items = pair.into_inner().map(build_prop_value).collect();
333 PropValue::List(items)
334 }
335 Rule::string_literal => {
336 let s = extract_string(pair);
337 PropValue::Literal(Literal::String(s))
338 }
339 Rule::number_literal => PropValue::Literal(parse_number_literal(pair.as_str())),
340 Rule::bool_literal => PropValue::Literal(Literal::Bool(pair.as_str() == "true")),
341 Rule::ident => PropValue::Ident(pair.as_str().to_string()),
342 Rule::expr | Rule::implies_expr => {
344 let inner = pair.into_inner().next().unwrap();
346 build_prop_value(inner)
347 }
348 _ => PropValue::Ident(pair.as_str().to_string()),
349 }
350}
351
352fn build_invariant_decl(pair: pest::iterators::Pair<'_, Rule>) -> InvariantDecl {
353 let span = span_of(&pair);
354 let mut doc = None;
355 let mut name = String::new();
356 let mut body = None;
357
358 for p in pair.into_inner() {
359 match p.as_rule() {
360 Rule::doc_block => doc = Some(build_doc_block(p)),
361 Rule::type_ident => name = p.as_str().to_string(),
362 Rule::expr => body = Some(build_expr(p)),
363 _ => {}
364 }
365 }
366
367 InvariantDecl {
368 doc,
369 name,
370 body: body.expect("invariant must have a body expression"),
371 span,
372 }
373}
374
375fn build_edge_cases_decl(pair: pest::iterators::Pair<'_, Rule>) -> EdgeCasesDecl {
376 let span = span_of(&pair);
377 let rules = pair.into_inner().map(build_edge_rule).collect();
378 EdgeCasesDecl { rules, span }
379}
380
381fn build_edge_rule(pair: pest::iterators::Pair<'_, Rule>) -> EdgeRule {
382 let span = span_of(&pair);
383 let mut inner = pair.into_inner();
384 let condition = build_or_expr(inner.next().unwrap());
385 let action = build_action_call(inner.next().unwrap());
386 EdgeRule {
387 condition,
388 action,
389 span,
390 }
391}
392
393fn build_action_call(pair: pest::iterators::Pair<'_, Rule>) -> ActionCall {
394 let span = span_of(&pair);
395 let mut inner = pair.into_inner();
396 let name = inner.next().unwrap().as_str().to_string();
397 let args = inner
398 .next()
399 .map(|p| p.into_inner().map(build_call_arg).collect())
400 .unwrap_or_default();
401 ActionCall { name, args, span }
402}
403
404fn build_type_expr(pair: pest::iterators::Pair<'_, Rule>) -> TypeExpr {
407 let span = span_of(&pair);
408 let mut optional = false;
409 let mut ty_kind = None;
410
411 for p in pair.into_inner() {
412 match p.as_rule() {
413 Rule::union_type => ty_kind = Some(build_union_type(p)),
414 Rule::optional_marker => optional = true,
415 _ => {}
416 }
417 }
418
419 TypeExpr {
420 ty: ty_kind.unwrap(),
421 optional,
422 span,
423 }
424}
425
426fn build_union_type(pair: pest::iterators::Pair<'_, Rule>) -> TypeKind {
427 let variants: Vec<TypeKind> = pair.into_inner().map(build_base_type).collect();
428 if variants.len() == 1 {
429 variants.into_iter().next().unwrap()
430 } else {
431 TypeKind::Union(variants)
432 }
433}
434
435fn build_base_type(pair: pest::iterators::Pair<'_, Rule>) -> TypeKind {
436 match pair.as_rule() {
437 Rule::list_type => {
438 let inner = pair.into_inner().next().unwrap();
439 TypeKind::List(Box::new(build_type_expr(inner)))
440 }
441 Rule::set_type => {
442 let inner = pair.into_inner().next().unwrap();
443 TypeKind::Set(Box::new(build_type_expr(inner)))
444 }
445 Rule::map_type => {
446 let mut inner = pair.into_inner();
447 let key = build_type_expr(inner.next().unwrap());
448 let value = build_type_expr(inner.next().unwrap());
449 TypeKind::Map(Box::new(key), Box::new(value))
450 }
451 Rule::parameterized_type => {
452 let mut inner = pair.into_inner();
453 let name = inner.next().unwrap().as_str().to_string();
454 let params = inner.map(build_type_param).collect();
455 TypeKind::Parameterized { name, params }
456 }
457 Rule::simple_type => {
458 let name = pair.into_inner().next().unwrap().as_str().to_string();
459 TypeKind::Simple(name)
460 }
461 _ => TypeKind::Simple(pair.as_str().to_string()),
462 }
463}
464
465fn build_type_param(pair: pest::iterators::Pair<'_, Rule>) -> TypeParam {
466 let span = span_of(&pair);
467 let mut inner = pair.into_inner();
468 let name = inner.next().unwrap().as_str().to_string();
469 let value = parse_number_literal(inner.next().unwrap().as_str());
470 TypeParam { name, value, span }
471}
472
473fn build_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
476 let span = span_of(&pair);
477 let inner = pair.into_inner().next().unwrap();
478 match inner.as_rule() {
479 Rule::implies_expr => build_implies_expr(inner),
480 _ => {
481 let kind = build_expr_kind(inner);
482 Expr { kind, span }
483 }
484 }
485}
486
487fn build_implies_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
488 let mut parts: Vec<pest::iterators::Pair<'_, Rule>> = Vec::new();
489
490 for p in pair.into_inner() {
491 match p.as_rule() {
492 Rule::implies_op => {}
493 _ => parts.push(p),
494 }
495 }
496
497 let mut result = build_or_expr(parts.remove(0));
498 for part in parts {
499 let right = build_or_expr(part);
500 let new_span = Span {
501 start: result.span.start,
502 end: right.span.end,
503 };
504 result = Expr {
505 kind: ExprKind::Implies(Box::new(result), Box::new(right)),
506 span: new_span,
507 };
508 }
509 result
510}
511
512fn build_or_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
513 let span = span_of(&pair);
514 let mut parts: Vec<pest::iterators::Pair<'_, Rule>> = Vec::new();
515
516 for p in pair.into_inner() {
517 match p.as_rule() {
518 Rule::or_op => {}
519 _ => parts.push(p),
520 }
521 }
522
523 if parts.is_empty() {
524 return Expr {
525 kind: ExprKind::Literal(Literal::Null),
526 span,
527 };
528 }
529
530 let mut result = build_and_expr(parts.remove(0));
531 for part in parts {
532 let right = build_and_expr(part);
533 let new_span = Span {
534 start: result.span.start,
535 end: right.span.end,
536 };
537 result = Expr {
538 kind: ExprKind::Or(Box::new(result), Box::new(right)),
539 span: new_span,
540 };
541 }
542 result
543}
544
545fn build_and_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
546 let span = span_of(&pair);
547 let mut parts: Vec<pest::iterators::Pair<'_, Rule>> = Vec::new();
548
549 for p in pair.into_inner() {
550 match p.as_rule() {
551 Rule::and_op => {}
552 _ => parts.push(p),
553 }
554 }
555
556 if parts.is_empty() {
557 return Expr {
558 kind: ExprKind::Literal(Literal::Null),
559 span,
560 };
561 }
562
563 let mut result = build_not_expr(parts.remove(0));
564 for part in parts {
565 let right = build_not_expr(part);
566 let new_span = Span {
567 start: result.span.start,
568 end: right.span.end,
569 };
570 result = Expr {
571 kind: ExprKind::And(Box::new(result), Box::new(right)),
572 span: new_span,
573 };
574 }
575 result
576}
577
578fn build_not_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
579 let span = span_of(&pair);
580 let mut inner = pair.into_inner();
581 let first = inner.next().unwrap();
582
583 match first.as_rule() {
584 Rule::not_op => {
585 let operand = build_not_expr(inner.next().unwrap());
586 Expr {
587 kind: ExprKind::Not(Box::new(operand)),
588 span,
589 }
590 }
591 Rule::cmp_expr => build_cmp_expr(first),
592 _ => {
593 let kind = build_expr_kind(first);
594 Expr { kind, span }
595 }
596 }
597}
598
599fn build_cmp_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
600 let span = span_of(&pair);
601 let mut inner = pair.into_inner();
602 let left = build_add_expr(inner.next().unwrap());
603
604 if let Some(op_pair) = inner.next() {
605 let op = match op_pair.as_str() {
606 "==" => CmpOp::Eq,
607 "!=" => CmpOp::Ne,
608 "<" => CmpOp::Lt,
609 ">" => CmpOp::Gt,
610 "<=" => CmpOp::Le,
611 ">=" => CmpOp::Ge,
612 _ => unreachable!("unknown cmp op: {}", op_pair.as_str()),
613 };
614 let right = build_add_expr(inner.next().unwrap());
615 Expr {
616 kind: ExprKind::Compare {
617 left: Box::new(left),
618 op,
619 right: Box::new(right),
620 },
621 span,
622 }
623 } else {
624 Expr {
625 kind: left.kind,
626 span,
627 }
628 }
629}
630
631fn build_add_expr(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
632 let mut children: Vec<pest::iterators::Pair<'_, Rule>> = pair.into_inner().collect();
633
634 if children.len() == 1 {
635 return build_primary(children.remove(0));
636 }
637
638 let mut iter = children.into_iter();
640 let mut result = build_primary(iter.next().unwrap());
641
642 while let Some(op_pair) = iter.next() {
643 let op = match op_pair.as_str() {
644 "+" => ArithOp::Add,
645 "-" => ArithOp::Sub,
646 _ => unreachable!("unknown add op"),
647 };
648 let right = build_primary(iter.next().unwrap());
649 let new_span = Span {
650 start: result.span.start,
651 end: right.span.end,
652 };
653 result = Expr {
654 kind: ExprKind::Arithmetic {
655 left: Box::new(result),
656 op,
657 right: Box::new(right),
658 },
659 span: new_span,
660 };
661 }
662
663 result
664}
665
666fn build_primary(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
667 let span = span_of(&pair);
668 let mut inner: Vec<pest::iterators::Pair<'_, Rule>> = pair.into_inner().collect();
669
670 let atom_pair = inner.remove(0);
672 let base = build_atom(atom_pair);
673
674 if inner.is_empty() {
675 return Expr {
676 kind: base.kind,
677 span,
678 };
679 }
680
681 let fields: Vec<String> = inner.into_iter().map(|p| p.as_str().to_string()).collect();
682
683 Expr {
684 kind: ExprKind::FieldAccess {
685 root: Box::new(base),
686 fields,
687 },
688 span,
689 }
690}
691
692fn build_atom(pair: pest::iterators::Pair<'_, Rule>) -> Expr {
693 let span = span_of(&pair);
694 match pair.as_rule() {
695 Rule::old_expr => {
696 let inner = pair.into_inner().next().unwrap();
697 let expr = build_expr(inner);
698 Expr {
699 kind: ExprKind::Old(Box::new(expr)),
700 span,
701 }
702 }
703 Rule::quantifier_expr => {
704 let mut inner = pair.into_inner();
705 let kw = inner.next().unwrap();
706 let kind = match kw.as_str() {
707 "forall" => QuantifierKind::Forall,
708 "exists" => QuantifierKind::Exists,
709 _ => unreachable!(),
710 };
711 let binding = inner.next().unwrap().as_str().to_string();
712 let ty = inner.next().unwrap().as_str().to_string();
713 let body = build_expr(inner.next().unwrap());
714 Expr {
715 kind: ExprKind::Quantifier {
716 kind,
717 binding,
718 ty,
719 body: Box::new(body),
720 },
721 span,
722 }
723 }
724 Rule::null_literal => Expr {
725 kind: ExprKind::Literal(Literal::Null),
726 span,
727 },
728 Rule::bool_literal => Expr {
729 kind: ExprKind::Literal(Literal::Bool(pair.as_str() == "true")),
730 span,
731 },
732 Rule::number_literal => Expr {
733 kind: ExprKind::Literal(parse_number_literal(pair.as_str())),
734 span,
735 },
736 Rule::string_literal => Expr {
737 kind: ExprKind::Literal(Literal::String(extract_string(pair))),
738 span,
739 },
740 Rule::list_literal => {
741 let items = pair.into_inner().map(build_expr).collect();
742 Expr {
743 kind: ExprKind::List(items),
744 span,
745 }
746 }
747 Rule::paren_expr => {
748 let inner = pair.into_inner().next().unwrap();
749 build_expr(inner)
750 }
751 Rule::call_or_ident => {
752 let text = pair.as_str();
756 let mut inner = pair.into_inner();
757 let name = inner.next().unwrap().as_str().to_string();
758 if text.contains('(') {
759 let args = inner
760 .next()
761 .map(|args_pair| args_pair.into_inner().map(build_call_arg).collect())
762 .unwrap_or_default();
763 Expr {
764 kind: ExprKind::Call { name, args },
765 span,
766 }
767 } else {
768 Expr {
769 kind: ExprKind::Ident(name),
770 span,
771 }
772 }
773 }
774 _ => Expr {
775 kind: ExprKind::Ident(pair.as_str().to_string()),
776 span,
777 },
778 }
779}
780
781fn build_call_arg(pair: pest::iterators::Pair<'_, Rule>) -> CallArg {
782 let mut inner = pair.into_inner();
783 let first = inner.next().unwrap();
784
785 match first.as_rule() {
786 Rule::named_arg => {
787 let span = span_of(&first);
788 let mut named_inner = first.into_inner();
789 let key = named_inner.next().unwrap().as_str().to_string();
790 let value = build_expr(named_inner.next().unwrap());
791 CallArg::Named { key, value, span }
792 }
793 _ => CallArg::Positional(build_expr(first)),
794 }
795}
796
797fn build_expr_kind(pair: pest::iterators::Pair<'_, Rule>) -> ExprKind {
798 match pair.as_rule() {
799 Rule::implies_expr => build_implies_expr(pair).kind,
800 Rule::or_expr => build_or_expr(pair).kind,
801 Rule::and_expr => build_and_expr(pair).kind,
802 Rule::not_expr => build_not_expr(pair).kind,
803 Rule::cmp_expr => build_cmp_expr(pair).kind,
804 Rule::add_expr => build_add_expr(pair).kind,
805 Rule::primary => build_primary(pair).kind,
806 _ => build_atom(pair).kind,
807 }
808}
809
810fn parse_number_literal(s: &str) -> Literal {
813 if s.contains('.') {
814 Literal::Decimal(s.to_string())
815 } else {
816 Literal::Int(s.parse().unwrap_or(0))
817 }
818}
819
820fn extract_string(pair: pest::iterators::Pair<'_, Rule>) -> String {
821 pair.into_inner()
822 .next()
823 .map(|p| p.as_str().to_string())
824 .unwrap_or_default()
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830
831 #[test]
832 fn parse_minimal_module() {
833 let src = "module Foo\n";
834 let file = parse_file(src).unwrap();
835 assert_eq!(file.module.name, "Foo");
836 assert!(file.items.is_empty());
837 }
838
839 #[test]
840 fn parse_entity() {
841 let src = r#"module Test
842
843entity Account {
844 id: UUID
845 balance: Decimal(precision: 2)
846 status: Active | Frozen | Closed
847 notes: String?
848}
849"#;
850 let file = parse_file(src).unwrap();
851 assert_eq!(file.items.len(), 1);
852 if let TopLevelItem::Entity(e) = &file.items[0] {
853 assert_eq!(e.name, "Account");
854 assert_eq!(e.fields.len(), 4);
855 assert_eq!(e.fields[0].name, "id");
856 assert!(e.fields[2].ty.optional == false);
857 assert!(e.fields[3].ty.optional == true);
858 } else {
859 panic!("expected entity");
860 }
861 }
862
863 #[test]
864 fn parse_action_with_requires_ensures() {
865 let src = r#"module Test
866
867action Transfer {
868 from: Account
869 amount: Decimal(precision: 2)
870
871 requires {
872 from.status == Active
873 amount > 0
874 }
875
876 ensures {
877 from.balance == old(from.balance) - amount
878 }
879}
880"#;
881 let file = parse_file(src).unwrap();
882 assert_eq!(file.items.len(), 1);
883 if let TopLevelItem::Action(a) = &file.items[0] {
884 assert_eq!(a.name, "Transfer");
885 assert_eq!(a.params.len(), 2);
886 assert_eq!(a.requires.as_ref().unwrap().conditions.len(), 2);
887 assert_eq!(a.ensures.as_ref().unwrap().items.len(), 1);
888 } else {
889 panic!("expected action");
890 }
891 }
892
893 #[test]
894 fn parse_invariant() {
895 let src = r#"module Test
896
897invariant NoNegativeBalances {
898 forall a: Account => a.balance >= 0
899}
900"#;
901 let file = parse_file(src).unwrap();
902 if let TopLevelItem::Invariant(inv) = &file.items[0] {
903 assert_eq!(inv.name, "NoNegativeBalances");
904 assert!(matches!(inv.body.kind, ExprKind::Quantifier { .. }));
905 } else {
906 panic!("expected invariant");
907 }
908 }
909
910 #[test]
911 fn parse_edge_cases() {
912 let src = r#"module Test
913
914edge_cases {
915 when amount > 10000.00 => require_approval(level: "manager")
916 when from == to => reject("Cannot transfer to same account")
917}
918"#;
919 let file = parse_file(src).unwrap();
920 if let TopLevelItem::EdgeCases(ec) = &file.items[0] {
921 assert_eq!(ec.rules.len(), 2);
922 assert_eq!(ec.rules[0].action.name, "require_approval");
923 assert_eq!(ec.rules[1].action.name, "reject");
924 } else {
925 panic!("expected edge_cases");
926 }
927 }
928
929 #[test]
930 fn parse_list_literal() {
931 let src = r#"module Test
932
933action SetTags {
934 item: Item
935
936 ensures {
937 item.tags == [1, 2, 3]
938 }
939}
940"#;
941 let file = parse_file(src).unwrap();
942 if let TopLevelItem::Action(a) = &file.items[0] {
943 let ensures = a.ensures.as_ref().unwrap();
944 if let EnsuresItem::Expr(expr) = &ensures.items[0] {
946 if let ExprKind::Compare { right, .. } = &expr.kind {
947 if let ExprKind::List(items) = &right.kind {
948 assert_eq!(items.len(), 3);
949 assert!(matches!(items[0].kind, ExprKind::Literal(Literal::Int(1))));
950 assert!(matches!(items[1].kind, ExprKind::Literal(Literal::Int(2))));
951 assert!(matches!(items[2].kind, ExprKind::Literal(Literal::Int(3))));
952 } else {
953 panic!("expected list literal on right side");
954 }
955 } else {
956 panic!("expected compare expr");
957 }
958 } else {
959 panic!("expected ensures expr");
960 }
961 } else {
962 panic!("expected action");
963 }
964 }
965
966 #[test]
967 fn parse_empty_list_literal() {
968 let src = r#"module Test
969
970action Clear {
971 item: Item
972
973 ensures {
974 item.tags == []
975 }
976}
977"#;
978 let file = parse_file(src).unwrap();
979 if let TopLevelItem::Action(a) = &file.items[0] {
980 let ensures = a.ensures.as_ref().unwrap();
981 if let EnsuresItem::Expr(expr) = &ensures.items[0] {
982 if let ExprKind::Compare { right, .. } = &expr.kind {
983 if let ExprKind::List(items) = &right.kind {
984 assert!(items.is_empty());
985 } else {
986 panic!("expected empty list literal on right side");
987 }
988 } else {
989 panic!("expected compare expr");
990 }
991 } else {
992 panic!("expected ensures expr");
993 }
994 } else {
995 panic!("expected action");
996 }
997 }
998
999 #[test]
1000 fn parse_use_whole_module() {
1001 let src = "module Foo\n\nuse Bar\n";
1002 let file = parse_file(src).unwrap();
1003 assert_eq!(file.imports.len(), 1);
1004 assert_eq!(file.imports[0].module_name, "Bar");
1005 assert_eq!(file.imports[0].item, None);
1006 }
1007
1008 #[test]
1009 fn parse_use_specific_item() {
1010 let src = "module Foo\n\nuse Bar.Account\n";
1011 let file = parse_file(src).unwrap();
1012 assert_eq!(file.imports.len(), 1);
1013 assert_eq!(file.imports[0].module_name, "Bar");
1014 assert_eq!(file.imports[0].item.as_deref(), Some("Account"));
1015 }
1016
1017 #[test]
1018 fn parse_multiple_imports() {
1019 let src = "module Foo\n\nuse Bar\nuse Baz.Entity\nuse Qux.Action\n";
1020 let file = parse_file(src).unwrap();
1021 assert_eq!(file.imports.len(), 3);
1022 assert_eq!(file.imports[0].module_name, "Bar");
1023 assert_eq!(file.imports[0].item, None);
1024 assert_eq!(file.imports[1].module_name, "Baz");
1025 assert_eq!(file.imports[1].item.as_deref(), Some("Entity"));
1026 assert_eq!(file.imports[2].module_name, "Qux");
1027 assert_eq!(file.imports[2].item.as_deref(), Some("Action"));
1028 }
1029
1030 #[test]
1031 fn parse_imports_with_doc_block() {
1032 let src = "module Foo\n\n--- A module that imports things.\n\nuse Bar\n\nentity Thing {\n id: UUID\n}\n";
1033 let file = parse_file(src).unwrap();
1034 assert!(file.doc.is_some());
1035 assert_eq!(file.imports.len(), 1);
1036 assert_eq!(file.items.len(), 1);
1037 }
1038
1039 #[test]
1040 fn parse_no_imports() {
1041 let src = "module Foo\n\nentity Bar {\n id: UUID\n}\n";
1042 let file = parse_file(src).unwrap();
1043 assert!(file.imports.is_empty());
1044 assert_eq!(file.items.len(), 1);
1045 }
1046
1047 #[test]
1048 fn parse_transfer_example() {
1049 let src = include_str!("../../../examples/transfer.intent");
1050 let file = parse_file(src).unwrap();
1051 assert_eq!(file.module.name, "TransferFunds");
1052 assert_eq!(file.items.len(), 7);
1054 }
1055
1056 #[test]
1057 fn parse_auth_example() {
1058 let src = include_str!("../../../examples/auth.intent");
1059 let file = parse_file(src).unwrap();
1060 assert_eq!(file.module.name, "Authentication");
1061 assert_eq!(file.items.len(), 7);
1063 }
1064}