Skip to main content

prolog2/
app.rs

1use std::{
2    env, fs,
3    io::{stdin, stdout, Write},
4    process::ExitCode,
5    sync::Arc,
6};
7
8use console::Term;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    heap::{
13        heap::{Cell, Heap},
14        query_heap::QueryHeap,
15        symbol_db::SymbolDB,
16    },
17    parser::{
18        build_tree::{TokenStream, TreeClause},
19        execute_tree::{build_clause, execute_tree},
20        tokeniser::tokenise,
21    },
22    predicate_modules::{load_predicate_module, PredicateModule},
23    program::predicate_table::PredicateTable,
24    resolution::proof::Proof,
25};
26
27/// Engine configuration loaded from a JSON setup file.
28#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
29pub struct Config {
30    /// Maximum proof search depth.
31    pub max_depth: usize,
32    /// Maximum number of learned clauses per hypothesis.
33    pub max_clause: usize,
34    /// Maximum number of invented predicates.
35    pub max_pred: usize,
36    /// Enable debug trace output.
37    #[serde(default)]
38    pub debug: bool,
39}
40
41/// A body predicate declaration in the setup file.
42#[derive(Serialize, Deserialize, Debug)]
43pub struct BodyClause {
44    /// Predicate name.
45    pub symbol: String,
46    /// Predicate arity.
47    pub arity: usize,
48}
49
50/// Positive and negative training examples.
51#[derive(Serialize, Deserialize, Debug)]
52pub struct Examples {
53    /// Positive examples (goals that should succeed).
54    pub pos: Vec<String>,
55    /// Negative examples (goals that should fail).
56    pub neg: Vec<String>,
57}
58
59/// Top-level setup loaded from a JSON configuration file.
60#[derive(Serialize, Deserialize, Debug)]
61pub struct SetUp {
62    pub config: Config,
63    pub body_predicates: Vec<BodyClause>,
64    pub files: Vec<String>,
65    pub examples: Option<Examples>,
66}
67
68impl Examples {
69    /// Convert the examples into a single Prolog query string.
70    ///
71    /// Positive examples become goals; negative examples are wrapped in `not(...)`.
72    pub fn to_query(&self) -> String {
73        let mut buffer = String::new();
74        for pos_ex in &self.pos {
75            buffer += pos_ex;
76            buffer += ",";
77        }
78        for neg_ex in &self.neg {
79            buffer += &format!("not({neg_ex}),");
80        }
81        buffer.pop();
82        buffer += ".";
83        buffer
84    }
85}
86
87/// Builder for configuring and running the Prolog² engine.
88///
89/// # Example
90///
91/// ```no_run
92/// use prolog2::app::App;
93/// use prolog2::predicate_modules::{MATHS, META_PREDICATES};
94///
95/// Prolog2::from_args()
96///     .add_module(&MATHS)
97///     .add_module(&META_PREDICATES)
98///     .run();
99/// ```
100pub struct App {
101    modules: Vec<&'static PredicateModule>,
102    config_path: String,
103    auto: bool,
104}
105
106impl App {
107    /// Create a new builder with no modules and default settings.
108    pub fn new() -> Self {
109        App {
110            modules: Vec::new(),
111            config_path: "setup.json".into(),
112            auto: false,
113        }
114    }
115
116    /// Create a builder pre-configured from command-line arguments.
117    ///
118    /// Parses `std::env::args()` for:
119    /// - A config file path (first non-flag argument, default: `"setup.json"`)
120    /// - `--all` / `-a` to auto-enumerate all solutions
121    pub fn from_args() -> Self {
122        let args: Vec<String> = env::args().collect();
123        let auto = args.iter().any(|arg| arg == "--all" || arg == "-a");
124
125        let config_path = args
126            .iter()
127            .filter(|arg| !arg.starts_with('-') && *arg != &args[0])
128            .next()
129            .cloned()
130            .unwrap_or_else(|| "setup.json".into());
131
132        App {
133            modules: Vec::new(),
134            config_path,
135            auto,
136        }
137    }
138
139    /// Register a predicate module.
140    pub fn add_module(mut self, module: &'static PredicateModule) -> Self {
141        self.modules.push(module);
142        self
143    }
144
145    /// Set the config file path.
146    pub fn config(mut self, path: impl Into<String>) -> Self {
147        self.config_path = path.into();
148        self
149    }
150
151    /// Set whether to auto-enumerate all solutions.
152    pub fn auto(mut self, value: bool) -> Self {
153        self.auto = value;
154        self
155    }
156
157    /// Load the configuration, register modules, and run.
158    ///
159    /// If the config contains examples, they are run as a query.
160    /// Otherwise an interactive REPL is started.
161    pub fn run(self) -> ExitCode {
162        let (config, predicate_table, heap, examples) = self.load_setup();
163
164        let predicate_table = Arc::new(predicate_table);
165        let heap = Arc::new(heap);
166
167        match examples {
168            Some(examples) => {
169                start_query(&examples.to_query(), predicate_table, heap, config, self.auto)
170                    .unwrap();
171                ExitCode::SUCCESS
172            }
173            None => main_loop(config, predicate_table, heap),
174        }
175    }
176
177    fn load_setup(&self) -> (Config, PredicateTable, Vec<Cell>, Option<Examples>) {
178        let mut heap = Vec::new();
179        let mut predicate_table = PredicateTable::new();
180
181        for module in &self.modules {
182            load_predicate_module(&mut predicate_table, module);
183        }
184
185        let setup: SetUp = serde_json::from_str(
186            &fs::read_to_string(&self.config_path)
187                .unwrap_or_else(|_| panic!("Failed to read config file: {}", self.config_path)),
188        )
189        .unwrap_or_else(|e| {
190            panic!("Failed to parse config file '{}': {}", self.config_path, e)
191        });
192        let config = setup.config;
193
194        for file_path in setup.files {
195            load_file(file_path, &mut predicate_table, &mut heap);
196        }
197
198        for BodyClause { symbol, arity } in setup.body_predicates {
199            let sym = SymbolDB::set_const(symbol.clone());
200            predicate_table
201                .set_body((sym, arity), true)
202                .unwrap_or_else(|e| panic!("{e}: {symbol}/{arity}"));
203        }
204
205        (config, predicate_table, heap, setup.examples)
206    }
207}
208
209fn load_file(file_path: String, predicate_table: &mut PredicateTable, heap: &mut Vec<Cell>) {
210    let file = fs::read_to_string(&file_path)
211        .unwrap_or_else(|_| panic!("Failed to read file: {}", file_path));
212    let syntax_tree = TokenStream::new(tokenise(file).unwrap())
213        .parse_all()
214        .unwrap();
215
216    execute_tree(syntax_tree, heap, predicate_table);
217}
218
219fn continue_proof(auto: bool) -> bool {
220    if auto {
221        return true;
222    }
223    let term = Term::stderr();
224    loop {
225        match term.read_key_raw().unwrap() {
226            console::Key::Enter | console::Key::Backspace | console::Key::Char('.') => {
227                return false
228            }
229            console::Key::Char(' ' | ';') | console::Key::Tab => return true,
230            _ => (),
231        }
232    }
233}
234
235fn start_query(
236    query_text: &str,
237    predicate_table: Arc<PredicateTable>,
238    heap: Arc<Vec<Cell>>,
239    config: Config,
240    auto: bool,
241) -> Result<(), String> {
242    let query = format!(":-{query_text}");
243    let literals = match TokenStream::new(tokenise(query)?).parse_clause()? {
244        Some(TreeClause::Directive(literals)) => literals,
245        _ => return Err(format!("Query: '{query_text}' incorrectly formatted")),
246    };
247
248    let mut query_heap = QueryHeap::new(heap, None);
249    let goals = build_clause(literals, None, None, &mut query_heap, true);
250    let mut vars = Vec::new();
251    for literal in goals.iter() {
252        vars.extend(query_heap.term_vars(*literal, false).iter().map(|addr| {
253            (
254                SymbolDB::get_var(*addr, query_heap.get_id()).unwrap(),
255                *addr,
256            )
257        }));
258    }
259    let mut proof = Proof::new(query_heap, &goals);
260
261    loop {
262        if proof.prove(predicate_table.clone(), config) {
263            println!("TRUE");
264            for (symbol, addr) in &vars {
265                println!("{symbol} = {}", proof.heap.term_string(*addr))
266            }
267            // TODO: display variable bindings
268            if proof.hypothesis.len() != 0 {
269                println!("{}", proof.hypothesis.to_string(&proof.heap));
270            }
271            if !continue_proof(auto) {
272                break;
273            }
274        } else {
275            println!("FALSE");
276            break;
277        }
278    }
279
280    drop(proof);
281
282    Ok(())
283}
284
285fn main_loop(
286    config: Config,
287    predicate_table: Arc<PredicateTable>,
288    heap: Arc<Vec<Cell>>,
289) -> ExitCode {
290    let mut buffer = String::new();
291    loop {
292        if buffer.is_empty() {
293            print!("?-");
294            stdout().flush().unwrap();
295        }
296        match stdin().read_line(&mut buffer) {
297            Ok(_) => {
298                if buffer.contains('.') {
299                    match start_query(
300                        &buffer,
301                        predicate_table.clone(),
302                        heap.clone(),
303                        config,
304                        false,
305                    ) {
306                        Ok(_) => buffer.clear(),
307                        Err(error) => println!("{error}"),
308                    }
309                } else {
310                    continue;
311                }
312            }
313            Err(error) => {
314                println!("error: {error}");
315                break;
316            }
317        }
318    }
319    ExitCode::SUCCESS
320}