modus_lib/
modusfile.rs

1// Modus, a language for building container images
2// Copyright (C) 2022 University College London
3
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as
6// published by the Free Software Foundation, either version 3 of the
7// License, or (at your option) any later version.
8
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17use codespan_reporting::diagnostic::Diagnostic;
18use codespan_reporting::diagnostic::Label;
19use nom::character::complete::line_ending;
20use nom::character::complete::not_line_ending;
21use nom_supreme::error::BaseErrorKind;
22use nom_supreme::error::ErrorTree;
23
24use std::collections::HashSet;
25use std::fmt;
26use std::str;
27
28use crate::logic;
29use crate::logic::parser::Span;
30use crate::logic::Predicate;
31use crate::logic::SpannedPosition;
32use crate::sld;
33
34use self::parser::process_raw_string;
35
36/// Represents expressions that could be found in the body of a ModusClause.
37/// Each enum variant will have some notion of span and whether it's negated.
38/// False would mean it is negated.
39#[derive(Clone, PartialEq, Debug)]
40pub enum Expression {
41    Literal(Literal),
42
43    // An operator applied to an expression
44    OperatorApplication(Option<SpannedPosition>, Box<Expression>, Operator),
45
46    // A conjunction of expressions.
47    And(
48        Option<SpannedPosition>,
49        bool,
50        Box<Expression>,
51        Box<Expression>,
52    ),
53
54    // A disjunction of expressions.
55    Or(
56        Option<SpannedPosition>,
57        bool,
58        Box<Expression>,
59        Box<Expression>,
60    ),
61}
62
63impl Expression {
64    #[cfg(test)]
65    fn eq_ignoring_position(&self, other: &Expression) -> bool {
66        match (self, other) {
67            (Expression::Literal(l), Expression::Literal(r)) => l.eq_ignoring_position(&r),
68            (
69                Expression::OperatorApplication(_, e1, op1),
70                Expression::OperatorApplication(_, e2, op2),
71            ) => e1.eq_ignoring_position(e2) && op1.eq_ignoring_position(op2),
72            (Expression::And(_, p1, l1, r1), Expression::And(_, p2, l2, r2))
73            | (Expression::Or(_, p1, l1, r1), Expression::Or(_, p2, l2, r2)) => {
74                p1 == p2 && l1.eq_ignoring_position(l2) && r1.eq_ignoring_position(r2)
75            }
76            (s, o) => s.eq(o),
77        }
78    }
79
80    pub fn get_spanned_position(&self) -> &Option<SpannedPosition> {
81        match self {
82            Expression::Literal(lit) => &lit.position,
83            Expression::OperatorApplication(s, ..) => &s,
84            Expression::And(s, ..) => &s,
85            Expression::Or(s, ..) => &s,
86        }
87    }
88
89    pub fn without_position(&self) -> Self {
90        match self {
91            Expression::Literal(lit) => Expression::Literal(Literal {
92                position: None,
93                ..lit.clone()
94            }),
95            Expression::OperatorApplication(_, e, op) => Expression::OperatorApplication(
96                None,
97                Box::new(e.without_position()),
98                op.clone().with_position(None),
99            ),
100            Expression::And(_, positive, e1, e2) => Expression::And(
101                None,
102                positive.clone(),
103                Box::new(e1.without_position()),
104                Box::new(e2.without_position()),
105            ),
106            Expression::Or(_, positive, e1, e2) => Expression::Or(
107                None,
108                positive.clone(),
109                Box::new(e1.without_position()),
110                Box::new(e2.without_position()),
111            ),
112        }
113    }
114
115    pub fn literals(&self) -> HashSet<Literal> {
116        match self {
117            Expression::Literal(lit) => vec![lit.clone()].into_iter().collect(),
118            Expression::OperatorApplication(_, e, _) => e.literals(),
119            Expression::And(_, _, e1, e2) | Expression::Or(_, _, e1, e2) => e1
120                .literals()
121                .into_iter()
122                .chain(e2.literals().into_iter())
123                .collect(),
124        }
125    }
126
127    /// Negates at the current expression level.
128    /// So, does not apply De Morgan's laws.
129    pub fn negate_current(&self) -> Expression {
130        match &self {
131            Expression::Literal(lit) => Expression::Literal(Literal {
132                positive: !lit.positive,
133                ..lit.clone()
134            }),
135            Expression::And(s, curr_p, l, r) => {
136                Expression::And(s.clone(), !curr_p, l.clone(), r.clone())
137            }
138            Expression::Or(s, curr_p, l, r) => {
139                Expression::Or(s.clone(), !curr_p, l.clone(), r.clone())
140            }
141            Expression::OperatorApplication(..) => {
142                panic!("Attempted to negate at operator application level.")
143            }
144        }
145    }
146}
147
148#[derive(Clone, PartialEq, Debug)]
149pub struct ModusClause {
150    pub head: Literal,
151    // If None, this clause is a fact.
152    pub body: Option<Expression>,
153}
154
155#[cfg(test)]
156impl ModusClause {
157    fn eq_ignoring_position(&self, other: &ModusClause) -> bool {
158        if let (Some(expr1), Some(expr2)) = (&self.body, &other.body) {
159            self.head.eq_ignoring_position(&other.head) && expr1.eq_ignoring_position(&expr2)
160        } else {
161            self.head.eq_ignoring_position(&other.head) && self.body.eq(&other.body)
162        }
163    }
164}
165
166#[derive(Clone, PartialEq, Eq, Hash, Debug)]
167pub enum FormatStringFragment {
168    /// A raw section of a f-string - escape characters unprocessed - and its span.
169    StringContent(SpannedPosition, String),
170    /// A variable as a string, and its span.
171    InterpolatedVariable(SpannedPosition, String),
172}
173
174impl fmt::Display for FormatStringFragment {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        match self {
177            FormatStringFragment::StringContent(_, s) => write!(f, "{s}"),
178            FormatStringFragment::InterpolatedVariable(_, v) => write!(f, "${{{v}}}"),
179        }
180    }
181}
182
183#[derive(Clone, PartialEq, Eq, Hash, Debug)]
184pub enum ModusTerm {
185    Constant(String),
186    /// A format string with '\$' left unhandled. This should be dealt with when
187    /// converting to the IR.
188    FormatString {
189        /// The position of this entire term, beginning from the 'f' in the source
190        position: SpannedPosition,
191        fragments: Vec<FormatStringFragment>,
192    },
193    UserVariable(String),
194    AnonymousVariable,
195    Array(SpannedPosition, Vec<ModusTerm>),
196}
197
198impl ModusTerm {
199    pub fn is_variable(&self) -> bool {
200        match self {
201            ModusTerm::FormatString { .. } | ModusTerm::UserVariable(_) => true,
202            _ => false,
203        }
204    }
205
206    /// Returns `true` if the modus term is [`FormatString`].
207    ///
208    /// [`FormatString`]: ModusTerm::FormatString
209    pub fn is_format_string(&self) -> bool {
210        matches!(self, Self::FormatString { .. })
211    }
212}
213
214impl fmt::Display for ModusTerm {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        match self {
217            ModusTerm::Constant(s) => write!(f, "\"{}\"", s),
218            ModusTerm::UserVariable(s) => write!(f, "{}", s),
219            ModusTerm::FormatString {
220                position: _,
221                fragments,
222            } => write!(
223                f,
224                "\"{}\"",
225                fragments
226                    .iter()
227                    .map(|x| x.to_string())
228                    .collect::<Vec<_>>()
229                    .join("")
230            ),
231            ModusTerm::AnonymousVariable => write!(f, "_"),
232            ModusTerm::Array(_, ts) => write!(
233                f,
234                "[{}]",
235                ts.iter()
236                    .map(|t| t.to_string())
237                    .collect::<Vec<_>>()
238                    .join(", ")
239            ),
240        }
241    }
242}
243
244impl str::FromStr for ModusTerm {
245    type Err = Vec<Diagnostic<()>>;
246
247    fn from_str(s: &str) -> Result<Self, Self::Err> {
248        let span = Span::new(s);
249        match parser::modus_term(span) {
250            Result::Ok((_, o)) => Ok(o),
251            Result::Err(nom::Err::Error(e) | nom::Err::Failure(e)) => Err(better_convert_error(e)),
252            _ => unimplemented!(),
253        }
254    }
255}
256
257impl From<ModusTerm> for logic::IRTerm {
258    fn from(modus_term: ModusTerm) -> Self {
259        match modus_term {
260            ModusTerm::Constant(c) => logic::IRTerm::Constant(process_raw_string(&c)),
261            ModusTerm::FormatString { .. } => {
262                unreachable!("BUG: analysis should've handled this case.")
263            }
264            ModusTerm::UserVariable(v) => logic::IRTerm::UserVariable(v),
265            ModusTerm::AnonymousVariable => sld::Auxiliary::aux(true),
266            ModusTerm::Array(_, _) => unimplemented!(),
267        }
268    }
269}
270
271impl fmt::Display for logic::Literal<ModusTerm> {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        match &*self.args {
274            [] => write!(f, "{}", self.predicate),
275            _ => write!(
276                f,
277                "{}({})",
278                self.predicate,
279                self.args
280                    .iter()
281                    .map(|a| a.to_string())
282                    .collect::<Vec<String>>()
283                    .join(", ")
284            ),
285        }
286    }
287}
288
289type Literal = logic::Literal<ModusTerm>;
290
291impl From<Literal> for logic::Literal {
292    fn from(modus_literal: Literal) -> Self {
293        Self {
294            positive: modus_literal.positive,
295            position: modus_literal.position,
296            predicate: modus_literal.predicate,
297            args: modus_literal
298                .args
299                .into_iter()
300                .map(|arg| arg.into())
301                .collect(),
302        }
303    }
304}
305
306#[derive(Clone, PartialEq, Eq, Hash, Debug)]
307pub struct Operator {
308    pub position: Option<SpannedPosition>,
309    pub predicate: Predicate,
310    pub args: Vec<ModusTerm>,
311}
312
313impl Operator {
314    #[cfg(test)]
315    pub fn eq_ignoring_position(&self, other: &Operator) -> bool {
316        self.predicate == other.predicate && self.args == other.args
317    }
318
319    pub fn with_position(self, position: Option<SpannedPosition>) -> Operator {
320        Operator { position, ..self }
321    }
322}
323
324impl fmt::Display for Operator {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        match &*self.args {
327            [] => write!(f, "{}", self.predicate),
328            _ => write!(
329                f,
330                "{}({})",
331                self.predicate,
332                self.args
333                    .iter()
334                    .map(|t| t.to_string())
335                    .collect::<Vec<_>>()
336                    .join(", ")
337            ),
338        }
339    }
340}
341
342#[derive(Clone, PartialEq, Debug)]
343pub struct Modusfile(pub Vec<ModusClause>);
344
345#[derive(Clone, PartialEq, Debug)]
346pub struct Version {
347    major: u32,
348    minor: u32,
349    patch: u32,
350    pre_release: String,
351    build: String,
352}
353
354/// Combines nom_supreme's error tree type, codespan's reporting and some custom logic
355/// that selects only a subset of a span to produce better error messages.
356fn better_convert_error(e: ErrorTree<Span>) -> Vec<Diagnostic<()>> {
357    fn generate_base_label(span: &Span, kind: &BaseErrorKind) -> Label<()> {
358        let length = if let BaseErrorKind::Expected(nom_supreme::error::Expectation::Tag(t)) = kind
359        {
360            t.len()
361        } else {
362            // Default to displaying a single character if we do not know what's expected.
363            // (Displaying the full span could be the entire rest of the source file.)
364            1
365        };
366        Label::primary((), span.location_offset()..span.location_offset() + length)
367    }
368
369    let mut diags = Vec::new();
370    match e {
371        ErrorTree::Base { location, kind } => {
372            let diag = Diagnostic::error()
373                .with_message(kind.to_string())
374                .with_labels(vec![generate_base_label(&location, &kind)]);
375            diags.push(diag);
376        }
377        ErrorTree::Stack { base, contexts } => {
378            let mut labels = Vec::new();
379            let diag;
380
381            let base_range;
382            match *base {
383                ErrorTree::Base { location, kind } => {
384                    labels.push(generate_base_label(&location, &kind));
385                    base_range = labels[0].range.clone();
386                    diag = Diagnostic::error().with_message(kind.to_string());
387                }
388                ErrorTree::Stack { .. } => panic!("base of an error stack was a stack"),
389                ErrorTree::Alt(alts) => {
390                    return alts
391                        .into_iter()
392                        .flat_map(|a| better_convert_error(a))
393                        .collect()
394                }
395            }
396
397            for (span, stack_context) in contexts.iter() {
398                labels.push(
399                    Label::secondary((), span.location_offset()..base_range.end)
400                        .with_message(stack_context.to_string()),
401                );
402            }
403            diags.push(diag.with_labels(labels))
404        }
405        ErrorTree::Alt(alts) => {
406            diags.extend(
407                alts.into_iter()
408                    .flat_map(|alt_tree| better_convert_error(alt_tree)),
409            );
410        }
411    }
412    diags
413}
414
415impl str::FromStr for Modusfile {
416    type Err = Vec<Diagnostic<()>>;
417
418    fn from_str(s: &str) -> Result<Self, Self::Err> {
419        let span = Span::new(s);
420        match parser::modusfile(span) {
421            Result::Ok((_, o)) => Ok(o),
422            Result::Err(nom::Err::Error(e) | nom::Err::Failure(e)) => Err(better_convert_error(e)),
423            _ => unimplemented!(),
424        }
425    }
426}
427
428impl str::FromStr for Expression {
429    type Err = Vec<Diagnostic<()>>;
430
431    fn from_str(s: &str) -> Result<Self, Self::Err> {
432        let span = Span::new(s);
433        match parser::body(span) {
434            Ok((_, o)) => Ok(o),
435            Err(nom::Err::Error(e) | nom::Err::Failure(e)) => Err(better_convert_error(e)),
436            _ => unimplemented!(),
437        }
438    }
439}
440
441impl fmt::Display for Expression {
442    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443        match self {
444            Expression::OperatorApplication(_, expr, op) => {
445                write!(f, "({})::{}", expr.to_string(), op)
446            }
447            Expression::Literal(l) => write!(f, "{}", l.to_string()),
448            Expression::And(_, positive, expr1, expr2) => {
449                // Explicit parenthesization when printing to output, looks a bit
450                // verbose but shouldn't affect user code.
451                write!(
452                    f,
453                    "{}({}, {})",
454                    if *positive { "" } else { "!" },
455                    expr1,
456                    expr2
457                )
458            }
459            Expression::Or(_, positive, expr1, expr2) => {
460                write!(
461                    f,
462                    "{}({}; {})",
463                    if *positive { "" } else { "!" },
464                    expr1,
465                    expr2
466                )
467            }
468        }
469    }
470}
471
472// could write a macro that generates these
473impl From<Literal> for Expression {
474    fn from(l: Literal) -> Self {
475        Expression::Literal(l)
476    }
477}
478
479impl str::FromStr for ModusClause {
480    type Err = String;
481
482    fn from_str(s: &str) -> Result<Self, Self::Err> {
483        let span = Span::new(s);
484        match parser::modus_clause(span) {
485            Result::Ok((_, o)) => Ok(o),
486            Result::Err(e) => Result::Err(format!("{}", e)),
487        }
488    }
489}
490
491impl fmt::Display for ModusClause {
492    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493        if let Some(e) = &self.body {
494            write!(f, "{} :- {}.", self.head, e.to_string(),)
495        } else {
496            write!(f, "{}.", self.head)
497        }
498    }
499}
500
501impl str::FromStr for Literal {
502    type Err = String;
503
504    fn from_str(s: &str) -> Result<Self, Self::Err> {
505        let span = Span::new(s);
506        match logic::parser::literal(parser::modus_term, parser::token_sep0)(span) {
507            Result::Ok((_, o)) => Ok(o),
508            Result::Err(e) => Result::Err(format!("{}", e)),
509        }
510    }
511}
512
513pub mod parser {
514    use crate::logic::parser::{literal, literal_identifier, recognized_span, IResult};
515    use crate::logic::Predicate;
516
517    use super::*;
518
519    use nom::bytes::complete::{escaped, is_a};
520    use nom::character::complete::{multispace0, none_of, one_of};
521    use nom::combinator::{cut, opt, recognize};
522    use nom::error::context;
523    use nom::multi::{many0_count, many1, separated_list0, separated_list1};
524    use nom::sequence::{pair, tuple};
525    use nom::{
526        branch::alt,
527        combinator::{eof, map},
528        multi::many0,
529        sequence::{delimited, preceded, separated_pair, terminated},
530    };
531    use nom_supreme::tag::complete::tag;
532
533    fn comment(s: Span) -> IResult<Span, Span> {
534        recognize(delimited(
535            tag("#"),
536            opt(not_line_ending),
537            alt((line_ending, eof)),
538        ))(s)
539    }
540
541    #[test]
542    fn test_comment_oneline() {
543        let input = Span::new("# comment");
544        let (rest, _) = comment(input).unwrap();
545        assert!(rest.is_empty());
546    }
547
548    fn comments(s: Span) -> IResult<Span, Vec<Span>> {
549        delimited(
550            multispace0,
551            many0(terminated(comment, multispace0)),
552            multispace0,
553        )(s)
554    }
555
556    #[test]
557    fn test_comments() {
558        let s = Span::new("# comment\n# comment\n");
559        let (rest, _) = comments(s).unwrap();
560        assert!(rest.is_empty());
561    }
562
563    #[test]
564    fn test_comments_oneline() {
565        let s = Span::new("# comment");
566        let (rest, _) = comments(s).unwrap();
567        assert!(rest.is_empty());
568    }
569
570    #[test]
571    fn test_comments_empty() {
572        let s = Span::new("");
573        comments(s).unwrap();
574    }
575
576    /// Parse either nothing, or anything that can separate tokens, including
577    /// spaces, comments, etc.
578    pub fn token_sep0(s: Span) -> IResult<Span, ()> {
579        map(comments, |_| ())(s)
580    }
581
582    #[test]
583    fn test_token_sep0_empty() {
584        let s = Span::new("");
585        token_sep0(s).unwrap();
586    }
587
588    #[test]
589    fn test_token_sep0() {
590        token_sep0(Span::new(" ")).unwrap();
591        token_sep0(Span::new(" \n#")).unwrap();
592        token_sep0(Span::new("#\n\n")).unwrap();
593        token_sep0(Span::new("   ")).unwrap();
594    }
595
596    fn head(i: Span) -> IResult<Span, Literal> {
597        context(stringify!(head), literal(modus_term, token_sep0))(i)
598    }
599
600    /// Parses `<term1> = <term2>` into a builtin call, `string_eq(term1, term2)`.
601    /// Supports the negated version with `!=`.
602    fn unification_sugar(i: Span) -> IResult<Span, Literal> {
603        map(
604            recognized_span(tuple((
605                modus_term,
606                delimited(token_sep0, alt((tag("!="), tag("="))), token_sep0),
607                cut(modus_term),
608            ))),
609            |(spanned_pos, (t1, op, t2))| Literal {
610                positive: op.fragment().len() == 1,
611                position: Some(spanned_pos),
612                predicate: Predicate("string_eq".to_owned()),
613                args: vec![t1, t2],
614            },
615        )(i)
616    }
617
618    fn modus_literal(i: Span) -> IResult<Span, Expression> {
619        map(literal(modus_term, token_sep0), Expression::Literal)(i)
620    }
621
622    /// Parses a parenthesized expression, taking into account any preceding negation.
623    fn parenthesized_expr(i: Span) -> IResult<Span, Expression> {
624        let l_paren_with_comments = |i| terminated(tag("("), comments)(i);
625        let r_paren_with_comments = |i| preceded(comments, cut(tag(")")))(i);
626
627        map(
628            pair(
629                many0_count(terminated(nom::character::complete::char('!'), token_sep0)),
630                delimited(l_paren_with_comments, body, r_paren_with_comments),
631            ),
632            |(neg_count, expr)| {
633                if neg_count % 2 == 0 {
634                    expr
635                } else {
636                    // negate the expression
637                    expr.negate_current()
638                }
639            },
640        )(i)
641    }
642
643    /// Parses an operator based on a literal, failing if negation is encountered.
644    fn operator(i: Span) -> IResult<Span, Operator> {
645        map(
646            recognized_span(pair(
647                terminated(literal_identifier, token_sep0),
648                opt(delimited(
649                    terminated(tag("("), token_sep0),
650                    separated_list1(
651                        terminated(tag(","), token_sep0),
652                        terminated(modus_term, token_sep0),
653                    ),
654                    cut(terminated(tag(")"), token_sep0)),
655                )),
656            )),
657            |(spanned_pos, (name, args))| Operator {
658                position: Some(spanned_pos),
659                predicate: Predicate(name.fragment().to_string()),
660                args: args.unwrap_or(Vec::new()),
661            },
662        )(i)
663    }
664
665    fn expression_inner(i: Span) -> IResult<Span, Expression> {
666        let unification_expr_parser = map(unification_sugar, Expression::Literal);
667        // These inner expression parsers can fully recurse.
668        let op_application_parser = map(
669            pair(
670                alt((modus_literal, parenthesized_expr)),
671                // :: separated list of operators
672                many1(recognized_span(preceded(
673                    delimited(token_sep0, tag("::"), token_sep0),
674                    cut(operator),
675                ))),
676            ),
677            |(expr, ops_with_span)| {
678                ops_with_span.into_iter().fold(expr, |acc, (span, op)| {
679                    // span for an op application is span for the expression and operator(s)
680                    let new_span: Option<SpannedPosition> = acc
681                        .get_spanned_position()
682                        .as_ref()
683                        .map(|s| SpannedPosition {
684                            offset: s.offset,
685                            length: span.offset + span.length - s.offset,
686                        });
687                    Expression::OperatorApplication(new_span, Box::new(acc), op)
688                })
689            },
690        );
691        alt((
692            unification_expr_parser,
693            op_application_parser,
694            modus_literal,
695            parenthesized_expr,
696        ))(i)
697    }
698
699    pub fn body(i: Span) -> IResult<Span, Expression> {
700        let comma_separated_exprs = map(
701            separated_list1(delimited(comments, tag(","), comments), expression_inner),
702            |es| {
703                es.into_iter()
704                    .reduce(|e1, e2| {
705                        // this was just parsed from source code, so it should have a position
706                        let s1 = e1.get_spanned_position().as_ref().unwrap();
707                        let s2 = e2.get_spanned_position().as_ref().unwrap();
708
709                        // This span should include the comma/spacing/etc between e1/e2
710                        let computed_span = SpannedPosition {
711                            offset: s1.offset,
712                            length: s2.offset + s2.length - s1.offset,
713                        };
714                        Expression::And(Some(computed_span), true, Box::new(e1), Box::new(e2))
715                    })
716                    .expect("Converting list to expression pairs.")
717            },
718        );
719        let semi_separated_exprs = map(
720            separated_list1(
721                delimited(comments, tag(";"), comments),
722                comma_separated_exprs,
723            ),
724            |es| {
725                es.into_iter()
726                    .reduce(|e1, e2| {
727                        let s1 = e1.get_spanned_position().as_ref().unwrap();
728                        let s2 = e2.get_spanned_position().as_ref().unwrap();
729
730                        let computed_span = SpannedPosition {
731                            offset: s1.offset,
732                            length: s2.offset + s2.length - s1.offset,
733                        };
734                        Expression::Or(Some(computed_span), true, Box::new(e1), Box::new(e2))
735                    })
736                    .expect("Converting list to expression pairs.")
737            },
738        );
739        // Parses the body as a semicolon separated list of comma separated inner expressions.
740        // This resolves ambiguity by making commas/and higher precedence.
741        preceded(comments, semi_separated_exprs)(i)
742    }
743
744    fn fact(i: Span) -> IResult<Span, ModusClause> {
745        // Custom definition of fact since datalog facts are normally "head :- ", but Moduslog
746        // defines it as "head."
747        context(
748            stringify!(fact),
749            map(
750                terminated(
751                    head,
752                    // NOTE: this is a failure ('cut') assuming the rule parser failed,
753                    // however if this is tried *before* the rule parser, this shouldn't be a
754                    // failure. This is just one of the subtleties of a parser combinator.
755                    cut(terminated(nom::character::complete::char('.'), token_sep0)),
756                ),
757                |h| ModusClause {
758                    head: h,
759                    body: None,
760                },
761            ),
762        )(i)
763    }
764
765    fn rule(i: Span) -> IResult<Span, ModusClause> {
766        context(
767            stringify!(rule),
768            map(
769                separated_pair(
770                    head,
771                    delimited(token_sep0, tag(":-"), token_sep0),
772                    context(
773                        "rule_body",
774                        cut(terminated(
775                            body,
776                            terminated(nom::character::complete::char('.'), token_sep0),
777                        )),
778                    ),
779                ),
780                |(head, body)| ModusClause {
781                    head,
782                    body: Some(body),
783                },
784            ),
785        )(i)
786    }
787
788    /// Processes the given string, converting escape substrings into the proper characters.
789    ///
790    /// This also supports string continuation, This allows users to write strings like: "Hello, \
791    ///                                                                                   World!"
792    /// which is actually just "Hello, World!".
793    pub fn process_raw_string(s: &str) -> String {
794        let mut processed = String::new();
795
796        let mut chars = s.chars().peekable();
797        while let Some(c) = chars.next() {
798            if c == '\\' {
799                match chars.next() {
800                    Some('"') => processed.push('"'),
801                    Some('\\') => processed.push('\\'),
802                    Some('n') => processed.push('\n'),
803                    Some('r') => processed.push('\r'),
804                    Some('t') => processed.push('\t'),
805                    Some('0') => processed.push('\0'),
806                    Some('\n') => {
807                        // string continuation so we'll ignore whitespace till we get to a non-whitespace.
808                        while let Some(c) = chars.peek() {
809                            if !c.is_whitespace() {
810                                break;
811                            }
812                            chars.next();
813                        }
814                    }
815                    Some(c) => {
816                        // leave it unchanged if we don't recognize the escape char
817                        processed.push('\\');
818                        processed.push(c);
819                    }
820                    None => panic!("given string ends with an escape character"),
821                }
822            } else {
823                processed.push(c);
824            }
825        }
826        processed
827    }
828
829    const STRING_ESCAPE_CHARS: &str = "\"\\nrt0\n";
830    const FORMAT_STRING_ESCAPE_CHARS: &str = "$\"\\nrt0\n";
831
832    /// Parses a string that possibly contains escaped characters, but doesn't actually
833    /// convert the escape characters.
834    fn string_content(i: Span) -> IResult<Span, String> {
835        let escape_parser = escaped(none_of("\\\""), '\\', one_of(STRING_ESCAPE_CHARS));
836        let (i, o) = opt(escape_parser)(i)?;
837        let parsed_str: &str = o.map(|span| *span.fragment()).unwrap_or("");
838        Ok((i, parsed_str.to_owned()))
839    }
840
841    /// Parses a non-empty substring outside of the interpolation part of a format string.
842    fn format_string_content(i: Span) -> IResult<Span, String> {
843        // This assumes that the escape char parser is only called after a control character, otherwise
844        // the 'cut' may not be valid.
845        // Could replace `one_of` with a bunch of alts if it's important to print the expected chars,
846        // ideally `one_of` would just have a better error type.
847        let (i, o) = escaped(
848            none_of("\\\"$"),
849            '\\',
850            cut(one_of(FORMAT_STRING_ESCAPE_CHARS)),
851        )(i)?;
852        let parsed_str: &str = o.fragment();
853        Ok((i, parsed_str.to_owned()))
854    }
855
856    pub fn modus_const(i: Span) -> IResult<Span, String> {
857        context(
858            stringify!(modus_const),
859            delimited(tag("\""), string_content, cut(tag("\""))),
860        )(i)
861    }
862
863    fn format_string_fragment(i: Span) -> IResult<Span, FormatStringFragment> {
864        let interpolation = delimited(
865            terminated(tag("${"), token_sep0),
866            cut(modus_var),
867            cut(preceded(token_sep0, tag("}"))),
868        );
869
870        alt((
871            map(interpolation, |v_span| {
872                FormatStringFragment::InterpolatedVariable(
873                    v_span.into(),
874                    v_span.fragment().to_string(),
875                )
876            }),
877            // this may bloat the fragment list but likely worth the convenience of
878            // using '$' without escaping it
879            map(tag("$"), |span: Span| {
880                FormatStringFragment::StringContent(span.into(), span.fragment().to_string())
881            }),
882            map(recognized_span(format_string_content), |(span, content)| {
883                FormatStringFragment::StringContent(span, content)
884            }),
885        ))(i)
886    }
887
888    pub fn modus_format_string(
889        i: Span,
890    ) -> IResult<Span, (SpannedPosition, Vec<FormatStringFragment>)> {
891        context(
892            stringify!(modus_format_string),
893            recognized_span(delimited(
894                tag("f\""),
895                cut(many0(format_string_fragment)),
896                cut(tag("\"")),
897            )),
898        )(i)
899    }
900
901    pub fn variable_identifier(i: Span) -> IResult<Span, Span> {
902        literal_identifier(i)
903    }
904
905    fn modus_var(i: Span) -> IResult<Span, Span> {
906        context(stringify!(modus_var), variable_identifier)(i)
907    }
908
909    pub fn string_interpolation(i: Span) -> IResult<Span, Span> {
910        delimited(
911            terminated(tag("${"), token_sep0),
912            modus_var,
913            cut(preceded(token_sep0, tag("}"))),
914        )(i)
915    }
916
917    fn modus_array_term(i: Span) -> IResult<Span, Vec<ModusTerm>> {
918        delimited(
919            terminated(tag("["), token_sep0),
920            separated_list0(delimited(token_sep0, tag(","), token_sep0), modus_term),
921            cut(preceded(token_sep0, tag("]"))),
922        )(i)
923    }
924
925    pub fn modus_term(i: Span) -> IResult<Span, ModusTerm> {
926        alt((
927            map(modus_const, ModusTerm::Constant),
928            map(recognized_span(modus_array_term), |(span, terms)| {
929                ModusTerm::Array(span, terms)
930            }),
931            map(modus_format_string, |(position, fragments)| {
932                ModusTerm::FormatString {
933                    position,
934                    fragments,
935                }
936            }),
937            map(is_a("_"), |_| ModusTerm::AnonymousVariable),
938            map(modus_var, |s| {
939                ModusTerm::UserVariable(s.fragment().to_string())
940            }),
941        ))(i)
942    }
943
944    pub fn modus_clause(i: Span) -> IResult<Span, ModusClause> {
945        alt((rule, fact))(i)
946    }
947
948    pub fn modusfile(i: Span) -> IResult<Span, Modusfile> {
949        map(
950            terminated(
951                many0(preceded(token_sep0, modus_clause)),
952                terminated(token_sep0, eof),
953            ),
954            Modusfile,
955        )(i)
956    }
957}
958
959#[cfg(test)]
960mod tests {
961    use rand::Rng;
962    use serial_test::serial;
963
964    use crate::modusfile::parser::modus_term;
965
966    use super::*;
967
968    type Rule = ModusClause;
969
970    #[test]
971    fn fact() {
972        let l1 = Literal {
973            positive: true,
974            position: None,
975            predicate: logic::Predicate("l1".into()),
976            args: Vec::new(),
977        };
978        let c = ModusClause {
979            head: l1,
980            body: None,
981        };
982
983        assert_eq!("l1.", c.to_string());
984
985        let actual: ModusClause = "l1.".parse().unwrap();
986        assert!(c.eq_ignoring_position(&actual));
987    }
988
989    #[test]
990    fn rule() {
991        let l1 = Literal {
992            positive: true,
993            position: None,
994            predicate: logic::Predicate("l1".into()),
995            args: Vec::new(),
996        };
997        let l2 = Literal {
998            positive: true,
999            position: None,
1000            predicate: logic::Predicate("l2".into()),
1001            args: Vec::new(),
1002        };
1003        let l3 = Literal {
1004            positive: true,
1005            position: None,
1006            predicate: logic::Predicate("l3".into()),
1007            args: Vec::new(),
1008        };
1009        let c = Rule {
1010            head: l1,
1011            body: Expression::And(None, true, Box::new(l2.into()), Box::new(l3.into())).into(),
1012        };
1013
1014        assert_eq!("l1 :- (l2, l3).", c.to_string());
1015
1016        let actual1: Rule = "l1 :- l2, l3.".parse().unwrap();
1017        assert!(c.eq_ignoring_position(&actual1));
1018        let actual2: Rule = "l1 :- l2,\n\tl3.".parse().unwrap();
1019        assert!(c.eq_ignoring_position(&actual2));
1020    }
1021
1022    #[test]
1023    fn rule_with_or() {
1024        let l1: Literal = "l1".parse().unwrap();
1025        let l2: Literal = "l2".parse().unwrap();
1026        let c = Rule {
1027            head: "foo".parse().unwrap(),
1028            body: Expression::Or(None, true, Box::new(l1.into()), Box::new(l2.into())).into(),
1029        };
1030
1031        assert_eq!("foo :- (l1; l2).", c.to_string());
1032
1033        let actual: Rule = "foo :- l1; l2.".parse().unwrap();
1034        assert!(c.eq_ignoring_position(&actual));
1035    }
1036
1037    #[test]
1038    fn rule_with_operator() {
1039        let foo = Literal {
1040            positive: true,
1041            position: None,
1042            predicate: logic::Predicate("foo".into()),
1043            args: Vec::new(),
1044        };
1045        let a = Literal {
1046            positive: true,
1047            position: None,
1048            predicate: logic::Predicate("a".into()),
1049            args: Vec::new(),
1050        };
1051        let b = Literal {
1052            positive: true,
1053            position: None,
1054            predicate: logic::Predicate("b".into()),
1055            args: Vec::new(),
1056        };
1057        let merge = Operator {
1058            position: None,
1059            predicate: logic::Predicate("merge".into()),
1060            args: Vec::new(),
1061        };
1062        let r1 = Rule {
1063            head: foo.clone(),
1064            body: Expression::OperatorApplication(
1065                None,
1066                Expression::And(None, true, Box::new(a.clone().into()), Box::new(b.into())).into(),
1067                merge.clone(),
1068            )
1069            .into(),
1070        };
1071        let r2 = Rule {
1072            head: foo,
1073            body: Expression::OperatorApplication(None, Box::new(Expression::Literal(a)), merge)
1074                .into(),
1075        };
1076
1077        assert_eq!("foo :- ((a, b))::merge.", r1.to_string());
1078        let actual1 = "foo :- ((a, b))::merge.".parse().unwrap();
1079        assert!(r1.eq_ignoring_position(&actual1));
1080        let actual2 = "foo :- (a, b)::merge.".parse().unwrap();
1081        assert!(r1.eq_ignoring_position(&actual2));
1082
1083        assert_eq!("foo :- (a)::merge.", r2.to_string());
1084        let actual3 = "foo :- a::merge.".parse().unwrap();
1085        assert!(r2.eq_ignoring_position(&actual3));
1086        let actual3 = "foo :- ( a )::merge.".parse().unwrap();
1087        assert!(r2.eq_ignoring_position(&actual3));
1088    }
1089
1090    #[test]
1091    #[serial]
1092    fn modusclause_to_clause() {
1093        crate::translate::reset_operator_pair_id();
1094        let foo = Literal {
1095            positive: true,
1096            position: None,
1097            predicate: logic::Predicate("foo".into()),
1098            args: Vec::new(),
1099        };
1100        let a = Literal {
1101            positive: true,
1102            position: None,
1103            predicate: logic::Predicate("a".into()),
1104            args: Vec::new(),
1105        };
1106        let b = Literal {
1107            positive: true,
1108            position: None,
1109            predicate: logic::Predicate("b".into()),
1110            args: Vec::new(),
1111        };
1112        let merge = Operator {
1113            position: None,
1114            predicate: logic::Predicate("merge".into()),
1115            args: Vec::new(),
1116        };
1117        let r = Rule {
1118            head: foo,
1119            body: Expression::OperatorApplication(
1120                None,
1121                Expression::And(None, true, Box::new(a.into()), Box::new(b.into())).into(),
1122                merge,
1123            )
1124            .into(),
1125        };
1126        assert_eq!("foo :- ((a, b))::merge.", r.to_string());
1127
1128        // Convert to the simpler syntax
1129        let c: Vec<logic::Clause> = (&r).into();
1130        assert_eq!(1, c.len());
1131        assert_eq!(
1132            r#"foo :- _operator_merge_begin("0"), a, b, _operator_merge_end("0")"#,
1133            c[0].to_string()
1134        );
1135    }
1136
1137    #[test]
1138    #[serial]
1139    fn modusclause_to_clause_with_or() {
1140        crate::translate::reset_operator_pair_id();
1141        let foo: Literal = "foo".parse().unwrap();
1142        let a: Literal = "a".parse().unwrap();
1143        let b: Literal = "b".parse().unwrap();
1144        let merge = Operator {
1145            position: None,
1146            predicate: logic::Predicate("merge".into()),
1147            args: Vec::new(),
1148        };
1149        let r1 = Rule {
1150            head: foo.clone(),
1151            body: Expression::OperatorApplication(
1152                None,
1153                Expression::Or(
1154                    None,
1155                    true,
1156                    Box::new(a.clone().into()),
1157                    Box::new(b.clone().into()),
1158                )
1159                .into(),
1160                merge,
1161            )
1162            .into(),
1163        };
1164        let r2 = Rule {
1165            head: foo.clone(),
1166            body: Expression::And(
1167                None,
1168                true,
1169                Box::new(a.clone().into()),
1170                Box::new(Expression::And(
1171                    None,
1172                    true,
1173                    Box::new(b.clone().into()),
1174                    Box::new(Expression::Or(
1175                        None,
1176                        true,
1177                        Box::new(a.clone().into()),
1178                        Box::new(b.clone().into()),
1179                    )),
1180                )),
1181            )
1182            .into(),
1183        };
1184        assert_eq!("foo :- ((a; b))::merge.", r1.to_string());
1185        assert_eq!("foo :- (a, (b, (a; b))).", r2.to_string());
1186
1187        let c1: Vec<logic::Clause> = (&r1).into();
1188        assert_eq!(2, c1.len());
1189        assert_eq!(
1190            r#"foo :- _operator_merge_begin("0"), a, _operator_merge_end("0")"#,
1191            c1[0].to_string()
1192        );
1193        assert_eq!(
1194            r#"foo :- _operator_merge_begin("1"), b, _operator_merge_end("1")"#,
1195            c1[1].to_string()
1196        );
1197
1198        let c2: Vec<logic::Clause> = (&r2).into();
1199        assert_eq!(2, c2.len());
1200        assert_eq!("foo :- a, b, a", c2[0].to_string());
1201        assert_eq!("foo :- a, b, b", c2[1].to_string());
1202    }
1203
1204    #[test]
1205    fn modus_constant() {
1206        // Could use https://crates.io/crates/test_case if this pattern occurs often
1207        let inp1 = r#""Hello\nWorld""#;
1208        let inp2 = r#""Tabs\tare\tbetter\tthan\tspaces""#;
1209        let inp3 = r#""Testing \
1210                       multiline.""#;
1211        let (_, s1) = parser::modus_const(Span::new(inp1.into())).unwrap();
1212        let (_, s2) = parser::modus_const(Span::new(inp2.into())).unwrap();
1213        let (_, s3) = parser::modus_const(Span::new(inp3.into())).unwrap();
1214
1215        assert_eq!(s1, r#"Hello\nWorld"#);
1216        assert_eq!(s2, r#"Tabs\tare\tbetter\tthan\tspaces"#);
1217        assert_eq!(
1218            s3,
1219            r#"Testing \
1220                       multiline."#
1221        );
1222    }
1223
1224    #[test]
1225    fn anonymous_variables() {
1226        let expected = Literal {
1227            positive: true,
1228            position: None,
1229            predicate: Predicate("l".into()),
1230            args: vec![
1231                ModusTerm::Constant("foo".to_string()),
1232                ModusTerm::AnonymousVariable,
1233            ],
1234        };
1235        let actual: Literal = "l(\"foo\", _)".parse().unwrap();
1236        assert!(expected.eq_ignoring_position(&actual));
1237    }
1238
1239    #[test]
1240    fn modus_expression() {
1241        let a: Literal = "a".parse().unwrap();
1242        let b: Literal = "b".parse().unwrap();
1243        let c: Literal = "c".parse().unwrap();
1244        let d: Literal = "d".parse().unwrap();
1245
1246        let e1 = Expression::And(
1247            None,
1248            true,
1249            Expression::Literal(a).into(),
1250            Expression::Literal(b).into(),
1251        );
1252        let e2 = Expression::And(
1253            None,
1254            true,
1255            Expression::Literal(c).into(),
1256            Expression::Literal(d).into(),
1257        );
1258
1259        let expr = Expression::Or(None, false, e1.into(), e2.into());
1260
1261        let expr_str = "!((a, b); (c, d))";
1262        assert_eq!(expr_str, expr.to_string());
1263
1264        let rule: ModusClause = format!("foo :- {}.", expr_str).parse().unwrap();
1265        assert!(expr.eq_ignoring_position(&rule.body.unwrap()))
1266    }
1267
1268    #[test]
1269    fn modus_unification() {
1270        let modus_clause: ModusClause = "foo(X, Y, A, B) :- X = Y, A != B.".parse().unwrap();
1271        let expected_body = Expression::And(
1272            None,
1273            true,
1274            Box::new(Expression::Literal("string_eq(X, Y)".parse().unwrap())),
1275            Box::new(Expression::Literal("!string_eq(A, B)".parse().unwrap())),
1276        );
1277        assert!(expected_body.eq_ignoring_position(&modus_clause.body.unwrap()));
1278    }
1279
1280    #[test]
1281    fn modus_negated_unification() {
1282        let inp = "foo(X, Y) :- X != Y.";
1283        let expected_lit: Literal = "!string_eq(X, Y)".parse().unwrap();
1284        let actual: Expression = inp.parse().map(|r: ModusClause| r.body).unwrap().unwrap();
1285        assert!(Expression::Literal(expected_lit).eq_ignoring_position(&actual));
1286    }
1287
1288    #[test]
1289    fn multiple_clause_with_different_ops() {
1290        let foo = Literal {
1291            positive: true,
1292            position: None,
1293            predicate: logic::Predicate("foo".into()),
1294            args: vec![ModusTerm::UserVariable("x".to_owned())],
1295        };
1296        let bar = Literal {
1297            positive: true,
1298            position: None,
1299            predicate: logic::Predicate("bar".into()),
1300            args: Vec::new(),
1301        };
1302        let baz = Literal {
1303            positive: true,
1304            position: None,
1305            predicate: logic::Predicate("baz".into()),
1306            args: Vec::new(),
1307        };
1308        let a = Rule {
1309            head: logic::Literal {
1310                positive: true,
1311                position: None,
1312                predicate: logic::Predicate("a".to_owned()),
1313                args: vec![],
1314            },
1315            body: Some(Expression::And(
1316                None,
1317                true,
1318                Box::new(Expression::OperatorApplication(
1319                    None,
1320                    Box::new(Expression::And(
1321                        None,
1322                        true,
1323                        Box::new(foo.into()),
1324                        Box::new(bar.into()),
1325                    )),
1326                    Operator {
1327                        position: None,
1328                        predicate: logic::Predicate("setenv".into()),
1329                        args: vec![
1330                            ModusTerm::Constant("a".to_owned()),
1331                            ModusTerm::Constant("foobar".to_owned()),
1332                        ],
1333                    },
1334                )),
1335                Box::new(Expression::OperatorApplication(
1336                    None,
1337                    Box::new(baz.into()),
1338                    Operator {
1339                        position: None,
1340                        predicate: logic::Predicate("setenv".into()),
1341                        args: vec![
1342                            ModusTerm::Constant("a".to_owned()),
1343                            ModusTerm::Constant("baz".to_owned()),
1344                        ],
1345                    },
1346                )),
1347            )),
1348        };
1349
1350        let actual: Rule = r#"a:-(foo(x),bar)::setenv("a","foobar"), (baz)::setenv("a", "baz")."#
1351            .parse()
1352            .unwrap();
1353        assert!(a.eq_ignoring_position(&actual));
1354    }
1355
1356    #[test]
1357    fn op_application_chained_with_spaces() {
1358        let r1: Rule = "a :- foo::set_env::in_env.".parse().unwrap();
1359        let r2: Rule = "a :- foo :: set_env :: in_env.".parse().unwrap();
1360        let r3: Rule = "a :- foo\n::\nset_env\n::\nin_env.".parse().unwrap();
1361
1362        let expected = Rule {
1363            head: logic::Literal {
1364                positive: true,
1365                position: None,
1366                predicate: logic::Predicate("a".to_owned()),
1367                args: vec![],
1368            },
1369            body: Some(Expression::OperatorApplication(
1370                None,
1371                Box::new(Expression::OperatorApplication(
1372                    None,
1373                    Box::new(Expression::Literal(logic::Literal {
1374                        positive: true,
1375                        position: None,
1376                        predicate: logic::Predicate("foo".into()),
1377                        args: Vec::new(),
1378                    })),
1379                    Operator {
1380                        position: None,
1381                        predicate: logic::Predicate("set_env".into()),
1382                        args: Vec::new(),
1383                    },
1384                )),
1385                Operator {
1386                    position: None,
1387                    predicate: logic::Predicate("in_env".into()),
1388                    args: Vec::new(),
1389                },
1390            )),
1391        };
1392
1393        assert!(expected.eq_ignoring_position(&r1));
1394        assert!(expected.eq_ignoring_position(&r2));
1395        assert!(expected.eq_ignoring_position(&r3));
1396    }
1397
1398    #[test]
1399    fn test_spaces() {
1400        fn add_spaces(tokens: &[&str], spaces: &[&str]) -> String {
1401            let mut s = String::new();
1402            let mut rng = rand::thread_rng();
1403            for token in tokens {
1404                if token.is_empty() {
1405                    continue;
1406                }
1407                if spaces.len() == 1 {
1408                    s.push_str(spaces[0]);
1409                } else {
1410                    s.push_str(spaces[rng.gen_range(0..spaces.len())]);
1411                }
1412                s.push_str(token.trim());
1413            }
1414            s
1415        }
1416
1417        fn do_test(lines: &str) {
1418            let tokens = lines.lines().collect::<Vec<_>>();
1419            fn should_parse(s: &str) {
1420                if let Err(e) = s.parse::<Modusfile>() {
1421                    panic!(
1422                        "Failed to parse: Error:\n{e:?}\nModusfile:\n{s}",
1423                        e = e,
1424                        s = s
1425                    );
1426                }
1427            }
1428            should_parse(&add_spaces(&tokens, &[""]));
1429            should_parse(&add_spaces(&tokens, &[" "]));
1430            should_parse(&add_spaces(&tokens, &["\n"]));
1431            should_parse(&add_spaces(&tokens, &["# Comment\n"]));
1432            should_parse(&add_spaces(&tokens, &["\n# Comment\n"]));
1433            should_parse(&add_spaces(&tokens, &["", " ", "\n", "\n# Comment\n"]));
1434        }
1435
1436        do_test(
1437            r#"
1438            final
1439            :-
1440            from
1441            (
1442            "alpine"
1443            )
1444            .
1445        "#,
1446        );
1447
1448        do_test(
1449            r#"
1450            final
1451            :-
1452            from
1453            (
1454            "alpine"
1455            )
1456            ::
1457            set_workdir
1458            (
1459            "/tmp"
1460            )
1461            ,
1462            run
1463            (
1464            "pwd"
1465            )
1466            .
1467        "#,
1468        );
1469
1470        do_test(
1471            r#"
1472            final
1473            :-
1474            a
1475            ,
1476            b
1477            ,
1478            c
1479            ;
1480            d
1481            ,
1482            (
1483            e
1484            ,
1485            f
1486            (
1487            V1
1488            ,
1489            V2
1490            )
1491            ,
1492            V1
1493            =
1494            V2
1495            )
1496            ;
1497            g
1498            ,
1499            h
1500            ::i
1501            ,
1502            j
1503            .
1504        "#,
1505        )
1506    }
1507
1508    #[test]
1509    fn reports_error_in_rule() {
1510        let modus_file: Result<Modusfile, Vec<Diagnostic<()>>> =
1511            "foo(X) :- bar(X), baz(X), .".parse();
1512        assert!(modus_file.is_err());
1513
1514        let diags = modus_file.err().unwrap();
1515        assert_eq!(1, diags.len());
1516        assert_eq!(
1517            diags[0].severity,
1518            codespan_reporting::diagnostic::Severity::Error
1519        );
1520        assert!(diags[0].labels[1].message.contains("body"));
1521        assert!(diags[0].labels[2].message.contains("rule"));
1522    }
1523
1524    #[test]
1525    fn format_string() {
1526        let case = "f\"foo ${ X }\"";
1527
1528        let expected: ModusTerm = ModusTerm::FormatString {
1529            position: SpannedPosition {
1530                offset: 0,
1531                length: 10 + 3,
1532            },
1533            fragments: vec![
1534                FormatStringFragment::StringContent(
1535                    SpannedPosition {
1536                        offset: 2,
1537                        length: 4,
1538                    },
1539                    "foo ".to_string(),
1540                ),
1541                FormatStringFragment::InterpolatedVariable(
1542                    SpannedPosition {
1543                        offset: 9,
1544                        length: 1,
1545                    },
1546                    "X".to_string(),
1547                ),
1548            ],
1549        };
1550
1551        assert_eq!(expected, modus_term(Span::new(case)).unwrap().1);
1552    }
1553
1554    #[test]
1555    fn format_string_starts_with_variable() {
1556        let case = r#"f"${var1} foo bar\tbaz""#;
1557
1558        let expected = ModusTerm::FormatString {
1559            position: SpannedPosition {
1560                offset: 0,
1561                length: 20 + 3,
1562            },
1563            fragments: vec![
1564                FormatStringFragment::InterpolatedVariable(
1565                    SpannedPosition {
1566                        offset: 4,
1567                        length: 4,
1568                    },
1569                    "var1".to_string(),
1570                ),
1571                FormatStringFragment::StringContent(
1572                    SpannedPosition {
1573                        offset: 9,
1574                        length: 13,
1575                    },
1576                    r#" foo bar\tbaz"#.to_string(),
1577                ),
1578            ],
1579        };
1580
1581        assert_eq!(expected, modus_term(Span::new(case)).unwrap().1);
1582    }
1583
1584    #[test]
1585    fn empty_format_string() {
1586        let case = "f\"\"";
1587
1588        let expected: ModusTerm = ModusTerm::FormatString {
1589            position: SpannedPosition {
1590                offset: 0,
1591                length: 0 + 3,
1592            },
1593            fragments: vec![],
1594        };
1595
1596        assert_eq!(expected, modus_term(Span::new(case)).unwrap().1);
1597    }
1598
1599    #[test]
1600    fn format_string_errors_with_multiple_vars() {
1601        let case = "f\"bar ${X X2}\"";
1602
1603        let actual = modus_term(Span::new(case));
1604        assert!(actual.is_err());
1605    }
1606
1607    #[test]
1608    fn format_string_dollar_without_interpolation() {
1609        let case = "f\"bar $ baz\"";
1610
1611        let expected = ModusTerm::FormatString {
1612            position: SpannedPosition {
1613                offset: 0,
1614                length: 9 + 3,
1615            },
1616            fragments: vec![
1617                FormatStringFragment::StringContent(
1618                    SpannedPosition {
1619                        offset: 2,
1620                        length: 4,
1621                    },
1622                    r#"bar "#.to_string(),
1623                ),
1624                FormatStringFragment::StringContent(
1625                    SpannedPosition {
1626                        offset: 6,
1627                        length: 1,
1628                    },
1629                    "$".to_string(),
1630                ),
1631                FormatStringFragment::StringContent(
1632                    SpannedPosition {
1633                        offset: 7,
1634                        length: 4,
1635                    },
1636                    " baz".to_string(),
1637                ),
1638            ],
1639        };
1640
1641        assert_eq!(expected, modus_term(Span::new(case)).unwrap().1);
1642    }
1643
1644    #[test]
1645    fn f_string_escaped_interpolation() {
1646        let case = r#"f"\${var} foo""#;
1647
1648        let expected = ModusTerm::FormatString {
1649            position: SpannedPosition {
1650                offset: 0,
1651                length: 11 + 3,
1652            },
1653            fragments: vec![FormatStringFragment::StringContent(
1654                SpannedPosition {
1655                    offset: 2,
1656                    length: 11,
1657                },
1658                r#"\${var} foo"#.to_string(),
1659            )],
1660        };
1661
1662        assert_eq!(expected, modus_term(Span::new(case)).unwrap().1);
1663    }
1664
1665    #[test]
1666    fn modus_empty_array_term() {
1667        let case = "[]";
1668        let expected = ModusTerm::Array(
1669            SpannedPosition {
1670                offset: 0,
1671                length: 2,
1672            },
1673            Vec::new(),
1674        );
1675        assert_eq!(expected, modus_term(Span::new(case)).unwrap().1);
1676    }
1677
1678    #[test]
1679    fn modus_simple_array_term() {
1680        let case = r#"[ "javac", javacParam ]"#;
1681        let expected = ModusTerm::Array(
1682            SpannedPosition {
1683                offset: 0,
1684                length: 23,
1685            },
1686            vec![
1687                ModusTerm::Constant("javac".to_string()),
1688                ModusTerm::UserVariable("javacParam".to_string()),
1689            ],
1690        );
1691        assert_eq!(expected, modus_term(Span::new(case)).unwrap().1);
1692    }
1693}