modus_lib/
sld.rs

1// Modus, a language for building container images
2// Copyright (C) 2022 University College London
3
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as
6// published by the Free Software Foundation, either version 3 of the
7// License, or (at your option) any later version.
8
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17use std::{
18    collections::{HashMap, HashSet},
19    fmt::{self, Debug},
20    hash::Hash,
21    io, iter,
22};
23
24use crate::{
25    analysis, builtin,
26    logic::Predicate,
27    modusfile::{self, Modusfile},
28    translate::translate_modusfile,
29    unification::{compose_extend, compose_no_extend, Rename, Substitution},
30};
31use crate::{builtin::SelectBuiltinResult, unification::RenameWithSubstitution};
32use crate::{
33    logic::{self, Signature},
34    unification::Substitute,
35    wellformed,
36};
37use codespan_reporting::diagnostic::{Diagnostic, Label, Severity};
38use colored::Colorize;
39use logic::{Clause, IRTerm, Literal};
40use ptree::{item::StringItem, print_tree, TreeBuilder, TreeItem};
41
42pub trait Auxiliary: Rename<Self> + Sized {
43    fn aux(anonymous: bool) -> Self;
44}
45
46type RuleId = usize;
47type GoalId = usize;
48type TreeLevel = usize;
49pub(crate) type Goal<T = IRTerm> = Vec<Literal<T>>;
50
51/// In this usage, a 'clause' represents some method of resolution.
52#[derive(Clone, PartialEq, Eq, Hash, Debug)]
53pub enum ClauseId {
54    Rule(RuleId),
55    Query,
56    Builtin(Literal<IRTerm>),
57
58    /// Stores the literal which we attempted to prove.
59    /// So it should be a positive literal.
60    NegationCheck(Literal<IRTerm>),
61}
62
63/// A literal origin can be uniquely identified through its source clause and its index in the clause body
64#[derive(Clone, PartialEq, Debug)]
65pub struct LiteralOrigin {
66    clause: ClauseId,
67    body_index: usize,
68}
69
70/// Literal identifier relative to the goal
71type LiteralGoalId = usize;
72
73/// literal, tree level at which it was introduced if any, where it came from
74#[derive(Clone, PartialEq, Debug)]
75struct LiteralWithHistory {
76    literal: Literal<IRTerm>,
77    introduction: TreeLevel,
78    origin: LiteralOrigin,
79}
80type GoalWithHistory = Vec<LiteralWithHistory>;
81
82/// An SLD tree consists of
83/// - a goal with its dependencies (at which level and from which part of body each literal was introduced)
84/// - a level, which is incremented as tree grows
85/// - a mapping from (selected literal in goal, applied rule) to (mgu after rule renaming, rule renaming, resolvent subtree)
86#[derive(Clone, PartialEq, Debug)]
87pub struct Tree {
88    goal: GoalWithHistory,
89    level: TreeLevel,
90
91    /// Branches that could lead to a successful path.
92    success_resolvents: HashMap<(LiteralGoalId, ClauseId), (Substitution, Substitution, Tree)>,
93
94    /// Branches that will lead to failing paths.
95    fail_resolvents: HashMap<(LiteralGoalId, ClauseId), (Substitution, Substitution, Tree)>,
96
97    /// Possible error associated with this node. It is probably a leaf if present.
98    /// If this is a negation check, this might not be a leaf node.
99    ///
100    /// Note that even if error is None, the tree may still have failing paths,
101    /// it's just that this particular node is not the point where it failed.
102    error: Option<ResolutionError>,
103}
104
105impl Tree {
106    fn is_success(&self) -> bool {
107        self.goal.is_empty() || !self.success_resolvents.is_empty()
108    }
109
110    fn resolvents(
111        &self,
112    ) -> HashMap<&(usize, ClauseId), &(HashMap<IRTerm, IRTerm>, HashMap<IRTerm, IRTerm>, Tree)>
113    {
114        self.success_resolvents
115            .iter()
116            .chain(&self.fail_resolvents)
117            .collect::<HashMap<_, _>>()
118    }
119
120    /// Converts this tree to a directed graph.
121    pub fn to_graph(&self, rules: &[Clause]) -> Graph {
122        fn convert(
123            t: &Tree,
124            rules: &[Clause],
125            nodes: &mut Vec<String>,
126            edges: &mut Vec<(usize, usize, String)>,
127        ) {
128            let curr_label = t
129                .goal
130                .iter()
131                .map(|l| l.literal.to_string())
132                .collect::<Vec<_>>()
133                .join(",\n");
134            nodes.push(curr_label);
135            let curr_index = nodes.len() - 1;
136
137            if !t.goal.is_empty() && t.resolvents().is_empty() {
138                let err = t
139                    .error
140                    .as_ref()
141                    .expect("Failed path should store SLD error.");
142                let error_msg = err.to_string();
143                nodes.push("FAIL".to_string());
144                edges.push((curr_index, curr_index + 1, error_msg));
145            } else {
146                let mut resolvent_pairs = t.resolvents().into_iter().collect::<Vec<_>>();
147                resolvent_pairs.sort_by_key(|(k, _)| k.0); // for some consistency
148
149                for (k, v) in resolvent_pairs {
150                    let new_index = nodes.len();
151                    convert(&v.2, rules, nodes, edges);
152
153                    let edge_label = match &k.1 {
154                        ClauseId::Rule(rid) => rules[*rid].head.to_string(),
155                        ClauseId::Query => "query".to_string(),
156                        ClauseId::Builtin(lit) => lit.to_string(),
157                        ClauseId::NegationCheck(lit) => format!("Check {lit}?"),
158                    };
159                    edges.push((curr_index, new_index, edge_label));
160                }
161            }
162        }
163
164        let name = "SLD_Tree";
165        let mut nodes = Vec::new();
166        let mut edges = Vec::new();
167        convert(self, rules, &mut nodes, &mut edges);
168        Graph { name, nodes, edges }
169    }
170
171    /// Returns a string explaining the SLD tree, using indentation, etc.
172    pub fn explain(&self, rules: &[Clause]) -> StringItem {
173        fn dfs(t: &Tree, rules: &[Clause], builder: &mut TreeBuilder) {
174            let mut resolvent_pairs = t.resolvents().into_iter().collect::<Vec<_>>();
175            resolvent_pairs.sort_by_key(|(k, _)| k.0);
176
177            for (k, v) in &resolvent_pairs {
178                let (goal_id, cid) = k;
179
180                let substitution_map: HashMap<_, _> =
181                    v.0.iter()
182                        .map(|(t1, t2)| (t1.get_original().clone(), t2.clone()))
183                        .collect();
184
185                let requirement = match cid {
186                    ClauseId::Rule(rid) => rules[*rid]
187                        .substitute(&substitution_map)
188                        .body
189                        .iter()
190                        .map(|x| x.to_string())
191                        .collect::<Vec<_>>()
192                        .join(", "),
193                    ClauseId::Query => unimplemented!(),
194                    ClauseId::Builtin(lit) => lit.substitute(&v.0).to_string(),
195                    ClauseId::NegationCheck(lit) => {
196                        format!("{} to have no proof", lit)
197                    }
198                };
199                let curr_attempt = format!(
200                    "{} requires {}",
201                    t.goal[*goal_id].literal,
202                    if requirement.is_empty() {
203                        "nothing, it's a fact.".to_owned().italic()
204                    } else {
205                        requirement.italic()
206                    }
207                );
208
209                if let Some(e) = &v.2.error {
210                    builder.begin_child(format!("{}", e.to_string().bright_red()));
211                } else {
212                    builder.begin_child(curr_attempt);
213                }
214                dfs(&v.2, rules, builder);
215                builder.end_child();
216            }
217
218            if t.goal.is_empty() {
219                builder.add_empty_child(format!("{}", "Success".green()));
220            }
221        }
222
223        let mut builder = TreeBuilder::new(format!(
224            "We require {}",
225            self.goal
226                .iter()
227                .map(|l| l.literal.to_string())
228                .collect::<Vec<_>>()
229                .join(", ")
230        ));
231        dfs(self, rules, &mut builder);
232        builder.build()
233    }
234}
235
236type Nd<'a> = (usize, &'a str);
237type Ed<'a> = (Nd<'a>, Nd<'a>, &'a str);
238
239pub struct Graph {
240    name: &'static str,
241    nodes: Vec<String>,
242    edges: Vec<(usize, usize, String)>, // labelled edges
243}
244
245impl<'a> dot::Labeller<'a, Nd<'a>, Ed<'a>> for Graph {
246    fn graph_id(&'a self) -> dot::Id<'a> {
247        dot::Id::new(self.name).unwrap()
248    }
249
250    fn node_id(&'a self, n: &Nd) -> dot::Id<'a> {
251        dot::Id::new(format!("N{}", n.0)).unwrap()
252    }
253
254    fn node_label(&'a self, n: &Nd<'a>) -> dot::LabelText<'a> {
255        dot::LabelText::LabelStr(n.1.into())
256    }
257
258    fn edge_label(&'a self, e: &Ed<'a>) -> dot::LabelText<'a> {
259        dot::LabelText::LabelStr(e.2.into())
260    }
261}
262
263impl<'a> dot::GraphWalk<'a, Nd<'a>, Ed<'a>> for Graph {
264    fn nodes(&'a self) -> dot::Nodes<'a, Nd<'a>> {
265        self.nodes.iter().map(|n| &n[..]).enumerate().collect()
266    }
267
268    fn edges(&'a self) -> dot::Edges<'a, Ed> {
269        self.edges
270            .iter()
271            .map(|(i, j, label)| {
272                (
273                    (*i, &self.nodes[*i][..]),
274                    (*j, &self.nodes[*j][..]),
275                    &label[..],
276                )
277            })
278            .collect()
279    }
280
281    fn source(&'a self, edge: &Ed<'a>) -> Nd {
282        edge.0
283    }
284
285    fn target(&'a self, edge: &Ed<'a>) -> Nd {
286        edge.1
287    }
288}
289
290/// A proof tree consist of
291/// - a clause
292/// - a valuation for this clause
293/// - proofs for parts of the clause body
294#[derive(Clone, Debug, PartialEq)]
295pub struct Proof {
296    pub clause: ClauseId,
297    pub valuation: Substitution,
298    pub children: Vec<Proof>,
299}
300
301impl Proof {
302    /// Returns the height of this proof tree, where a leaf node has height 0.
303    fn height(&self) -> usize {
304        self.children
305            .iter()
306            .map(|child| child.height() + 1)
307            .max()
308            .unwrap_or(0)
309    }
310
311    pub fn get_tree(
312        &self,
313        clauses: &Vec<Clause>,
314        pred_kind: &HashMap<Predicate, analysis::Kind>,
315        compact: bool,
316    ) -> impl TreeItem {
317        fn find_matching_end(
318            operator_start: &Literal,
319            start_index: usize,
320            children: &[Proof],
321        ) -> Option<usize> {
322            for i in start_index + 1..children.len() {
323                let child = &children[i];
324                if let ClauseId::Builtin(b) = &child.clause {
325                    if b.predicate.naive_predicate_kind().is_logic()
326                        && b.predicate.is_operator()
327                        && b.predicate.0.ends_with("_end")
328                        && b.predicate.0.replace("_end", "_begin") == operator_start.predicate.0
329                        && b.args.last() == operator_start.args.last()
330                    {
331                        return Some(i);
332                    }
333                }
334            }
335            None
336        }
337
338        fn dfs(
339            p: &Proof,
340            clauses: &Vec<Clause>,
341            builder: &mut TreeBuilder,
342            pred_kind: &HashMap<Predicate, analysis::Kind>,
343            compact: bool,
344        ) {
345            let mut prev_scope_start_index = 0;
346            let mut prev_scope_end_index = 0;
347            let mut dont_close: HashSet<usize> = HashSet::new();
348
349            for (i, child) in p.children.iter().enumerate() {
350                match &child.clause {
351                    ClauseId::Rule(rid) => {
352                        if !compact {
353                            let s = clauses[*rid].head.substitute(&child.valuation).to_string();
354                            builder.begin_child(format!(
355                                "{}",
356                                if pred_kind.get(&clauses[*rid].head.predicate)
357                                    == Some(&analysis::Kind::Image)
358                                {
359                                    s.cyan()
360                                } else {
361                                    s.normal()
362                                },
363                            ));
364                        }
365                        dfs(&child, clauses, builder, pred_kind, compact);
366                        if !compact {
367                            builder.end_child();
368                        }
369                    }
370                    ClauseId::Query => {
371                        builder.add_empty_child("query".to_string());
372                    }
373                    ClauseId::Builtin(b) => match b.predicate.naive_predicate_kind() {
374                        crate::analysis::Kind::Image => {
375                            builder.add_empty_child(format!(
376                                "{}",
377                                b.substitute(&child.valuation).to_string().cyan()
378                            ));
379                        }
380                        crate::analysis::Kind::Layer => {
381                            builder.add_empty_child(format!(
382                                "{}",
383                                b.substitute(&child.valuation).to_string()
384                            ));
385                        }
386                        crate::analysis::Kind::Logic => {
387                            if b.predicate.is_operator() {
388                                if b.predicate.0.ends_with("_begin") {
389                                    let curr_scope_end_index = find_matching_end(b, i, &p.children)
390                                        .expect("begin operator has an end");
391
392                                    let curr_scope_start_index = i;
393                                    if prev_scope_start_index + 1 == curr_scope_start_index
394                                        && curr_scope_end_index + 1 == prev_scope_end_index
395                                    {
396                                        // if we are directly inside the previous scope, we won't need to start a new scope
397                                        dont_close.insert(prev_scope_end_index);
398                                    } else {
399                                        builder.begin_child("(".to_string());
400                                    }
401                                    prev_scope_start_index = i;
402                                    prev_scope_end_index = curr_scope_end_index;
403                                } else {
404                                    if !dont_close.contains(&i) {
405                                        builder.end_child();
406                                    }
407
408                                    // HACK: Because we want to print the operators *after* their scope,
409                                    //       we add a leaf node here. It will look as expected in the term
410                                    //       but may not make sense as a DAG.
411                                    builder.add_empty_child(format!(
412                                        "{}::{}",
413                                        if dont_close.contains(&i) { " " } else { ")" },
414                                        b.substitute(&child.valuation).unmangle().to_string()
415                                    ));
416                                }
417                            }
418                        }
419                    },
420                    ClauseId::NegationCheck(_) => {} // negation checks are omitted from the proof tree
421                }
422            }
423        }
424
425        let mut builder = TreeBuilder::new("".to_string());
426        dfs(self, clauses, &mut builder, pred_kind, compact);
427
428        builder.build()
429    }
430
431    pub fn pretty_print(
432        &self,
433        clauses: &Vec<Clause>,
434        pred_kind: &HashMap<Predicate, analysis::Kind>,
435        compact: bool,
436    ) -> io::Result<()> {
437        print_tree(&self.get_tree(clauses, pred_kind, compact))
438    }
439}
440
441impl PartialOrd for Proof {
442    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
443        self.height().partial_cmp(&other.height())
444    }
445}
446
447impl Substitute<IRTerm> for GoalWithHistory {
448    type Output = GoalWithHistory;
449    fn substitute(&self, s: &Substitution<IRTerm>) -> Self::Output {
450        self.iter()
451            .map(
452                |LiteralWithHistory {
453                     literal,
454                     introduction,
455                     origin,
456                 }| LiteralWithHistory {
457                    literal: literal.substitute(s),
458                    introduction: *introduction,
459                    origin: origin.clone(),
460                },
461            )
462            .collect()
463    }
464}
465
466#[derive(Clone, Eq, PartialEq, Debug, Hash)]
467pub enum ResolutionError {
468    /// Contains the literal with the unknown predicate name.
469    UnknownPredicate(Literal),
470    /// Contains the relevant goals.
471    InsufficientGroundness(Vec<Literal>),
472    /// Contains the goals when the max depth was exceeded.
473    MaximumDepthExceeded(Vec<Literal>, usize),
474    /// Contains the relevant literal (builtin call), and the name of the selected builtin.
475    BuiltinFailure(Literal, &'static str),
476    /// Contains the literal that didn't match with any rule head.
477    InsufficientRules(Literal),
478    /// Contains the set of inconsistent signatures.
479    InconsistentGroundnessSignature(Vec<Signature>),
480    /// Proof of a negated literal was found.
481    NegationProof(Literal),
482}
483
484impl fmt::Display for ResolutionError {
485    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
486        match self {
487            ResolutionError::UnknownPredicate(literal) => {
488                if literal.predicate.is_operator() {
489                    write!(f, "unknown operator - {}", literal.predicate)
490                } else {
491                    write!(f, "unknown predicate - {}", literal.predicate)
492                }
493            }
494            ResolutionError::InsufficientGroundness(literals) => {
495                write!(f, "insufficient groundness for {} goal(s)", literals.len())
496            }
497            ResolutionError::MaximumDepthExceeded(_, max_depth) => {
498                write!(f, "exceeded maximum depth of {}", max_depth)
499            }
500            ResolutionError::BuiltinFailure(_, builtin_name) => {
501                write!(f, "builtin {} failed to apply or unify", builtin_name)
502            }
503            ResolutionError::InsufficientRules(literal) => write!(
504                f,
505                "could not find a rule to resolve with literal {}",
506                literal
507            ),
508            ResolutionError::InconsistentGroundnessSignature(signatures) => write!(
509                f,
510                "{} clause(s) have inconsistent signatures",
511                signatures.len()
512            ),
513            ResolutionError::NegationProof(lit) => {
514                write!(f, "A proof was found for {}", lit.negated())
515            }
516        }
517    }
518}
519
520impl ResolutionError {
521    pub fn get_diagnostic(self) -> Diagnostic<()> {
522        fn get_position_labels(literals: &[Literal]) -> Vec<Label<()>> {
523            literals
524                .iter()
525                .filter_map(|lit| lit.position.as_ref())
526                .map(|pos| Label::primary((), pos.offset..(pos.offset + pos.length)))
527                .collect()
528        }
529
530        // Displays literals that don't have a span in the notes of the diagnostic.
531        fn get_notes(literals: &[Literal]) -> Vec<String> {
532            literals
533                .iter()
534                .filter_map(|lit| {
535                    if lit.position.is_none() {
536                        Some(lit.to_string())
537                    } else {
538                        None
539                    }
540                })
541                .collect()
542        }
543
544        let message = self.to_string();
545        let (labels, notes, severity) = match self {
546            ResolutionError::UnknownPredicate(literal) => (
547                get_position_labels(&[literal.clone()]),
548                get_notes(&[literal]),
549                Severity::Error,
550            ),
551            ResolutionError::InsufficientGroundness(literals) => (
552                get_position_labels(&literals),
553                get_notes(&literals),
554                Severity::Warning,
555            ),
556            ResolutionError::MaximumDepthExceeded(literals, _) => (
557                get_position_labels(&literals),
558                get_notes(&literals),
559                Severity::Warning,
560            ),
561            ResolutionError::BuiltinFailure(literal, _) => (
562                get_position_labels(&[literal.clone()]),
563                get_notes(&[literal]),
564                Severity::Warning,
565            ),
566            ResolutionError::InsufficientRules(literal) => (
567                get_position_labels(&[literal.clone()]),
568                get_notes(&[literal]),
569                Severity::Warning,
570            ),
571            ResolutionError::InconsistentGroundnessSignature(sigs) => (
572                Vec::new(),
573                sigs.iter().map(|sig| sig.to_string()).collect(),
574                Severity::Error,
575            ),
576            ResolutionError::NegationProof(lit) => (
577                get_position_labels(&[lit.clone()]),
578                get_notes(&[lit]),
579                Severity::Warning,
580            ),
581        };
582
583        Diagnostic::new(severity)
584            .with_message(message)
585            .with_labels(labels)
586            .with_notes(notes)
587    }
588}
589
590/// Result of building the SLD tree.
591///
592/// Uses a custom result type in resolution since we often have some information about
593/// either state, an 'Ok' and 'Err' state is too disjoint for our purposes.
594pub struct SLDResult {
595    pub tree: Tree,
596    pub errors: HashSet<ResolutionError>,
597}
598
599impl From<SLDResult> for Result<Tree, Vec<Diagnostic<()>>> {
600    fn from(sld_result: SLDResult) -> Self {
601        if sld_result.tree.is_success() {
602            Ok(sld_result.tree)
603        } else {
604            Err(sld_result
605                .errors
606                .into_iter()
607                .map(ResolutionError::get_diagnostic)
608                .collect::<Vec<_>>())
609        }
610    }
611}
612
613/// Returns a tree that contains both successful and failed paths, also, any resolution errors.
614/// To save on memory usage, can avoid storing the failed paths by passing false to `store_full_tree`.
615pub fn sld(
616    rules: &[Clause<IRTerm>],
617    goal: &Goal,
618    maxdepth: TreeLevel,
619    store_full_tree: bool,
620) -> SLDResult {
621    /// Select leftmost literal with compatible groundness.
622    fn select(
623        goal: &GoalWithHistory,
624        grounded: &HashMap<Signature, Vec<bool>>,
625    ) -> Result<(LiteralGoalId, LiteralWithHistory), ResolutionError> {
626        for (id, lit) in goal.iter().enumerate() {
627            // TODO: could rewrite this to enumerate the different cases more explicitly.
628
629            let literal = &lit.literal;
630
631            // A negated literal must have only constants or anonymous variables (which represent
632            // variables that will not equal any other).
633            // Otherwise, something like !string_eq("constant", X) would be pointless, the
634            // user very likely means X to be bound through some other literal.
635            // An alternative approach would be to check other variables in the goal.
636            let positive_or_grounded_negation = literal.positive
637                || literal
638                    .args
639                    .iter()
640                    .all(|arg| arg.is_constant() || arg.is_underlying_anonymous_variable());
641
642            let select_builtin_res = builtin::select_builtin(literal);
643            if select_builtin_res.0.is_match() && positive_or_grounded_negation {
644                return Ok((id, lit.clone()));
645            }
646
647            // For any user-defined atom, we can get its groundness requirement
648            // (computed outside), and if a particular argument can not be
649            // ungrounded (grounded[arg_index] == false), variables will not be
650            // allowed there.
651            let lit_grounded = grounded.get(&literal.signature());
652            if let Some(lit_grounded) = lit_grounded {
653                debug_assert_eq!(lit_grounded.len(), literal.args.len());
654                if positive_or_grounded_negation
655                    && literal.args.iter().zip(lit_grounded.iter()).all(
656                        |(term, allows_ungrounded)| {
657                            *allows_ungrounded || term.is_constant_or_compound_constant()
658                        },
659                    )
660                {
661                    return Ok((id, lit.clone()));
662                } else {
663                    continue;
664                }
665            } else if select_builtin_res.0 == SelectBuiltinResult::GroundnessMismatch
666                || (select_builtin_res.0 == SelectBuiltinResult::Match
667                    && !positive_or_grounded_negation)
668            {
669                continue;
670            }
671
672            return Err(ResolutionError::UnknownPredicate(literal.clone()));
673        }
674
675        Err(ResolutionError::InsufficientGroundness(
676            goal.iter().map(|lit| lit.literal.clone()).collect(),
677        ))
678    }
679
680    /// NOTE: the new goals are added *first*, so the behaviour of SLD changes (but not the
681    /// semantics, I think), making it more 'eager' to resolve.
682    /// This makes it possible to get significant performance boosts by placing ground facts first
683    /// in the body of some expression, in your Modusfile(s).
684    /// For example, `fact(c), expensive_goal(c)`, may waste a lot of time and memory if `fact(c)` is
685    /// not true. With the 'eager' approach, SLD will quickly terminate if `fact(c)` is false.
686    fn resolve(
687        lid: LiteralGoalId,
688        rid: ClauseId,
689        goal: &GoalWithHistory,
690        mgu: &Substitution,
691        rule: &Clause,
692        level: TreeLevel,
693    ) -> GoalWithHistory {
694        let new_goals = rule.body.iter().enumerate().map(|(id, l)| {
695            let origin = LiteralOrigin {
696                clause: rid.clone(),
697                body_index: id,
698            };
699            LiteralWithHistory {
700                literal: l.clone(),
701                introduction: level,
702                origin,
703            }
704        });
705        let g = new_goals
706            .chain(goal.into_iter().enumerate().filter_map(|(i, v)| {
707                if i != lid {
708                    Some(v.clone())
709                } else {
710                    None
711                }
712            }))
713            .collect::<Vec<_>>();
714        g.substitute(mgu)
715    }
716
717    fn handle_negated_literal(
718        lid: LiteralGoalId,
719        l: LiteralWithHistory,
720        goal: &GoalWithHistory,
721        rules: &[Clause<IRTerm>],
722        maxdepth: TreeLevel,
723        level: TreeLevel,
724        grounded: &HashMap<Signature, Vec<bool>>,
725        store_full_tree: bool,
726    ) -> SLDResult {
727        let mut errs: HashSet<ResolutionError> = HashSet::new();
728
729        let singleton_goal = vec![LiteralWithHistory {
730            literal: l.literal.negated(),
731            ..l
732        }];
733
734        // Perform SLD resolution with this goal and check if it succeeds or not.
735        let sld_res = inner(
736            rules,
737            &singleton_goal,
738            // The stratifiability check should make it safe to use the same maxdepth.
739            maxdepth,
740            0,
741            grounded,
742            store_full_tree,
743        );
744
745        let rid = ClauseId::NegationCheck(l.literal.negated());
746        let mgu = HashMap::new();
747        let renaming = HashMap::new();
748
749        let mut success_resolvents = HashMap::new();
750        let mut fail_resolvents = HashMap::new();
751        if sld_res.tree.is_success() {
752            if store_full_tree {
753                fail_resolvents.insert((lid, rid), (mgu, renaming, sld_res.tree));
754            }
755
756            let err = ResolutionError::NegationProof(l.literal);
757            errs.insert(err.clone());
758            let tree = Tree {
759                goal: goal.to_owned(),
760                level,
761                success_resolvents,
762                fail_resolvents,
763                error: Some(err),
764            };
765
766            return SLDResult { tree, errors: errs };
767        } else {
768            let resolvent = resolve(
769                lid,
770                rid.clone(),
771                goal,
772                &mgu,
773                &Clause {
774                    head: l.literal,
775                    body: Vec::new(),
776                },
777                level + 1,
778            );
779            let SLDResult { tree, errors } = inner(
780                rules,
781                &resolvent,
782                maxdepth,
783                level + 1,
784                grounded,
785                store_full_tree,
786            );
787
788            if tree.is_success() {
789                success_resolvents.insert((lid, rid), (mgu, renaming, tree));
790            } else if store_full_tree {
791                fail_resolvents.insert((lid, rid), (mgu, renaming, tree));
792            }
793            errs.extend(errors);
794
795            let tree = Tree {
796                goal: goal.to_owned(),
797                level,
798                success_resolvents,
799                fail_resolvents,
800                error: None,
801            };
802
803            return SLDResult { tree, errors: errs };
804        }
805    }
806
807    fn inner(
808        rules: &[Clause<IRTerm>],
809        goal: &GoalWithHistory,
810        maxdepth: TreeLevel,
811        level: TreeLevel,
812        grounded: &HashMap<Signature, Vec<bool>>,
813        store_full_tree: bool,
814    ) -> SLDResult {
815        if goal.is_empty() {
816            let t = Tree {
817                goal: goal.to_owned(),
818                level,
819                success_resolvents: HashMap::new(),
820                fail_resolvents: HashMap::new(),
821                error: None,
822            };
823            SLDResult {
824                tree: t,
825                errors: HashSet::new(),
826            }
827        } else if level >= maxdepth {
828            let error = ResolutionError::MaximumDepthExceeded(
829                goal.iter()
830                    .map(|lit_hist| lit_hist.literal.clone())
831                    .collect(),
832                maxdepth,
833            );
834            let t = Tree {
835                goal: goal.to_owned(),
836                level,
837                success_resolvents: HashMap::default(),
838                fail_resolvents: HashMap::default(),
839                error: Some(error.clone()),
840            };
841            let errors = vec![error].into_iter().collect();
842            SLDResult { tree: t, errors }
843        } else {
844            let selection_res = select(goal, grounded);
845            if let Err(e) = selection_res {
846                let t = Tree {
847                    goal: goal.to_owned(),
848                    level,
849                    success_resolvents: HashMap::default(),
850                    fail_resolvents: HashMap::default(),
851                    error: Some(e.clone()),
852                };
853                return SLDResult {
854                    tree: t,
855                    errors: vec![e].into_iter().collect(),
856                };
857            }
858            let (lid, l) = selection_res.unwrap();
859
860            if !l.literal.positive {
861                return handle_negated_literal(
862                    lid,
863                    l,
864                    goal,
865                    rules,
866                    maxdepth,
867                    level,
868                    grounded,
869                    store_full_tree,
870                );
871            }
872
873            let mut errs: HashSet<ResolutionError> = HashSet::new();
874
875            let selected_builtin = builtin::select_builtin(&l.literal);
876            let builtin_resolves = match selected_builtin {
877                (SelectBuiltinResult::Match, lit) => lit,
878                _ => None,
879            }
880            .and_then(|pred| pred.apply(&l.literal))
881            .and_then(|unify_cand| {
882                unify_cand.unify(&l.literal).map(|mgu| {
883                    (
884                        ClauseId::Builtin(unify_cand.clone()),
885                        mgu.clone(),
886                        Substitution::<IRTerm>::new(),
887                        resolve(
888                            lid,
889                            ClauseId::Builtin(unify_cand.clone()),
890                            goal,
891                            &mgu,
892                            &Clause {
893                                head: unify_cand,
894                                body: Vec::new(), // TODO: allow builtin rules to return more conditions?
895                            },
896                            level + 1,
897                        ),
898                    )
899                })
900            });
901
902            let mut leaf_error = None;
903            if selected_builtin.0.is_match() && builtin_resolves.is_none() {
904                let err = ResolutionError::BuiltinFailure(
905                    l.literal.clone(),
906                    selected_builtin
907                        .1
908                        .expect("match should provide builtin")
909                        .name(),
910                );
911                errs.insert(err.clone());
912                leaf_error = Some(err);
913            }
914
915            let user_rules_resolves = rules
916                .iter()
917                .enumerate()
918                .filter(|(_, c)| c.head.signature() == l.literal.signature())
919                .map(|(rid, c)| (ClauseId::Rule(rid), c.rename_with_sub()))
920                .filter_map(|(rid, (c, renaming))| {
921                    c.head.unify(&l.literal).map(|mgu| {
922                        (
923                            rid.clone(),
924                            mgu.clone(),
925                            renaming,
926                            resolve(lid, rid, goal, &mgu, &c, level + 1),
927                        )
928                    })
929                })
930                .collect::<Vec<_>>();
931            if !selected_builtin.0.is_match() && user_rules_resolves.is_empty() {
932                let err = ResolutionError::InsufficientRules(l.literal.clone());
933                errs.insert(err.clone());
934                leaf_error = leaf_error.or(Some(err));
935            }
936
937            let mut success_resolvents: HashMap<
938                (LiteralGoalId, ClauseId),
939                (Substitution, Substitution, Tree),
940            > = HashMap::new();
941            let mut fail_resolvents: HashMap<
942                (LiteralGoalId, ClauseId),
943                (Substitution, Substitution, Tree),
944            > = HashMap::new();
945            for (rid, mgu, renaming, resolvent) in
946                builtin_resolves.into_iter().chain(user_rules_resolves)
947            {
948                let SLDResult { tree, errors } = inner(
949                    rules,
950                    &resolvent,
951                    maxdepth,
952                    level + 1,
953                    grounded,
954                    store_full_tree,
955                );
956                if tree.is_success() {
957                    success_resolvents.insert((lid, rid), (mgu, renaming, tree));
958                } else if store_full_tree {
959                    fail_resolvents.insert((lid, rid), (mgu, renaming, tree));
960                }
961                errs.extend(errors);
962            }
963
964            let tree = Tree {
965                goal: goal.to_owned(),
966                level,
967                success_resolvents,
968                fail_resolvents,
969                error: leaf_error,
970            };
971
972            SLDResult { tree, errors: errs }
973        }
974    }
975
976    let grounded_result = wellformed::check_grounded_variables(rules);
977    let goal_with_history = goal
978        .iter()
979        .enumerate()
980        .map(|(id, l)| {
981            let origin = LiteralOrigin {
982                clause: ClauseId::Query,
983                body_index: id,
984            };
985            LiteralWithHistory {
986                literal: l.clone(),
987                introduction: 0,
988                origin,
989            }
990        })
991        .collect();
992    match grounded_result {
993        Ok(grounded) => inner(
994            rules,
995            &goal_with_history,
996            maxdepth,
997            0,
998            &grounded,
999            store_full_tree,
1000        ),
1001        Err(e) => SLDResult {
1002            tree: Tree {
1003                goal: goal_with_history,
1004                level: 0,
1005                success_resolvents: HashMap::default(),
1006                fail_resolvents: HashMap::default(),
1007                error: Some(ResolutionError::InconsistentGroundnessSignature(
1008                    e.iter().cloned().collect(),
1009                )),
1010            },
1011            errors: vec![ResolutionError::InconsistentGroundnessSignature(
1012                e.into_iter().collect(),
1013            )]
1014            .into_iter()
1015            .collect(),
1016        },
1017    }
1018}
1019
1020pub fn solutions(tree: &Tree) -> HashSet<Goal> {
1021    fn inner(tree: &Tree) -> Vec<Substitution> {
1022        if tree.goal.is_empty() {
1023            let s = Substitution::new();
1024            return vec![s];
1025        }
1026        tree.success_resolvents
1027            .iter()
1028            .map(|(_, (mgu, _, subtree))| (mgu, inner(subtree)))
1029            .map(|(mgu, sub)| {
1030                sub.iter()
1031                    .map(|s| compose_extend(mgu, s))
1032                    .collect::<Vec<Substitution<IRTerm>>>()
1033            })
1034            .flatten()
1035            .collect()
1036    }
1037    inner(tree)
1038        .iter()
1039        .map(|s| {
1040            tree.goal
1041                .iter()
1042                .map(
1043                    |LiteralWithHistory {
1044                         literal,
1045                         introduction: _,
1046                         origin: _,
1047                     }| literal.substitute(s),
1048                )
1049                .collect()
1050        })
1051        .collect()
1052}
1053
1054#[derive(Clone)]
1055struct PathNode {
1056    resolvent: GoalWithHistory,
1057    applied: ClauseId,
1058    selected: LiteralGoalId,
1059    renaming: Substitution,
1060}
1061
1062// sequence of nodes and global mgu
1063type Path = (Vec<PathNode>, Substitution);
1064
1065pub fn proofs(tree: &Tree, rules: &[Clause], goal: &Goal) -> HashMap<Goal, Proof> {
1066    fn flatten_compose(
1067        lid: &LiteralGoalId,
1068        cid: &ClauseId,
1069        mgu: &Substitution,
1070        renaming: &Substitution,
1071        tree: &Tree,
1072    ) -> Vec<Path> {
1073        if tree.goal.is_empty() {
1074            return vec![(
1075                vec![PathNode {
1076                    resolvent: tree.goal.clone(),
1077                    applied: cid.clone(),
1078                    selected: *lid,
1079                    renaming: renaming.clone(),
1080                }],
1081                mgu.clone(),
1082            )];
1083        }
1084        tree.success_resolvents
1085            .iter()
1086            .map(|((sub_lid, sub_cid), (sub_mgu, sub_renaming, sub_tree))| {
1087                flatten_compose(sub_lid, sub_cid, sub_mgu, sub_renaming, sub_tree)
1088                    .iter()
1089                    .map(|(sub_path, sub_val)| {
1090                        let mut nodes = vec![PathNode {
1091                            resolvent: tree.goal.clone(),
1092                            applied: cid.clone(),
1093                            selected: *lid,
1094                            renaming: renaming.clone(),
1095                        }];
1096                        let val = compose_extend(mgu, sub_val);
1097                        nodes.extend(sub_path.clone());
1098                        (nodes, val)
1099                    })
1100                    .collect::<Vec<Path>>()
1101            })
1102            .flatten()
1103            .collect()
1104    }
1105    // reconstruct proof for a given tree level
1106    fn proof_for_level(
1107        path: &[PathNode],
1108        mgu: &Substitution,
1109        rules: &[Clause],
1110        level: TreeLevel,
1111    ) -> Proof {
1112        let mut sublevels_map: HashMap<usize, TreeLevel> = HashMap::new();
1113        for l in 0..path.len() {
1114            if !path[l].resolvent.is_empty() {
1115                let resolved_child = path[l].resolvent[path[l + 1].selected].clone();
1116                if resolved_child.introduction == level {
1117                    sublevels_map.insert(resolved_child.origin.body_index, l + 1);
1118                }
1119            }
1120        }
1121        let children_length = sublevels_map.len();
1122        match path[level].applied {
1123            ClauseId::Query => assert_eq!(children_length, path[0].resolvent.len()),
1124            ClauseId::Rule(rid) => assert_eq!(children_length, rules[rid].body.len()),
1125            ClauseId::Builtin(_) => assert_eq!(children_length, 0),
1126            // There shouldn't be a subtree here since the tree is currently only stored
1127            // if the negation check failed (i.e. we found a proof).
1128            ClauseId::NegationCheck(_) => assert_eq!(children_length, 0),
1129        };
1130
1131        let mut sublevels = Vec::<TreeLevel>::with_capacity(sublevels_map.len());
1132        for k in sublevels_map.keys() {
1133            assert!(*k < children_length);
1134        }
1135        for i in 0..children_length {
1136            sublevels.push(*sublevels_map.get(&i).unwrap());
1137        }
1138        Proof {
1139            clause: path[level].applied.clone(),
1140            valuation: compose_no_extend(&path[level].renaming, mgu),
1141            children: sublevels
1142                .iter()
1143                .map(|l| proof_for_level(path, mgu, rules, *l))
1144                .collect(),
1145        }
1146    }
1147    // assume lid of root is 0, as if it came from a clause "true :- goal" for query "true", but this is not used anyway
1148    let goal_vars = goal
1149        .iter()
1150        .map(|l| l.variables(true))
1151        .reduce(|mut l, r| {
1152            l.extend(r);
1153            l
1154        })
1155        .unwrap_or_default();
1156    let goal_id_renaming: Substitution<IRTerm> =
1157        goal_vars.iter().map(|v| (v.clone(), v.clone())).collect();
1158    let paths = flatten_compose(
1159        &0,
1160        &ClauseId::Query,
1161        &Substitution::new(),
1162        &goal_id_renaming,
1163        tree,
1164    );
1165    let all_proofs: Vec<Proof> = paths
1166        .iter()
1167        .map(|(path, mgu)| proof_for_level(path, mgu, rules, 0))
1168        .collect();
1169
1170    let mut solution_to_proof_tree: HashMap<Goal, Proof> = HashMap::new();
1171    for p in all_proofs {
1172        let solution: Goal = goal.substitute(&p.valuation);
1173        // keeps the minimal proof tree
1174        if let Some(existing_proof) = solution_to_proof_tree.get(&solution) {
1175            if existing_proof <= &p {
1176                continue;
1177            }
1178        }
1179        solution_to_proof_tree.insert(solution, p);
1180    }
1181    solution_to_proof_tree
1182}
1183
1184pub fn tree_from_modusfile(
1185    mf: Modusfile,
1186    query: modusfile::Expression,
1187    max_depth: usize,
1188    full_tree: bool,
1189) -> (Goal, Vec<Clause>, SLDResult) {
1190    // 1. Create a new clause with a nullary goal '_query', with a body of the user's query.
1191    // 2. Translate this and other clauses.
1192    // 3. Use the body of the IR clause with the '_query' head predicate as the goal.
1193
1194    let goal_pred = Predicate("_query".to_owned());
1195    let user_clause = modusfile::ModusClause {
1196        head: Literal {
1197            positive: true,
1198            position: None,
1199            predicate: goal_pred.clone(),
1200            args: Vec::new(),
1201        },
1202        body: Some(query),
1203    };
1204    let clauses: Vec<Clause> = translate_modusfile(&Modusfile(
1205        mf.0.into_iter().chain(iter::once(user_clause)).collect(),
1206    ));
1207
1208    let q_clause = clauses
1209        .iter()
1210        .find(|c| c.head.predicate == goal_pred)
1211        .expect("should find same predicate name after translation");
1212    let goal = &q_clause.body;
1213
1214    (
1215        goal.clone(),
1216        clauses.clone(),
1217        sld(&clauses, &goal, max_depth, full_tree),
1218    )
1219}
1220
1221#[cfg(test)]
1222mod tests {
1223    use crate::{
1224        logic::SpannedPosition,
1225        modusfile::{Expression, FormatStringFragment, ModusTerm},
1226    };
1227
1228    use super::*;
1229    use serial_test::serial;
1230
1231    // These tests all need to be marked as serial even though they don't
1232    // interfere with each other, because they can potentially modify
1233    // logic::AVAILABLE_VARIABLE_INDEX, but tests in the translate module depend
1234    // on the variable not being changed by something else during execution.
1235
1236    fn contains_ignoring_position(it: &HashSet<Vec<Literal>>, lits: &Vec<Literal>) -> bool {
1237        it.iter().any(|curr_lits| {
1238            curr_lits.len() == lits.len()
1239                && curr_lits
1240                    .iter()
1241                    .zip(lits)
1242                    .all(|(l1, l2)| l1.eq_ignoring_position(&l2))
1243        })
1244    }
1245
1246    #[test]
1247    #[serial]
1248    fn simple_solving() {
1249        let goal: Goal<logic::IRTerm> = vec!["a(X)".parse().unwrap()];
1250        let clauses: Vec<logic::Clause> = vec![
1251            "a(X) :- b(X).".parse().unwrap(),
1252            logic::Clause {
1253                head: "b(\"c\")".parse().unwrap(),
1254                body: vec![],
1255            },
1256            logic::Clause {
1257                head: "b(\"d\")".parse().unwrap(),
1258                body: vec![],
1259            },
1260        ];
1261        let tree = sld(&clauses, &goal, 10, true).tree;
1262        let solutions = solutions(&tree);
1263        assert_eq!(solutions.len(), 2);
1264
1265        assert!(contains_ignoring_position(
1266            &solutions,
1267            &vec!["a(\"c\")".parse::<logic::Literal>().unwrap()]
1268        ));
1269        assert!(contains_ignoring_position(
1270            &solutions,
1271            &vec!["a(\"d\")".parse::<logic::Literal>().unwrap()]
1272        ));
1273    }
1274
1275    #[test]
1276    #[serial]
1277    fn simple_solving_with_escape_chars() {
1278        let goal: Goal<logic::IRTerm> = vec!["a".parse().unwrap()];
1279        let clauses: Vec<logic::Clause> = vec![
1280            r#"a :- b("\n")."#.parse().unwrap(),
1281            logic::Clause {
1282                head: r#"b("\n")"#.parse().unwrap(),
1283                body: vec![],
1284            },
1285        ];
1286        let tree = sld(&clauses, &goal, 10, true).tree;
1287        let solutions = solutions(&tree);
1288        assert_eq!(solutions.len(), 1);
1289
1290        assert!(contains_ignoring_position(
1291            &solutions,
1292            &vec![r#"a"#.parse::<logic::Literal>().unwrap()]
1293        ));
1294    }
1295
1296    #[test]
1297    #[serial]
1298    fn simple_negation_solving() {
1299        let goal: Goal<logic::IRTerm> = vec!["a(\"c\")".parse().unwrap()];
1300        let clauses: Vec<logic::Clause> = vec![
1301            "a(X) :- !b(X).".parse().unwrap(),
1302            "b(\"d\").".parse().unwrap(),
1303        ];
1304        let sld_res = sld(&clauses, &goal, 10, true);
1305        let tree = sld_res.tree;
1306        let solutions = solutions(&tree);
1307        assert_eq!(solutions.len(), 1);
1308
1309        assert!(contains_ignoring_position(
1310            &solutions,
1311            &vec!["a(\"c\")".parse::<logic::Literal>().unwrap()]
1312        ));
1313    }
1314
1315    #[test]
1316    #[serial]
1317    fn simple_nongrounded() {
1318        let goal: Goal<logic::IRTerm> = vec!["a(\"b\")".parse().unwrap()];
1319        let clauses: Vec<logic::Clause> = vec![logic::Clause {
1320            head: "a(X)".parse().unwrap(),
1321            body: vec![],
1322        }];
1323        let tree = sld(&clauses, &goal, 10, true).tree;
1324        let solutions = solutions(&tree);
1325        assert_eq!(solutions.len(), 1);
1326        assert!(contains_ignoring_position(
1327            &solutions,
1328            &vec!["a(\"b\")".parse::<logic::Literal>().unwrap()]
1329        ));
1330    }
1331
1332    #[test]
1333    #[serial]
1334    fn simple_nongrounded_invalid() {
1335        let goal: Goal<logic::IRTerm> = vec!["a(X)".parse().unwrap()];
1336        let clauses: Vec<logic::Clause> = vec![logic::Clause {
1337            head: "a(X)".parse().unwrap(),
1338            body: vec![],
1339        }];
1340        let result = sld(&clauses, &goal, 10, true);
1341        assert_eq!(
1342            vec![ResolutionError::InsufficientGroundness(goal)],
1343            result.errors.into_iter().collect::<Vec<_>>()
1344        )
1345    }
1346
1347    #[test]
1348    #[serial]
1349    fn complex_goal() {
1350        let goal: Goal<logic::IRTerm> = vec!["a(X)".parse().unwrap(), "b(X)".parse().unwrap()];
1351        let clauses: Vec<logic::Clause> = vec![
1352            logic::Clause {
1353                head: "a(\"t\")".parse().unwrap(),
1354                body: vec![],
1355            },
1356            logic::Clause {
1357                head: "a(\"f\")".parse().unwrap(),
1358                body: vec![],
1359            },
1360            logic::Clause {
1361                head: "b(\"g\")".parse().unwrap(),
1362                body: vec![],
1363            },
1364            logic::Clause {
1365                head: "b(\"t\")".parse().unwrap(),
1366                body: vec![],
1367            },
1368        ];
1369        let tree = sld(&clauses, &goal, 10, true).tree;
1370        let solutions = solutions(&tree);
1371        assert_eq!(solutions.len(), 1);
1372        assert!(contains_ignoring_position(
1373            &solutions,
1374            &vec!["a(\"t\")".parse().unwrap(), "b(\"t\")".parse().unwrap()]
1375        ));
1376    }
1377
1378    #[test]
1379    #[serial]
1380    fn solving_with_binary_relations() {
1381        let goal: Goal<logic::IRTerm> = vec!["a(X)".parse().unwrap()];
1382        let clauses: Vec<logic::Clause> = vec![
1383            "a(X) :- b(X, Y), c(Y).".parse().unwrap(),
1384            logic::Clause {
1385                head: "b(\"t\", \"f\")".parse().unwrap(),
1386                body: vec![],
1387            },
1388            logic::Clause {
1389                head: "b(\"f\", \"t\")".parse().unwrap(),
1390                body: vec![],
1391            },
1392            logic::Clause {
1393                head: "b(\"g\", \"t\")".parse().unwrap(),
1394                body: vec![],
1395            },
1396            logic::Clause {
1397                head: "c(\"t\")".parse().unwrap(),
1398                body: vec![],
1399            },
1400        ];
1401        let tree = sld(&clauses, &goal, 10, true).tree;
1402        let solutions = solutions(&tree);
1403        assert_eq!(solutions.len(), 2);
1404        assert!(contains_ignoring_position(
1405            &solutions,
1406            &vec!["a(\"f\")".parse().unwrap()]
1407        ));
1408        assert!(contains_ignoring_position(
1409            &solutions,
1410            &vec!["a(\"g\")".parse().unwrap()]
1411        ));
1412    }
1413
1414    #[test]
1415    #[serial]
1416    fn simple_recursion() {
1417        let goal: Goal<logic::IRTerm> = vec!["reach(\"a\", X)".parse().unwrap()];
1418        let clauses: Vec<logic::Clause> = vec![
1419            "reach(X, Y) :- reach(X, Z), arc(Z, Y).".parse().unwrap(),
1420            "reach(X, Y) :- arc(X, Y).".parse().unwrap(),
1421            logic::Clause {
1422                head: "arc(\"a\", \"b\")".parse().unwrap(),
1423                body: vec![],
1424            },
1425            logic::Clause {
1426                head: "arc(\"b\", \"c\")".parse().unwrap(),
1427                body: vec![],
1428            },
1429            logic::Clause {
1430                head: "arc(\"c\", \"d\")".parse().unwrap(),
1431                body: vec![],
1432            },
1433            logic::Clause {
1434                head: "arc(\"d\", \"e\")".parse().unwrap(),
1435                body: vec![],
1436            },
1437            logic::Clause {
1438                head: "arc(\"f\", \"e\")".parse().unwrap(),
1439                body: vec![],
1440            },
1441            logic::Clause {
1442                head: "arc(\"g\", \"f\")".parse().unwrap(),
1443                body: vec![],
1444            },
1445            logic::Clause {
1446                head: "arc(\"g\", \"a\")".parse().unwrap(),
1447                body: vec![],
1448            },
1449        ];
1450        let tree = sld(&clauses, &goal, 15, true).tree;
1451        let solutions = solutions(&tree);
1452        assert_eq!(solutions.len(), 4);
1453        assert!(contains_ignoring_position(
1454            &solutions,
1455            &vec!["reach(\"a\", \"b\")".parse().unwrap()]
1456        ));
1457        assert!(contains_ignoring_position(
1458            &solutions,
1459            &vec!["reach(\"a\", \"c\")".parse().unwrap()]
1460        ));
1461        assert!(contains_ignoring_position(
1462            &solutions,
1463            &vec!["reach(\"a\", \"d\")".parse().unwrap()]
1464        ));
1465        assert!(contains_ignoring_position(
1466            &solutions,
1467            &vec!["reach(\"a\", \"e\")".parse().unwrap()]
1468        ));
1469    }
1470
1471    #[test]
1472    #[serial]
1473    fn string_concat() {
1474        let goal: Goal<logic::IRTerm> =
1475            vec!["string_concat(\"hello\", \"world\", X)".parse().unwrap()];
1476        let clauses: Vec<logic::Clause> = vec![];
1477        let tree = sld(&clauses, &goal, 10, true).tree;
1478        let solutions = solutions(&tree);
1479        assert_eq!(solutions.len(), 1);
1480        assert!(contains_ignoring_position(
1481            &solutions,
1482            &vec!["string_concat(\"hello\", \"world\", \"helloworld\")"
1483                .parse()
1484                .unwrap()]
1485        ));
1486    }
1487
1488    #[test]
1489    #[serial]
1490    fn string_concat_complex() {
1491        // TODO: figure out how to test empty string
1492        let good = ["aaabbb", "aabb"];
1493        let bad = ["aab", "a", "bb"];
1494        for (s, is_good) in good
1495            .iter()
1496            .map(|x| (*x, true))
1497            .chain(bad.iter().map(|x| (*x, false)))
1498        {
1499            let goal: Goal<logic::IRTerm> = vec![format!("a(\"{}\")", s).parse().unwrap()];
1500            let clauses: Vec<logic::Clause> = vec![
1501                logic::Clause {
1502                    head: "a(\"ab\")".parse().unwrap(),
1503                    body: vec![],
1504                },
1505                "a(S) :- string_concat(\"a\", X, S), string_concat(Y, \"b\", X), a(Y)"
1506                    .parse()
1507                    .unwrap(),
1508            ];
1509            let tree_res = sld(&clauses, &goal, 50, true);
1510            if is_good {
1511                let solutions = solutions(&tree_res.tree);
1512                assert_eq!(solutions.len(), 1);
1513                assert!(contains_ignoring_position(
1514                    &solutions,
1515                    &vec![format!("a(\"{}\")", s).parse().unwrap()]
1516                ));
1517            } else {
1518                assert!(!tree_res.tree.is_success());
1519            }
1520        }
1521    }
1522
1523    #[test]
1524    #[serial]
1525    fn leaf_height_is_zero() {
1526        let proof = Proof {
1527            clause: ClauseId::Rule(0),
1528            valuation: HashMap::default(),
1529            children: Vec::new(),
1530        };
1531        assert_eq!(proof.height(), 0);
1532    }
1533
1534    #[test]
1535    #[serial]
1536    fn proof_minimality() {
1537        let goal: Goal<logic::IRTerm> = vec!["foo(X)".parse().unwrap()];
1538        let clauses: Vec<logic::Clause> = vec![
1539            "foo(X) :- bar(X).".parse().unwrap(),
1540            "bar(\"test\").".parse().unwrap(),
1541            "foo(\"test\").".parse().unwrap(),
1542        ];
1543        let tree = sld(&clauses, &goal, 15, true).tree;
1544        let sld_proofs = proofs(&tree, &clauses, &goal);
1545        assert_eq!(sld_proofs.len(), 1);
1546        assert_eq!(
1547            sld_proofs.values().next().unwrap().height(),
1548            1,
1549            "{:?}",
1550            sld_proofs[&goal]
1551        );
1552    }
1553
1554    #[test]
1555    #[serial]
1556    fn tree_from_expression_query() {
1557        let mf: Modusfile = "base_image(\"alpine3.14\"). base_image(\"alpine3.15\")."
1558            .parse()
1559            .unwrap();
1560        let query = Expression::Literal(Literal {
1561            positive: true,
1562            position: None,
1563            predicate: Predicate("base_image".into()),
1564            args: vec!["f\"alpine${X}\"".parse().unwrap()],
1565        });
1566
1567        let (_, _, sld_res) = tree_from_modusfile(mf, query, 20, true);
1568        assert!(sld_res.tree.is_success());
1569    }
1570
1571    #[test]
1572    #[serial]
1573    fn negation_and_builtins() {
1574        let goal: Goal<logic::IRTerm> = vec!["!is_alpine(\"notalpine3.15\")".parse().unwrap()];
1575        let clauses: Vec<logic::Clause> = vec![
1576            "is_alpine(variant) :- string_concat(\"alpine\", version, variant)."
1577                .parse()
1578                .unwrap(),
1579        ];
1580        let sld_res = sld(&clauses, &goal, 10, true);
1581        let tree = sld_res.tree;
1582        let solutions = solutions(&tree);
1583        assert_eq!(solutions.len(), 1);
1584
1585        assert!(contains_ignoring_position(
1586            &solutions,
1587            &vec!["!is_alpine(\"notalpine3.15\")"
1588                .parse::<logic::Literal>()
1589                .unwrap()]
1590        ));
1591    }
1592
1593    #[test]
1594    #[serial]
1595    fn negation_and_anonymous_variable() {
1596        let goal: Goal<logic::IRTerm> = vec!["!is_alpine(\"notalpine3.15\", _)".parse().unwrap()];
1597        let clauses: Vec<logic::Clause> = vec![
1598            "is_alpine(variant, version) :- string_concat(\"alpine\", version, variant)."
1599                .parse()
1600                .unwrap(),
1601        ];
1602        let sld_res = sld(&clauses, &goal, 10, true);
1603        let tree = sld_res.tree;
1604        let solutions = solutions(&tree);
1605        assert_eq!(solutions.len(), 1);
1606
1607        assert!(contains_ignoring_position(&solutions, &goal));
1608    }
1609
1610    #[test]
1611    #[serial]
1612    fn negation_errors_when_unknown() {
1613        let goal: Goal<logic::IRTerm> = vec!["!is_alpine(\"notalpine3.15\", _)".parse().unwrap()];
1614        let clauses: Vec<logic::Clause> = vec![];
1615        let sld_res = sld(&clauses, &goal, 10, true);
1616
1617        assert_eq!(sld_res.errors.len(), 1);
1618        let is_match = matches!(
1619            sld_res.errors.iter().next(),
1620            Some(ResolutionError::UnknownPredicate(_))
1621        );
1622        assert!(is_match);
1623    }
1624
1625    #[test]
1626    #[serial]
1627    fn arrays() {
1628        let goal: Goal<logic::IRTerm> = vec!["app([\"sh\", \"--some-option\"])".parse().unwrap()];
1629        let clauses: Vec<logic::Clause> = vec![
1630            "app(entrypoint_params) :- from(\"alpine\")::set_entrypoint(entrypoint_params)."
1631                .parse()
1632                .unwrap(),
1633        ];
1634        let sld_res = sld(&clauses, &goal, 10, true);
1635        let tree = sld_res.tree;
1636        let solutions = solutions(&tree);
1637        assert_eq!(solutions.len(), 1);
1638
1639        assert!(contains_ignoring_position(
1640            &solutions,
1641            &vec!["app([\"sh\", \"--some-option\"])"
1642                .parse::<logic::Literal>()
1643                .unwrap()]
1644        ));
1645    }
1646
1647    #[test]
1648    #[serial]
1649    fn arrays_error_when_ungrounded() {
1650        let goal: Goal<logic::IRTerm> = vec!["app([\"sh\", someOption])".parse().unwrap()];
1651        let clauses: Vec<logic::Clause> = vec![
1652            "app(entrypoint_params) :- from(\"alpine\")::set_entrypoint(entrypoint_params)."
1653                .parse()
1654                .unwrap(),
1655        ];
1656        let sld_res = sld(&clauses, &goal, 10, true);
1657        assert_eq!(sld_res.errors.len(), 1);
1658        let is_match = matches!(
1659            sld_res.errors.iter().next(),
1660            Some(ResolutionError::InsufficientGroundness(_)),
1661        );
1662        assert!(is_match);
1663    }
1664}