1#![no_std]
13#![warn(missing_docs)]
15
16extern crate alloc;
17
18use alloc::string::String;
19use alloc::vec;
20use alloc::vec::Vec;
21use core::fmt;
22
23use nom::{
24 IResult, Parser,
25 branch::alt,
26 bytes::complete::{tag, take_while},
27 character::complete::{char, line_ending, satisfy, space0, space1},
28 combinator::{eof, opt, recognize, value},
29 multi::many0,
30 sequence::{delimited, preceded, terminated},
31};
32use nom_locate::LocatedSpan;
33
34pub type Span<'a> = LocatedSpan<&'a str>;
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct Located<'a, T> {
40 pub data: T,
42 pub span: Span<'a>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct Atom<'a> {
52 pub subject: &'a str,
54 pub predicate: &'a str,
56 pub object: Option<&'a str>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct Literal<'a> {
65 pub negated: bool,
67 pub atom: Atom<'a>,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum ListOp {
77 Exclusive,
80 Forbids,
83 OneOf,
85 AtLeast,
87}
88
89#[derive(Debug, Clone, PartialEq)]
91pub enum Body<'a> {
92 List {
94 op: ListOp,
96 atoms: Vec<Located<'a, Atom<'a>>>,
98 },
99 Impl {
101 antecedent: Vec<Located<'a, Literal<'a>>>,
103 consequent: Vec<Located<'a, Literal<'a>>>,
105 },
106}
107
108#[derive(Debug, Clone, PartialEq)]
110pub enum Statement<'a> {
111 Import(Located<'a, &'a str>),
113 Fact(Located<'a, Atom<'a>>),
115 Negation(Located<'a, Atom<'a>>),
117 Axiom {
119 name: Located<'a, &'a str>,
121 body: Body<'a>,
123 },
124 Rule {
126 name: Located<'a, &'a str>,
128 body: Body<'a>,
130 },
131 Check {
133 subject: Option<Located<'a, &'a str>>,
135 bidirectional: bool,
137 },
138}
139
140#[derive(Debug, Clone, PartialEq)]
142pub struct Program<'a> {
143 pub statements: Vec<Statement<'a>>,
146}
147
148pub const RESERVED: &[&str] = &[
150 "IMPORT",
151 "FACT",
152 "NOT",
153 "AXIOM",
154 "RULE",
155 "CHECK",
156 "BIDIRECTIONAL",
157 "WHEN",
158 "AND",
159 "THEN",
160 "EXCLUSIVE",
161 "FORBIDS",
162 "ONEOF",
163 "ATLEAST",
164];
165
166pub fn is_reserved(word: &str) -> bool {
168 RESERVED.contains(&word)
169}
170
171#[derive(Debug)]
175pub struct ParseError<'a> {
176 pub source: &'a str,
178 pub span: Span<'a>,
180 pub message: String,
182}
183
184impl<'a> fmt::Display for ParseError<'a> {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 let line = self.span.location_line() as usize;
187 let column = self.span.get_column();
188 let full_line = self
189 .source
190 .lines()
191 .nth(line.saturating_sub(1))
192 .unwrap_or("");
193 let indent = " ".repeat(if column > 0 { column - 1 } else { 0 });
194
195 write!(
196 f,
197 "Syntax Error at line {}, col {}: {}\n | {}\n | {}^--- here",
198 line, column, self.message, full_line, indent
199 )
200 }
201}
202
203#[derive(Debug, Clone)]
211struct Problem<'a> {
212 input: Span<'a>,
213 message: String,
214}
215
216impl<'a> nom::error::ParseError<Span<'a>> for Problem<'a> {
217 fn from_error_kind(input: Span<'a>, _: nom::error::ErrorKind) -> Self {
218 Problem {
219 input,
220 message: String::from("unexpected token"),
221 }
222 }
223 fn append(_: Span<'a>, _: nom::error::ErrorKind, other: Self) -> Self {
224 other
225 }
226}
227
228type PResult<'a, T> = IResult<Span<'a>, T, Problem<'a>>;
230
231fn promote<'a, T>(r: PResult<'a, T>, at: Span<'a>, msg: &str) -> PResult<'a, T> {
234 match r {
235 Err(nom::Err::Error(_)) => Err(nom::Err::Failure(Problem {
236 input: at,
237 message: String::from(msg),
238 })),
239 other => other,
240 }
241}
242
243fn perr<'a, T>(input: Span<'a>) -> PResult<'a, T> {
245 Err(nom::Err::Error(Problem {
246 input,
247 message: String::from("unexpected token"),
248 }))
249}
250
251fn is_ident_char(c: char) -> bool {
254 c.is_ascii_alphanumeric() || c == '_' || c == '.'
255}
256
257fn raw_identifier<'a>(input: Span<'a>) -> PResult<'a, Span<'a>> {
259 recognize((
260 satisfy(|c| c.is_ascii_alphabetic()),
261 take_while(is_ident_char),
262 ))
263 .parse(input)
264}
265
266fn identifier<'a>(input: Span<'a>) -> PResult<'a, Located<'a, &'a str>> {
268 let start = input;
269 let (rest, sp) = raw_identifier(input)?;
270 if is_reserved(sp.fragment()) {
271 return perr(start);
272 }
273 Ok((
274 rest,
275 Located {
276 data: *sp.fragment(),
277 span: start,
278 },
279 ))
280}
281
282fn comment<'a>(input: Span<'a>) -> PResult<'a, Span<'a>> {
284 recognize((tag("//"), take_while(|c| c != '\n' && c != '\r'))).parse(input)
285}
286
287fn eol<'a>(input: Span<'a>) -> PResult<'a, ()> {
290 value((), (space0, opt(comment), alt((line_ending, eof)))).parse(input)
291}
292
293fn noise_line<'a>(input: Span<'a>) -> PResult<'a, ()> {
295 value((), (space0, opt(comment), line_ending)).parse(input)
296}
297
298fn skip_noise<'a>(input: Span<'a>) -> PResult<'a, ()> {
300 value((), many0(noise_line)).parse(input)
301}
302
303fn atom<'a>(input: Span<'a>) -> PResult<'a, Located<'a, Atom<'a>>> {
307 let start = input;
308 let (input, subject) = identifier(input)?;
309 let (input, _) = space1(input)?;
310 let (input, predicate) = identifier(input)?;
311 let (input, object) = opt(preceded(space1, identifier)).parse(input)?;
312 Ok((
313 input,
314 Located {
315 data: Atom {
316 subject: subject.data,
317 predicate: predicate.data,
318 object: object.map(|o| o.data),
319 },
320 span: start,
321 },
322 ))
323}
324
325fn literal<'a>(input: Span<'a>) -> PResult<'a, Located<'a, Literal<'a>>> {
327 let start = input;
328 let (input, neg) = opt(terminated(tag("NOT"), space1)).parse(input)?;
329 let (input, a) = atom(input)?;
330 Ok((
331 input,
332 Located {
333 data: Literal {
334 negated: neg.is_some(),
335 atom: a.data,
336 },
337 span: start,
338 },
339 ))
340}
341
342fn atom_line<'a>(input: Span<'a>) -> PResult<'a, Located<'a, Atom<'a>>> {
344 let (input, _) = space0(input)?;
345 let (input, a) = atom(input)?;
346 let (input, _) = eol(input)?;
347 Ok((input, a))
348}
349
350fn list_op<'a>(input: Span<'a>) -> PResult<'a, ListOp> {
354 alt((
355 value(ListOp::Exclusive, tag("EXCLUSIVE")),
356 value(ListOp::Forbids, tag("FORBIDS")),
357 value(ListOp::OneOf, tag("ONEOF")),
358 value(ListOp::AtLeast, tag("ATLEAST")),
359 ))
360 .parse(input)
361}
362
363fn list_body<'a>(input: Span<'a>) -> PResult<'a, Body<'a>> {
372 let (input, _) = space0(input)?;
373 let (input, op) = list_op(input)?;
375 let (input, _) = promote(
377 eol(input),
378 input,
379 "expected a newline after the list operator",
380 )?;
381 let at = input;
382 let (input, first) = promote(
383 atom_line(input),
384 at,
385 "a list axiom needs at least two atoms",
386 )?;
387 let at = input;
388 let (input, second) = promote(
389 atom_line(input),
390 at,
391 "a list axiom needs at least two atoms",
392 )?;
393 let (input, rest) = many0(atom_line).parse(input)?;
394
395 let mut atoms = vec![first, second];
396 atoms.extend(rest);
397 Ok((input, Body::List { op, atoms }))
398}
399
400fn and_line<'a>(input: Span<'a>) -> PResult<'a, Located<'a, Literal<'a>>> {
403 let (input, _) = space0(input)?;
404 let (input, _) = (tag("AND"), space1).parse(input)?;
406 let at = input;
407 let (input, lit) = promote(
408 literal(input),
409 at,
410 "AND expects a literal: [NOT] <Subject> <predicate> [<object>]",
411 )?;
412 let (input, _) = promote(eol(input), input, "unexpected text after the AND literal")?;
413 Ok((input, lit))
414}
415
416fn impl_body<'a>(input: Span<'a>) -> PResult<'a, Body<'a>> {
423 let (input, _) = space0(input)?;
424 let (input, _) = (tag("WHEN"), space1).parse(input)?;
426 let at = input;
428 let (input, when) = promote(
429 literal(input),
430 at,
431 "WHEN expects a literal: [NOT] <Subject> <predicate> [<object>]",
432 )?;
433 let (input, _) = promote(eol(input), input, "unexpected text after the WHEN literal")?;
434 let (input, ante_rest) = many0(and_line).parse(input)?;
435
436 let (input, _) = space0(input)?;
437 let at = input;
438 let (input, _) = promote(
439 tag("THEN").parse(input),
440 at,
441 "expected THEN to complete the WHEN ... THEN implication",
442 )?;
443 let at = input;
444 let (input, then) = promote(
445 preceded(space1, literal).parse(input),
446 at,
447 "THEN expects a literal: [NOT] <Subject> <predicate> [<object>]",
448 )?;
449 let (input, _) = promote(eol(input), input, "unexpected text after the THEN literal")?;
450 let (input, cons_rest) = many0(and_line).parse(input)?;
451
452 let mut antecedent = vec![when];
453 antecedent.extend(ante_rest);
454 let mut consequent = vec![then];
455 consequent.extend(cons_rest);
456 Ok((
457 input,
458 Body::Impl {
459 antecedent,
460 consequent,
461 },
462 ))
463}
464
465fn stmt_import<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
469 let (input, _) = (tag("IMPORT"), space1).parse(input)?;
470 let start = input;
471 let (input, path) = promote(
472 delimited(char('"'), take_while(|c| c != '"' && c != '\n'), char('"')).parse(input),
473 start,
474 "IMPORT expects a quoted path, e.g. IMPORT \"physics.vrf\"",
475 )?;
476 let (input, _) = promote(eol(input), input, "unexpected text after the IMPORT path")?;
477 Ok((
478 input,
479 Statement::Import(Located {
480 data: *path.fragment(),
481 span: start,
482 }),
483 ))
484}
485
486fn stmt_fact<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
488 let (input, _) = (tag("FACT"), space1).parse(input)?;
489 let at = input;
490 let (input, a) = promote(
491 atom(input),
492 at,
493 "FACT expects an atom: <Subject> <predicate> [<object>]",
494 )?;
495 let (input, _) = promote(eol(input), input, "unexpected text after the FACT atom")?;
496 Ok((input, Statement::Fact(a)))
497}
498
499fn stmt_negation<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
502 let (input, _) = (tag("NOT"), space1).parse(input)?;
503 let at = input;
504 let (input, a) = promote(
505 atom(input),
506 at,
507 "NOT expects an atom: <Subject> <predicate> [<object>]",
508 )?;
509 let (input, _) = promote(eol(input), input, "unexpected text after the NOT atom")?;
510 Ok((input, Statement::Negation(a)))
511}
512
513fn stmt_check<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
515 let (input, _) = tag("CHECK").parse(input)?;
516 let (input, subject) = opt(preceded(space1, identifier)).parse(input)?;
517 let (input, bidir) = opt(preceded(space1, tag("BIDIRECTIONAL"))).parse(input)?;
518 let (input, _) = eol(input)?;
519 Ok((
520 input,
521 Statement::Check {
522 subject,
523 bidirectional: bidir.is_some(),
524 },
525 ))
526}
527
528fn stmt_axiom<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
530 let (input, _) = (tag("AXIOM"), space1).parse(input)?;
531 let at = input;
533 let (input, name) = promote(
534 identifier(input),
535 at,
536 "expected an axiom name (a lowercase identifier)",
537 )?;
538 let (input, _) = space0(input)?;
539 let (input, _) = promote(
540 char(':').parse(input),
541 input,
542 "expected ':' after the axiom name",
543 )?;
544 let (input, _) = promote(eol(input), input, "unexpected text after 'AXIOM <name>:'")?;
545 let at = input;
546 let (input, body) = promote(
547 alt((list_body, impl_body)).parse(input),
548 at,
549 "an axiom body must be a list (EXCLUSIVE/FORBIDS/ONEOF/ATLEAST) or WHEN ... THEN",
550 )?;
551 Ok((input, Statement::Axiom { name, body }))
552}
553
554fn stmt_rule<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
556 let (input, _) = (tag("RULE"), space1).parse(input)?;
557 let at = input;
558 let (input, name) = promote(
559 identifier(input),
560 at,
561 "expected a rule name (a lowercase identifier)",
562 )?;
563 let (input, _) = space0(input)?;
564 let (input, _) = promote(
565 char(':').parse(input),
566 input,
567 "expected ':' after the rule name",
568 )?;
569 let (input, _) = promote(eol(input), input, "unexpected text after 'RULE <name>:'")?;
570 let at = input;
571 let (input, body) = promote(impl_body(input), at, "a rule body must be WHEN ... THEN")?;
572 Ok((input, Statement::Rule { name, body }))
573}
574
575fn statement<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
579 alt((
580 stmt_import,
581 stmt_fact,
582 stmt_axiom,
583 stmt_rule,
584 stmt_check,
585 stmt_negation,
586 ))
587 .parse(input)
588}
589
590fn program<'a>(input: Span<'a>) -> PResult<'a, Vec<Statement<'a>>> {
594 let (input, _) = skip_noise(input)?;
595 many0(terminated(statement, skip_noise)).parse(input)
596}
597
598pub fn parse(src: &str) -> Result<Program<'_>, ParseError<'_>> {
603 let input = Span::new(src);
604 match program(input) {
605 Ok((rest, statements)) => {
606 if !trailing_is_empty(rest.fragment()) {
607 return Err(ParseError {
608 source: src,
609 span: rest,
610 message: String::from(
611 "expected a statement (IMPORT/FACT/NOT/AXIOM/RULE/CHECK)",
612 ),
613 });
614 }
615 Ok(Program { statements })
616 }
617 Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(ParseError {
618 source: src,
619 span: e.input,
620 message: e.message,
621 }),
622 Err(nom::Err::Incomplete(_)) => Err(ParseError {
623 source: src,
624 span: input,
625 message: String::from("incomplete input"),
626 }),
627 }
628}
629
630fn trailing_is_empty(tail: &str) -> bool {
632 for raw in tail.lines() {
633 let t = raw.trim();
634 if t.is_empty() || t.starts_with("//") {
635 continue;
636 }
637 return false;
638 }
639 true
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645 use alloc::format;
646
647 fn prog(src: &str) -> Program<'_> {
648 parse(src).expect("should parse")
649 }
650
651 type AtomShape<'a> = (&'a str, &'a str, Option<&'a str>);
653 type ListShape<'a> = (ListOp, Vec<AtomShape<'a>>);
655
656 fn atom_shapes<'a>(p: &Program<'a>) -> Vec<ListShape<'a>> {
659 p.statements
660 .iter()
661 .filter_map(|s| match s {
662 Statement::Axiom {
663 body: Body::List { op, atoms },
664 ..
665 } => Some((
666 *op,
667 atoms
668 .iter()
669 .map(|a| (a.data.subject, a.data.predicate, a.data.object))
670 .collect(),
671 )),
672 _ => None,
673 })
674 .collect()
675 }
676
677 #[test]
678 fn parses_fact_and_negation() {
679 let p = prog("FACT Creature.A has flying\nNOT Creature.A has cold_blood\n");
680 assert_eq!(p.statements.len(), 2);
681 match &p.statements[0] {
682 Statement::Fact(a) => {
683 assert_eq!(a.data.subject, "Creature.A");
684 assert_eq!(a.data.predicate, "has");
685 assert_eq!(a.data.object, Some("flying"));
686 }
687 other => panic!("expected fact, got {:?}", other),
688 }
689 match &p.statements[1] {
690 Statement::Negation(a) => {
691 assert_eq!(a.data.object, Some("cold_blood"));
692 }
693 other => panic!("expected negation, got {:?}", other),
694 }
695 }
696
697 #[test]
698 fn fact_without_object() {
699 let p = prog("FACT Motor over_100\n");
700 match &p.statements[0] {
701 Statement::Fact(a) => {
702 assert_eq!(a.data.subject, "Motor");
703 assert_eq!(a.data.predicate, "over_100");
704 assert_eq!(a.data.object, None);
705 }
706 other => panic!("expected fact, got {:?}", other),
707 }
708 }
709
710 #[test]
711 fn parses_import() {
712 let p = prog("IMPORT \"physics.vrf\"\n");
713 match &p.statements[0] {
714 Statement::Import(path) => assert_eq!(path.data, "physics.vrf"),
715 other => panic!("expected import, got {:?}", other),
716 }
717 }
718
719 #[test]
720 fn parses_exclusive_axiom() {
721 let src = "AXIOM fly_xor_swim:\n EXCLUSIVE\n Creature.A has flying\n Creature.A has swimming\n";
722 let p = prog(src);
723 match &p.statements[0] {
724 Statement::Axiom { name, body } => {
725 assert_eq!(name.data, "fly_xor_swim");
726 match body {
727 Body::List { op, atoms } => {
728 assert_eq!(*op, ListOp::Exclusive);
729 assert_eq!(atoms.len(), 2);
730 assert_eq!(atoms[1].data.object, Some("swimming"));
731 }
732 other => panic!("expected list body, got {:?}", other),
733 }
734 }
735 other => panic!("expected axiom, got {:?}", other),
736 }
737 }
738
739 #[test]
740 fn parses_implication_axiom_with_and() {
741 let src = "AXIOM wings_need_bone:\n WHEN Creature.A has flying\n THEN Creature.A has wing\n AND Creature.A has bone\n";
742 let p = prog(src);
743 match &p.statements[0] {
744 Statement::Axiom {
745 body:
746 Body::Impl {
747 antecedent,
748 consequent,
749 },
750 ..
751 } => {
752 assert_eq!(antecedent.len(), 1);
753 assert_eq!(antecedent[0].data.atom.object, Some("flying"));
754 assert_eq!(consequent.len(), 2);
755 assert_eq!(consequent[0].data.atom.object, Some("wing"));
756 assert_eq!(consequent[1].data.atom.object, Some("bone"));
757 }
758 other => panic!("expected impl axiom, got {:?}", other),
759 }
760 }
761
762 #[test]
763 fn antecedent_and_goes_before_then() {
764 let src = "AXIOM deploy:\n WHEN s tested\n AND s reviewed\n THEN s can_deploy\n";
765 let p = prog(src);
766 match &p.statements[0] {
767 Statement::Axiom {
768 body:
769 Body::Impl {
770 antecedent,
771 consequent,
772 },
773 ..
774 } => {
775 assert_eq!(antecedent.len(), 2);
776 assert_eq!(consequent.len(), 1);
777 }
778 other => panic!("unexpected: {:?}", other),
779 }
780 }
781
782 #[test]
783 fn parses_negated_literal_in_rule() {
784 let src = "RULE pick_slow:\n WHEN NOT Motor over_100\n THEN Motor uses slow_path\n";
785 let p = prog(src);
786 match &p.statements[0] {
787 Statement::Rule {
788 body: Body::Impl { antecedent, .. },
789 ..
790 } => {
791 assert!(antecedent[0].data.negated);
792 assert_eq!(antecedent[0].data.atom.predicate, "over_100");
793 }
794 other => panic!("expected rule, got {:?}", other),
795 }
796 }
797
798 #[test]
799 fn parses_check_variants() {
800 let p = prog("CHECK Creature.A BIDIRECTIONAL\n");
801 match &p.statements[0] {
802 Statement::Check {
803 subject,
804 bidirectional,
805 } => {
806 assert_eq!(subject.as_ref().unwrap().data, "Creature.A");
807 assert!(bidirectional);
808 }
809 other => panic!("expected check, got {:?}", other),
810 }
811
812 let p = prog("CHECK\n");
813 match &p.statements[0] {
814 Statement::Check {
815 subject,
816 bidirectional,
817 } => {
818 assert!(subject.is_none());
819 assert!(!bidirectional);
820 }
821 other => panic!("expected check, got {:?}", other),
822 }
823 }
824
825 #[test]
826 fn comments_and_blanks_are_ignored() {
827 let src = "// header\n\nFACT a b // trailing comment\n\n// tail\n";
828 let p = prog(src);
829 assert_eq!(p.statements.len(), 1);
830 }
831
832 #[test]
833 fn indentation_is_cosmetic() {
834 let flat = "AXIOM x:\nEXCLUSIVE\na b\na c\n";
835 let indented = "AXIOM x:\n EXCLUSIVE\n a b\n a c\n";
836 assert_eq!(atom_shapes(&prog(flat)), atom_shapes(&prog(indented)));
838 }
839
840 #[test]
841 fn full_creature_example_parses() {
842 let src = include_str!("../../../docs/examples/creature.vrf");
843 let p = prog(src);
844 assert_eq!(p.statements.len(), 7);
846 }
847
848 #[test]
849 fn import_demo_example_parses() {
850 let src = include_str!("../../../docs/examples/import-demo.vrf");
851 let p = prog(src);
852 assert!(matches!(p.statements[0], Statement::Import(_)));
853 }
854
855 #[test]
856 fn reserved_word_cannot_be_identifier() {
857 assert!(parse("FACT WHEN has x\n").is_err());
859 }
860
861 #[test]
862 fn pretty_error_points_at_offending_line() {
863 let src = "FACT a b\n!garbage here\nFACT c d\n";
864 let err = parse(src).expect_err("should fail");
865 let shown = format!("{}", err);
866 assert!(shown.contains("Syntax Error"));
867 assert!(shown.contains("line 2"));
868 assert!(shown.contains("!garbage here"));
869 assert!(shown.contains("^--- here"));
870 }
871
872 #[test]
873 fn crlf_line_endings() {
874 let p = prog("FACT a b\r\nCHECK a\r\n");
875 assert_eq!(p.statements.len(), 2);
876 }
877
878 #[test]
879 fn tabs_as_indentation() {
880 let p = prog("AXIOM e:\n\tEXCLUSIVE\n\t\tx a\n\t\tx b\n");
881 assert!(matches!(
882 p.statements[0],
883 Statement::Axiom {
884 body: Body::List {
885 op: ListOp::Exclusive,
886 ..
887 },
888 ..
889 }
890 ));
891 }
892
893 #[test]
894 fn parses_all_list_ops() {
895 for (kw, want) in [
896 ("EXCLUSIVE", ListOp::Exclusive),
897 ("FORBIDS", ListOp::Forbids),
898 ("ONEOF", ListOp::OneOf),
899 ("ATLEAST", ListOp::AtLeast),
900 ] {
901 let src = alloc::format!("AXIOM a:\n {kw}\n x a\n x b\n");
902 match &prog(&src).statements[0] {
903 Statement::Axiom {
904 body: Body::List { op, .. },
905 ..
906 } => assert_eq!(*op, want),
907 other => panic!("{kw}: unexpected {other:?}"),
908 }
909 }
910 }
911
912 #[test]
913 fn check_bidirectional_without_subject() {
914 match &prog("CHECK BIDIRECTIONAL\n").statements[0] {
915 Statement::Check {
916 subject,
917 bidirectional,
918 } => {
919 assert!(subject.is_none());
920 assert!(bidirectional);
921 }
922 other => panic!("unexpected {other:?}"),
923 }
924 }
925
926 #[test]
927 fn empty_and_comment_only_input_yield_no_statements() {
928 assert_eq!(prog("").statements.len(), 0);
929 assert_eq!(prog("// just a comment\n\n \n").statements.len(), 0);
930 }
931
932 #[test]
933 fn negation_with_object() {
934 match &prog("NOT Creature.A has wing\n").statements[0] {
935 Statement::Negation(a) => {
936 assert_eq!(a.data.subject, "Creature.A");
937 assert_eq!(a.data.object, Some("wing"));
938 }
939 other => panic!("unexpected {other:?}"),
940 }
941 }
942
943 #[test]
944 fn negated_consequent_then_not() {
945 let src = "AXIOM a:\n WHEN x on\n THEN NOT x off\n";
946 match &prog(src).statements[0] {
947 Statement::Axiom {
948 body: Body::Impl { consequent, .. },
949 ..
950 } => {
951 assert!(consequent[0].data.negated);
952 assert_eq!(consequent[0].data.atom.predicate, "off");
953 }
954 other => panic!("unexpected {other:?}"),
955 }
956 }
957
958 #[test]
959 fn multiple_imports_then_facts() {
960 let p = prog("IMPORT \"a.vrf\"\nIMPORT \"b.vrf\"\nFACT x y\n");
961 assert!(matches!(p.statements[0], Statement::Import(_)));
962 assert!(matches!(p.statements[1], Statement::Import(_)));
963 assert!(matches!(p.statements[2], Statement::Fact(_)));
964 }
965
966 #[test]
967 fn trailing_comment_without_final_newline() {
968 let p = prog("FACT a b\n// trailing, no newline");
969 assert_eq!(p.statements.len(), 1);
970 }
971}