Skip to main content

tensorlogic_compiler/import/
prolog.rs

1//! Prolog Syntax Parser
2//!
3//! Parses logic expressions from Prolog-like syntax into TensorLogic IR.
4//!
5//! ## Supported Syntax
6//!
7//! - **Facts**: `mortal(socrates).`
8//! - **Rules**: `mortal(X) :- human(X).`
9//! - **Conjunctions**: `human(X), mortal(X)`
10//! - **Disjunctions**: `human(X) ; mortal(X)`
11//! - **Negation**: `\+ mortal(X)` or `not(mortal(X))`
12//! - **Quantifiers**: Implicit universal quantification over variables
13//!
14//! ## Examples
15//!
16//! ```
17//! use tensorlogic_compiler::import::prolog::parse_prolog;
18//!
19//! // Simple fact
20//! let expr = parse_prolog("knows(alice, bob).").unwrap();
21//!
22//! // Rule with implication
23//! let expr = parse_prolog("mortal(X) :- human(X).").unwrap();
24//!
25//! // Conjunction
26//! let expr = parse_prolog("human(X), greek(X).").unwrap();
27//! ```
28
29use anyhow::{anyhow, Result};
30use tensorlogic_ir::{TLExpr, Term};
31
32/// Parse Prolog syntax into TLExpr
33///
34/// # Arguments
35///
36/// * `input` - Prolog syntax string (may end with `.`)
37///
38/// # Returns
39///
40/// Parsed `TLExpr` or error if parsing fails
41pub fn parse_prolog(input: &str) -> Result<TLExpr> {
42    let input = input.trim().trim_end_matches('.');
43
44    // Check for rule (:-) vs fact
45    if let Some(pos) = input.find(":-") {
46        let head = input[..pos].trim();
47        let body = input[pos + 2..].trim();
48
49        let head_expr = parse_prolog_term(head)?;
50        let body_expr = parse_prolog_term(body)?;
51
52        Ok(TLExpr::imply(body_expr, head_expr))
53    } else {
54        parse_prolog_term(input)
55    }
56}
57
58fn parse_prolog_term(input: &str) -> Result<TLExpr> {
59    let input = input.trim();
60
61    // Handle disjunction (;)
62    if let Some(pos) = find_operator(input, ';') {
63        let left = parse_prolog_term(input[..pos].trim())?;
64        let right = parse_prolog_term(input[pos + 1..].trim())?;
65        return Ok(TLExpr::or(left, right));
66    }
67
68    // Handle conjunction (,)
69    if let Some(pos) = find_operator(input, ',') {
70        let left = parse_prolog_term(input[..pos].trim())?;
71        let right = parse_prolog_term(input[pos + 1..].trim())?;
72        return Ok(TLExpr::and(left, right));
73    }
74
75    // Handle negation (\+)
76    if let Some(stripped) = input.strip_prefix("\\+") {
77        let inner = stripped.trim();
78        return Ok(TLExpr::negate(parse_prolog_term(inner)?));
79    }
80
81    // Handle negation (not(...))
82    if input.starts_with("not(") && input.ends_with(')') {
83        let inner = &input[4..input.len() - 1];
84        return Ok(TLExpr::negate(parse_prolog_term(inner)?));
85    }
86
87    // Handle parentheses
88    if input.starts_with('(') && input.ends_with(')') {
89        return parse_prolog_term(&input[1..input.len() - 1]);
90    }
91
92    // Parse predicate: name(args...)
93    if let Some(open_paren) = input.find('(') {
94        if !input.ends_with(')') {
95            return Err(anyhow!("Unmatched parentheses in: {}", input));
96        }
97
98        let pred_name = input[..open_paren].trim();
99        let args_str = &input[open_paren + 1..input.len() - 1];
100
101        let args = parse_arguments(args_str)?;
102
103        Ok(TLExpr::pred(pred_name, args))
104    } else {
105        // Atomic predicate with no arguments
106        Ok(TLExpr::pred(input, vec![]))
107    }
108}
109
110fn parse_arguments(args_str: &str) -> Result<Vec<Term>> {
111    if args_str.trim().is_empty() {
112        return Ok(vec![]);
113    }
114
115    let parts = split_arguments(args_str);
116    parts
117        .into_iter()
118        .map(|arg| parse_term(arg.trim()))
119        .collect()
120}
121
122fn parse_term(term_str: &str) -> Result<Term> {
123    // Variable: uppercase first letter
124    if term_str.chars().next().is_some_and(|c| c.is_uppercase()) {
125        Ok(Term::var(term_str))
126    } else {
127        // Constant (numeric or symbolic)
128        Ok(Term::constant(term_str))
129    }
130}
131
132fn split_arguments(args: &str) -> Vec<String> {
133    let mut result = Vec::new();
134    let mut current = String::new();
135    let mut depth = 0;
136
137    for ch in args.chars() {
138        match ch {
139            '(' => {
140                depth += 1;
141                current.push(ch);
142            }
143            ')' => {
144                depth -= 1;
145                current.push(ch);
146            }
147            ',' if depth == 0 => {
148                result.push(current.clone());
149                current.clear();
150            }
151            _ => current.push(ch),
152        }
153    }
154
155    if !current.is_empty() {
156        result.push(current);
157    }
158
159    result
160}
161
162fn find_operator(input: &str, op: char) -> Option<usize> {
163    let mut depth = 0;
164
165    for (i, ch) in input.chars().enumerate() {
166        match ch {
167            '(' => depth += 1,
168            ')' => depth -= 1,
169            c if c == op && depth == 0 => return Some(i),
170            _ => {}
171        }
172    }
173
174    None
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_simple_fact() {
183        let expr = parse_prolog("mortal(socrates).").unwrap();
184        assert!(matches!(expr, TLExpr::Pred { .. }));
185    }
186
187    #[test]
188    fn test_rule() {
189        let expr = parse_prolog("mortal(X) :- human(X).").unwrap();
190        assert!(matches!(expr, TLExpr::Imply { .. }));
191    }
192
193    #[test]
194    fn test_conjunction() {
195        let expr = parse_prolog("human(X), mortal(X).").unwrap();
196        assert!(matches!(expr, TLExpr::And(_, _)));
197    }
198
199    #[test]
200    fn test_disjunction() {
201        let expr = parse_prolog("human(X) ; god(X).").unwrap();
202        assert!(matches!(expr, TLExpr::Or(_, _)));
203    }
204
205    #[test]
206    fn test_negation_prefix() {
207        let expr = parse_prolog("\\+ mortal(X).").unwrap();
208        assert!(matches!(expr, TLExpr::Not(_)));
209    }
210
211    #[test]
212    fn test_negation_function() {
213        let expr = parse_prolog("not(mortal(X)).").unwrap();
214        assert!(matches!(expr, TLExpr::Not(_)));
215    }
216
217    #[test]
218    fn test_complex_rule() {
219        let expr = parse_prolog("mortal(X) :- human(X), \\+ god(X).").unwrap();
220        assert!(matches!(expr, TLExpr::Imply { .. }));
221    }
222
223    #[test]
224    fn test_predicate_no_args() {
225        let expr = parse_prolog("alive.").unwrap();
226        assert!(matches!(expr, TLExpr::Pred { .. }));
227    }
228
229    #[test]
230    fn test_multiple_args() {
231        let expr = parse_prolog("knows(alice, bob, charlie).").unwrap();
232        if let TLExpr::Pred { name, args } = expr {
233            assert_eq!(name, "knows");
234            assert_eq!(args.len(), 3);
235        } else {
236            panic!("Expected Pred");
237        }
238    }
239
240    #[test]
241    fn test_variables_and_constants() {
242        let expr = parse_prolog("age(Person, 25).").unwrap();
243        if let TLExpr::Pred { name: _, args } = expr {
244            assert!(matches!(args[0], Term::Var(_)));
245            assert!(matches!(args[1], Term::Const(_)));
246        } else {
247            panic!("Expected Pred");
248        }
249    }
250}