modus_lib/
builtin.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::collections::HashMap;
18
19use crate::{
20    analysis::Kind,
21    logic::{Clause, IRTerm, Literal, Predicate},
22};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum SelectBuiltinResult {
26    Match,
27    GroundnessMismatch,
28    NoMatch,
29}
30
31impl SelectBuiltinResult {
32    pub fn is_match(&self) -> bool {
33        match self {
34            SelectBuiltinResult::Match => true,
35            _ => false,
36        }
37    }
38}
39
40pub trait BuiltinPredicate {
41    fn name(&self) -> &'static str;
42
43    /// The kind of this predicate or operator.
44    /// Should match https://github.com/modus-continens/docs/blob/main/src/library/README.md
45    fn kind(&self) -> Kind;
46
47    /// Return if the argument is allowed to be ungrounded. This means that a "false" here will force a constant.
48    fn arg_groundness(&self) -> &'static [bool];
49
50    fn select(&self, lit: &Literal) -> SelectBuiltinResult {
51        let Literal {
52            ref predicate,
53            ref args,
54            ..
55        } = lit;
56        if &predicate.0 != self.name() {
57            return SelectBuiltinResult::NoMatch;
58        }
59        if args.len() == self.arg_groundness().len()
60            && args.iter().zip(self.arg_groundness().into_iter()).all(
61                |(term, allows_ungrounded)| {
62                    *allows_ungrounded || term.is_constant_or_compound_constant()
63                },
64            )
65        {
66            SelectBuiltinResult::Match
67        } else {
68            SelectBuiltinResult::GroundnessMismatch
69        }
70    }
71
72    /// Return a new literal specifically constructed to unify with the input
73    /// literal. The returned literal will essentially be used as the head of a
74    /// new "hidden" rule, which will hopefully unify with the input literal.
75    /// The rule will contain no body literals.
76    ///
77    /// For example, the implementation of run should simply return the input
78    /// literal, after checking that it only contains a constant. (Returning any
79    /// unresolved variables can make the actual generation of build
80    /// instructions impossible)
81    ///
82    /// Renaming will not be done on this literal, so if variables are needed
83    /// they must all be either auxillary or some existing variables from the
84    /// input.
85    fn apply(&self, lit: &Literal) -> Option<Literal>;
86}
87
88mod string_concat {
89    use super::BuiltinPredicate;
90    use crate::logic::{IRTerm, Literal, Predicate, SpannedPosition};
91
92    fn string_concat_result(
93        a: &str,
94        b: &str,
95        c: &str,
96        pos: &Option<SpannedPosition>,
97    ) -> Option<Literal> {
98        Some(Literal {
99            positive: true,
100            position: pos.clone(),
101            predicate: Predicate("string_concat".to_owned()),
102            args: vec![
103                IRTerm::Constant(a.to_owned()),
104                IRTerm::Constant(b.to_owned()),
105                IRTerm::Constant(c.to_owned()),
106            ],
107        })
108    }
109
110    pub struct StringConcat1;
111    impl BuiltinPredicate for StringConcat1 {
112        fn name(&self) -> &'static str {
113            "string_concat"
114        }
115
116        fn kind(&self) -> crate::analysis::Kind {
117            crate::analysis::Kind::Logic
118        }
119
120        fn arg_groundness(&self) -> &'static [bool] {
121            &[false, false, true]
122        }
123
124        fn apply(&self, lit: &Literal) -> Option<Literal> {
125            let a = lit.args[0].as_constant()?;
126            let b = lit.args[1].as_constant()?;
127            let c = a.to_owned() + b;
128            string_concat_result(a, b, &c, &lit.position)
129        }
130    }
131
132    pub struct StringConcat2;
133    impl BuiltinPredicate for StringConcat2 {
134        fn name(&self) -> &'static str {
135            "string_concat"
136        }
137
138        fn kind(&self) -> crate::analysis::Kind {
139            crate::analysis::Kind::Logic
140        }
141
142        fn arg_groundness(&self) -> &'static [bool] {
143            &[true, false, false]
144        }
145
146        fn apply(&self, lit: &Literal) -> Option<Literal> {
147            let b = lit.args[1].as_constant()?;
148            let c = lit.args[2].as_constant()?;
149            if let Some(a) = c.strip_suffix(&b) {
150                string_concat_result(a, b, c, &lit.position)
151            } else {
152                None
153            }
154        }
155    }
156
157    pub struct StringConcat3;
158    impl BuiltinPredicate for StringConcat3 {
159        fn name(&self) -> &'static str {
160            "string_concat"
161        }
162
163        fn kind(&self) -> crate::analysis::Kind {
164            crate::analysis::Kind::Logic
165        }
166
167        fn arg_groundness(&self) -> &'static [bool] {
168            &[false, true, false]
169        }
170
171        fn apply(&self, lit: &Literal) -> Option<Literal> {
172            let a = lit.args[0].as_constant()?;
173            let c = lit.args[2].as_constant()?;
174            if let Some(b) = c.strip_prefix(&a) {
175                string_concat_result(a, b, c, &lit.position)
176            } else {
177                None
178            }
179        }
180    }
181}
182
183mod equality {
184    use crate::logic::{IRTerm, Literal, Predicate};
185
186    use super::BuiltinPredicate;
187
188    pub struct StringEq1;
189    impl BuiltinPredicate for StringEq1 {
190        fn name(&self) -> &'static str {
191            "string_eq"
192        }
193
194        fn kind(&self) -> crate::analysis::Kind {
195            crate::analysis::Kind::Logic
196        }
197
198        fn arg_groundness(&self) -> &'static [bool] {
199            &[false, true]
200        }
201
202        fn apply(&self, lit: &crate::logic::Literal) -> Option<crate::logic::Literal> {
203            let a = lit.args[0].as_constant()?;
204            Some(Literal {
205                positive: true,
206                position: lit.position.clone(),
207                predicate: Predicate("string_eq".to_owned()),
208                args: vec![
209                    IRTerm::Constant(a.to_owned()),
210                    IRTerm::Constant(a.to_owned()),
211                ],
212            })
213        }
214    }
215
216    pub struct StringEq2;
217    impl BuiltinPredicate for StringEq2 {
218        fn name(&self) -> &'static str {
219            "string_eq"
220        }
221
222        fn kind(&self) -> crate::analysis::Kind {
223            crate::analysis::Kind::Logic
224        }
225
226        fn arg_groundness(&self) -> &'static [bool] {
227            &[true, false]
228        }
229
230        fn apply(&self, lit: &crate::logic::Literal) -> Option<crate::logic::Literal> {
231            let b = lit.args[1].as_constant()?;
232            Some(Literal {
233                positive: true,
234                position: lit.position.clone(),
235                predicate: Predicate("string_eq".to_owned()),
236                args: vec![
237                    IRTerm::Constant(b.to_owned()),
238                    IRTerm::Constant(b.to_owned()),
239                ],
240            })
241        }
242    }
243}
244
245mod number {
246    use super::BuiltinPredicate;
247
248    macro_rules! define_number_comparison {
249        ($name:ident, $cond:expr) => {
250            #[allow(non_camel_case_types)]
251            pub struct $name;
252            impl BuiltinPredicate for $name {
253                fn name(&self) -> &'static str {
254                    stringify!($name)
255                }
256
257                fn kind(&self) -> crate::analysis::Kind {
258                    crate::analysis::Kind::Logic
259                }
260
261                fn arg_groundness(&self) -> &'static [bool] {
262                    &[false, false]
263                }
264
265                /// Parses and checks that arg1 > arg2.
266                fn apply(&self, lit: &crate::logic::Literal) -> Option<crate::logic::Literal> {
267                    let a: f64 = lit.args[0].as_constant().and_then(|s| s.parse().ok())?;
268                    let b: f64 = lit.args[1].as_constant().and_then(|s| s.parse().ok())?;
269                    if $cond(a, b) {
270                        Some(lit.clone())
271                    } else {
272                        None
273                    }
274                }
275            }
276        };
277    }
278
279    define_number_comparison!(number_eq, |a, b| a == b);
280    define_number_comparison!(number_gt, |a, b| a > b);
281    define_number_comparison!(number_lt, |a, b| a < b);
282    define_number_comparison!(number_geq, |a, b| a >= b);
283    define_number_comparison!(number_leq, |a, b| a <= b);
284}
285
286mod semver {
287    use super::BuiltinPredicate;
288    use semver::{Comparator, Op, Version};
289
290    fn parse_partial_version(s: &str) -> Option<Version> {
291        if let Ok(v) = Version::parse(s) {
292            return Some(v);
293        }
294        let mut s = String::from(s);
295        s.push_str(".0");
296        if let Ok(v) = Version::parse(&s) {
297            return Some(v);
298        }
299        s.push_str(".0");
300        if let Ok(v) = Version::parse(&s) {
301            return Some(v);
302        }
303        return None;
304    }
305
306    macro_rules! define_semver_comparison {
307        ($name:ident, $cond:expr) => {
308            #[allow(non_camel_case_types)]
309            pub struct $name;
310            impl BuiltinPredicate for $name {
311                fn name(&self) -> &'static str {
312                    stringify!($name)
313                }
314
315                fn kind(&self) -> crate::analysis::Kind {
316                    crate::analysis::Kind::Logic
317                }
318
319                fn arg_groundness(&self) -> &'static [bool] {
320                    &[false, false]
321                }
322
323                /// Parses and checks that arg1 > arg2.
324                fn apply(&self, lit: &crate::logic::Literal) -> Option<crate::logic::Literal> {
325                    let a: Version = lit.args[0]
326                        .as_constant()
327                        .and_then(|s| parse_partial_version(s))?;
328                    let b: Comparator = lit.args[1]
329                        .as_constant()
330                        .and_then(|s| Comparator::parse(&format!("{}{}", $cond, s)).ok())?;
331                    if b.matches(&a) {
332                        Some(lit.clone())
333                    } else {
334                        None
335                    }
336                }
337            }
338        };
339    }
340
341    define_semver_comparison!(semver_exact, "=");
342    define_semver_comparison!(semver_gt, ">");
343    define_semver_comparison!(semver_lt, "<");
344    define_semver_comparison!(semver_geq, ">=");
345    define_semver_comparison!(semver_leq, "<=");
346}
347
348macro_rules! intrinsic_predicate {
349    ($name:ident, $kind:expr, $($arg_groundness:expr),*) => {
350        #[allow(non_camel_case_types)]
351        pub struct $name;
352        impl BuiltinPredicate for $name {
353            fn name(&self) -> &'static str {
354                stringify!($name)
355            }
356
357            fn kind(&self) -> Kind {
358                $kind
359            }
360
361            fn arg_groundness(&self) -> &'static [bool] {
362                &[$($arg_groundness),*]
363            }
364
365            fn apply(&self, lit: &Literal) -> Option<Literal> {
366                Some(lit.clone())
367            }
368        }
369    };
370}
371
372intrinsic_predicate!(run, crate::analysis::Kind::Layer, false);
373intrinsic_predicate!(from, crate::analysis::Kind::Image, false);
374intrinsic_predicate!(
375    _operator_copy_begin,
376    crate::analysis::Kind::Image,
377    false,
378    false,
379    false
380);
381intrinsic_predicate!(
382    _operator_copy_end,
383    crate::analysis::Kind::Image,
384    false,
385    false,
386    false
387);
388intrinsic_predicate!(
389    _operator_in_workdir_begin,
390    crate::analysis::Kind::Layer,
391    false,
392    false
393);
394intrinsic_predicate!(
395    _operator_in_workdir_end,
396    crate::analysis::Kind::Layer,
397    false,
398    false
399);
400intrinsic_predicate!(
401    _operator_set_workdir_begin,
402    crate::analysis::Kind::Image,
403    false,
404    false
405);
406intrinsic_predicate!(
407    _operator_set_workdir_end,
408    crate::analysis::Kind::Image,
409    false,
410    false
411);
412intrinsic_predicate!(
413    _operator_set_entrypoint_begin,
414    crate::analysis::Kind::Image,
415    false,
416    false
417);
418intrinsic_predicate!(
419    _operator_set_entrypoint_end,
420    crate::analysis::Kind::Image,
421    false,
422    false
423);
424intrinsic_predicate!(
425    _operator_set_cmd_begin,
426    crate::analysis::Kind::Image,
427    false,
428    false
429);
430intrinsic_predicate!(
431    _operator_set_cmd_end,
432    crate::analysis::Kind::Image,
433    false,
434    false
435);
436intrinsic_predicate!(
437    _operator_set_label_begin,
438    crate::analysis::Kind::Image,
439    false,
440    false,
441    false
442);
443intrinsic_predicate!(
444    _operator_set_label_end,
445    crate::analysis::Kind::Image,
446    false,
447    false,
448    false
449);
450intrinsic_predicate!(
451    _operator_set_env_begin,
452    crate::analysis::Kind::Image,
453    false,
454    false,
455    false
456);
457intrinsic_predicate!(
458    _operator_set_env_end,
459    crate::analysis::Kind::Image,
460    false,
461    false,
462    false
463);
464intrinsic_predicate!(
465    _operator_in_env_begin,
466    crate::analysis::Kind::Layer,
467    false,
468    false,
469    false
470);
471intrinsic_predicate!(
472    _operator_in_env_end,
473    crate::analysis::Kind::Layer,
474    false,
475    false,
476    false
477);
478intrinsic_predicate!(
479    _operator_append_path_begin,
480    crate::analysis::Kind::Image,
481    false,
482    false
483);
484intrinsic_predicate!(
485    _operator_append_path_end,
486    crate::analysis::Kind::Image,
487    false,
488    false
489);
490intrinsic_predicate!(
491    _operator_set_user_begin,
492    crate::analysis::Kind::Image,
493    false,
494    false
495);
496intrinsic_predicate!(
497    _operator_set_user_end,
498    crate::analysis::Kind::Image,
499    false,
500    false
501);
502intrinsic_predicate!(copy, crate::analysis::Kind::Layer, false, false);
503intrinsic_predicate!(_operator_merge_begin, crate::analysis::Kind::Layer, false);
504intrinsic_predicate!(_operator_merge_end, crate::analysis::Kind::Layer, false);
505
506/// Convenience macro that returns Some(b) for the first b that can be selected.
507macro_rules! select_builtins {
508    ( $lit:expr, $( $x:expr ),+, ) => {{
509        let mut has_ground_mismatch = false;
510        $(
511            match $x.select($lit) {
512                SelectBuiltinResult::Match => return (SelectBuiltinResult::Match, Some(&$x)),
513                SelectBuiltinResult::GroundnessMismatch => {
514                    has_ground_mismatch = true;
515                },
516                _ => {}
517            }
518        );+
519        if has_ground_mismatch {
520            return (SelectBuiltinResult::GroundnessMismatch, None);
521        } else {
522            return (SelectBuiltinResult::NoMatch, None);
523        }
524    }};
525}
526
527pub fn select_builtin<'a>(
528    lit: &Literal,
529) -> (SelectBuiltinResult, Option<&'a dyn BuiltinPredicate>) {
530    select_builtins!(
531        lit,
532        string_concat::StringConcat1,
533        string_concat::StringConcat2,
534        string_concat::StringConcat3,
535        run,
536        from,
537        _operator_copy_begin,
538        _operator_copy_end,
539        _operator_in_workdir_begin,
540        _operator_in_workdir_end,
541        _operator_set_workdir_begin,
542        _operator_set_workdir_end,
543        _operator_set_entrypoint_begin,
544        _operator_set_entrypoint_end,
545        _operator_set_cmd_begin,
546        _operator_set_cmd_end,
547        _operator_set_label_begin,
548        _operator_set_label_end,
549        _operator_set_env_begin,
550        _operator_set_env_end,
551        _operator_in_env_begin,
552        _operator_in_env_end,
553        _operator_append_path_begin,
554        _operator_append_path_end,
555        _operator_set_user_begin,
556        _operator_set_user_end,
557        copy,
558        equality::StringEq1,
559        equality::StringEq2,
560        _operator_merge_begin,
561        _operator_merge_end,
562        number::number_eq,
563        number::number_gt,
564        number::number_lt,
565        number::number_geq,
566        number::number_leq,
567        semver::semver_exact,
568        semver::semver_gt,
569        semver::semver_lt,
570        semver::semver_geq,
571        semver::semver_leq,
572    )
573}
574
575lazy_static! {
576    // An operator can take an expression of one kind and produce another kind.
577    pub static ref OPERATOR_KIND_MAP: HashMap<&'static str, (Kind, Kind)> = {
578        let mut m = HashMap::new();
579        m.insert("copy", (Kind::Image, Kind::Layer));
580        m.insert("set_env", (Kind::Image, Kind::Image));
581        m.insert("set_entrypoint", (Kind::Image, Kind::Image));
582        m.insert("set_cmd", (Kind::Image, Kind::Image));
583        m.insert("set_workdir", (Kind::Image, Kind::Image));
584        m.insert("set_label", (Kind::Image, Kind::Image));
585        m.insert("set_user", (Kind::Image, Kind::Image));
586        m.insert("append_path", (Kind::Image, Kind::Image));
587        m.insert("in_workdir", (Kind::Layer, Kind::Layer));
588        m.insert("in_env", (Kind::Layer, Kind::Layer));
589        m.insert("merge", (Kind::Layer, Kind::Layer));
590        m
591    };
592}
593
594#[cfg(test)]
595mod test {
596    use crate::{analysis::Kind, builtin::SelectBuiltinResult, logic::IRTerm};
597
598    #[test]
599    pub fn test_select() {
600        use crate::logic::{Literal, Predicate};
601
602        let lit = Literal {
603            positive: true,
604            position: None,
605            predicate: Predicate("run".to_owned()),
606            args: vec![IRTerm::Constant("hello".to_owned())],
607        };
608        let b = super::select_builtin(&lit);
609        assert!(b.0.is_match());
610        let b = b.1.unwrap();
611        assert_eq!(b.name(), "run");
612        assert_eq!(b.kind(), Kind::Layer);
613        assert_eq!(b.apply(&lit), Some(lit));
614
615        let lit = Literal {
616            positive: true,
617            position: None,
618            predicate: Predicate("string_concat".to_owned()),
619            args: vec![
620                IRTerm::Constant("hello".to_owned()),
621                IRTerm::Constant("world".to_owned()),
622                IRTerm::UserVariable("X".to_owned()),
623            ],
624        };
625        let b = super::select_builtin(&lit);
626        assert!(b.0.is_match());
627        let b = b.1.unwrap();
628        assert_eq!(b.name(), "string_concat");
629        assert_eq!(b.kind(), Kind::Logic);
630        assert_eq!(
631            b.apply(&lit),
632            Some(Literal {
633                positive: true,
634                position: None,
635                predicate: Predicate("string_concat".to_owned()),
636                args: vec![
637                    IRTerm::Constant("hello".to_owned()),
638                    IRTerm::Constant("world".to_owned()),
639                    IRTerm::Constant("helloworld".to_owned()),
640                ]
641            })
642        );
643
644        let lit = Literal {
645            positive: true,
646            position: None,
647            predicate: Predicate("xxx".to_owned()),
648            args: vec![IRTerm::Constant("hello".to_owned())],
649        };
650        let b = super::select_builtin(&lit);
651        assert_eq!(b.0, SelectBuiltinResult::NoMatch);
652    }
653
654    #[test]
655    pub fn test_from_run() {
656        use crate::logic::{Clause, Literal, Predicate};
657
658        let rules = vec![Clause {
659            head: Literal {
660                positive: true,
661                position: None,
662                predicate: Predicate("a".to_owned()),
663                args: vec![],
664            },
665            body: vec![
666                Literal {
667                    positive: true,
668                    position: None,
669                    predicate: Predicate("from".to_owned()),
670                    args: vec![IRTerm::Constant("ubuntu".to_owned())],
671                },
672                Literal {
673                    positive: true,
674                    position: None,
675                    predicate: Predicate("run".to_owned()),
676                    args: vec![IRTerm::Constant("rm -rf /".to_owned())],
677                },
678            ],
679        }];
680        let goals = vec![Literal {
681            positive: true,
682            position: None,
683            predicate: Predicate("a".to_owned()),
684            args: vec![],
685        }];
686        let tree = crate::sld::sld(&rules, &goals, 100, true).tree;
687        let solutions = crate::sld::solutions(&tree);
688        assert_eq!(solutions.len(), 1);
689        assert!(solutions.contains(&goals));
690        let proof = crate::sld::proofs(&tree, &rules, &goals);
691        assert_eq!(proof.len(), 1);
692    }
693
694    #[test]
695    pub fn test_number_and_semver_compare() {
696        use crate::logic::{Literal, Predicate};
697
698        let tests = vec![
699            (
700                "number_eq",
701                vec![
702                    ("1", "1"),
703                    ("1.0", "1"),
704                    ("0.0", "0.0"),
705                    ("0", "-0"),
706                    ("0.2", "0.2"),
707                    ("1e-10", "1e-10"),
708                    ("1e100", "1e100"),
709                    ("42.0", "42.0"),
710                ],
711                vec![
712                    ("0", "1"),
713                    ("0", "0.01"),
714                    ("1", "-1"),
715                    ("1e-10", "0"),
716                    ("42.0", "-273.15"),
717                    ("NaN", "NaN"),
718                ],
719            ),
720            (
721                "number_gt",
722                vec![
723                    ("1", "0"),
724                    ("1e-10", "0"),
725                    ("42.0", "-273.15"),
726                    ("1e100", "0"),
727                ],
728                vec![
729                    ("42.0", "42.0"),
730                    ("42.0", "42.1"),
731                    ("0", "1e-10"),
732                    ("NaN", "NaN"),
733                ],
734            ),
735            (
736                "number_lt",
737                vec![
738                    ("0", "1"),
739                    ("0", "1e-10"),
740                    ("-273.15", "42.0"),
741                    ("0", "1e100"),
742                ],
743                vec![
744                    ("42.0", "42.0"),
745                    ("42.1", "42.0"),
746                    ("1e-10", "0"),
747                    ("NaN", "NaN"),
748                ],
749            ),
750            (
751                "number_geq",
752                vec![
753                    ("1", "0"),
754                    ("1e-10", "0"),
755                    ("42.0", "-273.15"),
756                    ("1e100", "0"),
757                    ("42.0", "42.0"),
758                    ("42", "42.0"),
759                ],
760                vec![("42.0", "42.1"), ("0", "1e-10"), ("NaN", "NaN")],
761            ),
762            (
763                "number_leq",
764                vec![
765                    ("0", "1"),
766                    ("0", "1e-10"),
767                    ("-273.15", "42.0"),
768                    ("0", "1e100"),
769                    ("42.0", "42.0"),
770                ],
771                vec![("42.1", "42.0"), ("1e-10", "0"), ("NaN", "NaN")],
772            ),
773            (
774                "semver_exact",
775                vec![
776                    ("1.0.0", "1.0.0"),
777                    ("1.0.0", "1.0"),
778                    ("1.0.0", "1"),
779                    ("0.0.0", "0.0.0"),
780                    ("0.1.0-alpha", "0.1.0-alpha"),
781                    // TODO: do we want to allow things like this?
782                    ("1", "1"),
783                    ("1.0", "1"),
784                    ("1", "1.0"),
785                    ("0.2", "0.2"),
786                    ("1.0.1", "1.0"),
787                ],
788                vec![
789                    ("1.0.0", "1.0.1"),
790                    ("1.0.0", "1.1"),
791                    ("1.0.0", "2"),
792                    ("1.0.0", "0"),
793                    ("0.0.0", "0.0.1"),
794                    ("1", "1.2"),
795                    ("0", "-0"),
796                    ("0.1.0-beta", "0.1.0-alpha"),
797                ],
798            ),
799            (
800                "semver_gt",
801                vec![
802                    ("3.2.1", "1.2.3"),
803                    ("1.2.3", "1.2.1"),
804                    ("0.1.0-beta", "0.1.0-alpha"),
805                    ("1", "0"),
806                    ("1.1", "1.0"),
807                    ("1.0.1", "1.0.0"),
808                ],
809                vec![
810                    ("1.1", "1"),
811                    ("1.1", "1.2"),
812                    ("1.1", "1.1"),
813                    ("3.2.1", "3.4.1"),
814                ],
815            ),
816            (
817                "semver_lt",
818                vec![
819                    ("1.2.3", "3.2.1"),
820                    ("3.2.1", "3.4.1"),
821                    ("1.1", "1.2"),
822                    ("1.1", "1.1.1"),
823                ],
824                vec![
825                    ("1", "0"),
826                    ("1.1", "1.0"),
827                    ("1.1", "1"),
828                    ("1.1", "1"),
829                    ("1.1", "1.1"),
830                    ("1.1.0", "1.1.0"),
831                    ("1.1", "1.1.0"),
832                    ("1.1.1", "1.1.1"),
833                ],
834            ),
835            (
836                "semver_geq",
837                vec![
838                    ("1.0.1", "1.0.0"),
839                    ("1.0.1", "1.0"),
840                    ("1.2.3", "1.2.1"),
841                    ("1.1", "1.0"),
842                    ("1.1", "1"),
843                    ("1.1", "1.1"),
844                    ("1.1.0", "1.1.0"),
845                    ("1.1", "1.1.0"),
846                    ("1.1.1", "1.1.1"),
847                ],
848                vec![("1.2.3", "3.2.1"), ("1.1", "1.2"), ("1.1", "1.1.1")],
849            ),
850            (
851                "semver_leq",
852                vec![
853                    ("1.2.3", "3.2.1"),
854                    ("1.1", "1.2"),
855                    ("1.1", "1.1.1"),
856                    ("1.1", "1"),
857                    ("1.1", "1.1"),
858                    ("1.1.0", "1.1.0"),
859                    ("1.1", "1.1.0"),
860                    ("1.1.1", "1.1.1"),
861                ],
862                vec![("1", "0"), ("1.1", "1.0"), ("1.2.3", "1.2.1")],
863            ),
864        ];
865
866        for (name, true_cases, false_cases) in tests.into_iter() {
867            for (left, right) in true_cases.into_iter() {
868                let lit = Literal {
869                    positive: true,
870                    position: None,
871                    predicate: Predicate(name.to_owned()),
872                    args: vec![
873                        IRTerm::Constant(left.to_owned()),
874                        IRTerm::Constant(right.to_owned()),
875                    ],
876                };
877                let b = super::select_builtin(&lit);
878                assert!(b.0.is_match());
879                let b = b.1.unwrap();
880                assert_eq!(b.name(), name);
881                assert_eq!(b.kind(), Kind::Logic);
882                if b.apply(&lit).as_ref() != Some(&lit) {
883                    panic!("Expected {} to resolve (got false)", lit);
884                }
885            }
886            for (left, right) in false_cases.into_iter() {
887                let lit = Literal {
888                    positive: true,
889                    position: None,
890                    predicate: Predicate(name.to_owned()),
891                    args: vec![
892                        IRTerm::Constant(left.to_owned()),
893                        IRTerm::Constant(right.to_owned()),
894                    ],
895                };
896                let b = super::select_builtin(&lit);
897                assert!(b.0.is_match());
898                let b = b.1.unwrap();
899                assert_eq!(b.name(), name);
900                assert_eq!(b.kind(), Kind::Logic);
901                if b.apply(&lit) != None {
902                    panic!("Expected {} to fail (but resolved)", lit);
903                }
904            }
905        }
906    }
907}