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