1use 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 pub fn is_image(&self) -> bool {
55 matches!(self, Self::Image)
56 }
57
58 pub fn is_layer(&self) -> bool {
62 matches!(self, Self::Layer)
63 }
64
65 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 pub messages: Vec<Diagnostic<()>>,
80 pub errs: Vec<Diagnostic<()>>,
81}
82
83pub trait ModusSemantics {
85 fn kinds(&self) -> KindResult;
86}
87
88lazy_static! {
89 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 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 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 }
274 }
275 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 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 }
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 }
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 &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 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 type RuleId = usize; 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 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 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 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 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 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 if positivity { 0.0 } else { -1.0 },
625 );
626 }
627 }
628
629 (g, label_to_idx)
630 }
631
632 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 if k.is_some() && k != Some(&Kind::Logic) && !lit.positive {
661 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
746pub 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)); 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 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()); }
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}