1use alloc::string::{String, ToString};
12use alloc::vec;
13use alloc::vec::Vec;
14
15use nom::{
16 IResult, Parser,
17 branch::alt,
18 bytes::complete::{tag, take_while},
19 character::complete::{char, line_ending, satisfy, space0, space1},
20 combinator::{eof, opt, recognize, value},
21 multi::many0,
22 sequence::{delimited, preceded, terminated},
23};
24
25use crate::ast::{
26 Atom, Body, CloseKind, Conn, ListOp, Literal, Located, Program, Quant, Span, Statement,
27};
28use crate::diag::{Diagnostic, Diagnostics};
29use crate::keywords::{is_reserved, is_top_level, keyword_in, kw};
30
31#[derive(Debug, Clone)]
39struct Problem<'a> {
40 input: Span<'a>,
41 message: String,
42}
43
44impl<'a> nom::error::ParseError<Span<'a>> for Problem<'a> {
45 fn from_error_kind(input: Span<'a>, _: nom::error::ErrorKind) -> Self {
46 Problem {
47 input,
48 message: String::from("unexpected token"),
49 }
50 }
51 fn append(_: Span<'a>, _: nom::error::ErrorKind, other: Self) -> Self {
52 other
53 }
54}
55
56type PResult<'a, T> = IResult<Span<'a>, T, Problem<'a>>;
58
59fn promote<'a, T>(r: PResult<'a, T>, at: Span<'a>, msg: &str) -> PResult<'a, T> {
62 match r {
63 Err(nom::Err::Error(_)) => Err(nom::Err::Failure(Problem {
64 input: at,
65 message: String::from(msg),
66 })),
67 other => other,
68 }
69}
70
71fn perr<'a, T>(input: Span<'a>) -> PResult<'a, T> {
73 Err(nom::Err::Error(Problem {
74 input,
75 message: String::from("unexpected token"),
76 }))
77}
78
79fn is_ident_char(c: char) -> bool {
85 c.is_alphanumeric() || c == '_'
86}
87
88fn raw_identifier<'a>(input: Span<'a>) -> PResult<'a, Span<'a>> {
92 recognize((satisfy(|c| c.is_alphabetic()), take_while(is_ident_char))).parse(input)
93}
94
95fn identifier<'a>(input: Span<'a>) -> PResult<'a, Located<'a, &'a str>> {
97 let start = input;
98 let (rest, sp) = raw_identifier(input)?;
99 if is_reserved(sp.fragment()) {
100 return perr(start);
101 }
102 Ok((
103 rest,
104 Located {
105 data: *sp.fragment(),
106 span: start,
107 },
108 ))
109}
110
111fn comment<'a>(input: Span<'a>) -> PResult<'a, Span<'a>> {
113 recognize((tag("//"), take_while(|c| c != '\n' && c != '\r'))).parse(input)
114}
115
116fn eol<'a>(input: Span<'a>) -> PResult<'a, ()> {
119 value((), (space0, opt(comment), alt((line_ending, eof)))).parse(input)
120}
121
122fn noise_line<'a>(input: Span<'a>) -> PResult<'a, ()> {
124 value((), (space0, opt(comment), line_ending)).parse(input)
125}
126
127fn skip_noise<'a>(input: Span<'a>) -> PResult<'a, ()> {
129 value((), many0(noise_line)).parse(input)
130}
131
132fn atom<'a>(input: Span<'a>) -> PResult<'a, Located<'a, Atom<'a>>> {
141 let start = input;
142 let (input, domain) = opt(terminated(identifier, char('.'))).parse(input)?;
143 let (input, subject) = identifier(input)?;
144 let (input, predicate) = opt(preceded(space1, identifier)).parse(input)?;
145 let (input, object) = match predicate {
147 Some(_) => opt(preceded(space1, identifier)).parse(input)?,
148 None => (input, None),
149 };
150 Ok((
151 input,
152 Located {
153 data: Atom {
154 domain: domain.map(|d| d.data),
155 subject: subject.data,
156 predicate: predicate.map(|p| p.data),
157 object: object.map(|o| o.data),
158 },
159 span: start,
160 },
161 ))
162}
163
164fn literal<'a>(input: Span<'a>) -> PResult<'a, Located<'a, Literal<'a>>> {
166 let start = input;
167 let (input, neg) = opt(terminated(tag(kw::NOT), space1)).parse(input)?;
168 let (input, a) = atom(input)?;
169 Ok((
170 input,
171 Located {
172 data: Literal {
173 negated: neg.is_some(),
174 atom: a.data,
175 },
176 span: start,
177 },
178 ))
179}
180
181fn bool_lit<'a>(input: Span<'a>) -> PResult<'a, bool> {
186 let (rest, sp) = raw_identifier(input)?;
187 match *sp.fragment() {
188 "true" => Ok((rest, true)),
189 "false" => Ok((rest, false)),
190 _ => perr(input),
191 }
192}
193
194fn atom_line<'a>(input: Span<'a>) -> PResult<'a, Located<'a, Atom<'a>>> {
196 let (input, _) = space0(input)?;
197 let (input, a) = atom(input)?;
198 let (input, _) = eol(input)?;
199 Ok((input, a))
200}
201
202fn element_line<'a>(input: Span<'a>) -> PResult<'a, Located<'a, &'a str>> {
206 let (input, _) = space0(input)?;
207 let (input, id) = identifier(input)?;
208 let (input, _) = eol(input)?;
209 Ok((input, id))
210}
211
212fn list_op<'a>(input: Span<'a>) -> PResult<'a, ListOp> {
216 alt((
217 value(ListOp::Exclusive, tag(kw::EXCLUSIVE)),
218 value(ListOp::Forbids, tag(kw::FORBIDS)),
219 value(ListOp::OneOf, tag(kw::ONEOF)),
220 value(ListOp::AtLeast, tag(kw::ATLEAST)),
221 ))
222 .parse(input)
223}
224
225const LIST_NEEDS_TWO_ATOMS: &str = "a list premise needs at least two atoms";
236
237fn list_body<'a>(input: Span<'a>) -> PResult<'a, Body<'a>> {
238 let (input, _) = space0(input)?;
239 let (input, op) = list_op(input)?;
241 let (input, _) = promote(
243 eol(input),
244 input,
245 "expected a newline after the list operator",
246 )?;
247 let at = input;
248 let (input, first) = promote(atom_line(input), at, LIST_NEEDS_TWO_ATOMS)?;
249 let at = input;
250 let (input, second) = promote(atom_line(input), at, LIST_NEEDS_TWO_ATOMS)?;
251 let (input, rest) = many0(atom_line).parse(input)?;
252
253 let mut atoms = vec![first, second];
254 atoms.extend(rest);
255 Ok((input, Body::List { op, atoms }))
256}
257
258fn cont_line<'a>(input: Span<'a>) -> PResult<'a, (Conn, Located<'a, Literal<'a>>)> {
262 let (input, _) = space0(input)?;
263 let (input, conn) =
265 alt((value(Conn::And, tag(kw::AND)), value(Conn::Or, tag(kw::OR)))).parse(input)?;
266 let (input, _) = space1(input)?;
267 let at = input;
268 let (input, lit) = promote(
269 literal(input),
270 at,
271 "AND/OR expects a literal: [NOT] <Subject> <predicate> [<object>]",
272 )?;
273 let (input, _) = promote(
274 eol(input),
275 input,
276 "unexpected text after the AND/OR literal",
277 )?;
278 Ok((input, (conn, lit)))
279}
280
281fn group_conn<'a>(conts: &[(Conn, Located<'a, Literal<'a>>)]) -> Result<Conn, Span<'a>> {
284 let mut seen: Option<Conn> = None;
285 for (conn, lit) in conts {
286 match seen {
287 None => seen = Some(*conn),
288 Some(s) if s != *conn => return Err(lit.span),
289 _ => {}
290 }
291 }
292 Ok(seen.unwrap_or(Conn::And))
293}
294
295fn fail_at<'a, T>(at: Span<'a>, msg: &str) -> PResult<'a, T> {
297 Err(nom::Err::Failure(Problem {
298 input: at,
299 message: String::from(msg),
300 }))
301}
302
303fn impl_body<'a>(input: Span<'a>) -> PResult<'a, Body<'a>> {
310 let (input, _) = space0(input)?;
311 let (input, _) = (tag(kw::WHEN), space1).parse(input)?;
313 let at = input;
315 let (input, when) = promote(
316 literal(input),
317 at,
318 "WHEN expects a literal: [NOT] <Subject> <predicate> [<object>]",
319 )?;
320 let (input, _) = promote(eol(input), input, "unexpected text after the WHEN literal")?;
321 let (input, ante_rest) = many0(cont_line).parse(input)?;
322 let ante_conn = match group_conn(&ante_rest) {
323 Ok(c) => c,
324 Err(span) => {
325 return fail_at(
326 span,
327 "don't mix AND and OR in one WHEN group — split it into separate premises",
328 );
329 }
330 };
331
332 let (input, _) = space0(input)?;
333 let at = input;
334 let (input, _) = promote(
335 tag(kw::THEN).parse(input),
336 at,
337 "expected THEN to complete the WHEN ... THEN implication",
338 )?;
339 let at = input;
340 let (input, then) = promote(
341 preceded(space1, literal).parse(input),
342 at,
343 "THEN expects a literal: [NOT] <Subject> <predicate> [<object>]",
344 )?;
345 let (input, _) = promote(eol(input), input, "unexpected text after the THEN literal")?;
346 let (input, cons_rest) = many0(cont_line).parse(input)?;
347 let cons_conn = match group_conn(&cons_rest) {
348 Ok(c) => c,
349 Err(span) => {
350 return fail_at(
351 span,
352 "don't mix AND and OR in one THEN group — split it into separate premises",
353 );
354 }
355 };
356
357 let mut antecedent = vec![when];
358 antecedent.extend(ante_rest.into_iter().map(|(_, l)| l));
359 let mut consequent = vec![then];
360 consequent.extend(cons_rest.into_iter().map(|(_, l)| l));
361 Ok((
362 input,
363 Body::Impl {
364 antecedent,
365 ante_conn,
366 consequent,
367 cons_conn,
368 },
369 ))
370}
371
372fn stmt_import<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
377 let (input, _) = (tag(kw::IMPORT), space1).parse(input)?;
378 let start = input;
379 let (input, path) = promote(
380 delimited(char('"'), take_while(|c| c != '"' && c != '\n'), char('"')).parse(input),
381 start,
382 "IMPORT expects a quoted path, e.g. IMPORT \"physics.vrf\"",
383 )?;
384 let (input, alias) = opt(preceded((space1, tag(kw::AS), space1), identifier)).parse(input)?;
386 let (input, _) = promote(
387 eol(input),
388 input,
389 "unexpected text after the IMPORT path (did you mean AS <alias>?)",
390 )?;
391 Ok((
392 input,
393 Statement::Import {
394 path: Located {
395 data: *path.fragment(),
396 span: start,
397 },
398 alias,
399 },
400 ))
401}
402
403fn stmt_domain<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
405 let (input, _) = (tag(kw::DOMAIN), space1).parse(input)?;
406 let at = input;
407 let (input, name) = promote(
408 identifier(input),
409 at,
410 "DOMAIN expects a name (a lowercase identifier), e.g. DOMAIN physics",
411 )?;
412 let (input, _) = promote(eol(input), input, "unexpected text after the DOMAIN name")?;
413 Ok((input, Statement::Domain(name)))
414}
415
416fn stmt_fact<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
418 let (input, _) = (tag(kw::FACT), space1).parse(input)?;
419 let at = input;
420 let (input, a) = promote(
421 atom(input),
422 at,
423 "FACT expects an atom: <Subject> <predicate> [<object>]",
424 )?;
425 let (input, _) = promote(eol(input), input, "unexpected text after the FACT atom")?;
426 Ok((input, Statement::Fact(a)))
427}
428
429fn stmt_assume<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
432 let (input, _) = (tag(kw::ASSUME), space1).parse(input)?;
433 let at = input;
434 let (input, lit) = promote(
435 literal(input),
436 at,
437 "ASSUME expects an atom: [NOT] <Subject> <predicate> [<object>]",
438 )?;
439 let (input, _) = promote(eol(input), input, "unexpected text after the ASSUME atom")?;
440 Ok((input, Statement::Assume(lit)))
441}
442
443fn stmt_negation<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
446 let (input, _) = (tag(kw::NOT), space1).parse(input)?;
447 let at = input;
448 let (input, a) = promote(
449 atom(input),
450 at,
451 "NOT expects an atom: <Subject> <predicate> [<object>]",
452 )?;
453 let (input, _) = promote(eol(input), input, "unexpected text after the NOT atom")?;
454 Ok((input, Statement::Negation(a)))
455}
456
457fn stmt_check<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
459 let (input, _) = tag(kw::CHECK).parse(input)?;
460 let (input, subject) = opt(preceded(space1, identifier)).parse(input)?;
461 let (input, bidir) = opt(preceded(space1, tag(kw::BIDIRECTIONAL))).parse(input)?;
462 let (input, _) = eol(input)?;
463 Ok((
464 input,
465 Statement::Check {
466 subject,
467 bidirectional: bidir.is_some(),
468 },
469 ))
470}
471
472fn stmt_set<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
475 let (input, _) = (tag(kw::SET), space1).parse(input)?;
476 let at = input;
477 let (input, name) = promote(
478 identifier(input),
479 at,
480 "SET expects a name (a lowercase identifier), e.g. SET tasks",
481 )?;
482 let (input, _) = promote(eol(input), input, "unexpected text after the SET name")?;
483 let at = input;
484 let (input, first) = promote(
485 element_line(input),
486 at,
487 "a SET needs at least one element — one identifier per line",
488 )?;
489 let (input, rest) = many0(element_line).parse(input)?;
490 let mut elements = vec![first];
491 elements.extend(rest);
492 Ok((input, Statement::Set { name, elements }))
493}
494
495fn stmt_var<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
498 let (input, _) = (tag(kw::VAR), space1).parse(input)?;
499 let at = input;
500 let (input, name) = promote(
501 identifier(input),
502 at,
503 "VAR expects a name (a lowercase identifier), e.g. VAR db_ready",
504 )?;
505 let (input, has_default) = opt(preceded(space1, tag(kw::DEFAULT))).parse(input)?;
507 let (input, default) = if has_default.is_some() {
508 let (input, _) = promote(
509 space1(input),
510 input,
511 "DEFAULT expects a value: DEFAULT true|false",
512 )?;
513 let at = input;
514 let (input, v) = promote(bool_lit(input), at, "DEFAULT expects true or false")?;
515 (input, Some(v))
516 } else {
517 (input, None)
518 };
519 let (input, _) = promote(
520 eol(input),
521 input,
522 "unexpected text after the VAR declaration",
523 )?;
524 Ok((input, Statement::Var { name, default }))
525}
526
527fn stmt_provide<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
532 let (input, _) = (tag(kw::PROVIDE), space1).parse(input)?;
533 let at = input;
534 let (input, atom) = promote(
535 atom(input),
536 at,
537 "PROVIDE expects a port or atom, e.g. PROVIDE db_ready: true",
538 )?;
539 let (input, _) = space0(input)?;
540 let (input, _) = promote(
541 char(':').parse(input),
542 input,
543 "PROVIDE expects ':' then a value: PROVIDE <port>: true|false",
544 )?;
545 let (input, _) = space0(input)?;
546 let at = input;
547 let (input, value) = promote(bool_lit(input), at, "PROVIDE expects true or false")?;
548 let (input, _) = promote(eol(input), input, "unexpected text after the PROVIDE value")?;
549 Ok((input, Statement::Provide { atom, value }))
550}
551
552fn stmt_close<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
554 let (input, _) = (tag(kw::CLOSE), space1).parse(input)?;
555 let at = input;
556 let (input, relation) = promote(
557 identifier(input),
558 at,
559 "CLOSE expects a relation name, e.g. CLOSE depends_on TRANSITIVE",
560 )?;
561 let (input, _) = promote(
562 (space1, tag(kw::TRANSITIVE)).parse(input),
563 input,
564 "CLOSE expects a closure kind: CLOSE <relation> TRANSITIVE",
565 )?;
566 let (input, _) = promote(
567 eol(input),
568 input,
569 "unexpected text after 'CLOSE … TRANSITIVE'",
570 )?;
571 Ok((
572 input,
573 Statement::Close {
574 relation,
575 kind: CloseKind::Transitive,
576 },
577 ))
578}
579
580fn for_each<'a>(input: Span<'a>) -> PResult<'a, Quant<'a>> {
587 let (input, _) = tag(kw::FOR).parse(input)?;
589 let at = input;
591 let (input, _) = promote(
592 (space1, tag(kw::EACH), space1).parse(input),
593 at,
594 "FOR must be followed by EACH: FOR EACH <binder> IN <set> (or FOR EACH <a> <relation> <b>)",
595 )?;
596 let at = input;
597 let (input, first) = promote(
598 identifier(input),
599 at,
600 "expected a binder name after FOR EACH",
601 )?;
602 let (input, _) = promote(
603 space1(input),
604 input,
605 "expected `IN <set>` or a relation `<rel> <binder>` after the FOR EACH binder",
606 )?;
607 let after_in: PResult<'a, (Span<'a>, Span<'a>)> = (tag(kw::IN), space1).parse(input);
609 if let Ok((rest, _)) = after_in {
610 let at = rest;
611 let (rest, set) = promote(identifier(rest), at, "expected a set name after IN")?;
612 return Ok((rest, Quant::InSet { binder: first, set }));
613 }
614 let at = input;
615 let (input, predicate) = promote(
616 identifier(input),
617 at,
618 "expected a relation name (FOR EACH <a> <relation> <b>)",
619 )?;
620 let (input, _) = promote(
621 space1(input),
622 input,
623 "expected the second binder after the relation (FOR EACH <a> <relation> <b>)",
624 )?;
625 let at = input;
626 let (input, right) = promote(
627 identifier(input),
628 at,
629 "expected the second binder (FOR EACH <a> <relation> <b>)",
630 )?;
631 Ok((
632 input,
633 Quant::Relation {
634 left: first,
635 predicate,
636 right,
637 },
638 ))
639}
640
641fn named_header<'a>(
647 input: Span<'a>,
648 name_msg: &'static str,
649 colon_msg: &'static str,
650 tail_msg: &'static str,
651) -> PResult<'a, (Located<'a, &'a str>, Option<Quant<'a>>)> {
652 let at = input;
653 let (input, name) = promote(identifier(input), at, name_msg)?;
654 let (input, quant) = opt(preceded(space1, for_each)).parse(input)?;
655 let (input, _) = space0(input)?;
656 let (input, _) = promote(char(':').parse(input), input, colon_msg)?;
657 let (input, _) = promote(eol(input), input, tail_msg)?;
658 Ok((input, (name, quant)))
659}
660
661fn stmt_premise<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
662 let (input, _) = (tag(kw::PREMISE), space1).parse(input)?;
663 let (input, (name, quant)) = named_header(
665 input,
666 "expected a premise name (a lowercase identifier)",
667 "expected ':' after the premise name",
668 "unexpected text after 'PREMISE <name>:'",
669 )?;
670 let at = input;
671 let (input, body) = promote(
672 alt((list_body, impl_body)).parse(input),
673 at,
674 "a premise body must be a list (EXCLUSIVE/FORBIDS/ONEOF/ATLEAST) or WHEN ... THEN",
675 )?;
676 Ok((input, Statement::Premise { name, quant, body }))
677}
678
679fn stmt_rule<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
682 let (input, _) = (tag(kw::RULE), space1).parse(input)?;
683 let (input, (name, quant)) = named_header(
684 input,
685 "expected a rule name (a lowercase identifier)",
686 "expected ':' after the rule name",
687 "unexpected text after 'RULE <name>:'",
688 )?;
689 let at = input;
690 let (input, body) = promote(impl_body(input), at, "a rule body must be WHEN ... THEN")?;
691 Ok((input, Statement::Rule { name, quant, body }))
692}
693
694fn statement<'a>(input: Span<'a>) -> PResult<'a, Statement<'a>> {
698 let (input, _) = space0(input)?;
702 alt((
703 stmt_domain,
704 stmt_import,
705 stmt_set,
706 stmt_close,
707 stmt_var,
708 stmt_provide,
709 stmt_fact,
710 stmt_assume,
711 stmt_premise,
712 stmt_rule,
713 stmt_check,
714 stmt_negation,
715 ))
716 .parse(input)
717}
718
719fn not_a_statement() -> String {
725 alloc::format!(
726 "expected a statement — a line must start with {}",
727 crate::keywords::top_level_menu()
728 )
729}
730
731pub fn parse(src: &str) -> Result<Program<'_>, Diagnostics> {
739 let mut input = Span::new(src);
740 let mut statements = Vec::new();
741 let mut errors: Vec<Diagnostic> = Vec::new();
742
743 loop {
744 if let Ok((rest, _)) = skip_noise(input) {
745 input = rest;
746 }
747 if at_end(input.fragment()) {
750 break;
751 }
752 match statement(input) {
753 Ok((rest, stmt)) => {
754 statements.push(stmt);
755 input = rest;
756 }
757 Err(nom::Err::Failure(p)) => {
759 errors.push(make_diag(src, p.input, p.message, false));
760 input = resync(p.input);
761 }
762 Err(nom::Err::Error(p)) => {
764 errors.push(make_diag(src, p.input, not_a_statement(), true));
765 input = resync(p.input);
766 }
767 Err(nom::Err::Incomplete(_)) => break,
769 }
770 }
771
772 if errors.is_empty() {
773 Ok(Program { statements })
774 } else {
775 Err(Diagnostics { file: None, errors })
776 }
777}
778
779fn at_end(frag: &str) -> bool {
783 let t = frag.trim_start();
784 t.is_empty() || (t.starts_with("//") && !t.contains('\n'))
785}
786
787fn make_diag(src: &str, at: Span<'_>, message: String, general: bool) -> Diagnostic {
791 let line = at.location_line() as usize;
792 let col = at.get_column();
793 let line_text = src.lines().nth(line.saturating_sub(1)).unwrap_or("");
794 let keyword = if general { None } else { keyword_in(&message) };
799 Diagnostic {
800 line,
801 col,
802 width: caret_width(line_text, col),
803 message,
804 keyword,
805 general,
806 line_text: line_text.to_string(),
807 }
808}
809
810fn caret_width(line_text: &str, col: usize) -> usize {
813 let start = col.saturating_sub(1);
814 let trimmed_len = line_text.trim_end().chars().count();
815 trimmed_len.saturating_sub(start).max(1)
816}
817
818fn resync(at: Span<'_>) -> Span<'_> {
822 let mut input = consume_line(at);
823 loop {
824 if input.fragment().is_empty() || starts_top_level(input) {
825 return input;
826 }
827 input = consume_line(input);
828 }
829}
830
831fn consume_line(input: Span<'_>) -> Span<'_> {
834 let parsed: PResult<'_, Span<'_>> =
835 recognize((take_while(|c| c != '\n' && c != '\r'), opt(line_ending))).parse(input);
836 parsed.map(|(rest, _)| rest).unwrap_or(input)
837}
838
839fn starts_top_level(input: Span<'_>) -> bool {
841 let after = match space0::<_, Problem<'_>>(input) {
842 Ok((rest, _)) => rest,
843 Err(_) => return false,
844 };
845 let word: String = after
846 .fragment()
847 .chars()
848 .take_while(|c| c.is_ascii_uppercase())
849 .collect();
850 is_top_level(&word)
851}