Skip to main content

prolog2/
app.rs

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