suiron/
parse_terms.rs

1//! Functions for parsing unifiable terms and lists of terms.
2//!
3// Cleve Lendon 2023
4
5use super::infix::*;
6use super::parse_goals::*;
7use super::s_linked_list::*;
8use super::logic_var::*;
9use super::s_complex::*;
10use super::unifiable::{*, Unifiable::*};
11use super::built_in_functions::*;
12
13use crate::atom;
14use crate::sfunction;
15use crate::str_to_chars;
16use crate::chars_to_string;
17
18/// Parses a list of terms (arguments).
19///
20/// Parses a comma separated list of terms to produce a vector
21/// of [Unifiable](../unifiable/enum.Unifiable.html) terms.
22///
23/// # Arguments
24/// * string to parse
25/// # Return
26/// * vector of
27/// [Unifiable](../unifiable/enum.Unifiable.html) terms or error message
28/// # Usage
29/// ```
30/// use suiron::*;
31///
32/// if let Ok(terms) = parse_arguments("Argon, 18") {
33///     println!("{:?}", terms);
34/// }
35/// // Should print: [Atom("Argon"), SInteger(18)]
36/// ```
37pub fn parse_arguments(to_parse: &str) -> Result<Vec<Unifiable>, String> {
38
39    let s = to_parse.trim();
40    let chrs = str_to_chars!(s);
41    let length_chrs = chrs.len();
42
43    if length_chrs == 0 {
44        let err = "parse_arguments() - Empty argument list: ";
45        return Err(err.to_string());
46    }
47
48    if chrs[0] == ',' {
49        let err = "parse_arguments() - Missing first argument: ";
50        return Err(err.to_string());
51    }
52
53    // A comma at the end probably indicates a missing argument, but...
54    // make sure comma is not escaped, because this is valid: "term1, term2, \,"
55    if chrs[length_chrs - 1] == ',' {
56        // Length must be longer than 1, because comma
57        // is not the first character.
58        let prev = chrs[length_chrs - 2];
59        if prev != '\\' {   // escape character
60           let err = "parse_arguments() - Missing last argument: ";
61           return Err(err.to_string());
62        }
63    }
64
65    let mut has_digit     = false;
66    let mut has_non_digit = false;
67    let mut has_period    = false;
68    let mut open_quote    = false;
69
70    let mut num_quotes    = 0;
71    let mut round_depth   = 0;   // depth of round parentheses (())
72    let mut square_depth  = 0;   // depth of square brackets [[]]
73
74    let mut argument = "".to_string();
75    let mut term_list = Vec::<Unifiable>::new();
76
77    let mut start = 0;
78
79    let mut i = start;
80    while i < length_chrs {
81
82        let ch = chrs[i];
83
84        // If this argument is between double quotes,
85        // it must be an Atom.
86        if open_quote {
87            argument.push(ch);
88            if ch == '"' {
89                open_quote = false;
90                num_quotes += 1;
91            }
92        }
93        else {
94            if ch == '[' {
95                argument.push(ch);
96                square_depth += 1;
97            }
98            else if ch == ']' {
99                argument.push(ch);
100                square_depth -= 1;
101            }
102            else if ch == '(' {
103                argument.push(ch);
104                round_depth += 1;
105            }
106            else if ch == ')' {
107                argument.push(ch);
108                round_depth -= 1
109            }
110            else if round_depth == 0 && square_depth == 0 {
111
112                if ch == ',' {
113
114                    let s2 = argument.trim();
115                    match check_quotes(s2, num_quotes) {
116                        Some(err) => { return Err(err); },
117                        None => {},
118                    }
119                    num_quotes = 0;
120
121                    let term = make_term(s2, has_digit, has_non_digit, has_period)?;
122                    term_list.push(term);
123                    argument    = "".to_string();
124                    has_digit   = false;
125                    has_non_digit = false;
126                    has_period  = false;
127                    start = i + 1;    // past comma
128                }
129                else if ch >= '0' && ch <= '9' {
130                    argument.push(ch);
131                    has_digit = true
132                }
133                else if ch == '+' || ch == '-' {
134                    argument.push(ch);
135                    // Plus or minus might be in front of a number: +7, -3.8
136                    // In this case, it is part of the number.
137                    let mut next_ch = 'x';
138                    if i < length_chrs - 1 { next_ch = chrs[i + 1]; }
139                    let mut prev_ch = ' ';
140                    if i > 0 { prev_ch = chrs[i]; }
141                    if prev_ch == ' ' && (next_ch < '0' || next_ch > '9') {
142                        has_non_digit = true;
143                    }
144                }
145                else if ch == '.' {
146                    argument.push(ch);
147                    has_period = true
148                }
149                else if ch == '\\' {  // escape character, must include next character
150                    if i < length_chrs {
151                        i += 1;
152                        argument.push(chrs[i]);
153                    }
154                    else {  // must be at end of argument string
155                        argument.push(ch);
156                    }
157                }
158                else if ch == '"' {
159                    argument.push(ch);
160                    open_quote = true;  // first quote
161                    num_quotes += 1;
162                }
163                else {
164                    argument.push(ch);
165                    if ch > ' ' { has_non_digit = true; }
166                }
167            }
168            else {
169                // Must be between () or []. Just add character.
170                argument.push(ch);
171            }
172        } // not open_quote
173
174        i += 1;
175
176    } // while
177
178    if start < length_chrs {
179
180        let s2 = argument.trim();
181        match check_quotes(s2, num_quotes) {
182            Some(err) => {
183                return Err(err);
184            },
185            None => {},
186        }
187
188        let term = make_term(s2, has_digit, has_non_digit, has_period)?;
189        term_list.push(term);
190    }
191
192    if round_depth != 0 {
193        let err = "parse_arguments() - Unmatched parentheses: ";
194        return Err(err.to_string());
195    }
196
197    if square_depth != 0 {
198        let err = "parse_arguments() - Unmatched brackets: ";
199        return Err(err.to_string());
200    }
201
202    return Ok(term_list);
203
204} // parse_arguments()
205
206
207// make_term()
208// Creates a Unifiable term from the given string.
209//
210// Arguments
211//    string to parse
212//    has_digit     - boolean, true if to_parse has digit
213//    has_non_digit - boolean, true if to_parse has non-digit
214//    has_period    - boolean, true if to_parse has period
215// Return
216//    unifiable term or erro message
217fn make_term(to_parse: &str,
218             has_digit: bool,
219             has_non_digit: bool,
220             has_period: bool) -> Result<Unifiable, String> {
221
222    let s = to_parse.trim();
223
224    let term_chars = str_to_chars!(s);
225    let length_term = term_chars.len();
226
227    if length_term == 0 {
228        let err = mt_error("Length of term is 0", s);
229        return Err(err);
230    }
231
232    let first: char = term_chars[0];
233    if first == '$' {
234
235        // Anonymous variable.
236        if s == "$_" { return Ok(Anonymous); }
237
238        // If the string is not a valid LogicVar
239        // (perhaps $ or $10), make it an Atom.
240        match make_logic_var(s.to_string()) {
241            Ok(var) => { return Ok(var); },
242            Err(_)  => { return Ok(atom!(s)); },
243        }
244    }
245
246    // If the argument begins and ends with a quotation mark,
247    // the argument is an Atom. Strip off quotation marks.
248    if length_term >= 2 {
249        let last = term_chars[length_term - 1];
250        if first == '"' {
251            if last == '"' {
252                let chars2: Vec<char> = term_chars[1..length_term - 1].to_vec();
253                if chars2.len() == 0 {
254                    let err = mt_error("Invalid term. Length is 0", s);
255                    return Err(err);
256                }
257                let s2 = chars_to_string!(chars2);
258                return Ok(Atom(s2.to_string()));
259            } else {
260                let err = mt_error("Invalid term. Unmatched quote mark", s);
261                return Err(err)
262            }
263        } else if first == '[' && last == ']' {
264            return parse_linked_list(s);
265        }
266        // Try complex terms, eg.:  job(programmer)
267        else if first != '(' && last == ')' {
268            // Check for built-in functions.
269            if s.starts_with("join(")     { return parse_function(s); }
270            if s.starts_with("add(")      { return parse_function(s); }
271            if s.starts_with("subtract(") { return parse_function(s); }
272            if s.starts_with("multiply(") { return parse_function(s); }
273            if s.starts_with("divide(")   { return parse_function(s); }
274            return parse_complex(s);
275        }
276    } // length >= 2
277
278    if has_digit && !has_non_digit { // Must be Integer or Float.
279        if has_period {
280            match s.parse::<f64>() {
281                Ok(fl) => { return Ok(SFloat(fl)); },
282                Err(_) => { return Err("Invalid float.".to_string()) },
283            }
284        } else {
285            match s.parse::<i64>() {
286                Ok(i) => { return Ok(SInteger(i)); },
287                Err(_) => { return Err("Invalid integer".to_string()); },
288            }
289        }
290    }
291    return Ok(atom!(s));
292
293}  // make_term
294
295/// Checks validity of double quote marks in a string.
296///
297/// An argument may be enclosed in double quotation marks, eg. `"Sophie"`.
298/// If there are unpaired quotation marks, such as in `""Sophie"`, an error
299/// message will be returned. Otherwise, None is returned.
300/// # Arguments
301/// * string to check
302/// * number of double quotes in string (previously counted)
303/// # Return
304/// * error message or None
305/// # Usage
306/// Note: In Rust source, double quotes are escaped with a backslash: \\
307/// ```
308/// use suiron::*;
309///
310/// if let Some(error_message) = check_quotes("\"\"Sophie\"", 3) {
311///     println!("{}", error_message);
312/// }
313/// // Should print: check_quotes() - Unmatched quotes: ""Sophie"
314/// ```
315pub fn check_quotes(to_check: &str, count: usize) -> Option<String> {
316    if count == 0 { return None; }
317    if count != 2 {
318        return Some(cq_error("Unmatched quotes", to_check));
319    }
320    let chrs = str_to_chars!(to_check);
321    let first = chrs[0];
322    if first != '"' {
323        return Some(cq_error("Text before opening quote", to_check));
324    }
325    let last = chrs[chrs.len() - 1];
326    if last != '"' {
327        return Some(cq_error("Text after closing quote", to_check));
328    }
329    None
330} // check_quotes()
331
332/// Parses a string to produce a [Unifiable](../unifiable/enum.Unifiable.html) term.
333///
334/// parse_term(\"$_\") ➔ [Anonymous](../unifiable/enum.Unifiable.html#variant.Anonymous)<br>
335/// parse_term(\"verb\") ➔ [Atom](../unifiable/enum.Unifiable.html#variant.Atom)<br>
336/// parse_term(\"1.7\") ➔ [SFloat](../unifiable/enum.Unifiable.html#variant.SFloat)<br>
337/// parse_term(\"46\") ➔ [SInteger](../unifiable/enum.Unifiable.html#variant.SInteger)<br>
338/// parse_term(\"$X\") ➔ [LogicVar](../unifiable/enum.Unifiable.html#variant.LogicVar)<br>
339/// parse_term(\"animal(horse, mammal)\") ➔
340/// [SComplex](../unifiable/enum.Unifiable.html#variant.SComplex)<br>
341/// parse_term(\"[a, b, c]\") ➔
342/// [SLinkedList](../unifiable/enum.Unifiable.html#variant.SLinkedList)<br>
343/// parse_term(\"$X + 6\") ➔
344/// [SFunction](../unifiable/enum.Unifiable.html#variant.SFunction)
345///
346/// # Arguments
347/// * string to parse
348/// # Return
349/// * [Unifiable](../unifiable/enum.Unifiable.html) term or error message
350/// # Usage
351/// ```
352/// use suiron::*;
353///
354/// match parse_term(" animal(horse, mammal) ") {
355///     Ok(term) => { println!("{}", term); },
356///     Err(msg) => { println!("{}", msg); },
357/// }
358/// // Should print: animal(horse, mammal)
359/// ```
360pub fn parse_term(to_parse: &str) -> Result<Unifiable, String> {
361
362    let mut s = to_parse.trim();
363
364    let mut has_digit     = false;
365    let mut has_non_digit = false;
366    let mut has_period    = false;
367
368    let chrs = str_to_chars!(&s);
369
370    // First, let's check for an arithmetic function with an infix,
371    // such as $X + 100 or $X / 100.
372    let (infix, index) = check_arithmetic_infix(&chrs);
373
374    if infix == Infix::Plus || infix == Infix::Minus ||
375       infix == Infix::Multiply || infix == Infix::Divide {
376
377        let (left, right) = get_left_and_right(chrs, index, 1)?;
378
379        let sfunc = match infix {
380            Infix::Plus     => { sfunction!("add", left, right) },
381            Infix::Minus    => { sfunction!("subtract", left, right) },
382            Infix::Multiply => { sfunction!("multiply", left, right) },
383            Infix::Divide   => { sfunction!("divide", left, right) },
384            _ => {
385                let s = format!("parse_term() - Invalid infix {}", infix);
386                return Err(s);
387            },
388        };
389        return Ok(sfunc);
390    }
391
392    for ch in &chrs {
393        if *ch >= '0' && *ch <= '9' {
394            has_digit = true;
395        } else if *ch == '.' {
396            has_period = true;
397        } else {
398            has_non_digit = true;
399        }
400    }
401
402    // Check for escaped characters, eg: \,
403    if chrs.len() == 2 && chrs[0] == '\\' { s = &s[1..]; }
404
405    return make_term(s, has_digit, has_non_digit, has_period);
406
407}  // parse_term
408
409// Formats an error message for make_term().
410// Arguments:
411//   err - error description
412//   bad - string which caused the error
413// Return:
414//   error message (String)
415fn mt_error(err: &str, bad: &str) -> String {
416    format!("make_term() - {}: {}", err, bad)
417}
418
419// Formats an error message for check_quotes().
420// Arguments:
421//   err - error description
422//   bad - string which caused the error
423// Return:
424//   error message (String)
425fn cq_error(err: &str, bad: &str) -> String {
426    format!("check_quotes() - {}: {}", err, bad)
427}
428
429#[cfg(test)]
430mod test {
431
432    use super::*;
433    use crate::unifiable::Unifiable;
434
435    #[test]
436    fn test_check_quotes() {
437
438        // Check: ""Hello"
439        if let Some(error_message) = check_quotes("\"\"Hello\"", 3) {
440            assert_eq!(error_message,
441                       "check_quotes() - Unmatched quotes: \"\"Hello\"");
442        }
443        // Check: x"Hello"
444        if let Some(error_message) = check_quotes("x\"Hello\"", 2) {
445            assert_eq!(error_message,
446                       "check_quotes() - Text before opening quote: x\"Hello\"");
447        }
448        // Check: "Hello"x
449        if let Some(error_message) = check_quotes("\"Hello\"x", 2) {
450            assert_eq!(error_message,
451                       "check_quotes() - Text after closing quote: \"Hello\"x");
452        }
453        // Check: "Hello"
454        if let Some(error_message) = check_quotes("\"Hello\"", 2) {
455            panic!("The string should be OK: {}", error_message);
456        }
457        // Check: Hello
458        if let Some(error_message) = check_quotes("Hello", 0) {
459            panic!("The string should be OK: {}", error_message);
460        }
461    } // test_check_quotes()
462
463    #[test]
464    fn test_parse_term() {
465        match parse_term(" $_ ") {
466            Ok(term) => {
467                if matches!(term, Unifiable::Anonymous) {
468                    assert_eq!("$_", term.to_string());
469                } else {
470                    panic!("Should create an Anonymous variable: {}", term)
471                }
472            },
473            Err(msg) => { panic!("{}", msg); },
474        }
475        match parse_term(" $X ") {
476            Ok(term) => {
477                if let Unifiable::LogicVar{id, name} = term {
478                    assert_eq!(0, id);
479                    assert_eq!("$X", name);
480                } else {
481                    panic!("Should create a LogicVar: {}", term)
482                }
483            },
484            Err(msg) => { panic!("{}", msg); },
485        }
486        match parse_term(" $10 ") {
487            Ok(term) => {
488                if let Unifiable::Atom(s) = term {
489                    assert_eq!("$10", s);
490                } else {
491                    panic!("Should create an Atom: {}", term)
492                }
493            },
494            Err(msg) => { panic!("{}", msg); },
495        }
496        match parse_term(" verb ") {
497            Ok(term) => {
498                if matches!(term, Unifiable::Atom(_)) {
499                    assert_eq!("verb", term.to_string());
500                } else {
501                    panic!("Should create an Atom: {}", term)
502                }
503            },
504            Err(msg) => { panic!("{}", msg); },
505        }
506        match parse_term(" 1.7 ") {
507            Ok(term) => {
508                if matches!(term, Unifiable::SFloat(_)) {
509                    assert_eq!("1.7", term.to_string());
510                } else {
511                    panic!("Should create an SFloat: {}", term)
512                }
513            },
514            Err(msg) => { panic!("{}", msg); },
515        }
516        match parse_term(" 46 ") {
517            Ok(term) => {
518                if matches!(term, Unifiable::SInteger(_)) {
519                    assert_eq!("46", term.to_string());
520                } else {
521                    panic!("Should create an SInteger: {}", term)
522                }
523            },
524            Err(msg) => { panic!("{}", msg); },
525        }
526        match parse_term(" animal(horse, mammal) ") {
527            Ok(term) => {
528                if let Unifiable::SComplex(terms) = term {
529                    assert_eq!("mammal", terms[2].to_string());
530                } else {
531                    panic!("Should create an SComplex: {}", term)
532                }
533            },
534            Err(msg) => { panic!("{}", msg); },
535        }
536    } // test_parse_term()
537
538    #[test]
539    fn test_parse_arguments() {
540
541        let terms_str = "8, 5.9, symptom, [], [a, b | $T], city(Toronto, 2.79)";
542
543        match parse_arguments(terms_str) {
544            Ok(terms) => {
545                match terms[0] {
546                    SInteger(i) => { assert_eq!(i, 8, "Incorrect integer."); },
547                    _ => { panic!("Should create SInteger: {}", terms[0]); },
548                };
549                match terms[1] {
550                    SFloat(f) => { assert_eq!(f, 5.9, "Incorrect float."); },
551                    _ => { panic!("Should create SFloat: {}", terms[1]); },
552                };
553                match &terms[2] {
554                    Atom(a) => { assert_eq!(a, "symptom", "Incorrect atom"); },
555                    _ => { panic!("Should create an Atom: {}", terms[2]); },
556                };
557                match &terms[3] {
558                    SLinkedList{term: _, next: _, count: c, tail_var: _} => {
559                        let size: usize = 0;
560                        assert_eq!(*c, size, "Should be empty list.");
561                    },
562                    _ => { panic!("Should create an empty list: {}", terms[3]); },
563                };
564                match &terms[4] {
565                    SLinkedList{term: _, next: _, count: c, tail_var: _} => {
566                        let size: usize = 3;
567                        assert_eq!(*c, size, "Incorrect list size.");
568                        let s = terms[4].to_string();
569                        assert_eq!(s, "[a, b | $T]", "Incorrect list.");
570                    },
571                    _ => { panic!("Should create a list: {}", terms[4]); },
572                };
573                match &terms[5] {
574                    SComplex(args) => {
575                        let arity = args.len() - 1;  // exclude functor
576                        assert_eq!(arity, 2, "Complex term, incorrect arity.");
577                        let s = terms[5].to_string();
578                        assert_eq!(s, "city(Toronto, 2.79)", "Incorrect complex term.");
579                    },
580                    _ => { panic!("Should create a complex term: {}", terms[5]); },
581                };
582            },
583            Err(err) => { panic!("{}", err); },
584        }
585
586    } // test_parse_arguments()
587
588} // test