modus_lib/
logic.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
17//! This module contains logical structures that define the intermediate language used by Modus.
18//!
19//! Currently, these structures are generic, parameterized over the types they may use for constants
20//! or variables.
21
22use nom::character::complete::multispace0;
23use nom_locate::LocatedSpan;
24
25use crate::analysis::Kind;
26use crate::logic::parser::Span;
27use crate::sld;
28use crate::unification::Rename;
29
30use std::convert::TryInto;
31use std::fmt;
32use std::fmt::Debug;
33use std::ops::Range;
34use std::str;
35use std::sync::atomic::{AtomicU32, Ordering};
36use std::{collections::HashSet, hash::Hash};
37
38impl fmt::Display for IRTerm {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            IRTerm::Constant(s) => write!(f, "\"{}\"", s),
42            IRTerm::UserVariable(s) => write!(f, "{}", s),
43            IRTerm::Array(ts) => write!(
44                f,
45                "[{}]",
46                ts.iter()
47                    .map(|x| x.to_string())
48                    .collect::<Vec<_>>()
49                    .join(", ")
50            ),
51            // there may be aux variables after translating to IR
52            IRTerm::AuxiliaryVariable(i) => write!(f, "__AUX_{}", i),
53            IRTerm::RenamedVariable(i, t) => write!(f, "{}_{}", t, i),
54            IRTerm::AnonymousVariable(_) => write!(f, "_"),
55        }
56    }
57}
58
59pub static AVAILABLE_VARIABLE_INDEX: AtomicU32 = AtomicU32::new(0);
60
61impl Rename<IRTerm> for IRTerm {
62    fn rename(&self) -> IRTerm {
63        match self {
64            IRTerm::Constant(_) => (*self).clone(),
65            IRTerm::Array(ts) => IRTerm::Array(ts.iter().map(|t| t.rename()).collect()),
66            _ => {
67                let index = AVAILABLE_VARIABLE_INDEX.fetch_add(1, Ordering::SeqCst);
68                IRTerm::RenamedVariable(index, Box::new((*self).clone()))
69            }
70        }
71    }
72}
73
74impl sld::Auxiliary for IRTerm {
75    fn aux(anonymous: bool) -> IRTerm {
76        let index = AVAILABLE_VARIABLE_INDEX.fetch_add(1, Ordering::SeqCst);
77        if anonymous {
78            IRTerm::AnonymousVariable(index)
79        } else {
80            IRTerm::AuxiliaryVariable(index)
81        }
82    }
83}
84
85/// A predicate symbol
86#[derive(Clone, PartialEq, Eq, Hash, Debug)]
87pub struct Predicate(pub String);
88
89impl Predicate {
90    /// True if this predicate symbol represents an operator.
91    pub fn is_operator(&self) -> bool {
92        self.0.starts_with("_operator_")
93    }
94
95    /// Unmangles the name if it's an operator.
96    pub fn unmangle(self) -> Predicate {
97        if self.is_operator() {
98            Predicate(
99                self.0
100                    .trim_start_matches("_operator_")
101                    .trim_end_matches("_begin")
102                    .trim_end_matches("_end")
103                    .to_string(),
104            )
105        } else {
106            self
107        }
108    }
109
110    /// Returns the kind based on this predicate name.
111    /// May not be the true kind in an actual Modus program.
112    pub fn naive_predicate_kind(&self) -> Kind {
113        match self.0.as_str() {
114            "from" => Kind::Image,
115            "run" | "copy" => Kind::Layer,
116            _ => Kind::Logic,
117        }
118    }
119}
120
121impl From<String> for Predicate {
122    fn from(s: String) -> Self {
123        Predicate(s)
124    }
125}
126
127#[derive(Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
128pub enum IRTerm {
129    Constant(String),
130    UserVariable(String),
131    Array(Vec<IRTerm>),
132
133    /// Primarily used to establish f-string constraints.
134    AuxiliaryVariable(u32),
135
136    RenamedVariable(u32, Box<IRTerm>),
137
138    /// It should be safe to assume that a given AnonymousVariable(i) will not appear
139    /// again in the AST.
140    AnonymousVariable(u32),
141}
142
143impl IRTerm {
144    /// Returns `true` if the IRTerm is [`Constant`].
145    ///
146    /// [`Constant`]: IRTerm::Constant
147    pub fn is_constant(&self) -> bool {
148        matches!(self, Self::Constant(..))
149    }
150
151    pub fn is_constant_or_compound_constant(&self) -> bool {
152        match self {
153            Self::Constant(_) => true,
154            Self::Array(ts) => ts.iter().all(|t| t.is_constant_or_compound_constant()),
155            _ => false,
156        }
157    }
158
159    /// Returns `true` if the IRTerm is [`AnonymousVariable`] or it was
160    /// renamed from an [`AnonymousVariable`].
161    ///
162    /// [`AnonymousVariable`]: IRTerm::AnonymousVariable
163    pub fn is_underlying_anonymous_variable(&self) -> bool {
164        match self {
165            Self::AnonymousVariable(_) => true,
166            Self::RenamedVariable(_, t) => t.is_underlying_anonymous_variable(),
167            _ => false,
168        }
169    }
170
171    pub fn as_constant(&self) -> Option<&str> {
172        match self {
173            IRTerm::Constant(c) => Some(&c[..]),
174            _ => None,
175        }
176    }
177
178    /// Gets the original IRTerm from a renamed one, or returns itself.
179    pub fn get_original(&self) -> &IRTerm {
180        match self {
181            IRTerm::RenamedVariable(_, t) => t.get_original(),
182            t => t,
183        }
184    }
185
186    /// Returns `true` if the irterm is [`AnonymousVariable`].
187    ///
188    /// [`AnonymousVariable`]: IRTerm::AnonymousVariable
189    pub fn is_anonymous_variable(&self) -> bool {
190        matches!(self, Self::AnonymousVariable(..))
191    }
192}
193
194/// Structure that holds information about the position of some section of the source code.
195///
196/// Not to be confused with `parser::Span`.
197#[derive(Clone, PartialEq, Eq, Hash, Debug)]
198pub struct SpannedPosition {
199    /// The relative offset of this spanned position from the original input.
200    pub offset: usize,
201
202    /// Length of this spanned position. Assumes ASCII text (i.e. each character is a byte).
203    pub length: usize,
204}
205
206impl From<&SpannedPosition> for Range<usize> {
207    fn from(s: &SpannedPosition) -> Self {
208        s.offset..(s.offset + s.length)
209    }
210}
211
212impl From<Span<'_>> for SpannedPosition {
213    fn from(s: Span) -> Self {
214        SpannedPosition {
215            length: s.fragment().len(),
216            offset: s.location_offset(),
217        }
218    }
219}
220
221#[derive(Clone, PartialEq, Eq, Hash, Debug)]
222pub struct Literal<T = IRTerm> {
223    /// True if if this is a positive literal, else it's a negated literal.
224    /// Double negations should be collapsed, if any.
225    pub positive: bool,
226
227    pub position: Option<SpannedPosition>,
228    pub predicate: Predicate,
229    pub args: Vec<T>,
230}
231
232#[cfg(test)]
233impl<T: PartialEq> Literal<T> {
234    /// Checks for equality, ignoring the position fields.
235    pub fn eq_ignoring_position(&self, other: &Literal<T>) -> bool {
236        self.positive == other.positive
237            && self.predicate == other.predicate
238            && self.args == other.args
239    }
240}
241
242#[derive(Clone, PartialEq, Eq, Hash, Debug)]
243pub struct Signature(pub Predicate, pub u32);
244
245#[derive(Clone, PartialEq, Debug)]
246pub struct Clause<T = IRTerm> {
247    pub head: Literal<T>,
248    pub body: Vec<Literal<T>>,
249}
250
251#[cfg(test)]
252impl<T: PartialEq> Clause<T> {
253    pub fn eq_ignoring_position(&self, other: &Clause<T>) -> bool {
254        self.head.eq_ignoring_position(&other.head)
255            && self.body.len() == other.body.len()
256            && self
257                .body
258                .iter()
259                .enumerate()
260                .all(|(i, l)| l.eq_ignoring_position(&other.body[i]))
261    }
262}
263
264pub trait Ground {
265    fn is_ground(&self) -> bool;
266}
267
268impl IRTerm {
269    pub fn variables(&self, include_anonymous: bool) -> HashSet<IRTerm> {
270        let mut set = HashSet::<IRTerm>::new();
271        match (self, include_anonymous) {
272            (IRTerm::AnonymousVariable(_), true) => {
273                set.insert(self.clone());
274            }
275            (IRTerm::Array(ts), b) => {
276                set.extend(ts.iter().flat_map(|t| t.variables(b)));
277            }
278            (IRTerm::AuxiliaryVariable(_), _)
279            | (IRTerm::RenamedVariable(..), _)
280            | (IRTerm::UserVariable(_), _) => {
281                set.insert(self.clone());
282            }
283            (IRTerm::Constant(_), _) | (IRTerm::AnonymousVariable(_), false) => (),
284        }
285        set
286    }
287}
288
289impl Literal {
290    pub fn signature(&self) -> Signature {
291        Signature(self.predicate.clone(), self.args.len().try_into().unwrap())
292    }
293    pub fn variables(&self, include_anonymous: bool) -> HashSet<IRTerm> {
294        self.args
295            .iter()
296            .map(|r| r.variables(include_anonymous))
297            .reduce(|mut l, r| {
298                l.extend(r);
299                l
300            })
301            .unwrap_or_default()
302    }
303
304    /// Unmangles this literal if it represents an operator. Replaces it's predicate name
305    /// and removes argument used for matching the operator.
306    pub fn unmangle(self) -> Literal {
307        if self.predicate.is_operator() {
308            Literal {
309                predicate: self.predicate.unmangle(),
310                args: self.args[1..].to_vec(),
311                ..self
312            }
313        } else {
314            self
315        }
316    }
317
318    pub fn negated(&self) -> Literal {
319        Literal {
320            positive: !self.positive,
321            ..self.clone()
322        }
323    }
324}
325
326impl<T> Literal<T> {
327    pub fn with_position(self, position: Option<SpannedPosition>) -> Literal<T> {
328        Literal { position, ..self }
329    }
330}
331
332impl Clause {
333    pub fn variables(&self, include_anonymous: bool) -> HashSet<IRTerm> {
334        let mut body = self
335            .body
336            .iter()
337            .map(|r| r.variables(include_anonymous))
338            .reduce(|mut l, r| {
339                l.extend(r);
340                l
341            })
342            .unwrap_or_default();
343        body.extend(self.head.variables(include_anonymous));
344        body
345    }
346}
347
348impl Ground for IRTerm {
349    fn is_ground(&self) -> bool {
350        matches!(self, IRTerm::Constant(_))
351    }
352}
353
354impl Ground for Literal {
355    fn is_ground(&self) -> bool {
356        self.variables(true).is_empty()
357    }
358}
359
360impl Ground for Clause {
361    fn is_ground(&self) -> bool {
362        self.variables(true).is_empty()
363    }
364}
365
366impl fmt::Display for Signature {
367    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368        write!(f, "{}/{}", self.0, self.1)
369    }
370}
371
372impl fmt::Display for Predicate {
373    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374        write!(f, "{}", self.0)
375    }
376}
377
378fn display_sep<T: fmt::Display>(seq: &[T], sep: &str) -> String {
379    return seq
380        .iter()
381        .map(|t| t.to_string())
382        .collect::<Vec<String>>()
383        .join(sep);
384}
385
386impl fmt::Display for Literal {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        match &*self.args {
389            [] => write!(
390                f,
391                "{}{}",
392                if self.positive { "" } else { "!" },
393                self.predicate
394            ),
395            _ => write!(
396                f,
397                "{}{}({})",
398                if self.positive { "" } else { "!" },
399                self.predicate,
400                display_sep(&self.args, ", ")
401            ),
402        }
403    }
404}
405
406impl fmt::Display for Clause {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        write!(f, "{} :- {}", self.head, display_sep(&self.body, ", "))
409    }
410}
411
412impl str::FromStr for Clause {
413    type Err = String;
414
415    fn from_str(s: &str) -> Result<Self, Self::Err> {
416        let span = Span::new(s);
417        match parser::clause(parser::term)(span) {
418            Result::Ok((_, o)) => Ok(o),
419            Result::Err(e) => Result::Err(format!("{}", e)),
420        }
421    }
422}
423
424impl str::FromStr for Literal {
425    type Err = String;
426
427    fn from_str(s: &str) -> Result<Self, Self::Err> {
428        let span = Span::new(s);
429        match parser::literal(parser::term, multispace0)(span) {
430            Result::Ok((_, o)) => Ok(o),
431            Result::Err(e) => Result::Err(format!("{}", e)),
432        }
433    }
434}
435
436/// The parser for the IR is only for convenience in writing tests.
437pub mod parser {
438
439    use super::*;
440
441    use nom::{
442        branch::alt,
443        bytes::complete::{is_a, take_until},
444        character::complete::{alpha1, alphanumeric1, multispace0},
445        combinator::{cut, map, opt, recognize},
446        multi::{many0, many0_count, separated_list0, separated_list1},
447        sequence::{delimited, pair, preceded, terminated, tuple},
448        Offset, Slice,
449    };
450    use nom_supreme::{error::ErrorTree, tag::complete::tag};
451
452    pub type Span<'a> = LocatedSpan<&'a str>;
453
454    /// Redeclaration that uses ErrorTree instead of the default nom::Error.
455    pub type IResult<T, O> = nom::IResult<T, O, ErrorTree<T>>;
456
457    /// Creates a parser that returns a `SpannedPosition` that spans the consumed input
458    /// of a given parser. Also returns the actual output of the parser.
459    pub fn recognized_span<'a, P, T>(
460        mut inner: P,
461    ) -> impl FnMut(Span<'a>) -> IResult<Span<'a>, (SpannedPosition, T)>
462    where
463        P: FnMut(Span<'a>) -> IResult<Span<'a>, T>,
464    {
465        move |i| {
466            let original_i = i.clone();
467
468            let (i, o) = inner(i)?;
469
470            let index = original_i.offset(&i);
471            let recognized_section = original_i.slice(..index);
472            let spanned_pos: SpannedPosition = recognized_section.into();
473
474            Ok((i, (spanned_pos, o)))
475        }
476    }
477
478    fn ws<'a, F: 'a, O>(inner: F) -> impl FnMut(Span<'a>) -> IResult<Span<'a>, O>
479    where
480        F: FnMut(Span<'a>) -> IResult<Span<'a>, O>,
481    {
482        delimited(multispace0, inner, multispace0)
483    }
484
485    fn constant(i: Span) -> IResult<Span, Span> {
486        delimited(tag("\""), take_until("\""), tag("\""))(i)
487    }
488
489    fn variable(i: Span) -> IResult<Span, Span> {
490        literal_identifier(i)
491    }
492
493    fn array_term(i: Span) -> IResult<Span, Vec<IRTerm>> {
494        delimited(
495            terminated(tag("["), multispace0),
496            separated_list0(delimited(multispace0, tag(","), multispace0), term),
497            preceded(multispace0, tag("]")),
498        )(i)
499    }
500
501    pub fn term(i: Span) -> IResult<Span, IRTerm> {
502        alt((
503            map(array_term, IRTerm::Array),
504            map(constant, |s| IRTerm::Constant(s.fragment().to_string())),
505            map(is_a("_"), |_| sld::Auxiliary::aux(true)),
506            map(variable, |s| IRTerm::UserVariable(s.fragment().to_string())),
507        ))(i)
508    }
509
510    //TODO: I need to think more carefully how to connect this to stage name
511    pub fn literal_identifier(i: Span) -> IResult<Span, Span> {
512        recognize(pair(
513            alt((alpha1, tag("_"))),
514            many0(alt((alphanumeric1, tag("_"), tag("-")))),
515        ))(i)
516    }
517
518    /// Parses a literal with a generic term type.
519    pub fn literal<'a, FT: 'a, T, S, Any>(
520        term: FT,
521        space0: S,
522    ) -> impl FnMut(Span<'a>) -> IResult<Span<'a>, Literal<T>>
523    where
524        FT: FnMut(Span<'a>) -> IResult<Span<'a>, T> + Clone,
525        S: FnMut(Span<'a>) -> IResult<Span<'a>, Any> + Clone,
526    {
527        move |i| {
528            let (i, (spanned_pos, (neg_count, name, args))) = recognized_span(tuple((
529                // allow whitespace between '!'
530                many0_count(terminated(
531                    nom::character::complete::char('!'),
532                    space0.clone(),
533                )),
534                terminated(literal_identifier, space0.clone()),
535                opt(delimited(
536                    terminated(tag("("), space0.clone()),
537                    separated_list1(
538                        terminated(tag(","), space0.clone()),
539                        terminated(term.clone(), space0.clone()),
540                    ),
541                    cut(terminated(tag(")"), space0.clone())),
542                )),
543            )))(i)?;
544
545            Ok((
546                i,
547                Literal {
548                    positive: neg_count % 2 == 0,
549                    position: Some(spanned_pos),
550                    predicate: Predicate(name.fragment().to_string()),
551                    args: match args {
552                        Some(args) => args,
553                        None => Vec::new(),
554                    },
555                },
556            ))
557        }
558    }
559
560    pub fn clause<'a, FT: 'a, T>(term: FT) -> impl FnMut(Span<'a>) -> IResult<Span<'a>, Clause<T>>
561    where
562        FT: FnMut(Span) -> IResult<Span, T> + Clone,
563    {
564        map(
565            pair(
566                literal(term.clone(), multispace0),
567                opt(preceded(
568                    ws(tag(":-")),
569                    separated_list0(ws(tag(",")), literal(term, multispace0)),
570                )),
571            ),
572            |(head, body)| Clause {
573                head,
574                body: body.unwrap_or(Vec::new()),
575            },
576        )
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    #[test]
585    fn simple_term() {
586        let inp = "\"\"";
587
588        let expected = IRTerm::Constant("".into());
589        let actual: IRTerm = parser::term(Span::new(inp)).unwrap().1;
590
591        assert_eq!(expected, actual);
592    }
593
594    #[test]
595    fn literals() {
596        let l1 = Literal {
597            positive: true,
598            position: None,
599            predicate: Predicate("l1".into()),
600            args: vec![IRTerm::Constant("c".into()), IRTerm::Constant("d".into())],
601        };
602
603        assert_eq!("l1(\"c\", \"d\")", l1.to_string());
604
605        let actual1: Literal = "l1(\"c\", \"d\")".parse().unwrap();
606        let actual2: Literal = "l1(\"c\",\n\t\"d\")".parse().unwrap();
607        assert!(l1.eq_ignoring_position(&actual1));
608        assert!(l1.eq_ignoring_position(&actual2));
609    }
610
611    #[test]
612    fn literal_with_variable() {
613        let l1 = Literal {
614            positive: true,
615            position: None,
616            predicate: Predicate("l1".into()),
617            args: vec![
618                IRTerm::Constant("".into()),
619                IRTerm::UserVariable("X".into()),
620            ],
621        };
622
623        assert_eq!("l1(\"\", X)", l1.to_string());
624
625        let actual: Literal = "l1(\"\", X)".parse().unwrap();
626        assert!(l1.eq_ignoring_position(&actual));
627    }
628
629    #[test]
630    fn negated_literal() {
631        let l1 = Literal {
632            positive: false,
633            position: None,
634            predicate: Predicate("l1".into()),
635            args: vec![
636                IRTerm::Constant("".into()),
637                IRTerm::UserVariable("X".into()),
638            ],
639        };
640
641        assert_eq!("!l1(\"\", X)", l1.to_string());
642
643        let actual: Literal = "!!!l1(\"\", X)".parse().unwrap();
644        assert!(l1.eq_ignoring_position(&actual));
645    }
646
647    #[test]
648    fn span_of_literal() {
649        let spanned_pos = SpannedPosition {
650            length: 22,
651            offset: 0,
652        };
653
654        let actual: Literal = "l1(\"test_constant\", X)".parse().unwrap();
655        assert_eq!(Some(spanned_pos), actual.position);
656    }
657
658    #[test]
659    fn simple_rule() {
660        let c = IRTerm::Constant("c".into());
661        let va = IRTerm::UserVariable("A".into());
662        let vb = IRTerm::UserVariable("B".into());
663        let l1 = Literal {
664            positive: true,
665            position: None,
666            predicate: Predicate("l1".into()),
667            args: vec![va.clone(), vb.clone()],
668        };
669        let l2 = Literal {
670            positive: true,
671            position: None,
672            predicate: Predicate("l2".into()),
673            args: vec![va.clone(), c.clone()],
674        };
675        let l3 = Literal {
676            positive: true,
677            position: None,
678            predicate: Predicate("l3".into()),
679            args: vec![vb.clone(), c.clone()],
680        };
681        let r = Clause {
682            head: l1,
683            body: vec![l2, l3],
684        };
685
686        assert_eq!("l1(A, B) :- l2(A, \"c\"), l3(B, \"c\")", r.to_string());
687
688        let actual: Clause = "l1(A, B) :- l2(A, \"c\"), l3(B, \"c\")".parse().unwrap();
689        assert!(r.eq_ignoring_position(&actual));
690    }
691
692    #[test]
693    fn nullary_predicate() {
694        let va = IRTerm::UserVariable("A".into());
695        let l1 = Literal {
696            positive: true,
697            position: None,
698            predicate: Predicate("l1".into()),
699            args: Vec::new(),
700        };
701        let l2 = Literal {
702            positive: true,
703            position: None,
704            predicate: Predicate("l2".into()),
705            args: vec![va.clone()],
706        };
707        let r = Clause {
708            head: l1,
709            body: vec![l2],
710        };
711
712        assert_eq!("l1 :- l2(A)", r.to_string());
713
714        let actual: Clause = "l1 :- l2(A)".parse().unwrap();
715        assert!(r.eq_ignoring_position(&actual))
716    }
717}