suiron/
substitution_set.rs

1//! A substitution set is an array of bindings for logic variables.
2//!
3//! As the inference engine attempts to solve a goal (or query), it
4//! generates substitution sets, which record logic variable bindings.
5//! A substitution set can be thought of as a solution, or partial
6//! solution, for a given goal.
7//
8// Cleve Lendon 2023
9
10use std::rc::Rc;
11
12use super::unifiable::{*, Unifiable::*};
13
14/// Records bindings of logic variables to unifiable terms.
15///
16/// Logic variable IDs are used to index into the substitution set.
17///
18/// [substitution_set](../substitution_set/index.html)
19///
20// Note:
21// A vector is heap allocated, so why is the Box necessary?
22// One of the speed bottlenecks of the inference engine is the
23// time it takes to copy an entire substitution set. The size
24// of Unifiable is 56 bytes, while the size of a Box pointer is
25// only 8.
26// Also note:
27// It is not possible to add methods, because SubstitutionSet
28// is an alias of built-in data types.
29pub type SubstitutionSet<'a> = Vec<Option<Rc<Unifiable>>>;
30
31/// Is the logic variable bound?
32///
33/// A logic variable is bound if an entry for it exists in the substitution set.
34///
35/// # Arguments
36/// * `term` - must be a [LogicVar](../unifiable/enum.Unifiable.html#variant.LogicVar) 
37/// * `ss`  - substitution set
38/// # Return
39/// * True if bound. False otherwise.
40/// # Panics
41/// * If `term` is not a logic variable.
42/// # Usage
43/// ```
44/// use std::rc::Rc;
45/// use suiron::*;
46///
47/// let a = atom!("Promethium");
48/// let x = logic_var!(next_id(), "$X");
49///
50/// let ss = empty_ss!();
51/// let ss = x.unify(&a, &ss).unwrap();   // x -> a
52///
53/// if is_bound(&x, &ss) { println!("X is bound."); }
54/// ```
55pub fn is_bound(term: &Unifiable, ss: &SubstitutionSet) -> bool {
56    if let LogicVar{id, name: _} = *term {
57        if id >= ss.len() { return false; }
58        return ss[id] != None;
59    }
60    else {
61        panic!("is_bound() - First argument must be a logic variable.");
62    }
63} // is_bound()
64
65/// Gets the term which a logic variable is bound to.
66///
67/// If the variable is not bound, returns None.
68///
69/// # Notes
70/// * The bound term is not necessarily a ground term.<br>
71/// It might be another variable.
72/// * This function is used only for debugging and testing.
73///
74/// # Arguments
75/// * `term` - must be a [LogicVar](../unifiable/enum.Unifiable.html#variant.LogicVar) 
76/// * `ss`  - substitution set
77/// # Return
78/// * `bound term` - [Unifiable](../unifiable/enum.Unifiable.html)
79/// # Panics
80/// * If `term` is not a logic variable.
81/// # Usage
82/// ```
83/// use std::rc::Rc;
84/// use suiron::*;
85///
86/// clear_id();
87/// let ne = atom!("Neon");
88/// let x = logic_var!(next_id(), "$X");
89/// let y = logic_var!(next_id(), "$Y");
90/// let ss = empty_ss!();
91///
92/// // Bind $X_1 -> Neon
93/// let ss = x.unify(&ne, &ss).unwrap();
94/// // Bind $Y_2 -> $X_1 -> Neon
95/// let ss = y.unify(&x, &ss).unwrap();
96///
97/// let binding = get_binding(&y, &ss);
98/// match binding {
99///     None => { panic!("$Y is not bound."); },
100///     Some(entry) => {
101///         let s = format!("{}", *entry);
102///         println!("{}", s);  // Prints: $X_1
103///     },
104/// }
105/// ```
106///
107pub fn get_binding<'a>(term: &Unifiable, ss: &'a SubstitutionSet)
108                       -> Option<&'a Unifiable> {
109    if let LogicVar{id, name: _} = *term {
110        if id >= ss.len() { return None; }
111        match &ss[id] {
112            None => { return None; },
113            Some(entry) => { return Some(&*entry); },
114        }
115    }
116    else { panic!("get_binding() - First argument must be a logic variable."); }
117} // get_binding()
118
119
120/// Is the logic variable ultimately bound to a ground term?
121///
122/// If the given logic variable is bound to a ground term, return true.
123/// If the logic variable is bound to another logic variable, follow the
124/// bindings in the substitution set until a ground term or None is found.
125///
126/// # Arguments
127/// * `term` - must be a
128/// [LogicVar](../unifiable/enum.Unifiable.html#variant.LogicVar) 
129/// * `ss`  - substitution set
130/// # Return
131/// * True if ground. False otherwise.
132/// # Panics
133/// * If `term` is not a logic variable.
134/// # Usage
135/// ```
136/// use std::rc::Rc;
137/// use suiron::*;
138///
139/// let a = atom!("Promethium");
140/// let x = logic_var!(next_id(), "$X");
141/// let y = logic_var!(next_id(), "$Y");
142/// let z = logic_var!(next_id(), "$Z");
143///
144/// let ss = empty_ss!();
145///
146/// let ss = x.unify(&a, &ss).unwrap(); // x -> a
147/// let ss = y.unify(&x, &ss).unwrap(); // y -> x -> a
148/// let ss = z.unify(&y, &ss).unwrap(); // z -> y -> x -> a
149///
150/// if is_ground_variable(&z, &ss) { println!("Z is grounded."); }
151/// else { println!("Z is NOT grounded."); }
152/// // Prints: Z is grounded.
153/// ```
154pub fn is_ground_variable(term: &Unifiable, ss: &SubstitutionSet) -> bool {
155    if let LogicVar{id, name: _} = *term {
156        let mut id = id;
157        loop {
158            if id >= ss.len() { return false; }
159            match &ss[id] {
160                None => { return false; },
161                Some(term) => {
162                    if let LogicVar{id: id2, name: _} = **term { id = id2; }
163                    else { return true; }
164                },
165            } // match
166        } // loop
167    }
168    else {
169        panic!("is_ground_variable() - First argument must be a logic variable.");
170    }
171} // is_ground_variable()
172
173
174/// Gets the ground term which a logic variable is ultimately bound to.
175///
176/// If the given term is already a ground term, simply return it.
177/// If the given term is a logic variable, check the substitution set to
178/// determine whether it is bound to a ground term.
179/// If it is, return the ground term.
180/// If the logic variable is bound to another variable, keep checking the
181/// substitution set until a ground term is found, or
182/// [Nil](../unifiable/enum.Unifiable.html#variant.Nil).
183/// # Note
184/// In Prolog, 'ground term' refers to a term which has no logic variables
185/// in it. (Complex terms can contain constants and/or variables.)
186/// Here, however, 'ground term' simply means 'not a logic variable'.
187/// # Arguments
188/// * `term`  - [Unifiable](../unifiable/enum.Unifiable.html)
189/// * `ss`  - substitution set
190/// # Return
191/// * `Option` - Some([Unifiable](../unifiable/enum.Unifiable.html)) or None
192/// # Usage
193/// ```
194/// use std::rc::Rc;
195/// use suiron::*;
196///
197/// let a = atom!("Promethium");
198/// let x = logic_var!(next_id(), "$X");
199/// let y = logic_var!(next_id(), "$Y");
200/// let z = logic_var!(next_id(), "$Z");
201///
202/// let ss = empty_ss!();
203///
204/// let ss = x.unify(&a, &ss).unwrap(); // x -> a
205/// let ss = y.unify(&x, &ss).unwrap(); // y -> x -> a
206/// let ss = z.unify(&y, &ss).unwrap(); // z -> y -> x -> a
207///
208/// let gt = get_ground_term(&z, &ss).unwrap();
209/// println!("{}", gt); // Prints: Promethium
210/// ```
211pub fn get_ground_term<'a>(term: &'a Unifiable, ss: &'a SubstitutionSet)
212                           -> Option<&'a Unifiable> {
213    let mut term2 = term;
214    loop {
215        if let LogicVar{id, name: _} = *term2 {
216            if id >= ss.len() { return None; }
217            match &ss[id] {
218                None => { return None; },
219                Some(entry) => { term2 = &*entry; },
220            }
221        }
222        else { return Some(term2); }
223    } // loop
224} // get_ground_term()
225
226
227/// Checks if a term is a complex term or bound to a complex term.
228///
229/// If the given term is a complex term, simply return it.
230/// If the given term is a logic variable, check the substitution
231/// set to determine whether it is ultimately bound to a complex term.
232/// If it is, return the complex term. Otherwise, return None.
233/// # Note
234/// In other implementations of Suiron, this function is equivalent to cast_complex().
235/// # Arguments
236/// * `term`  - [Unifiable](../unifiable/enum.Unifiable.html)
237/// * `ss`  - substitution set
238/// # Return
239/// * `Option` - Some([SComplex](../unifiable/enum.Unifiable.html#variant.SComplex)) or None
240/// # Usage
241/// ```
242/// use std::rc::Rc;
243/// use suiron::*;
244///
245/// let zol = parse_complex("music(Dilnaz, Zolotoi)").unwrap();
246/// let x = logic_var!(next_id(), "$X");
247/// let y = logic_var!(next_id(), "$Y");
248/// let ss = empty_ss!();
249/// let ss = x.unify(&zol, &ss).unwrap();  // $X -> music()
250/// let ss = y.unify(&x, &ss).unwrap();    // $Y -> $X -> music()
251///
252/// let result = get_complex(&y, &ss).unwrap();
253/// println!("{}", result);  // Prints: music(Dilnaz, Zolotoi)
254/// ```
255///
256pub fn get_complex<'a>(term: &'a Unifiable, ss: &'a SubstitutionSet) -> Option<&'a Unifiable> {
257    match *term {
258        Unifiable::SComplex(_) => { return Some(term); },
259        Unifiable::LogicVar{id: _, name: _} => {
260            match get_ground_term(term, ss) {
261                Some(gt) => {
262                    match gt {
263                        Unifiable::SComplex(_) => { return Some(gt); },
264                        _ => None,
265                    }
266                },
267                None => None,
268            }
269        },
270        _ => None,
271    }
272} // get_complex()
273
274/// Checks if a term is a list or bound to a list.
275///
276/// If the given term is a list, simply return it.
277/// If the given term is a logic variable, check the substitution
278/// set to determine whether it is ultimately bound to a list.
279/// If it is, return the list. Otherwise, return None.
280/// # Note
281/// In other implementations of Suiron, this function is equivalent
282/// to cast_linked_list().
283/// # Arguments
284/// * `term`  - [Unifiable](../unifiable/enum.Unifiable.html)
285/// * `ss`  - substitution set
286/// # Return
287/// * `Option` -
288///    Some([SLinkedList](../unifiable/enum.Unifiable.html#variant.SLinkedList))
289///    or None
290/// # Usage
291/// ```
292/// use std::rc::Rc;
293/// use suiron::*;
294///
295/// // First make a list.
296/// let ar = atom!("Argon");
297/// let kr = atom!("Krypton");
298/// let l1 = slist!(false, ar, kr);  // [Argon, Krypton]
299///
300/// // Unify $X with [Argon, Krypton].
301/// let x = logic_var!(next_id(), "$X");
302/// let y = logic_var!(next_id(), "$Y");
303/// let ss = empty_ss!();
304/// let ss = x.unify(&l1, &ss).unwrap();  // $X -> [Argon, Krypton]
305/// let ss = y.unify(&x, &ss).unwrap();  // $Y -> $X -> [Argon, Krypton]
306///
307/// let result = get_list(&y, &ss).unwrap();
308/// println!("{}", result);  // Prints: [Argon, Krypton]
309/// ```
310///
311pub fn get_list<'a>(term: &'a Unifiable, ss: &'a SubstitutionSet)
312                    -> Option<&'a Unifiable> {
313    match *term {
314        Unifiable::SLinkedList{term: _, next: _, count: _, tail_var: _}
315            => { return Some(term); },
316        Unifiable::LogicVar{id: _, name: _} => {
317            match get_ground_term(term, ss) {
318                Some(gt) => {
319                    match gt {
320                        Unifiable::SLinkedList{term: _, next: _, count: _, tail_var: _}
321                          => { return Some(gt); },
322                        _ => None,
323                    }
324                },
325                None => None,
326            }
327        },
328        _ => None,
329    }
330} // get_list()
331
332/// Checks whether a term is a constant or bound to a constant.
333///
334/// If the given term is a simple constant (Atom, SFloat or SInteger),
335/// return it. If the given term is a logic variable, check the
336/// substitution set to determine whether it is ultimately bound to
337/// a constant. If it is, return the constant. Otherwise, return None.
338///
339/// # Arguments
340/// * `term`  - [Unifiable](../unifiable/enum.Unifiable.html)
341/// * `ss` - substitution set
342/// # Return
343/// * `Option` - Some([Unifiable](../unifiable/enum.Unifiable.html)) or None
344/// # Usage
345/// ```
346/// use std::rc::Rc;
347/// use suiron::*;
348///
349/// let kr = atom!("Krypton");
350/// let x = logic_var!(next_id(), "$X");
351/// let y = logic_var!(next_id(), "$Y");
352/// let ss = empty_ss!();
353/// let ss = kr.unify(&x, &ss).unwrap(); // $X -> Argon
354/// let ss = y.unify(&x, &ss).unwrap(); // $Y -> $X -> Argon
355///
356/// let result = get_constant(&y, &ss).unwrap();
357/// println!("{:?}", result);  // Prints: Atom("Krypton");
358/// ```
359///
360pub fn get_constant<'a>(term: &'a Unifiable, ss: &'a SubstitutionSet)
361                        -> Option<&'a Unifiable> {
362    match *term {
363        Unifiable::SFloat(_) |
364        Unifiable::SInteger(_) |
365        Unifiable::Atom(_) => { return Some(term); },
366        Unifiable::LogicVar{id: _, name: _} => {
367            match get_ground_term(term, ss) {
368                Some(gt) => {
369                    match gt {
370                        Unifiable::SFloat(_) |
371                        Unifiable::SInteger(_) |
372                        Unifiable::Atom(_) => { return Some(gt); },
373                        _ => None,
374                    }
375                },
376                None => None,
377            }
378        },
379        _ => None,
380    }
381} // get_constant()
382
383
384/// Formats a substitution set for display. Use for debugging.
385/// # Note
386/// * It is not possible to implement the Display trait for
387///   SubstitutionSet, because it is a type alias.
388/// # Arguments
389/// * `ss` - substitution set
390/// # Return
391/// * `String`
392/// # Usage
393/// ```
394/// use std::rc::Rc;
395/// use suiron::*;
396///
397/// clear_id();
398/// let ss = empty_ss!();
399/// let ar = atom!("Argon");
400/// let x = logic_var!(next_id(), "$X");
401/// let ss = ar.unify(&x, &ss).unwrap();
402/// let f = format_ss(&ss);
403/// println!("{}", f);
404/// ```
405/// Prints:
406/// <pre>
407/// ----- Substitution Set -----
408/// 0	None
409/// 1	Argon
410/// ----------------------------
411/// </pre>
412pub fn format_ss(ss: &SubstitutionSet) -> String {
413    let mut out = "----- Substitution Set -----\n".to_string();
414    if ss.len() == 0 { out += "\tEmpty\n"; }
415    else {
416        for (i, term) in ss.iter().enumerate() {
417            match term {
418                None => { out += &format!("{}\tNone\n", i); },
419                Some(uni) => { out += &format!("{}\t{}\n", i, *uni); },
420            }
421        }
422    }
423    out += "----------------------------";
424    return out;
425} // format_ss()
426
427
428/// Prints a formatted substitution set. Use for debugging.
429/// # Arguments
430/// * `ss` - substitution set
431/// # Usage
432/// ```
433/// use std::rc::Rc;
434/// use suiron::*;
435///
436/// clear_id();
437/// let ss = empty_ss!();
438/// let ar = atom!("Argon");
439/// let x = logic_var!(next_id(), "$X");
440/// let ss = ar.unify(&x, &ss).unwrap();
441/// print_ss(&ss);
442/// ```
443/// Prints:
444/// <pre>
445/// ----- Substitution Set -----
446/// 0	None
447/// 1	Argon
448/// ----------------------------
449/// </pre>
450pub fn print_ss(ss: &SubstitutionSet) {
451    println!("{}", format_ss(ss));
452} // print_ss()
453
454
455#[cfg(test)]
456mod test {
457
458    use std::rc::Rc;
459    use crate::*;
460
461    #[test]
462    fn test_format_ss() {
463
464        let mut ss = empty_ss!();
465
466        let s = "----- Substitution Set -----\n\t\
467                Empty\n----------------------------";
468        assert_eq!(s, format_ss(&ss));
469
470        let ar = atom!("Argon");
471        let x = logic_var!(1, "$X");
472
473        // Binding: $X -> Argon
474        if let Some(ss2) = ar.unify(&x, &ss) {
475            ss = ss2;
476        }
477        else { panic!("Cannot bind $X = Argon."); }
478
479        let s = "----- Substitution Set -----\n\
480                0\tNone\n\
481                1\tArgon\n\
482                ----------------------------";
483        assert_eq!(s, format_ss(&ss));
484
485    } // test_format_ss()
486
487    // Test is_bound(), is_ground_variable(), get_binding(), get_ground_term().
488    #[test]
489    fn test_bound_and_ground() {
490
491        let a = atom!("Alpha");
492        let w = logic_var!(1, "$W");
493        let x = logic_var!(2, "$X");
494        let y = logic_var!(3, "$Y");
495        let z = logic_var!(4, "$Z");
496
497        let mut ss = empty_ss!();
498
499        if let Some(ss2) = x.unify(&w, &ss) { ss = ss2; }
500        else { panic!("Cannot unify $X = $W."); }
501
502        if let Some(ss2) = a.unify(&y, &ss) { ss = ss2; }
503        else { panic!("Cannot unify $Y = Alpha."); }
504
505        if let Some(ss2) = z.unify(&y, &ss) { ss = ss2; }
506        else { panic!("Cannot unify $Z = $Y."); }
507
508        if is_bound(&w, &ss) { panic!("is_bound() - $W should not be bound."); }
509        if !is_bound(&x, &ss) { panic!("is_bound() - $X should be bound."); }
510
511        if is_ground_variable(&x, &ss) {
512             panic!("is_ground_variable() - $X should not be ground.");
513        }
514        if !is_ground_variable(&z, &ss) {
515             panic!("is_ground_variable() - $Z should be ground.");
516        }
517
518        let b = get_binding(&z, &ss);
519        match b {
520            None => { panic!("get_binding() - $Z should be bound to $Y."); },
521            Some(entry) => {
522                let s = format!("{}", *entry);
523                assert_eq!("$Y_3", s, "get_binding() - $Z should be bound to $Y.");
524            },
525        }
526
527        if let Some(c) = get_ground_term(&z, &ss) {
528            assert_eq!("Alpha", c.to_string(),
529                       "get_ground_term() - \
530                        The ground term of $Z should be Alpha.");
531        }
532
533    } // test_bound_and_ground()
534
535    #[test]
536    fn test_get_complex() {
537
538        let ar = atom!("Argon");
539        let kr = atom!("Krypton");
540        let ne = atom!("Neon");
541        let el = atom!("element");
542        let c1 = scomplex!(el, ar, SInteger(18));
543        let el = atom!("element");
544        let c2 = scomplex!(el, kr, SInteger(36));
545
546        let x = logic_var!(1, "$X");
547        let y = logic_var!(2, "$Y");
548
549        let mut ss = empty_ss!();
550
551        // Binding: $X -> element(Argon, 18)
552        if let Some(ss2) = c1.unify(&x, &ss) { ss = ss2; }
553        else { panic!("Cannot bind $X = element(Argon, 18)."); }
554
555        // Binding: $Y -> $X -> element(Argon, 18)
556        if let Some(ss2) = y.unify(&x, &ss) { ss = ss2; }
557        else { panic!("Cannot bind $Y = $X."); }
558
559        // C2 is a complex term. It should simply be returned.
560        if let Some(res1) = get_complex(&c2, &ss) {
561            assert_eq!("element(Krypton, 36)", res1.to_string(),
562                       "First test - Must return same complex term.");
563        }
564        else { panic!("First test - element(Krypton, 36) should be returned."); }
565
566        // If term is not a complex term, return None.
567        if let Some(res2) = get_complex(&ne, &ss) {
568            panic!("Second test - Should return None. Got {}.", res2);
569        }
570
571        // The ground term of $Y is a complex term: element(Argon, 18).
572        // The function should be able to get this.
573        if let Some(res3) = get_complex(&y, &ss) {
574            assert_eq!("element(Argon, 18)", res3.to_string(),
575                       "Third test - Should get ground term.");
576        }
577        else { panic!("Third test - Cannot get ground term: element(Argon, 18)."); }
578    } // test_get_complex
579
580    #[test]
581    fn test_get_list() {
582
583        let ar = atom!("Argon");
584        let kr = atom!("Krypton");
585        let ne = atom!("Neon");
586        let l1 = slist!(false, ar, kr);
587        let l2 = slist!();
588
589        let x = logic_var!(1, "$X");
590        let y = logic_var!(2, "$Y");
591
592        let mut ss = empty_ss!();
593
594        // Binding: $X -> [Argon, Krypton]
595        if let Some(ss2) = l1.unify(&x, &ss) { ss = ss2; }
596        else { panic!("Cannot bind $X = [Argon, Krypton]."); }
597
598        // Binding: $Y -> $X -> element(Argon, 18)
599        if let Some(ss2) = y.unify(&x, &ss) { ss = ss2; }
600        else { panic!("Cannot bind $Y = $X."); }
601
602        // L2 is a list. It should simply be returned.
603        if let Some(res1) = get_list(&l2, &ss) {
604            assert_eq!("[]", res1.to_string(),
605                       "First test - Must return same list.");
606        }
607        else { panic!("First test - [] should be returned."); }
608
609        // If term is not a list, return None.
610        if let Some(res2) = get_list(&ne, &ss) {
611            panic!("Second test - Should return None. Got {}.", res2);
612        }
613
614        // The ground term of $Y is a list: [Argon, Krypton].
615        // The function should be able to get this.
616        if let Some(res3) = get_list(&y, &ss) {
617            assert_eq!("[Argon, Krypton]", res3.to_string(),
618                       "Third test - Should get ground term.");
619        }
620        else { panic!("Third test - Cannot get ground term: [Argon, Krypton]."); }
621
622    } // test_get_list
623
624    #[test]
625    fn test_get_constant() {
626
627        let ar = atom!("Argon");
628        let kr = atom!("Krypton");
629        let ne = atom!("Neon");
630        let l = slist!(false, ne);
631        let x = logic_var!(1, "$X");
632        let y = logic_var!(2, "$Y");
633
634        let mut ss = empty_ss!();
635
636        // Binding: $X -> Argon
637        if let Some(ss2) = ar.unify(&x, &ss) { ss = ss2; }
638        else { panic!("Cannot bind $X = Argon."); }
639
640        // Binding: $Y -> $X -> Argon
641        if let Some(ss2) = y.unify(&x, &ss) { ss = ss2; }
642        else { panic!("Cannot bind $Y = $X."); }
643
644        // Kryton is an atom. It should simply be returned.
645        if let Some(res1) = get_constant(&kr, &ss) {
646            assert_eq!("Krypton", res1.to_string(),
647                       "First test - Must return same atom.");
648        }
649        else { panic!("First test - Krypton is an atom. It should be returned."); }
650
651        // If term is a list (not an atom), return None.
652        if let Some(res2) = get_constant(&l, &ss) {
653            panic!("Second test - Should return None. Got {}.", res2);
654        }
655
656        // The ground term of $Y is an atom (Argon).
657        // The function should be able to get this.
658        if let Some(res3) = get_constant(&y, &ss) {
659            assert_eq!("Argon", res3.to_string(),
660                       "Third test - Should get ground term.");
661        }
662        else { panic!("Third test - Cannot get ground term (Argon)."); }
663    } // test_get_constant
664
665} // test