modus_lib/
analysis.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 provides helpers to perform (semantic) analysis over Modusfile ASTs.
18//!
19//! We use a fixpoint approach to evaluate predicate kinds, which deals with the issue of recursion.
20//! Clauses are repeatedly 'applied' until a fixpoint.
21//! Instead of trying to work out the proper order we should 'apply' clauses, we apply all of them,
22//! and if we cannot evaluate some expression body because we haven't evaluated a future predicate kind
23//! yet, we move on - eventually it will be handled.
24//! However, we make an assumption on what an expression body's kind is whenever an incorrect assumption
25//! must lead to an error. For example, in `e1 ; e2`, if e1 is an image kind then we'll error if e2
26//! is not an image kind, so we may as well assume it is and error later if needed.
27
28use std::collections::{HashMap, HashSet};
29use std::io::Write;
30use std::ops::Range;
31
32use codespan_reporting::diagnostic::{Diagnostic, Label, Severity};
33use codespan_reporting::files::Files;
34use codespan_reporting::term::{self, Config};
35use petgraph::algo::find_negative_cycle;
36
37use crate::builtin::{select_builtin, OPERATOR_KIND_MAP};
38use crate::logic::{self, Literal, Predicate, SpannedPosition};
39use crate::modusfile::{Expression, ModusClause, Operator};
40use crate::modusfile::{ModusTerm, Modusfile};
41use crate::translate::translate_modusfile;
42
43#[derive(Debug, PartialEq, Clone, Copy)]
44pub enum Kind {
45    Image,
46    Layer,
47    Logic,
48}
49
50impl Kind {
51    /// Returns `true` if the kind is [`Image`].
52    ///
53    /// [`Image`]: Kind::Image
54    pub fn is_image(&self) -> bool {
55        matches!(self, Self::Image)
56    }
57
58    /// Returns `true` if the kind is [`Layer`].
59    ///
60    /// [`Layer`]: Kind::Layer
61    pub fn is_layer(&self) -> bool {
62        matches!(self, Self::Layer)
63    }
64
65    /// Returns `true` if the kind is [`Logic`].
66    ///
67    /// [`Logic`]: Kind::Logic
68    pub fn is_logic(&self) -> bool {
69        matches!(self, Self::Logic)
70    }
71}
72
73#[derive(Debug, Clone)]
74pub struct KindResult {
75    pub pred_kind: HashMap<Predicate, Kind>,
76
77    /// For convenience, informational diagnostic messages that describe the predicate
78    /// kind using spans.
79    pub messages: Vec<Diagnostic<()>>,
80    pub errs: Vec<Diagnostic<()>>,
81}
82
83/// A trait for objects that have some interpretation w.r.t. the build graph.
84pub trait ModusSemantics {
85    fn kinds(&self) -> KindResult;
86}
87
88lazy_static! {
89    /// Enumerates the possible `e1 , e2 = e3` kinds. One entry represents (e1_kind, e2_kind, resulting_kind).
90    /// e.g. if two of the kinds are known, then the third kind is a valid possibility.
91    static ref AND_KIND_TABLE: Vec<(Kind, Kind, Kind)> = vec![
92        (Kind::Image, Kind::Logic, Kind::Image),
93        (Kind::Image, Kind::Layer, Kind::Image),
94        (Kind::Logic, Kind::Image, Kind::Image),
95
96        (Kind::Layer, Kind::Layer, Kind::Layer),
97        (Kind::Logic, Kind::Layer, Kind::Layer),
98        (Kind::Layer, Kind::Logic, Kind::Layer),
99
100        (Kind::Logic, Kind::Logic, Kind::Logic),
101    ];
102
103    static ref OR_KIND_TABLE: Vec<(Kind, Kind, Kind)> = vec![
104        (Kind::Image, Kind::Image, Kind::Image),
105        (Kind::Layer, Kind::Layer, Kind::Layer),
106        (Kind::Logic, Kind::Logic, Kind::Logic),
107    ];
108}
109
110impl ModusSemantics for Modusfile {
111    fn kinds(&self) -> KindResult {
112        fn generate_err_diagnostic(
113            span: &Option<SpannedPosition>,
114            e1: &Expression,
115            e2: &Expression,
116            sem1: &Kind,
117            sem2: &Kind,
118        ) -> Diagnostic<()> {
119            let message = "Couldn't determine kind of `Expression`.";
120            let mut labels = Vec::new();
121            if span.is_some() {
122                labels.push(
123                    Label::secondary((), Range::from(e1.get_spanned_position().as_ref().unwrap()))
124                        .with_message(format!("this is found to be a `{:?}` expression", sem1)),
125                );
126                labels.push(
127                    Label::secondary((), Range::from(e2.get_spanned_position().as_ref().unwrap()))
128                        .with_message(format!("this is found to be a `{:?}` expression", sem2)),
129                );
130            }
131
132            Diagnostic::error()
133                .with_message(message)
134                .with_labels(labels)
135        }
136
137        fn generate_msg_diagnostic(c1: &ModusClause, k1: &Kind) -> Diagnostic<()> {
138            let message = format!("{} is of kind {:?}", c1.head.predicate.0, k1);
139            let labels = if let Some(span) = c1.body.as_ref().unwrap().get_spanned_position() {
140                vec![Label::primary((), Range::from(span))
141                    .with_message(format!("since this is found to be a {:?} expression", k1))]
142            } else {
143                Vec::new()
144            };
145            Diagnostic::note().with_message(message).with_labels(labels)
146        }
147
148        fn generate_unknown_operator_diag(op: &Operator) -> Diagnostic<()> {
149            let diag =
150                Diagnostic::error().with_message(format!("Unknown operator: {}", op.predicate));
151            if let Some(pos) = &op.position {
152                diag.with_labels(vec![Label::primary(
153                    (),
154                    pos.offset..pos.offset + pos.length,
155                )])
156            } else {
157                diag
158            }
159        }
160
161        fn generate_failed_assumption(
162            expr: &Expression,
163            expected: &Kind,
164            actual: &Kind,
165        ) -> Diagnostic<()> {
166            let diag = Diagnostic::error().with_message(format!("Expected kind: {expected:?}"));
167            let mut labels = Vec::new();
168
169            if let Some(pos) = expr.get_spanned_position() {
170                labels.push(
171                    Label::primary((), pos.offset..pos.offset + pos.length)
172                        .with_message(format!("expression found to be {actual:?}")),
173                );
174            }
175
176            diag.with_labels(labels)
177        }
178
179        /// Attempts to get the predicate kind of this expression, based on the current findings
180        /// of the predicate kind map.
181        ///
182        /// If assertion is Some(k), then we will either assume that it is of kind k if we don't know,
183        /// or we will assert it.
184        ///
185        /// If head_pred is Some(p), we assume that this expression is the body of a corresponding
186        /// rule and modify pred_kind accordingly. This is used to assist in some recursive cases,
187        /// i.e. where we know that the top level expression outcome must be some kind k.
188        fn evaluate_or_assert_expression(
189            expression: &Expression,
190            pred_kind: &mut HashMap<Predicate, Kind>,
191            clauses: &[ModusClause],
192            assertion: Option<Kind>,
193            head_pred: Option<&Predicate>,
194        ) -> Result<Kind, Diagnostic<()>> {
195            fn get_expression_possibilities(
196                table: &[(Kind, Kind, Kind)],
197                e1: Option<Kind>,
198                e2: Option<Kind>,
199                e3: Option<Kind>,
200            ) -> Vec<(Kind, Kind, Kind)> {
201                let num_some = e1.is_some() as i32 + e2.is_some() as i32 + e3.is_some() as i32;
202                table
203                    .iter()
204                    .cloned()
205                    .filter(|(t1, t2, t3)| {
206                        let match_count = (Some(t1) == e1.as_ref()) as i32
207                            + (Some(t2) == e2.as_ref()) as i32
208                            + (Some(t3) == e3.as_ref()) as i32;
209                        match_count >= num_some
210                    })
211                    .collect()
212            }
213
214            match expression {
215                Expression::Literal(lit) => {
216                    match (pred_kind.get(&lit.predicate).copied(), assertion) {
217                        (None, None) => {
218                            if clauses
219                                .iter()
220                                .any(|c| c.body.is_some() && c.head.predicate == lit.predicate)
221                            {
222                                // If there is some rule with the desired predicate, we'll defer to evaluating it, instead of
223                                // assuming that it is a logical kind.
224                                //
225                                // This may lead to issues with cases like:
226                                // ````
227                                // foo(X) :- bar(X).
228                                // bar(X) :- foo(X).
229                                // ````
230                                // Since both will 'defer' to each other. However, this isn't a sensible program on it's own anyway.
231                                Err(Diagnostic::warning()
232                                    .with_message(format!("{} not determined yet.", lit.predicate)))
233                            } else {
234                                Ok(Kind::Logic)
235                            }
236                        }
237                        (None, Some(kind_assumption)) => {
238                            pred_kind.insert(lit.predicate.clone(), kind_assumption);
239                            Ok(kind_assumption)
240                        }
241                        (Some(existing_k), None) => Ok(existing_k),
242                        (Some(existing_k), Some(kind_assumption)) => {
243                            if existing_k != kind_assumption {
244                                Err(generate_failed_assumption(
245                                    expression,
246                                    &kind_assumption,
247                                    &existing_k,
248                                ))
249                            } else {
250                                Ok(kind_assumption)
251                            }
252                        }
253                    }
254                }
255                Expression::OperatorApplication(_, expr, op) => {
256                    let op_kind_map = OPERATOR_KIND_MAP.get(op.predicate.0.as_str()).copied();
257
258                    if let Some((inp_kind, out_kind)) = op_kind_map {
259                        if let Some(kind_assumption) = assertion {
260                            if out_kind != kind_assumption {
261                                return Err(generate_failed_assumption(
262                                    expression,
263                                    &kind_assumption,
264                                    &out_kind,
265                                ));
266                            }
267                        }
268                        if let Some(p) = head_pred {
269                            if !pred_kind.contains_key(&p) {
270                                pred_kind.insert(p.clone(), out_kind);
271                            } else {
272                                // could enforce existing is equal to assumption here
273                            }
274                        }
275                        // assert that the input expression is as expected and return out_kind if no errors
276                        let _ = evaluate_or_assert_expression(
277                            expr,
278                            pred_kind,
279                            clauses,
280                            Some(inp_kind),
281                            None,
282                        )?;
283                        Ok(out_kind)
284                    } else {
285                        // unknown operator
286                        Err(generate_unknown_operator_diag(op))
287                    }
288                }
289                Expression::And(span, true, e1, e2) => {
290                    let sem1_res =
291                        evaluate_or_assert_expression(e1, pred_kind, clauses, None, None);
292                    let sem2_res =
293                        evaluate_or_assert_expression(e2, pred_kind, clauses, None, None);
294
295                    let possibilities = get_expression_possibilities(
296                        &AND_KIND_TABLE,
297                        sem1_res.clone().ok(),
298                        sem2_res.clone().ok(),
299                        assertion,
300                    );
301                    if possibilities.len() == 1 {
302                        let possibility = possibilities[0];
303
304                        if let Some(p) = head_pred {
305                            if !pred_kind.contains_key(&p) {
306                                pred_kind.insert(p.clone(), possibility.2);
307                            } else {
308                                // could enforce existing is equal to assumption here
309                            }
310                        }
311
312                        let _ = evaluate_or_assert_expression(
313                            e1,
314                            pred_kind,
315                            clauses,
316                            Some(possibility.0),
317                            None,
318                        )?;
319                        let _ = evaluate_or_assert_expression(
320                            e2,
321                            pred_kind,
322                            clauses,
323                            Some(possibility.1),
324                            None,
325                        )?;
326                        Ok(possibility.2)
327                    } else {
328                        match (sem1_res, sem2_res) {
329                            (Ok(sem1), Ok(sem2)) => {
330                                Err(generate_err_diagnostic(span, e1, e2, &sem1, &sem2))
331                            }
332                            (Err(e), _) | (_, Err(e)) => Err(e),
333                        }
334                    }
335                }
336                Expression::Or(span, true, e1, e2) => {
337                    let sem1_res =
338                        evaluate_or_assert_expression(e1, pred_kind, clauses, assertion, None);
339                    let sem2_res =
340                        evaluate_or_assert_expression(e2, pred_kind, clauses, assertion, None);
341
342                    let possibilities = get_expression_possibilities(
343                        &OR_KIND_TABLE,
344                        sem1_res.clone().ok(),
345                        sem2_res.clone().ok(),
346                        assertion,
347                    );
348                    if possibilities.len() == 1 {
349                        let possibility = possibilities[0];
350
351                        if let Some(p) = head_pred {
352                            if !pred_kind.contains_key(&p) {
353                                pred_kind.insert(p.clone(), possibility.2);
354                            } else {
355                                // could enforce existing is equal to assumption here
356                            }
357                        }
358
359                        let _ = evaluate_or_assert_expression(
360                            e1,
361                            pred_kind,
362                            clauses,
363                            Some(possibility.0),
364                            None,
365                        )?;
366                        let _ = evaluate_or_assert_expression(
367                            e2,
368                            pred_kind,
369                            clauses,
370                            Some(possibility.1),
371                            None,
372                        )?;
373                        Ok(possibility.2)
374                    } else {
375                        match (sem1_res, sem2_res) {
376                            (Ok(sem1), Ok(sem2)) => {
377                                Err(generate_err_diagnostic(span, e1, e2, &sem1, &sem2))
378                            }
379                            (Err(e), _) | (_, Err(e)) => Err(e),
380                        }
381                    }
382                }
383                // A negated expression is a check for whether we can prove the expression,
384                // so `!foo` is always a logical kind, regardless of foo.
385                &Expression::And(_, false, ..) | Expression::Or(_, false, ..) => match assertion {
386                    None | Some(Kind::Logic) => Ok(Kind::Logic),
387                    Some(k) => Err(generate_failed_assumption(expression, &k, &Kind::Logic)),
388                },
389            }
390        }
391
392        let from_pred = Predicate("from".into());
393        let run_pred = Predicate("run".into());
394        let copy_pred = Predicate("copy".into());
395        // This initializes the map with the kinds of from/run/copy.
396        let mut pred_kind: HashMap<Predicate, Kind> = vec![
397            (
398                from_pred.clone(),
399                select_builtin(&Literal {
400                    positive: true,
401                    position: None,
402                    predicate: from_pred,
403                    args: vec![logic::IRTerm::Constant("".to_string())],
404                })
405                .1
406                .unwrap()
407                .kind(),
408            ),
409            (
410                run_pred.clone(),
411                select_builtin(&Literal {
412                    positive: true,
413                    position: None,
414                    predicate: run_pred,
415                    args: vec![logic::IRTerm::Constant("".to_string())],
416                })
417                .1
418                .unwrap()
419                .kind(),
420            ),
421            (
422                copy_pred.clone(),
423                select_builtin(&Literal {
424                    positive: true,
425                    position: None,
426                    predicate: copy_pred,
427                    args: vec![
428                        logic::IRTerm::Constant("".to_string()),
429                        logic::IRTerm::Constant("".to_string()),
430                    ],
431                })
432                .1
433                .unwrap()
434                .kind(),
435            ),
436        ]
437        .into_iter()
438        .collect();
439
440        // Compute the index positions of the predicates
441        type RuleId = usize; // index position w.r.t. clauses
442        let mut pred_to_rid: HashMap<&Predicate, Vec<RuleId>> = HashMap::new();
443        for (i, c) in self.0.iter().enumerate() {
444            let curr = pred_to_rid.entry(&c.head.predicate).or_default();
445            curr.push(i);
446
447            if c.body.is_none() {
448                // facts are logical kinds
449                pred_kind.insert(c.head.predicate.clone(), Kind::Logic);
450            }
451        }
452
453        loop {
454            let mut new_pred = false;
455            for c in self.0.iter().filter(|c| c.body.is_some()) {
456                let maybe_existing_kind = pred_kind.get(&c.head.predicate).copied();
457                let k_res = evaluate_or_assert_expression(
458                    c.body.as_ref().unwrap(),
459                    &mut pred_kind,
460                    &self.0,
461                    maybe_existing_kind,
462                    Some(&c.head.predicate),
463                );
464                if let Ok(k) = k_res {
465                    if maybe_existing_kind == None {
466                        new_pred = true;
467                        pred_kind.insert(c.head.predicate.clone(), k);
468                    }
469                }
470            }
471
472            if !new_pred {
473                break;
474            }
475        }
476
477        let mut messages = Vec::new();
478        let mut errs = Vec::new();
479        // evaluate all the expression bodies a final time to pick up any conflicting definitions
480        for c in self.0.iter().filter(|c| c.body.is_some()) {
481            let assertion = pred_kind.get(&c.head.predicate).copied();
482            match evaluate_or_assert_expression(
483                c.body.as_ref().unwrap(),
484                &mut pred_kind,
485                &self.0,
486                assertion,
487                Some(&c.head.predicate),
488            ) {
489                Ok(kind) => {
490                    let pred = &c.head.predicate;
491                    if let Some(k) = pred_kind.get(pred) {
492                        if k != &kind {
493                            // Display an error if there is a conflicting predicate kind
494                            let maybe_curr_span =
495                                c.body.as_ref().unwrap().get_spanned_position().as_ref();
496                            let maybe_prev_span = pred_to_rid[&pred].iter().find_map(|rid| {
497                                self.0[*rid]
498                                    .body
499                                    .as_ref()
500                                    .and_then(|e| e.get_spanned_position().as_ref())
501                            });
502                            let labels = if let (Some(span_curr), Some(span_prev)) =
503                                (maybe_curr_span, maybe_prev_span)
504                            {
505                                vec![
506                                    Label::secondary((), Range::from(span_prev)).with_message(
507                                        format!("previous kind was found to be {:?}", k),
508                                    ),
509                                    Label::primary((), Range::from(span_curr)).with_message(
510                                        format!("but this was found to be a {:?} kind", kind),
511                                    ),
512                                ]
513                            } else {
514                                Vec::new()
515                            };
516                            errs.push(
517                                Diagnostic::error()
518                                    .with_message(
519                                        "A rule with matching head predicate has a different kind.",
520                                    )
521                                    .with_labels(labels),
522                            );
523                            continue;
524                        }
525                    }
526
527                    messages.push(generate_msg_diagnostic(c, &kind));
528                    pred_kind.insert(pred.clone(), kind);
529                }
530                Err(e) => errs.push(e.with_notes(vec![format!(
531                    "Therefore, couldn't evaluate clause with head {}.",
532                    c.head,
533                )])),
534            }
535        }
536
537        KindResult {
538            pred_kind,
539            messages,
540            errs,
541        }
542    }
543}
544
545trait PredicateDependency {
546    /// Returns a graph where an edge, (n1, n2), means that the predicate
547    /// n1 depends on n2 to compute it's type.
548    ///
549    /// This uses the assumption that predicate names refer to a unique groundness
550    /// signature.
551    fn compute_dependency(
552        &self,
553    ) -> (
554        petgraph::Graph<&str, f32>,
555        HashMap<&str, petgraph::graph::NodeIndex>,
556    );
557
558    fn stratifiable(&self) -> Result<(), Vec<&str>>;
559}
560
561impl PredicateDependency for Modusfile {
562    fn compute_dependency(
563        &self,
564    ) -> (
565        petgraph::Graph<&str, f32>,
566        HashMap<&str, petgraph::graph::NodeIndex>,
567    ) {
568        fn get_predicate_positivity(expr: &Expression) -> Vec<(&str, bool)> {
569            match expr {
570                Expression::Literal(lit) => vec![(&lit.predicate.0, lit.positive)],
571                Expression::OperatorApplication(_, expr, _) => get_predicate_positivity(expr),
572                Expression::And(_, _, e1, e2) => {
573                    let mut pred1 = get_predicate_positivity(e1);
574                    pred1.append(&mut get_predicate_positivity(e2));
575                    pred1
576                }
577                Expression::Or(_, _, e1, e2) => {
578                    let mut pred1 = get_predicate_positivity(e1);
579                    pred1.append(&mut get_predicate_positivity(e2));
580                    pred1
581                }
582            }
583        }
584
585        let mut predicate_to_dependency: HashMap<&str, HashSet<(&str, bool)>> = HashMap::new();
586        for clause in &self.0 {
587            let head_pred = clause.head.predicate.0.as_str();
588            if !predicate_to_dependency.contains_key(head_pred) {
589                predicate_to_dependency.insert(head_pred, HashSet::new());
590            }
591
592            if let Some(expr) = &clause.body {
593                let preds = get_predicate_positivity(expr);
594                for (pred, _) in &preds {
595                    if !predicate_to_dependency.contains_key(pred) {
596                        predicate_to_dependency.insert(pred, HashSet::new());
597                    }
598                }
599
600                let s = predicate_to_dependency.get_mut(head_pred).unwrap();
601                s.extend(preds);
602            }
603        }
604
605        // there may be a better way to init a petgraph, but for now use another map to
606        // store indices
607        let mut label_to_idx: HashMap<&str, petgraph::graph::NodeIndex> = HashMap::new();
608        let mut g = petgraph::Graph::<&str, f32>::new();
609        for (k, v) in predicate_to_dependency.iter() {
610            if !label_to_idx.contains_key(k) {
611                let idx = g.add_node(k);
612                label_to_idx.insert(k, idx);
613            }
614
615            for &(pred, positivity) in v {
616                if !label_to_idx.contains_key(pred) {
617                    label_to_idx.insert(pred, g.add_node(pred));
618                }
619                g.add_edge(
620                    label_to_idx[k],
621                    label_to_idx[pred],
622                    // this allows us to find *a cycle with a negative edge* using
623                    // an algorithm for *negative cycles*
624                    if positivity { 0.0 } else { -1.0 },
625                );
626            }
627        }
628
629        (g, label_to_idx)
630    }
631
632    /// "A logic program is stratified iff the dependency graph contains no cycles
633    /// containing a negative edge."
634    /// - https://core.ac.uk/download/pdf/228424655.pdf
635    fn stratifiable(&self) -> Result<(), Vec<&str>> {
636        let (g, node_indices) = self.compute_dependency();
637
638        for id in node_indices.values() {
639            if let Some(ids) = find_negative_cycle(&g, *id) {
640                return Err(ids
641                    .into_iter()
642                    .map(|id| *node_indices.iter().find(|&(_, v)| *v == id).unwrap().0)
643                    .collect());
644            }
645        }
646
647        Ok(())
648    }
649}
650
651fn check_negated_logic_kind(
652    ir_clauses: &Vec<logic::Clause>,
653    pred_kind: &HashMap<Predicate, Kind>,
654) -> Result<(), Vec<Diagnostic<()>>> {
655    let mut errs = Vec::new();
656    for c in ir_clauses {
657        for lit in &c.body {
658            let k = pred_kind.get(&lit.predicate);
659            // We move on if we don't know what kind this pred is.
660            if k.is_some() && k != Some(&Kind::Logic) && !lit.positive {
661                // We disallow negating non-logical predicate to encourage better written
662                // Modusfiles.
663                // Also, maybe SLDNF should be considered an implementation detail and
664                // so this would make it easier to switch to different negation semantics.
665                let mut diag = Diagnostic::error()
666                    .with_message("Negating a non-logical predicate is disallowed.")
667                    .with_notes(vec![format!(
668                        "{} was found to be of kind {:?}.",
669                        lit.predicate,
670                        k.unwrap()
671                    )]);
672                if let Some(s) = &lit.position {
673                    diag =
674                        diag.with_labels(vec![Label::primary((), s.offset..(s.offset + s.length))]);
675                }
676                errs.push(diag);
677            }
678        }
679    }
680
681    if errs.is_empty() {
682        Ok(())
683    } else {
684        Err(errs)
685    }
686}
687
688fn head_term_check(mf: &Modusfile) -> Result<(), Vec<Diagnostic<()>>> {
689    fn generate_f_string_diag(pos: &SpannedPosition) -> Diagnostic<()> {
690        Diagnostic::error()
691            .with_message("A format string was found in a head literal.")
692            .with_labels(vec![Label::primary(
693                (),
694                pos.offset..pos.offset + pos.length,
695            )])
696    }
697
698    fn generate_array_diag(pos: &SpannedPosition) -> Diagnostic<()> {
699        Diagnostic::error()
700            .with_message("An array was found in a head literal. This is not supported currently.")
701            .with_labels(vec![Label::primary(
702                (),
703                pos.offset..pos.offset + pos.length,
704            )])
705    }
706
707    fn check_no_f_string(terms: &[ModusTerm]) -> Result<(), Diagnostic<()>> {
708        for t in terms {
709            match t {
710                ModusTerm::FormatString { position, .. } => {
711                    return Err(generate_f_string_diag(position));
712                }
713                ModusTerm::Array(_, ts) => check_no_f_string(ts)?,
714                _ => (),
715            }
716        }
717        Ok(())
718    }
719
720    let mut diags = Vec::new();
721
722    for modus_clause in &mf.0 {
723        for arg in &modus_clause.head.args {
724            match arg {
725                ModusTerm::FormatString { position, .. } => {
726                    diags.push(generate_f_string_diag(position))
727                }
728                ModusTerm::Array(position, ts) => {
729                    diags.push(generate_array_diag(position));
730                    if let Err(d) = check_no_f_string(ts) {
731                        diags.push(d)
732                    }
733                }
734                _ => (),
735            }
736        }
737    }
738
739    if diags.is_empty() {
740        Ok(())
741    } else {
742        Err(diags)
743    }
744}
745
746/// Returns true if the results of the check were satisfactory; we don't need to terminate.
747pub fn check_and_output_analysis<
748    'files,
749    W: Write + codespan_reporting::term::termcolor::WriteColor,
750    F: Files<'files, FileId = ()>,
751>(
752    kind_res: &KindResult,
753    mf: &Modusfile,
754    verbose: bool,
755    out: &mut W,
756    config: &Config,
757    file: &'files F,
758) -> bool {
759    if verbose {
760        for msg in &kind_res.messages {
761            term::emit(out, config, file, &msg).expect("Error when writing to stderr.");
762        }
763    }
764
765    let head_check_res = head_term_check(mf);
766    let head_errors = head_check_res.err().unwrap_or_default();
767
768    let can_translate = head_errors.is_empty();
769
770    let negation_errors = if can_translate {
771        let ir_clauses = translate_modusfile(mf);
772        check_negated_logic_kind(&ir_clauses, &kind_res.pred_kind)
773            .err()
774            .unwrap_or_default()
775    } else {
776        Vec::new()
777    };
778
779    let errs = kind_res
780        .errs
781        .iter()
782        .chain(&negation_errors)
783        .chain(&head_errors)
784        .collect::<Vec<_>>();
785    for err in &errs {
786        term::emit(out, config, file, err).expect("Error when writing to stderr.");
787    }
788
789    let is_stratifiable = mf.stratifiable();
790    if let Err(path) = is_stratifiable {
791        let path_string = path
792            .iter()
793            .map(|x| x.to_string())
794            .collect::<Vec<_>>()
795            .join(" -> ");
796        let path_string = "Cycle: ... -> ".to_string() + &path_string + " -> ...";
797        let diag = Diagnostic::error()
798            .with_message("Program is not stratifiable. Recursive dependency on negation found.")
799            .with_notes(vec![path_string]);
800        term::emit(out, config, file, &diag).expect("Error when writing to stderr.");
801        return false;
802    }
803
804    errs.iter().all(|err| err.severity != Severity::Error)
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810
811    use petgraph::algo::{is_cyclic_directed, is_isomorphic};
812
813    #[test]
814    fn acyclic_dependency() {
815        let clauses = vec!["foo(X) :- bar(X).", "bar(X) :- baz(X)."];
816        let mf: Modusfile = clauses.join("\n").parse().unwrap();
817
818        let mut expected_dep = petgraph::Graph::<&str, &str>::new();
819        let f = expected_dep.add_node("foo");
820        let b = expected_dep.add_node("bar");
821        let bz = expected_dep.add_node("baz");
822        expected_dep.extend_with_edges(&[(f, b), (b, bz)]);
823
824        let (actual_dep, _) = mf.compute_dependency();
825        assert!(is_isomorphic(&expected_dep, &actual_dep)); // isomorphism check may be expensive
826        assert!(!is_cyclic_directed(&actual_dep));
827    }
828
829    #[test]
830    fn cyclic_dependency() {
831        let clauses = vec![
832            "foo(X) :- from(\"ubuntu\"), run(\"apt-get update\"), bar(X).",
833            "bar(X) :- foo(X).",
834        ];
835        let mf: Modusfile = clauses.join("\n").parse().unwrap();
836
837        let mut expected_dep = petgraph::Graph::<&str, &str>::new();
838        let f = expected_dep.add_node("foo");
839        let fr = expected_dep.add_node("from");
840        let ru = expected_dep.add_node("run");
841        let ba = expected_dep.add_node("bar");
842        expected_dep.extend_with_edges(&[(f, fr), (f, ru), (f, ba), (ba, f)]);
843
844        let (actual_dep, _) = mf.compute_dependency();
845        assert!(is_isomorphic(&expected_dep, &actual_dep));
846        assert!(is_cyclic_directed(&actual_dep));
847    }
848
849    #[test]
850    fn stratifiable_program() {
851        let clauses = vec![
852            "foo(X) :- from(\"ubuntu\"), run(\"apt-get update\"), bar(X).",
853            "bar(X) :- foo(X).",
854        ];
855        let mf: Modusfile = clauses.join("\n").parse().unwrap();
856        assert!(mf.stratifiable().is_ok());
857    }
858
859    #[test]
860    fn unstratifiable_program() {
861        let clauses = vec![
862            "foo(X) :- from(\"ubuntu\"), run(\"apt-get update\"), bar(X).",
863            "bar(X) :- !foo(X).",
864        ];
865        let mf: Modusfile = clauses.join("\n").parse().unwrap();
866        assert!(mf.stratifiable().is_err());
867    }
868
869    #[test]
870    fn simple_image_predicate_kind() {
871        let clauses = vec!["a :- from(\"ubuntu\"), run(\"apt-get update\"), run(\"echo hello\")."];
872        let mf: Modusfile = clauses.join("\n").parse().unwrap();
873
874        let kind_res = mf.kinds();
875        let pred = &Predicate("a".into());
876        assert_eq!(kind_res.errs.len(), 0);
877        assert!(kind_res.pred_kind.contains_key(pred));
878        assert_eq!(&Kind::Image, kind_res.pred_kind.get(pred).unwrap());
879    }
880
881    #[test]
882    fn layer_predicate_kind() {
883        let clauses = vec![
884            "a :- from(\"ubuntu\"), run(\"apt-get update\").",
885            "b :- a::copy(\".\", \".\").",
886        ];
887        let mf: Modusfile = clauses.join("\n").parse().unwrap();
888
889        let kind_res = mf.kinds();
890        assert_eq!(kind_res.errs.len(), 0);
891        assert_eq!(
892            Some(&Kind::Layer),
893            kind_res.pred_kind.get(&Predicate("b".into()))
894        );
895    }
896
897    #[test]
898    fn simple_logic_predicate_kind() {
899        let clauses = vec!["b(X) :- X = \"test\"."];
900        let mf: Modusfile = clauses.join("\n").parse().unwrap();
901
902        let kind_res = mf.kinds();
903        assert_eq!(kind_res.errs.len(), 0);
904        assert_eq!(
905            Some(&Kind::Logic),
906            kind_res.pred_kind.get(&Predicate("b".into()))
907        );
908    }
909
910    #[test]
911    fn simple_indirect_kind() {
912        let clauses = vec![
913            "a(X) :- from(X), run(\"apt-get update\").",
914            "b(X) :- a(X).",
915            "c(X) :- b(X).",
916        ];
917        let mf: Modusfile = clauses.join("\n").parse().unwrap();
918
919        let kind_res = mf.kinds();
920        assert_eq!(kind_res.errs.len(), 0);
921        assert_eq!(
922            Some(&Kind::Image),
923            kind_res.pred_kind.get(&Predicate("a".into()))
924        );
925        assert_eq!(
926            Some(&Kind::Image),
927            kind_res.pred_kind.get(&Predicate("b".into()))
928        );
929        assert_eq!(
930            Some(&Kind::Image),
931            kind_res.pred_kind.get(&Predicate("c".into()))
932        );
933    }
934
935    #[test]
936    fn handles_indirect_recursion() {
937        // Since we can evaluate foo in the third clause, we should be able to evaluate
938        // `bar`.
939        let clauses = vec![
940            "foo(X) :- bar(X).",
941            "bar(X) :- foo(X).",
942            "foo(X) :- from(X), run(\"apt-get update\").",
943        ];
944        let mf: Modusfile = clauses.join("\n").parse().unwrap();
945
946        let kind_res = mf.kinds();
947        assert_eq!(kind_res.errs.len(), 0);
948        assert_eq!(
949            Some(&Kind::Image),
950            kind_res.pred_kind.get(&Predicate("foo".into()))
951        );
952        assert_eq!(
953            Some(&Kind::Image),
954            kind_res.pred_kind.get(&Predicate("bar".into()))
955        );
956    }
957
958    #[test]
959    fn handles_recursion_through_presumptions() {
960        let clauses = vec![
961            "foo(mode) :- ( \
962               mode=\"prod\",from(\"alpine\"),foo(\"dev\")::copy(\".\", \".\") \
963             ; \
964               mode=\"dev\",from(\"gcc\"),run(\"echo hello\") \
965             ).",
966        ];
967        let mf: Modusfile = clauses.join("\n").parse().unwrap();
968
969        let kind_res = mf.kinds();
970        assert_eq!(kind_res.errs.len(), 0);
971        assert_eq!(
972            Some(&Kind::Image),
973            kind_res.pred_kind.get(&Predicate("foo".into()))
974        );
975    }
976
977    #[test]
978    fn handles_disjunct_recursion_through_presumptions() {
979        let clauses = vec![
980            "foo(mode) :- ( \
981               mode=\"prod\",foo(\"dev\"),run(\"make prod\") \
982             ; \
983               mode=\"dev\",copy(\".\", \"/app\"),run(\"echo hello\") \
984             ).",
985        ];
986        let mf: Modusfile = clauses.join("\n").parse().unwrap();
987
988        let kind_res = mf.kinds();
989        assert_eq!(kind_res.errs.len(), 0);
990        assert_eq!(
991            Some(&Kind::Layer),
992            kind_res.pred_kind.get(&Predicate("foo".into()))
993        );
994    }
995
996    #[test]
997    fn warns_recursion() {
998        let clauses = vec![
999            "recurse :- recurse.",
1000            "a(X) :- from(X), run(\"apt-get update\").",
1001        ];
1002        let mf: Modusfile = clauses.join("\n").parse().unwrap();
1003
1004        let kind_res = mf.kinds();
1005        assert_eq!(kind_res.errs.len(), 1);
1006        assert_eq!(kind_res.errs[0].severity, Severity::Warning);
1007        assert_eq!(
1008            Some(&Kind::Image),
1009            kind_res.pred_kind.get(&Predicate("a".into()))
1010        );
1011    }
1012
1013    #[test]
1014    fn warns_indirect_recursion() {
1015        let clauses = vec!["foo(X) :- bar(X).", "bar(X) :- foo(X)."];
1016        let mf: Modusfile = clauses.join("\n").parse().unwrap();
1017
1018        let kind_res = mf.kinds();
1019        assert_eq!(kind_res.errs.len(), 2);
1020        assert_eq!(kind_res.errs[0].severity, Severity::Warning);
1021        assert_eq!(kind_res.errs[1].severity, Severity::Warning);
1022    }
1023
1024    #[test]
1025    fn errors_conflicting_predicate_kind() {
1026        let clauses = vec!["foo :- \"1\" = \"1\".", "foo(X) :- from(X)."];
1027        let mf: Modusfile = clauses.join("\n").parse().unwrap();
1028
1029        let kind_res = mf.kinds();
1030        assert_eq!(kind_res.errs.len(), 1);
1031        assert_eq!(kind_res.errs[0].severity, Severity::Error);
1032    }
1033
1034    #[test]
1035    fn errors_f_string_present_in_head() {
1036        let clauses = vec![
1037            "fact_f(f\"foo ${X}\", f\"${Y}\").",
1038            "fact_f(f\"foo\").",
1039            "fact_f(f\"foo ${X}\") :- bar, lar.",
1040        ];
1041        let mf: Modusfile = clauses.join("\n").parse().unwrap();
1042
1043        let res = head_term_check(&mf);
1044        assert!(res.is_err());
1045        assert_eq!(4, res.err().unwrap().len()); // one for each arg
1046    }
1047
1048    #[test]
1049    fn errors_f_string_present_in_head_array() {
1050        let clauses = vec!["fact_f([X, f\"foo ${X}\"], f\"${Y}\")."];
1051        let mf: Modusfile = clauses.join("\n").parse().unwrap();
1052
1053        let res = head_term_check(&mf);
1054        assert!(res.is_err());
1055        assert_eq!(res.as_ref().err().unwrap()[0].severity, Severity::Error);
1056        assert_eq!(1 + 2, res.err().unwrap().len());
1057    }
1058
1059    #[test]
1060    fn kind_errors_with_unknown_operator() {
1061        let clauses = vec!["head :- bar::foobar(X, Y), lar.", "lar."];
1062        let mf: Modusfile = clauses.join("\n").parse().unwrap();
1063
1064        let kind_res = mf.kinds();
1065        assert_eq!(kind_res.errs[0].severity, Severity::Error);
1066        assert!(kind_res.errs[0].message.contains("Unknown operator"));
1067    }
1068
1069    #[test]
1070    fn kind_errors_with_incorrect_operator_inp() {
1071        let clauses = vec![
1072            "run_alias :- run(\"echo foobar\").",
1073            "head :- run_alias::set_entrypoint(\"bash\").",
1074        ];
1075        let mf: Modusfile = clauses.join("\n").parse().unwrap();
1076
1077        let kind_res = mf.kinds();
1078        assert_eq!(kind_res.errs[0].severity, Severity::Error);
1079        assert!(kind_res.errs[0].message.contains("Expected kind: Image"));
1080    }
1081}