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