diff --git a/benches/run_iai.rs b/benches/run_iai.rs index c29645c2d..27f003394 100644 --- a/benches/run_iai.rs +++ b/benches/run_iai.rs @@ -5,7 +5,7 @@ mod setup; mod iai { use iai_callgrind::{library_benchmark, library_benchmark_group, main}; - use scryer_prolog::QueryResolution; + use scryer_prolog::LeafAnswer; use super::setup; @@ -13,7 +13,7 @@ mod iai { #[bench::count_edges(setup::prolog_benches()["count_edges"].setup())] #[bench::numlist(setup::prolog_benches()["numlist"].setup())] #[bench::csv_codename(setup::prolog_benches()["csv_codename"].setup())] - fn bench(mut run: impl FnMut() -> QueryResolution) -> QueryResolution { + fn bench(mut run: impl FnMut() -> Vec) -> Vec { run() } diff --git a/benches/setup.rs b/benches/setup.rs index 6087be446..ac055c083 100644 --- a/benches/setup.rs +++ b/benches/setup.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, fs, path::Path}; use maplit::btreemap; -use scryer_prolog::{Machine, QueryResolution, Value}; +use scryer_prolog::{LeafAnswer, Machine, MachineBuilder, Term}; pub fn prolog_benches() -> BTreeMap<&'static str, PrologBenchmark> { [ @@ -10,21 +10,21 @@ pub fn prolog_benches() -> BTreeMap<&'static str, PrologBenchmark> { "benches/edges.pl", // name of the prolog module file to load. use the same file in multiple benchmarks "independent_set_count(ky, Count).", // query to benchmark in the context of the loaded module. consider making the query adjustable to tune the run time to ~0.1s Strategy::Reuse, - btreemap! { "Count" => Value::Integer(2869176.into()) }, + btreemap! { "Count" => Term::integer(2869176) }, ), ( "numlist", "benches/numlist.pl", "run_numlist(1000000, Head).", Strategy::Reuse, - btreemap! { "Head" => Value::Integer(1.into())}, + btreemap! { "Head" => Term::integer(1) }, ), ( "csv_codename", "benches/csv.pl", "get_codename(\"0020\",Name).", Strategy::Reuse, - btreemap! { "Name" => Value::String("SPACE".into())}, + btreemap! { "Name" => Term::string("SPACE") }, ), ] .map(|b| { @@ -54,7 +54,7 @@ pub struct PrologBenchmark { pub filename: &'static str, pub query: &'static str, pub strategy: Strategy, - pub bindings: BTreeMap<&'static str, Value>, + pub bindings: BTreeMap<&'static str, Term>, } impl PrologBenchmark { @@ -64,28 +64,34 @@ impl PrologBenchmark { .file_stem() .and_then(|s| s.to_str()) .unwrap(); - let mut machine = Machine::new_lib(); + let mut machine = MachineBuilder::default().build(); machine.load_module_string(module_name, program); machine } #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - pub fn setup(&self) -> impl FnMut() -> QueryResolution { + pub fn setup(&self) -> impl FnMut() -> Vec { let mut machine = self.make_machine(); let query = self.query; move || { use criterion::black_box; - black_box(machine.run_query(black_box(query.to_string()))).unwrap() + black_box( + machine + .run_query(black_box(query)) + .collect::, _>>() + .unwrap(), + ) } } } #[cfg(test)] mod test { + #[test] fn validate_benchmarks() { use super::prolog_benches; - use scryer_prolog::{QueryMatch, QueryResolution}; + use scryer_prolog::LeafAnswer; use std::{fmt::Write, fs}; struct BenchResult { @@ -100,10 +106,13 @@ mod test { let mut machine = r.make_machine(); let setup_inference_count = machine.get_inference_count(); - let result = machine.run_query(r.query.to_string()).unwrap(); + let result: Vec<_> = machine + .run_query(r.query) + .collect::>() + .unwrap(); let query_inference_count = machine.get_inference_count() - setup_inference_count; - let expected = QueryResolution::Matches(vec![QueryMatch::from(r.bindings.clone())]); + let expected = [LeafAnswer::from_bindings(r.bindings.clone())]; assert_eq!(result, expected, "validating benchmark {}", r.name); results.push(BenchResult { diff --git a/src/lib.rs b/src/lib.rs index 2a736a1ea..85ee726ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,9 @@ +//! A free software ISO Prolog system. #![recursion_limit = "4112"] +#![deny(missing_docs)] #[macro_use] extern crate static_assertions; -#[cfg(test)] -#[macro_use] -extern crate maplit; #[macro_use] pub(crate) mod macros; @@ -46,9 +45,9 @@ use wasm_bindgen::prelude::*; // Re-exports pub use machine::config::*; pub use machine::lib_machine::*; -pub use machine::parsed_results::*; pub use machine::Machine; +/// Eval a source file in Wasm. #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn eval_code(s: &str) -> String { @@ -56,14 +55,15 @@ pub fn eval_code(s: &str) -> String { console_error_panic_hook::set_once(); - let mut wam = Machine::with_test_streams(); + let mut wam = MachineBuilder::default().build(); let bytes = wam.test_load_string(s); String::from_utf8_lossy(&bytes).to_string() } +/// The entry point for the Scryer Prolog CLI. pub fn run_binary() -> std::process::ExitCode { use crate::atom_table::Atom; - use crate::machine::{Machine, INTERRUPT}; + use crate::machine::INTERRUPT; #[cfg(feature = "repl")] ctrlc::set_handler(move || { @@ -84,7 +84,9 @@ pub fn run_binary() -> std::process::ExitCode { .unwrap(); runtime.block_on(async move { - let mut wam = Machine::new(Default::default()); + let mut wam = MachineBuilder::default() + .with_streams(StreamConfig::stdio()) + .build(); wam.run_module_predicate(atom!("$toplevel"), (atom!("$repl"), 0)) }) } diff --git a/src/machine/config.rs b/src/machine/config.rs index 6268c8d81..2981899d0 100644 --- a/src/machine/config.rs +++ b/src/machine/config.rs @@ -1,32 +1,189 @@ -pub struct MachineConfig { - pub streams: StreamConfig, - pub toplevel: &'static str, +use std::borrow::Cow; + +use rand::{rngs::StdRng, SeedableRng}; + +use crate::Machine; + +use super::{ + bootstrapping_compile, current_dir, import_builtin_impls, libraries, load_module, Atom, + CompilationTarget, IndexStore, ListingSource, MachineArgs, MachineState, Stream, StreamOptions, +}; + +/// Describes how the streams of a [`Machine`](crate::Machine) will be handled. +#[derive(Default)] +pub struct StreamConfig { + inner: StreamConfigInner, +} + +impl StreamConfig { + /// Binds the input, output and error streams to stdin, stdout and stderr. + pub fn stdio() -> Self { + StreamConfig { + inner: StreamConfigInner::Stdio, + } + } + + /// Binds the output stream to a memory buffer, and the error stream to stderr. + /// + /// The input stream is ignored. + pub fn in_memory() -> Self { + StreamConfig { + inner: StreamConfigInner::Memory, + } + } } -pub enum StreamConfig { +#[derive(Default)] +enum StreamConfigInner { Stdio, + #[default] Memory, } -impl Default for MachineConfig { +/// Describes how a [`Machine`](crate::Machine) will be configured. +pub struct MachineBuilder { + pub(crate) streams: StreamConfig, + pub(crate) toplevel: Cow<'static, str>, +} + +impl Default for MachineBuilder { + /// Defaults to using in-memory streams. fn default() -> Self { - MachineConfig { - streams: StreamConfig::Stdio, - toplevel: include_str!("../toplevel.pl"), + MachineBuilder { + streams: Default::default(), + toplevel: default_toplevel().into(), } } } -impl MachineConfig { - pub fn in_memory() -> Self { - MachineConfig { - streams: StreamConfig::Memory, - ..Default::default() - } +impl MachineBuilder { + /// Creates a default configuration. + pub fn new() -> Self { + Default::default() } - pub fn with_toplevel(mut self, toplevel: &'static str) -> Self { - self.toplevel = toplevel; + /// Uses the given `crate::StreamConfig` in this configuration. + pub fn with_streams(mut self, streams: StreamConfig) -> Self { + self.streams = streams; self } + + /// Uses the given toplevel in this configuration. + pub fn with_toplevel(mut self, toplevel: impl Into>) -> Self { + self.toplevel = toplevel.into(); + self + } + + /// Builds the [`Machine`](crate::Machine) from this configuration. + pub fn build(self) -> Machine { + let args = MachineArgs::new(); + let mut machine_st = MachineState::new(); + + let (user_input, user_output, user_error) = match self.streams.inner { + StreamConfigInner::Stdio => ( + Stream::stdin(&mut machine_st.arena, args.add_history), + Stream::stdout(&mut machine_st.arena), + Stream::stderr(&mut machine_st.arena), + ), + StreamConfigInner::Memory => ( + Stream::Null(StreamOptions::default()), + Stream::from_owned_string("".to_owned(), &mut machine_st.arena), + Stream::stderr(&mut machine_st.arena), + ), + }; + + let mut wam = Machine { + machine_st, + indices: IndexStore::new(), + code: vec![], + user_input, + user_output, + user_error, + load_contexts: vec![], + #[cfg(feature = "ffi")] + foreign_function_table: Default::default(), + rng: StdRng::from_entropy(), + }; + + let mut lib_path = current_dir(); + + lib_path.pop(); + lib_path.push("lib"); + + wam.add_impls_to_indices(); + + bootstrapping_compile( + Stream::from_static_string( + libraries::get("ops_and_meta_predicates") + .expect("library ops_and_meta_predicates should exist"), + &mut wam.machine_st.arena, + ), + &mut wam, + ListingSource::from_file_and_path( + atom!("ops_and_meta_predicates.pl"), + lib_path.clone(), + ), + ) + .unwrap(); + + bootstrapping_compile( + Stream::from_static_string( + libraries::get("builtins").expect("library builtins should exist"), + &mut wam.machine_st.arena, + ), + &mut wam, + ListingSource::from_file_and_path(atom!("builtins.pl"), lib_path.clone()), + ) + .unwrap(); + + if let Some(builtins) = wam.indices.modules.get_mut(&atom!("builtins")) { + load_module( + &mut wam.machine_st, + &mut wam.indices.code_dir, + &mut wam.indices.op_dir, + &mut wam.indices.meta_predicates, + &CompilationTarget::User, + builtins, + ); + + import_builtin_impls(&wam.indices.code_dir, builtins); + } else { + unreachable!() + } + + lib_path.pop(); // remove the "lib" at the end + + bootstrapping_compile( + Stream::from_static_string(include_str!("../loader.pl"), &mut wam.machine_st.arena), + &mut wam, + ListingSource::from_file_and_path(atom!("loader.pl"), lib_path.clone()), + ) + .unwrap(); + + wam.configure_modules(); + + if let Some(loader) = wam.indices.modules.get(&atom!("loader")) { + load_module( + &mut wam.machine_st, + &mut wam.indices.code_dir, + &mut wam.indices.op_dir, + &mut wam.indices.meta_predicates, + &CompilationTarget::User, + loader, + ); + } else { + unreachable!() + } + + wam.load_special_forms(); + wam.load_top_level(self.toplevel); + wam.configure_streams(); + + wam + } +} + +/// Returns a static string slice to the default toplevel +pub fn default_toplevel() -> &'static str { + include_str!("../toplevel.pl") } diff --git a/src/machine/lib_machine.rs b/src/machine/lib_machine.rs deleted file mode 100644 index e44e54e4e..000000000 --- a/src/machine/lib_machine.rs +++ /dev/null @@ -1,832 +0,0 @@ -use std::collections::BTreeMap; - -use crate::atom_table; -use crate::machine::machine_indices::VarKey; -use crate::machine::mock_wam::CompositeOpDir; -use crate::machine::{BREAK_FROM_DISPATCH_LOOP_LOC, LIB_QUERY_SUCCESS}; -use crate::parser::ast::{Var, VarPtr}; -use crate::parser::parser::{Parser, Tokens}; -use crate::read::{write_term_to_heap, TermWriteResult}; -use indexmap::IndexMap; - -use super::{ - streams::Stream, Atom, AtomCell, HeapCellValue, HeapCellValueTag, Machine, MachineConfig, - QueryResolutionLine, QueryResult, Value, -}; - -pub struct QueryState<'a> { - machine: &'a mut Machine, - term: TermWriteResult, - stub_b: usize, - var_names: IndexMap, - called: bool, -} - -impl Drop for QueryState<'_> { - fn drop(&mut self) { - // This may be wrong if the iterator is not fully consumend, but from testing it seems - // fine. - self.machine.trust_me(); - } -} - -impl Iterator for QueryState<'_> { - type Item = Result; - - fn next(&mut self) -> Option { - let var_names = &mut self.var_names; - let term_write_result = &self.term; - let machine = &mut self.machine; - - // No more choicepoints, end iteration - if self.called && machine.machine_st.b <= self.stub_b { - return None; - } - - machine.dispatch_loop(); - - self.called = true; - - if !machine.machine_st.ball.stub.is_empty() { - // NOTE: this means an exception was thrown, at which - // point we backtracked to the stub choice point. - // this should halt the search for solutions as it - // does in the Scryer top-level. the exception term is - // contained in self.machine_st.ball. - let error_string = self - .machine - .machine_st - .ball - .stub - .iter() - .filter(|h| { - matches!( - h.get_tag(), - HeapCellValueTag::Atom | HeapCellValueTag::Fixnum - ) - }) - .map(|h| match h.get_tag() { - HeapCellValueTag::Atom => { - let (name, _) = cell_as_atom_cell!(h).get_name_and_arity(); - name.as_str().to_string() - } - HeapCellValueTag::Fixnum => h.get_value().clone().to_string(), - _ => unreachable!(), - }) - .collect::>() - .join(" "); - - return Some(Err(error_string)); - } - - if machine.machine_st.p == LIB_QUERY_SUCCESS { - if term_write_result.var_dict.is_empty() { - self.machine.machine_st.backtrack(); - return Some(Ok(QueryResolutionLine::True)); - } - } else if machine.machine_st.p == BREAK_FROM_DISPATCH_LOOP_LOC { - return Some(Ok(QueryResolutionLine::False)); - } - - let mut bindings: BTreeMap = BTreeMap::new(); - - let var_dict = &term_write_result.var_dict; - - for (var_key, term_to_be_printed) in var_dict.iter() { - let mut var_name = var_key.to_string(); - if var_name.starts_with('_') { - let should_print = var_names.values().any(|x| match x.borrow().clone() { - Var::Named(v) => v == var_name, - _ => false, - }); - if !should_print { - continue; - } - } - - let mut term = - Value::from_heapcell(machine, *term_to_be_printed, &mut var_names.clone()); - - if let Value::Var(ref term_str) = term { - if *term_str == var_name { - continue; - } - - // Var dict is in the order things appear in the query. If var_name appears - // after term in the query, switch their places. - let var_name_idx = var_dict - .get_index_of(&VarKey::VarPtr(Var::Named(var_name.clone()).into())) - .unwrap(); - let term_idx = - var_dict.get_index_of(&VarKey::VarPtr(Var::Named(term_str.clone()).into())); - if let Some(idx) = term_idx { - if idx < var_name_idx { - let new_term = Value::Var(var_name); - let new_var_name = term_str.into(); - term = new_term; - var_name = new_var_name; - } - } - } - - bindings.insert(var_name, term); - } - - // NOTE: there are outstanding choicepoints, backtrack - // through them for further solutions. if - // self.machine_st.b == stub_b we've backtracked to the stub - // choice point, so we should break. - self.machine.machine_st.backtrack(); - - Some(Ok(QueryResolutionLine::Match(bindings))) - } -} - -impl Machine { - pub fn new_lib() -> Self { - Machine::new(MachineConfig::in_memory()) - } - - pub fn load_module_string(&mut self, module_name: &str, program: String) { - let stream = Stream::from_owned_string(program, &mut self.machine_st.arena); - self.load_file(module_name, stream); - } - - pub fn consult_module_string(&mut self, module_name: &str, program: String) { - let stream = Stream::from_owned_string(program, &mut self.machine_st.arena); - self.machine_st.registers[1] = stream_as_cell!(stream); - self.machine_st.registers[2] = atom_as_cell!(&atom_table::AtomTable::build_with( - &self.machine_st.atom_tbl, - module_name - )); - - self.run_module_predicate(atom!("loader"), (atom!("consult_stream"), 2)); - } - - fn allocate_stub_choice_point(&mut self) { - // NOTE: create a choice point to terminate the dispatch_loop - // if an exception is thrown. - - let stub_b = self.machine_st.stack.allocate_or_frame(0); - let or_frame = self.machine_st.stack.index_or_frame_mut(stub_b); - - or_frame.prelude.num_cells = 0; - or_frame.prelude.e = 0; - or_frame.prelude.cp = 0; - or_frame.prelude.b = 0; - or_frame.prelude.bp = BREAK_FROM_DISPATCH_LOOP_LOC; - or_frame.prelude.boip = 0; - or_frame.prelude.biip = 0; - or_frame.prelude.tr = 0; - or_frame.prelude.h = 0; - or_frame.prelude.b0 = 0; - or_frame.prelude.attr_var_queue_len = 0; - - self.machine_st.b = stub_b; - self.machine_st.hb = self.machine_st.heap.len(); - self.machine_st.block = stub_b; - } - - pub fn run_query(&mut self, query: String) -> QueryResult { - self.run_query_iter(query).collect() - } - - pub fn run_query_iter(&mut self, query: String) -> QueryState { - let mut parser = Parser::new( - Stream::from_owned_string(query, &mut self.machine_st.arena), - &mut self.machine_st, - ); - let op_dir = CompositeOpDir::new(&self.indices.op_dir, None); - let term = parser - .read_term(&op_dir, Tokens::Default) - .expect("Failed to parse query"); - - self.allocate_stub_choice_point(); - - // Write parsed term to heap - let term_write_result = - write_term_to_heap(&term, &mut self.machine_st.heap, &self.machine_st.atom_tbl) - .expect("couldn't write term to heap"); - - let var_names: IndexMap<_, _> = term_write_result - .var_dict - .iter() - .map(|(var_key, cell)| match var_key { - // NOTE: not the intention behind Var::InSitu here but - // we can hijack it to store anonymous variables - // without creating problems. - VarKey::AnonVar(h) => (*cell, VarPtr::from(Var::InSitu(*h))), - VarKey::VarPtr(var_ptr) => (*cell, var_ptr.clone()), - }) - .collect(); - - // Write term to heap - self.machine_st.registers[1] = self.machine_st.heap[term_write_result.heap_loc]; - - self.machine_st.cp = LIB_QUERY_SUCCESS; // BREAK_FROM_DISPATCH_LOOP_LOC; - let call_index_p = self - .indices - .code_dir - .get(&(atom!("call"), 1)) - .expect("couldn't get code index") - .local() - .unwrap(); - - self.machine_st.execute_at_index(1, call_index_p); - - let stub_b = self.machine_st.b; - QueryState { - machine: self, - term: term_write_result, - stub_b, - var_names, - called: false, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::machine::{QueryMatch, QueryResolution, Value}; - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn programatic_query() { - let mut machine = Machine::new_lib(); - - machine.load_module_string( - "facts", - String::from( - r#" - triple("a", "p1", "b"). - triple("a", "p2", "b"). - "#, - ), - ); - - let query = String::from(r#"triple("a",P,"b")."#); - let output = machine.run_query(query); - assert_eq!( - output, - Ok(QueryResolution::Matches(vec![ - QueryMatch::from(btreemap! { - "P" => Value::from("p1"), - }), - QueryMatch::from(btreemap! { - "P" => Value::from("p2"), - }), - ])) - ); - - assert_eq!( - machine.run_query(String::from(r#"triple("a","p1","b")."#)), - Ok(QueryResolution::True) - ); - - assert_eq!( - machine.run_query(String::from(r#"triple("x","y","z")."#)), - Ok(QueryResolution::False) - ); - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn failing_query() { - let mut machine = Machine::new_lib(); - let query = String::from(r#"triple("a",P,"b")."#); - let output = machine.run_query(query); - assert_eq!( - output, - Err(String::from( - "error existence_error procedure / triple 3 / triple 3" - )) - ); - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn complex_results() { - let mut machine = Machine::new_lib(); - machine.load_module_string( - "facts", - r#" - :- discontiguous(subject_class/2). - :- discontiguous(constructor/2). - - subject_class("Todo", c). - constructor(c, '[{action: "addLink", source: "this", predicate: "todo://state", target: "todo://ready"}]'). - - subject_class("Recipe", xyz). - constructor(xyz, '[{action: "addLink", source: "this", predicate: "recipe://title", target: "literal://string:Meta%20Muffins"}]'). - "#.to_string()); - - let result = machine.run_query(String::from( - "subject_class(\"Todo\", C), constructor(C, Actions).", - )); - assert_eq!( - result, - Ok(QueryResolution::Matches(vec![QueryMatch::from( - btreemap! { - "C" => Value::Atom("c".into()), - "Actions" => Value::Atom("[{action: \"addLink\", source: \"this\", predicate: \"todo://state\", target: \"todo://ready\"}]".into()), - } - ),])) - ); - - let result = machine.run_query(String::from( - "subject_class(\"Recipe\", C), constructor(C, Actions).", - )); - assert_eq!( - result, - Ok(QueryResolution::Matches(vec![QueryMatch::from( - btreemap! { - "C" => Value::Atom("xyz".into()), - "Actions" => Value::Atom("[{action: \"addLink\", source: \"this\", predicate: \"recipe://title\", target: \"literal://string:Meta%20Muffins\"}]".into()), - } - ),])) - ); - - let result = machine.run_query(String::from("subject_class(Class, _).")); - assert_eq!( - result, - Ok(QueryResolution::Matches(vec![ - QueryMatch::from(btreemap! { - "Class" => Value::String("Todo".into()) - }), - QueryMatch::from(btreemap! { - "Class" => Value::String("Recipe".into()) - }), - ])) - ); - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn empty_predicate() { - let mut machine = Machine::new_lib(); - machine.load_module_string( - "facts", - r#" - :- discontiguous(subject_class/2). - "# - .to_string(), - ); - - let result = machine.run_query(String::from("subject_class(X, _).")); - assert_eq!(result, Ok(QueryResolution::False)); - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn list_results() { - let mut machine = Machine::new_lib(); - machine.load_module_string( - "facts", - r#" - list([1,2,3]). - "# - .to_string(), - ); - - let result = machine.run_query(String::from("list(X).")); - assert_eq!( - result, - Ok(QueryResolution::Matches(vec![QueryMatch::from( - btreemap! { - "X" => Value::List(vec![ - Value::Integer(1.into()), - Value::Integer(2.into()), - Value::Integer(3.into()), - ]), - } - ),])) - ); - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn consult() { - let mut machine = Machine::new_lib(); - - machine.consult_module_string( - "facts", - String::from( - r#" - triple("a", "p1", "b"). - triple("a", "p2", "b"). - "#, - ), - ); - - let query = String::from(r#"triple("a",P,"b")."#); - let output = machine.run_query(query); - assert_eq!( - output, - Ok(QueryResolution::Matches(vec![ - QueryMatch::from(btreemap! { - "P" => Value::from("p1"), - }), - QueryMatch::from(btreemap! { - "P" => Value::from("p2"), - }), - ])) - ); - - assert_eq!( - machine.run_query(String::from(r#"triple("a","p1","b")."#)), - Ok(QueryResolution::True) - ); - - assert_eq!( - machine.run_query(String::from(r#"triple("x","y","z")."#)), - Ok(QueryResolution::False) - ); - - machine.consult_module_string( - "facts", - String::from( - r#" - triple("a", "new", "b"). - "#, - ), - ); - - assert_eq!( - machine.run_query(String::from(r#"triple("a","p1","b")."#)), - Ok(QueryResolution::False) - ); - - assert_eq!( - machine.run_query(String::from(r#"triple("a","new","b")."#)), - Ok(QueryResolution::True) - ); - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - #[ignore = "uses old flawed interface"] - fn integration_test() { - let mut machine = Machine::new_lib(); - - // File with test commands, i.e. program code to consult and queries to run - let code = include_str!("./lib_integration_test_commands.txt"); - - // Split the code into blocks - let blocks = code.split("====="); - - let mut i = 0; - let mut last_result: Option<_> = None; - // Iterate over the blocks - for block in blocks { - // Trim the block to remove any leading or trailing whitespace - let block = block.trim(); - - // Skip empty blocks - if block.is_empty() { - continue; - } - - // Check if the block is a query - if let Some(query) = block.strip_prefix("query") { - // Parse and execute the query - let result = machine.run_query(query.to_string()); - assert!(result.is_ok()); - - last_result = Some(result); - } else if let Some(code) = block.strip_prefix("consult") { - // Load the code into the machine - machine.consult_module_string("facts", code.to_string()); - } else if let Some(result) = block.strip_prefix("result") { - i += 1; - if let Some(Ok(ref last_result)) = last_result { - println!("\n\n=====Result No. {i}=======\n{last_result}\n==============="); - assert_eq!(last_result.to_string(), result.to_string().trim(),) - } - } - } - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn findall() { - let mut machine = Machine::new_lib(); - - machine.consult_module_string( - "facts", - String::from( - r#" - triple("a", "p1", "b"). - triple("a", "p2", "b"). - "#, - ), - ); - - let query = - String::from(r#"findall([Predicate, Target], triple(_,Predicate,Target), Result)."#); - let output = machine.run_query(query); - assert_eq!( - output, - Ok(QueryResolution::Matches(vec![QueryMatch::from( - btreemap! { - "Result" => Value::List( - Vec::from([ - Value::List([Value::from("p1"), Value::from("b")].into()), - Value::List([Value::from("p2"), Value::from("b")].into()), - ]) - ), - } - ),])) - ); - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn dont_return_partial_matches() { - let mut machine = Machine::new_lib(); - - machine.consult_module_string( - "facts", - String::from( - r#" - :- discontiguous(property_resolve/2). - subject_class("Todo", c). - "#, - ), - ); - - let query = String::from(r#"property_resolve(C, "isLiked"), subject_class("Todo", C)."#); - let output = machine.run_query(query); - assert_eq!(output, Ok(QueryResolution::False)); - - let query = String::from(r#"subject_class("Todo", C), property_resolve(C, "isLiked")."#); - let output = machine.run_query(query); - assert_eq!(output, Ok(QueryResolution::False)); - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn dont_return_partial_matches_without_discountiguous() { - let mut machine = Machine::new_lib(); - - machine.consult_module_string( - "facts", - String::from( - r#" - a("true for a"). - b("true for b"). - "#, - ), - ); - - let query = String::from(r#"a("true for a")."#); - let output = machine.run_query(query); - assert_eq!(output, Ok(QueryResolution::True)); - - let query = String::from(r#"a("true for a"), b("true for b")."#); - let output = machine.run_query(query); - assert_eq!(output, Ok(QueryResolution::True)); - - let query = String::from(r#"a("true for b"), b("true for b")."#); - let output = machine.run_query(query); - assert_eq!(output, Ok(QueryResolution::False)); - - let query = String::from(r#"a("true for a"), b("true for a")."#); - let output = machine.run_query(query); - assert_eq!(output, Ok(QueryResolution::False)); - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn non_existent_predicate_should_not_cause_panic_when_other_predicates_are_defined() { - let mut machine = Machine::new_lib(); - - machine.consult_module_string( - "facts", - String::from( - r#" - triple("a", "p1", "b"). - triple("a", "p2", "b"). - "#, - ), - ); - - let query = String::from("non_existent_predicate(\"a\",\"p1\",\"b\")."); - - let result = machine.run_query(query); - - assert_eq!( - result, - Err(String::from("error existence_error procedure / non_existent_predicate 3 / non_existent_predicate 3")) - ); - } - - #[test] - #[cfg_attr(miri, ignore)] - fn atom_quoting() { - let mut machine = Machine::new_lib(); - - let query = "X = '.'.".into(); - - let result = machine.run_query(query); - - assert_eq!( - result, - Ok(QueryResolution::Matches(vec![QueryMatch::from( - btreemap! { - "X" => Value::Atom(".".into()), - } - )])) - ); - } - - #[test] - #[cfg_attr(miri, ignore)] - fn rational_number() { - use crate::parser::dashu::rational::RBig; - let mut machine = Machine::new_lib(); - - let query = "X is 1 rdiv 2.".into(); - - let result = machine.run_query(query); - - assert_eq!( - result, - Ok(QueryResolution::Matches(vec![QueryMatch::from( - btreemap! { - "X" => Value::Rational(RBig::from_parts(1.into(), 2u32.into())), - } - )])) - ); - } - - #[test] - #[cfg_attr(miri, ignore)] - fn big_integer() { - use crate::parser::dashu::integer::IBig; - let mut machine = Machine::new_lib(); - - let query = "X is 10^100.".into(); - - let result = machine.run_query(query); - - assert_eq!( - result, - Ok(QueryResolution::Matches(vec![QueryMatch::from( - btreemap! { - "X" => Value::Integer(IBig::from(10).pow(100)), - } - )])) - ); - } - - #[test] - #[cfg_attr(miri, ignore)] - fn complicated_term() { - let mut machine = Machine::new_lib(); - - let query = "X = a(\"asdf\", [42, 2.54, asdf, a, [a,b|_], Z]).".into(); - - let result = machine.run_query(query); - - let expected = Value::Structure( - // Composite term - "a".into(), - vec![ - Value::String("asdf".into()), // String - Value::List(vec![ - Value::Integer(42.into()), // Fixnum - Value::Float(2.54.into()), // Float - Value::Atom("asdf".into()), // Atom - Value::Atom("a".into()), // Char - Value::Structure( - // Partial string - ".".into(), - vec![ - Value::Atom("a".into()), - Value::Structure( - ".".into(), - vec![ - Value::Atom("b".into()), - Value::Var("_A".into()), // Anonymous variable - ], - ), - ], - ), - Value::Var("Z".into()), // Named variable - ]), - ], - ); - - assert_eq!( - result, - Ok(QueryResolution::Matches(vec![QueryMatch::from( - btreemap! { - "X" => expected, - } - )])) - ); - } - - #[test] - #[cfg_attr(miri, ignore = "it takes too long to run")] - fn issue_2341() { - let mut machine = Machine::new_lib(); - - machine.load_module_string( - "facts", - String::from( - r#" - male(stephen). - parent(albert,edward). - father(F,C):-parent(F,C),male(F). - "#, - ), - ); - - let query = String::from(r#"father(F,C)."#); - let output = machine.run_query(query); - - assert_eq!(output, Ok(QueryResolution::False)); - } - - #[test] - #[cfg_attr(miri, ignore)] - fn query_iterator_determinism() { - let mut machine = Machine::new_lib(); - - { - let mut iterator = machine.run_query_iter("X = 1.".into()); - - iterator.next(); - assert_eq!(iterator.next(), None); - } - - { - let mut iterator = machine.run_query_iter("X = 1 ; false.".into()); - - iterator.next(); - - assert_eq!(iterator.next(), Some(Ok(QueryResolutionLine::False))); - assert_eq!(iterator.next(), None); - } - - { - let mut iterator = machine.run_query_iter("false.".into()); - - assert_eq!(iterator.next(), Some(Ok(QueryResolutionLine::False))); - assert_eq!(iterator.next(), None); - } - } - - #[test] - #[cfg_attr(miri, ignore)] - fn query_iterator_backtracking_when_no_variables() { - let mut machine = Machine::new_lib(); - - let mut iterator = machine.run_query_iter("true;false.".into()); - - assert_eq!(iterator.next(), Some(Ok(QueryResolutionLine::True))); - assert_eq!(iterator.next(), Some(Ok(QueryResolutionLine::False))); - assert_eq!(iterator.next(), None); - } - - #[test] - #[cfg_attr(miri, ignore)] - fn differentiate_anonymous_variables() { - let mut machine = Machine::new_lib(); - - let result = machine.run_query("A = [_,_], _B = 1 ; B = [_,_].".into()); - - assert_eq!( - result, - Ok(QueryResolution::Matches(vec![ - QueryMatch::from(btreemap! { - "A" => Value::List(vec![Value::Var("_A".into()), Value::Var("_C".into())]), - "_B" => Value::Integer(1.into()), - }), - QueryMatch::from(btreemap! { - "B" => Value::List(vec![Value::Var("_A".into()), Value::Var("_C".into())]), - }), - ])) - ); - } - - #[test] - #[cfg_attr(miri, ignore)] - fn order_of_variables_in_binding() { - let mut machine = Machine::new_lib(); - - let result = machine.run_query("X = Y, Z = W.".into()); - - assert_eq!( - result, - Ok(QueryResolution::Matches(vec![QueryMatch::from( - btreemap! { - "X" => Value::Var("Y".into()), - "Z" => Value::Var("W".into()), - } - ),])) - ); - } -} diff --git a/src/machine/lib_integration_test_commands.txt b/src/machine/lib_machine/lib_integration_test_commands.txt similarity index 100% rename from src/machine/lib_integration_test_commands.txt rename to src/machine/lib_machine/lib_integration_test_commands.txt diff --git a/src/machine/lib_machine/mod.rs b/src/machine/lib_machine/mod.rs new file mode 100644 index 000000000..24f22fe78 --- /dev/null +++ b/src/machine/lib_machine/mod.rs @@ -0,0 +1,624 @@ +use std::cmp::Ordering; +use std::collections::BTreeMap; + +use crate::atom_table; +use crate::heap_iter::{stackful_post_order_iter, NonListElider}; +use crate::machine::machine_indices::VarKey; +use crate::machine::mock_wam::CompositeOpDir; +use crate::machine::{ + F64Offset, F64Ptr, Fixnum, Number, BREAK_FROM_DISPATCH_LOOP_LOC, LIB_QUERY_SUCCESS, +}; +use crate::parser::ast::{Var, VarPtr}; +use crate::parser::parser::{Parser, Tokens}; +use crate::read::{write_term_to_heap, TermWriteResult}; + +use dashu::{Integer, Rational}; +use indexmap::IndexMap; + +use super::{streams::Stream, Atom, AtomCell, HeapCellValue, HeapCellValueTag, Machine}; + +#[cfg(test)] +mod tests; + +/// Represents a leaf answer from a query. +#[derive(Debug, Clone, PartialEq)] +pub enum LeafAnswer { + /// A `true` leaf answer. + True, + /// A `false` leaf answer. + /// + /// This means that there are no more answers for the query. + False, + /// An exception leaf answer. + Exception(Term), + /// A leaf answer with bindings. + #[non_exhaustive] + LeafAnswer { + /// The bindings of variables in the query. + bindings: BTreeMap, + //residual_goals: Vec, + }, +} + +impl LeafAnswer { + /// Creates a leaf answer with no residual goals. + pub fn from_bindings>(bindings: impl IntoIterator) -> Self { + LeafAnswer::LeafAnswer { + bindings: bindings.into_iter().map(|(k, v)| (k.into(), v)).collect(), + } + } +} + +/// Represents a Prolog term. +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub enum Term { + /// An arbitrary precision integer. + Integer(Integer), + /// An arbitrary precision rational. + Rational(Rational), + /// A float. + Float(f64), + /// A Prolog atom. + Atom(String), + /// A Prolog string. + /// + /// In particular, this represents Prolog lists of characters. + String(String), + /// A Prolog list. + List(Vec), + /// A Prolog compound term. + Compound(String, Vec), + /// A Prolog variable. + Var(String), +} + +impl Term { + /// Creates an integer term. + pub fn integer(value: impl Into) -> Self { + Term::Integer(value.into()) + } + + /// Creates a rational term. + pub fn rational(value: impl Into) -> Self { + Term::Rational(value.into()) + } + + /// Creates a float term. + pub fn float(value: impl Into) -> Self { + Term::Float(value.into()) + } + + /// Creates an atom term. + pub fn atom(value: impl Into) -> Self { + Term::Atom(value.into()) + } + + /// Creates a string term. + /// + /// In specific, this represents a list of chars in Prolog. + pub fn string(value: impl Into) -> Self { + Term::String(value.into()) + } + + /// Creates a list term. + pub fn list(value: impl IntoIterator) -> Self { + Term::List(value.into_iter().collect()) + } + + /// Creates a compound term. + pub fn compound(functor: impl Into, args: impl IntoIterator) -> Self { + Term::Compound(functor.into(), args.into_iter().collect()) + } + + /// Creates a variable. + pub fn variable(value: impl Into) -> Self { + Term::Var(value.into()) + } + + /// Creates a conjunction, giving the atom `true` if empty. + pub fn conjunction(value: impl IntoIterator) -> Self { + Term::try_conjunction(value).unwrap_or(Term::atom("true")) + } + + /// Creates a conjunction, giving `None` if empty. + pub fn try_conjunction(value: impl IntoIterator) -> Option { + let mut iter = value.into_iter(); + iter.next().map(|first| { + Term::try_conjunction(iter) + .map(|rest| Term::compound(",", [first.clone(), rest])) + .unwrap_or(first) + }) + } + + /// Creates a disjunction, giving the atom `false` if empty. + pub fn disjunction(value: impl IntoIterator) -> Self { + Term::try_disjunction(value).unwrap_or(Term::atom("false")) + } + + /// Creates a disjunction, giving `None` if empty. + pub fn try_disjunction(value: impl IntoIterator) -> Option { + let mut iter = value.into_iter(); + iter.next().map(|first| { + Term::try_disjunction(iter) + .map(|rest| Term::compound(";", [first.clone(), rest])) + .unwrap_or(first) + }) + } +} + +/// This is an auxiliary function to turn a count into names of anonymous variables like _A, _B, +/// _AB, etc... +fn count_to_letter_code(mut count: usize) -> String { + let mut letters = Vec::new(); + + loop { + let letter_idx = (count % 26) as u32; + letters.push(char::from_u32('A' as u32 + letter_idx).unwrap()); + count /= 26; + + if count == 0 { + break; + } + } + + letters.into_iter().chain("_".chars()).rev().collect() +} + +impl Term { + pub(crate) fn from_heapcell( + machine: &mut Machine, + heap_cell: HeapCellValue, + var_names: &mut IndexMap, + ) -> Self { + // Adapted from MachineState::read_term_from_heap + let mut term_stack = vec![]; + let iter = stackful_post_order_iter::( + &mut machine.machine_st.heap, + &mut machine.machine_st.stack, + heap_cell, + ); + + let mut anon_count: usize = 0; + let var_ptr_cmp = |a, b| match a { + Var::Named(name_a) => match b { + Var::Named(name_b) => name_a.cmp(&name_b), + _ => Ordering::Less, + }, + _ => match b { + Var::Named(_) => Ordering::Greater, + _ => Ordering::Equal, + }, + }; + + for addr in iter { + let addr = unmark_cell_bits!(addr); + + read_heap_cell!(addr, + (HeapCellValueTag::Lis) => { + let tail = term_stack.pop().unwrap(); + let head = term_stack.pop().unwrap(); + + let list = match tail { + Term::Atom(atom) if atom == "[]" => match head { + Term::Atom(ref a) if a.chars().collect::>().len() == 1 => { + // Handle lists of char as strings + Term::String(a.to_string()) + } + _ => Term::List(vec![head]), + }, + Term::List(elems) if elems.is_empty() => match head { + Term::Atom(ref a) if a.chars().collect::>().len() == 1 => { + // Handle lists of char as strings + Term::String(a.to_string()) + }, + _ => Term::List(vec![head]), + }, + Term::List(mut elems) => { + elems.insert(0, head); + Term::List(elems) + }, + Term::String(mut elems) => match head { + Term::Atom(ref a) if a.chars().collect::>().len() == 1 => { + // Handle lists of char as strings + elems.insert(0, a.chars().next().unwrap()); + Term::String(elems) + }, + _ => { + let mut elems: Vec = elems + .chars() + .map(|x| Term::Atom(x.into())) + .collect(); + elems.insert(0, head); + Term::List(elems) + } + }, + _ => { + Term::Compound(".".into(), vec![head, tail]) + } + }; + term_stack.push(list); + } + (HeapCellValueTag::Var | HeapCellValueTag::AttrVar | HeapCellValueTag::StackVar) => { + let var = var_names.get(&addr).map(|x| x.borrow().clone()); + match var { + Some(Var::Named(name)) => term_stack.push(Term::Var(name)), + _ => { + let anon_name = loop { + // Generate a name for the anonymous variable + let anon_name = count_to_letter_code(anon_count); + + // Find if this name is already being used + var_names.sort_by(|_, a, _, b| { + var_ptr_cmp(a.borrow().clone(), b.borrow().clone()) + }); + let binary_result = var_names.binary_search_by(|_,a| { + let var_ptr = Var::Named(anon_name.clone()); + var_ptr_cmp(a.borrow().clone(), var_ptr.clone()) + }); + + match binary_result { + Ok(_) => anon_count += 1, // Name already used + Err(_) => { + // Name not used, assign it to this variable + let var_ptr = VarPtr::from(Var::Named(anon_name.clone())); + var_names.insert(addr, var_ptr); + break anon_name; + }, + } + }; + term_stack.push(Term::Var(anon_name)); + }, + } + } + (HeapCellValueTag::F64, f) => { + term_stack.push(Term::Float((*f).into())); + } + (HeapCellValueTag::Char, c) => { + term_stack.push(Term::Atom(c.into())); + } + (HeapCellValueTag::Fixnum, n) => { + term_stack.push(Term::Integer(n.into())); + } + (HeapCellValueTag::Cons) => { + match Number::try_from(addr) { + Ok(Number::Integer(i)) => term_stack.push(Term::Integer((*i).clone())), + Ok(Number::Rational(r)) => term_stack.push(Term::Rational((*r).clone())), + _ => {} + } + } + (HeapCellValueTag::CStr, s) => { + term_stack.push(Term::String(s.as_str().to_string())); + } + (HeapCellValueTag::Atom, (name, arity)) => { + //let h = iter.focus().value() as usize; + //let mut arity = arity; + + // Not sure why/if this is needed. + // Might find out with better testing later. + /* + if iter.heap.len() > h + arity + 1 { + let value = iter.heap[h + arity + 1]; + + if let Some(idx) = get_structure_index(value) { + // in the second condition, arity == 0, + // meaning idx cannot pertain to this atom + // if it is the direct subterm of a larger + // structure. + if arity > 0 || !iter.direct_subterm_of_str(h) { + term_stack.push( + Term::Literal(Cell::default(), Literal::CodeIndex(idx)) + ); + + arity += 1; + } + } + } + */ + + if arity == 0 { + let atom_name = name.as_str().to_string(); + if atom_name == "[]" { + term_stack.push(Term::List(vec![])); + } else { + term_stack.push(Term::Atom(atom_name)); + } + } else { + let subterms = term_stack + .drain(term_stack.len() - arity ..) + .collect(); + + term_stack.push(Term::Compound(name.as_str().to_string(), subterms)); + } + } + (HeapCellValueTag::PStr, atom) => { + let tail = term_stack.pop().unwrap(); + + match tail { + Term::Atom(atom) => { + if atom == "[]" { + term_stack.push(Term::String(atom.as_str().to_string())); + } + }, + Term::List(l) => { + let mut list: Vec = atom + .as_str() + .to_string() + .chars() + .map(|x| Term::Atom(x.to_string())) + .collect(); + list.extend(l.into_iter()); + term_stack.push(Term::List(list)); + }, + _ => { + let mut list: Vec = atom + .as_str() + .to_string() + .chars() + .map(|x| Term::Atom(x.to_string())) + .collect(); + + let mut partial_list = Term::Compound( + ".".into(), + vec![ + list.pop().unwrap(), + tail, + ], + ); + + while let Some(last) = list.pop() { + partial_list = Term::Compound( + ".".into(), + vec![ + last, + partial_list, + ], + ); + } + + term_stack.push(partial_list); + } + } + } + // I dont know if this is needed here. + /* + (HeapCellValueTag::PStrLoc, h) => { + let atom = cell_as_atom_cell!(iter.heap[h]).get_name(); + let tail = term_stack.pop().unwrap(); + + term_stack.push(Term::PartialString( + Cell::default(), + atom.as_str().to_owned(), + Box::new(tail), + )); + } + */ + _ => { + } + ); + } + + debug_assert_eq!(term_stack.len(), 1); + term_stack.pop().unwrap() + } +} + +/// An iterator though the leaf answers of a query. +pub struct QueryState<'a> { + machine: &'a mut Machine, + term: TermWriteResult, + stub_b: usize, + var_names: IndexMap, + called: bool, +} + +impl Drop for QueryState<'_> { + fn drop(&mut self) { + // FIXME: This may be wrong if the iterator is not fully consumend, but from testing it + // seems fine. Is this really ok? + self.machine.trust_me(); + } +} + +impl Iterator for QueryState<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + let var_names = &mut self.var_names; + let term_write_result = &self.term; + let machine = &mut self.machine; + + // No more choicepoints, end iteration + if self.called && machine.machine_st.b <= self.stub_b { + return None; + } + + machine.dispatch_loop(); + + self.called = true; + + if !machine.machine_st.ball.stub.is_empty() { + // NOTE: this means an exception was thrown, at which + // point we backtracked to the stub choice point. + // this should halt the search for solutions as it + // does in the Scryer top-level. the exception term is + // contained in self.machine_st.ball. + let h = machine.machine_st.heap.len(); + machine + .machine_st + .heap + .extend(machine.machine_st.ball.stub.clone()); + let exception_term = + Term::from_heapcell(machine, machine.machine_st.heap[h], &mut var_names.clone()); + + if let Term::Compound(functor, args) = &exception_term { + if functor == "error" && args.len() == 2 { + // We have an error + return Some(Err(exception_term)); + } + } + + // We have an exception that is not an error + return Some(Ok(LeafAnswer::Exception(exception_term))); + } + + if machine.machine_st.p == LIB_QUERY_SUCCESS { + if term_write_result.var_dict.is_empty() { + self.machine.machine_st.backtrack(); + return Some(Ok(LeafAnswer::True)); + } + } else if machine.machine_st.p == BREAK_FROM_DISPATCH_LOOP_LOC { + return Some(Ok(LeafAnswer::False)); + } + + let mut bindings: BTreeMap = BTreeMap::new(); + + let var_dict = &term_write_result.var_dict; + + for (var_key, term_to_be_printed) in var_dict.iter() { + let mut var_name = var_key.to_string(); + if var_name.starts_with('_') { + let should_print = var_names.values().any(|x| match x.borrow().clone() { + Var::Named(v) => v == var_name, + _ => false, + }); + if !should_print { + continue; + } + } + + let mut term = + Term::from_heapcell(machine, *term_to_be_printed, &mut var_names.clone()); + + if let Term::Var(ref term_str) = term { + if *term_str == var_name { + continue; + } + + // Var dict is in the order things appear in the query. If var_name appears + // after term in the query, switch their places. + let var_name_idx = var_dict + .get_index_of(&VarKey::VarPtr(Var::Named(var_name.clone()).into())) + .unwrap(); + let term_idx = + var_dict.get_index_of(&VarKey::VarPtr(Var::Named(term_str.clone()).into())); + if let Some(idx) = term_idx { + if idx < var_name_idx { + let new_term = Term::Var(var_name); + let new_var_name = term_str.into(); + term = new_term; + var_name = new_var_name; + } + } + } + + bindings.insert(var_name, term); + } + + // NOTE: there are outstanding choicepoints, backtrack + // through them for further solutions. if + // self.machine_st.b == stub_b we've backtracked to the stub + // choice point, so we should break. + self.machine.machine_st.backtrack(); + + Some(Ok(LeafAnswer::LeafAnswer { bindings })) + } +} + +impl Machine { + /// Loads a module into the [`Machine`] from a string. + pub fn load_module_string(&mut self, module_name: &str, program: impl Into) { + let stream = Stream::from_owned_string(program.into(), &mut self.machine_st.arena); + self.load_file(module_name, stream); + } + + /// Consults a module into the [`Machine`] from a string. + pub fn consult_module_string(&mut self, module_name: &str, program: impl Into) { + let stream = Stream::from_owned_string(program.into(), &mut self.machine_st.arena); + self.machine_st.registers[1] = stream_as_cell!(stream); + self.machine_st.registers[2] = atom_as_cell!(&atom_table::AtomTable::build_with( + &self.machine_st.atom_tbl, + module_name + )); + + self.run_module_predicate(atom!("loader"), (atom!("consult_stream"), 2)); + } + + fn allocate_stub_choice_point(&mut self) { + // NOTE: create a choice point to terminate the dispatch_loop + // if an exception is thrown. + + let stub_b = self.machine_st.stack.allocate_or_frame(0); + let or_frame = self.machine_st.stack.index_or_frame_mut(stub_b); + + or_frame.prelude.num_cells = 0; + or_frame.prelude.e = 0; + or_frame.prelude.cp = 0; + or_frame.prelude.b = 0; + or_frame.prelude.bp = BREAK_FROM_DISPATCH_LOOP_LOC; + or_frame.prelude.boip = 0; + or_frame.prelude.biip = 0; + or_frame.prelude.tr = 0; + or_frame.prelude.h = 0; + or_frame.prelude.b0 = 0; + or_frame.prelude.attr_var_queue_len = 0; + + self.machine_st.b = stub_b; + self.machine_st.hb = self.machine_st.heap.len(); + self.machine_st.block = stub_b; + } + + /// Runs a query. + pub fn run_query(&mut self, query: impl Into) -> QueryState { + let mut parser = Parser::new( + Stream::from_owned_string(query.into(), &mut self.machine_st.arena), + &mut self.machine_st, + ); + let op_dir = CompositeOpDir::new(&self.indices.op_dir, None); + let term = parser + .read_term(&op_dir, Tokens::Default) + .expect("Failed to parse query"); + + self.allocate_stub_choice_point(); + + // Write parsed term to heap + let term_write_result = + write_term_to_heap(&term, &mut self.machine_st.heap, &self.machine_st.atom_tbl) + .expect("couldn't write term to heap"); + + let var_names: IndexMap<_, _> = term_write_result + .var_dict + .iter() + .map(|(var_key, cell)| match var_key { + // NOTE: not the intention behind Var::InSitu here but + // we can hijack it to store anonymous variables + // without creating problems. + VarKey::AnonVar(h) => (*cell, VarPtr::from(Var::InSitu(*h))), + VarKey::VarPtr(var_ptr) => (*cell, var_ptr.clone()), + }) + .collect(); + + // Write term to heap + self.machine_st.registers[1] = self.machine_st.heap[term_write_result.heap_loc]; + + self.machine_st.cp = LIB_QUERY_SUCCESS; // BREAK_FROM_DISPATCH_LOOP_LOC; + let call_index_p = self + .indices + .code_dir + .get(&(atom!("call"), 1)) + .expect("couldn't get code index") + .local() + .unwrap(); + + self.machine_st.execute_at_index(1, call_index_p); + + let stub_b = self.machine_st.b; + QueryState { + machine: self, + term: term_write_result, + stub_b, + var_names, + called: false, + } + } +} diff --git a/src/machine/lib_machine/tests.rs b/src/machine/lib_machine/tests.rs new file mode 100644 index 000000000..5b89d67da --- /dev/null +++ b/src/machine/lib_machine/tests.rs @@ -0,0 +1,610 @@ +use super::*; +use crate::MachineBuilder; + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn programatic_query() { + let mut machine = MachineBuilder::default().build(); + + machine.load_module_string( + "facts", + String::from( + r#" + triple("a", "p1", "b"). + triple("a", "p2", "b"). + "#, + ), + ); + + let query = r#"triple("a",P,"b")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + + assert_eq!( + complete_answer, + [ + LeafAnswer::from_bindings([("P", Term::string("p1")),]), + LeafAnswer::from_bindings([("P", Term::string("p2")),]), + ], + ); + + let query = r#"triple("a","p1","b")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + + assert_eq!(complete_answer, [LeafAnswer::True],); + + let query = r#"triple("x","y","z")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + + assert_eq!(complete_answer, [LeafAnswer::False],); +} + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn failing_query() { + let mut machine = MachineBuilder::default().build(); + let query = r#"triple("a",P,"b")."#; + let complete_answer: Result, _> = machine.run_query(query).collect(); + + assert_eq!( + complete_answer, + Err(Term::compound( + "error", + [ + Term::compound( + "existence_error", + [ + Term::atom("procedure"), + Term::compound("/", [Term::atom("triple"), Term::integer(3)]), + ] + ), + Term::compound("/", [Term::atom("triple"), Term::integer(3)]), + ], + )) + ); +} + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn complex_results() { + let mut machine = MachineBuilder::default().build(); + machine.load_module_string( + "facts", + r#" + :- discontiguous(subject_class/2). + :- discontiguous(constructor/2). + + subject_class("Todo", c). + constructor(c, '[{action: "addLink", source: "this", predicate: "todo://state", target: "todo://ready"}]'). + + subject_class("Recipe", xyz). + constructor(xyz, '[{action: "addLink", source: "this", predicate: "recipe://title", target: "literal://string:Meta%20Muffins"}]'). + "#, + ); + + let complete_answer: Vec<_> = machine + .run_query(r#"subject_class("Todo", C), constructor(C, Actions)."#) + .collect::>() + .unwrap(); + assert_eq!( + complete_answer, + [LeafAnswer::from_bindings([ + ("C", Term::atom("c")), + ( + "Actions", + Term::atom( + r#"[{action: "addLink", source: "this", predicate: "todo://state", target: "todo://ready"}]"# + ) + ), + ])], + ); + + let complete_answer: Vec<_> = machine + .run_query(r#"subject_class("Recipe", C), constructor(C, Actions)."#) + .collect::>() + .unwrap(); + assert_eq!( + complete_answer, + [LeafAnswer::from_bindings([ + ("C", Term::atom("xyz")), + ( + "Actions", + Term::atom( + r#"[{action: "addLink", source: "this", predicate: "recipe://title", target: "literal://string:Meta%20Muffins"}]"# + ) + ), + ])], + ); + + let complete_answer: Vec<_> = machine + .run_query("subject_class(Class, _).") + .collect::>() + .unwrap(); + assert_eq!( + complete_answer, + [ + LeafAnswer::from_bindings([("Class", Term::string("Todo"))]), + LeafAnswer::from_bindings([("Class", Term::string("Recipe"))]), + ], + ); +} + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn empty_predicate() { + let mut machine = MachineBuilder::default().build(); + machine.load_module_string( + "facts", + r#" + :- discontiguous(subject_class/2). + "#, + ); + + let complete_answer: Vec<_> = machine + .run_query("subject_class(X, _).") + .collect::>() + .unwrap(); + assert_eq!(complete_answer, [LeafAnswer::False]); +} + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn list_results() { + let mut machine = MachineBuilder::default().build(); + machine.load_module_string( + "facts", + r#" + list([1,2,3]). + "#, + ); + + let complete_answer: Vec<_> = machine + .run_query("list(X).") + .collect::>() + .unwrap(); + assert_eq!( + complete_answer, + [LeafAnswer::from_bindings([( + "X", + Term::list([Term::integer(1), Term::integer(2), Term::integer(3)]), + )])], + ); +} + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn consult() { + let mut machine = MachineBuilder::default().build(); + + machine.consult_module_string( + "facts", + r#" + triple("a", "p1", "b"). + triple("a", "p2", "b"). + "#, + ); + + let query = r#"triple("a",P,"b")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!( + complete_answer, + [ + LeafAnswer::from_bindings([("P", Term::string("p1"))]), + LeafAnswer::from_bindings([("P", Term::string("p2"))]), + ], + ); + + let query = r#"triple("a","p1","b")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!(complete_answer, [LeafAnswer::True],); + + let query = r#"triple("x","y","z")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!(complete_answer, [LeafAnswer::False],); + + machine.consult_module_string( + "facts", + r#" + triple("a", "new", "b"). + "#, + ); + + let query = r#"triple("a","p1","b")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!(complete_answer, [LeafAnswer::False],); + + let query = r#"triple("a","new","b")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!(complete_answer, [LeafAnswer::True]); +} + +/* +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +#[ignore = "uses old flawed interface"] +fn integration_test() { + let mut machine = MachineBuilder::default().build(); + + // File with test commands, i.e. program code to consult and queries to run + let code = include_str!("./lib_integration_test_commands.txt"); + + // Split the code into blocks + let blocks = code.split("====="); + + let mut i = 0; + let mut last_result: Option<_> = None; + // Iterate over the blocks + for block in blocks { + // Trim the block to remove any leading or trailing whitespace + let block = block.trim(); + + // Skip empty blocks + if block.is_empty() { + continue; + } + + // Check if the block is a query + if let Some(query) = block.strip_prefix("query") { + // Parse and execute the query + let result = machine.run_query(query.to_string()); + assert!(result.is_ok()); + + last_result = Some(result); + } else if let Some(code) = block.strip_prefix("consult") { + // Load the code into the machine + machine.consult_module_string("facts", code.to_string()); + } else if let Some(result) = block.strip_prefix("result") { + i += 1; + if let Some(Ok(ref last_result)) = last_result { + println!("\n\n=====Result No. {i}=======\n{last_result}\n==============="); + assert_eq!(last_result.to_string(), result.to_string().trim(),) + } + } + } +} +*/ + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn findall() { + let mut machine = MachineBuilder::default().build(); + + machine.consult_module_string( + "facts", + r#" + triple("a", "p1", "b"). + triple("a", "p2", "b"). + "#, + ); + + let query = r#"findall([Predicate, Target], triple(_,Predicate,Target), Result)."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!( + complete_answer, + [LeafAnswer::from_bindings([( + "Result", + Term::list([ + Term::list([Term::string("p1"), Term::string("b")]), + Term::list([Term::string("p2"), Term::string("b")]), + ]) + )])] + ); +} + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn dont_return_partial_matches() { + let mut machine = MachineBuilder::default().build(); + + machine.consult_module_string( + "facts", + r#" + :- discontiguous(property_resolve/2). + subject_class("Todo", c). + "#, + ); + + let query = r#"property_resolve(C, "isLiked"), subject_class("Todo", C)."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!(complete_answer, [LeafAnswer::False]); + + let query = r#"subject_class("Todo", C), property_resolve(C, "isLiked")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!(complete_answer, [LeafAnswer::False]); +} + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn dont_return_partial_matches_without_discountiguous() { + let mut machine = MachineBuilder::default().build(); + + machine.consult_module_string( + "facts", + r#" + a("true for a"). + b("true for b"). + "#, + ); + + let query = r#"a("true for a")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!(complete_answer, [LeafAnswer::True]); + + let query = r#"a("true for a"), b("true for b")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!(complete_answer, [LeafAnswer::True]); + + let query = r#"a("true for b"), b("true for b")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!(complete_answer, [LeafAnswer::False]); + + let query = r#"a("true for a"), b("true for a")."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + assert_eq!(complete_answer, [LeafAnswer::False]); +} + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn non_existent_predicate_should_not_cause_panic_when_other_predicates_are_defined() { + let mut machine = MachineBuilder::default().build(); + + machine.consult_module_string( + "facts", + r#" + triple("a", "p1", "b"). + triple("a", "p2", "b"). + "#, + ); + + let query = r#"non_existent_predicate("a","p1","b")."#; + let complete_answer: Result, _> = machine.run_query(query).collect(); + + assert_eq!( + complete_answer, + Err(Term::compound( + "error", + [ + Term::compound( + "existence_error", + [ + Term::atom("procedure"), + Term::compound( + "/", + [Term::atom("non_existent_predicate"), Term::integer(3)], + ), + ], + ), + Term::compound( + "/", + [Term::atom("non_existent_predicate"), Term::integer(3)] + ), + ], + )) + ); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn atom_quoting() { + let mut machine = MachineBuilder::default().build(); + + let query = "X = '.'."; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + + assert_eq!( + complete_answer, + [LeafAnswer::from_bindings([("X", Term::atom("."))])] + ); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn rational_number() { + use crate::parser::dashu::rational::RBig; + let mut machine = MachineBuilder::default().build(); + + let query = "X is 1 rdiv 2."; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + + assert_eq!( + complete_answer, + [LeafAnswer::from_bindings([( + "X", + Term::rational(RBig::from_parts(1.into(), 2u32.into())) + )])] + ); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn big_integer() { + use crate::parser::dashu::integer::IBig; + let mut machine = MachineBuilder::default().build(); + + let query = "X is 10^100."; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + + assert_eq!( + complete_answer, + [LeafAnswer::from_bindings([( + "X", + Term::integer(IBig::from(10).pow(100)) + )])], + ); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn complicated_term() { + let mut machine = MachineBuilder::default().build(); + + let query = r#"X = a("asdf", [42, 2.54, asdf, a, [a,b|_], Z])."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + + let expected = Term::Compound( + // Compound term + "a".into(), + vec![ + Term::String("asdf".into()), // String + Term::List(vec![ + Term::Integer(42.into()), // Fixnum + Term::Float(2.54), // Float + Term::Atom("asdf".into()), // Atom + Term::Atom("a".into()), // Char + Term::Compound( + // Partial string + ".".into(), + vec![ + Term::Atom("a".into()), + Term::Compound( + ".".into(), + vec![ + Term::Atom("b".into()), + Term::Var("_A".into()), // Anonymous variable + ], + ), + ], + ), + Term::Var("Z".into()), // Named variable + ]), + ], + ); + + assert_eq!( + complete_answer, + [LeafAnswer::from_bindings([("X", expected)])] + ); +} + +#[test] +#[cfg_attr(miri, ignore = "it takes too long to run")] +fn issue_2341() { + let mut machine = MachineBuilder::default().build(); + + machine.load_module_string( + "facts", + r#" + male(stephen). + parent(albert,edward). + father(F,C):-parent(F,C),male(F). + "#, + ); + + let query = r#"father(F,C)."#; + let complete_answer: Vec<_> = machine.run_query(query).collect::>().unwrap(); + + assert_eq!(complete_answer, [LeafAnswer::False]); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn query_iterator_determinism() { + let mut machine = MachineBuilder::default().build(); + + { + let mut iterator = machine.run_query("X = 1."); + + iterator.next(); + assert_eq!(iterator.next(), None); + } + + { + let mut iterator = machine.run_query("X = 1 ; false."); + + iterator.next(); + + assert_eq!(iterator.next(), Some(Ok(LeafAnswer::False))); + assert_eq!(iterator.next(), None); + } + + { + let mut iterator = machine.run_query("false."); + + assert_eq!(iterator.next(), Some(Ok(LeafAnswer::False))); + assert_eq!(iterator.next(), None); + } +} + +#[test] +#[cfg_attr(miri, ignore)] +fn query_iterator_backtracking_when_no_variables() { + let mut machine = MachineBuilder::default().build(); + + let mut iterator = machine.run_query("true;false."); + + assert_eq!(iterator.next(), Some(Ok(LeafAnswer::True))); + assert_eq!(iterator.next(), Some(Ok(LeafAnswer::False))); + assert_eq!(iterator.next(), None); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn differentiate_anonymous_variables() { + let mut machine = MachineBuilder::default().build(); + + let complete_answer: Vec<_> = machine + .run_query("A = [_,_], _B = 1 ; B = [_,_].") + .collect::>() + .unwrap(); + + assert_eq!( + complete_answer, + [ + LeafAnswer::from_bindings([ + ( + "A", + Term::list([Term::variable("_A"), Term::variable("_C")]) + ), + ("_B", Term::integer(1)), + ]), + LeafAnswer::from_bindings([( + "B", + Term::list([Term::variable("_A"), Term::variable("_C")]) + ),]), + ] + ); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn order_of_variables_in_binding() { + let mut machine = MachineBuilder::default().build(); + + let complete_answer: Vec<_> = machine + .run_query("X = Y, Z = W.") + .collect::>() + .unwrap(); + + assert_eq!( + complete_answer, + [LeafAnswer::from_bindings([ + ("X", Term::variable("Y")), + ("Z", Term::variable("W")), + ])] + ); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn errors_and_exceptions() { + let mut machine = MachineBuilder::default().build(); + + let complete_answer: Vec<_> = machine.run_query("functor(_,_,_).").collect(); + + assert_eq!( + complete_answer, + [Err(Term::compound( + "error", + [ + Term::atom("instantiation_error"), + Term::compound("/", [Term::atom("functor"), Term::integer(3)]), + ], + ))] + ); + + let complete_answer: Vec<_> = machine.run_query("throw(a).").collect(); + + assert_eq!( + complete_answer, + [Ok(LeafAnswer::Exception(Term::atom("a")))] + ); +} diff --git a/src/machine/mock_wam.rs b/src/machine/mock_wam.rs index 90aefa0e7..457afacfd 100644 --- a/src/machine/mock_wam.rs +++ b/src/machine/mock_wam.rs @@ -231,10 +231,7 @@ pub(crate) fn parse_and_write_parsed_term_to_heap( } impl Machine { - pub fn with_test_streams() -> Self { - Machine::new(MachineConfig::in_memory()) - } - + /// For use in tests. pub fn test_load_file(&mut self, file: &str) -> Vec { let stream = Stream::from_owned_string( std::fs::read_to_string(AsRef::::as_ref(file)).unwrap(), @@ -245,6 +242,7 @@ impl Machine { self.user_output.bytes().map(|b| b.unwrap()).collect() } + /// For use in tests. pub fn test_load_string(&mut self, code: &str) -> Vec { let stream = Stream::from_owned_string(code.to_owned(), &mut self.machine_st.arena); diff --git a/src/machine/mod.rs b/src/machine/mod.rs index f35288926..a62c0caa8 100644 --- a/src/machine/mod.rs +++ b/src/machine/mod.rs @@ -20,7 +20,6 @@ pub mod machine_indices; pub mod machine_state; pub mod machine_state_impl; pub mod mock_wam; -pub mod parsed_results; pub mod partial_string; pub mod preprocessor; pub mod stack; @@ -55,7 +54,7 @@ use lazy_static::lazy_static; use ordered_float::OrderedFloat; use rand::rngs::StdRng; -use rand::SeedableRng; +use std::borrow::Cow; use std::cmp::Ordering; use std::env; use std::io::Read; @@ -63,13 +62,13 @@ use std::path::PathBuf; use std::sync::atomic::AtomicBool; use std::sync::OnceLock; -use self::config::MachineConfig; -use self::parsed_results::*; - lazy_static! { pub static ref INTERRUPT: AtomicBool = AtomicBool::new(false); } +/// An instance of Scryer Prolog. +/// +/// Created with [`MachineBuilder::build`](crate::machine::config::MachineBuilder::build). #[derive(Debug)] pub struct Machine { pub(super) machine_st: MachineState, @@ -262,6 +261,7 @@ impl Machine { ) } + /// Gets the current inference count. pub fn get_inference_count(&mut self) -> u64 { self.machine_st .cwil @@ -298,13 +298,16 @@ impl Machine { self.run_module_predicate(atom!("loader"), (atom!("file_load"), 2)); } - fn load_top_level(&mut self, program: &'static str) { + fn load_top_level(&mut self, program: Cow<'static, str>) { let mut path_buf = current_dir(); path_buf.push("src/toplevel.pl"); let path = path_buf.to_str().unwrap(); - let toplevel_stream = Stream::from_static_string(program, &mut self.machine_st.arena); + let toplevel_stream = match program { + Cow::Borrowed(s) => Stream::from_static_string(s, &mut self.machine_st.arena), + Cow::Owned(s) => Stream::from_owned_string(s, &mut self.machine_st.arena), + }; self.load_file(path, toplevel_stream); @@ -480,114 +483,6 @@ impl Machine { } } - #[allow(clippy::new_without_default)] - pub fn new(config: MachineConfig) -> Self { - let args = MachineArgs::new(); - let mut machine_st = MachineState::new(); - - let (user_input, user_output, user_error) = match config.streams { - config::StreamConfig::Stdio => ( - Stream::stdin(&mut machine_st.arena, args.add_history), - Stream::stdout(&mut machine_st.arena), - Stream::stderr(&mut machine_st.arena), - ), - config::StreamConfig::Memory => ( - Stream::Null(StreamOptions::default()), - Stream::from_owned_string("".to_owned(), &mut machine_st.arena), - Stream::stderr(&mut machine_st.arena), - ), - }; - - let mut wam = Machine { - machine_st, - indices: IndexStore::new(), - code: vec![], - user_input, - user_output, - user_error, - load_contexts: vec![], - #[cfg(feature = "ffi")] - foreign_function_table: Default::default(), - rng: StdRng::from_entropy(), - }; - - let mut lib_path = current_dir(); - - lib_path.pop(); - lib_path.push("lib"); - - wam.add_impls_to_indices(); - - bootstrapping_compile( - Stream::from_static_string( - libraries::get("ops_and_meta_predicates") - .expect("library ops_and_meta_predicates should exist"), - &mut wam.machine_st.arena, - ), - &mut wam, - ListingSource::from_file_and_path( - atom!("ops_and_meta_predicates.pl"), - lib_path.clone(), - ), - ) - .unwrap(); - - bootstrapping_compile( - Stream::from_static_string( - libraries::get("builtins").expect("library builtins should exist"), - &mut wam.machine_st.arena, - ), - &mut wam, - ListingSource::from_file_and_path(atom!("builtins.pl"), lib_path.clone()), - ) - .unwrap(); - - if let Some(builtins) = wam.indices.modules.get_mut(&atom!("builtins")) { - load_module( - &mut wam.machine_st, - &mut wam.indices.code_dir, - &mut wam.indices.op_dir, - &mut wam.indices.meta_predicates, - &CompilationTarget::User, - builtins, - ); - - import_builtin_impls(&wam.indices.code_dir, builtins); - } else { - unreachable!() - } - - lib_path.pop(); // remove the "lib" at the end - - bootstrapping_compile( - Stream::from_static_string(include_str!("../loader.pl"), &mut wam.machine_st.arena), - &mut wam, - ListingSource::from_file_and_path(atom!("loader.pl"), lib_path.clone()), - ) - .unwrap(); - - wam.configure_modules(); - - if let Some(loader) = wam.indices.modules.get(&atom!("loader")) { - load_module( - &mut wam.machine_st, - &mut wam.indices.code_dir, - &mut wam.indices.op_dir, - &mut wam.indices.meta_predicates, - &CompilationTarget::User, - loader, - ); - } else { - unreachable!() - } - - wam.load_special_forms(); - wam.load_top_level(config.toplevel); - wam.configure_streams(); - - wam - } - pub(crate) fn configure_streams(&mut self) { self.user_input .options_mut() diff --git a/src/machine/parsed_results.rs b/src/machine/parsed_results.rs deleted file mode 100644 index b76239edd..000000000 --- a/src/machine/parsed_results.rs +++ /dev/null @@ -1,643 +0,0 @@ -use crate::atom_table::*; -use crate::heap_iter::{stackful_post_order_iter, NonListElider}; -use crate::machine::{F64Offset, F64Ptr, Fixnum, HeapCellValueTag}; -use crate::parser::ast::{Var, VarPtr}; -use dashu::*; -use indexmap::IndexMap; -use ordered_float::OrderedFloat; -use std::cmp::Ordering; -use std::collections::BTreeMap; -use std::collections::HashMap; -use std::fmt::Display; -use std::fmt::Write; -use std::iter::FromIterator; - -use super::Machine; -use super::{HeapCellValue, Number}; - -pub type QueryResult = Result; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum QueryResolution { - True, - False, - Matches(Vec), -} - -fn write_prolog_value_as_json( - writer: &mut W, - value: &Value, -) -> Result<(), std::fmt::Error> { - match value { - Value::Integer(i) => write!(writer, "{}", i), - Value::Float(f) => write!(writer, "{}", f), - Value::Rational(r) => write!(writer, "{}", r), - Value::Atom(a) => writer.write_str(a.as_str()), - Value::String(s) => { - if let Err(_e) = serde_json::from_str::(s.as_str()) { - //treat as string literal - //escape double quotes - write!( - writer, - "\"{}\"", - s.replace('\"', "\\\"") - .replace('\n', "\\n") - .replace('\t', "\\t") - .replace('\r', "\\r") - ) - } else { - //return valid json string - writer.write_str(s) - } - } - Value::List(l) => { - writer.write_char('[')?; - if let Some((first, rest)) = l.split_first() { - write_prolog_value_as_json(writer, first)?; - - for other in rest { - writer.write_char(',')?; - write_prolog_value_as_json(writer, other)?; - } - } - writer.write_char(']') - } - Value::Structure(s, l) => { - write!(writer, "\"{}\":[", s.as_str())?; - - if let Some((first, rest)) = l.split_first() { - write_prolog_value_as_json(writer, first)?; - for other in rest { - writer.write_char(',')?; - write_prolog_value_as_json(writer, other)?; - } - } - writer.write_char(']') - } - _ => writer.write_str("null"), - } -} - -fn write_prolog_match_as_json( - writer: &mut W, - query_match: &QueryMatch, -) -> Result<(), std::fmt::Error> { - writer.write_char('{')?; - let mut iter = query_match.bindings.iter(); - - if let Some((k, v)) = iter.next() { - write!(writer, "\"{k}\":")?; - write_prolog_value_as_json(writer, v)?; - - for (k, v) in iter { - write!(writer, ",\"{k}\":")?; - write_prolog_value_as_json(writer, v)?; - } - } - writer.write_char('}') -} - -impl Display for QueryResolution { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - QueryResolution::True => f.write_str("true"), - QueryResolution::False => f.write_str("false"), - QueryResolution::Matches(matches) => { - f.write_char('[')?; - if let Some((first, rest)) = matches.split_first() { - write_prolog_match_as_json(f, first)?; - for other in rest { - f.write_char(',')?; - write_prolog_match_as_json(f, other)?; - } - } - f.write_char(']') - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct QueryMatch { - pub bindings: BTreeMap, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum QueryResolutionLine { - True, - False, - Match(BTreeMap), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Value { - Integer(Integer), - Rational(Rational), - Float(OrderedFloat), - Atom(String), - String(String), - List(Vec), - Structure(String, Vec), - Var(String), -} - -/// This is an auxiliary function to turn a count into names of anonymous variables like _A, _B, -/// _AB, etc... -fn count_to_letter_code(mut count: usize) -> String { - let mut letters = Vec::new(); - - loop { - let letter_idx = (count % 26) as u32; - letters.push(char::from_u32('A' as u32 + letter_idx).unwrap()); - count /= 26; - - if count == 0 { - break; - } - } - - letters.into_iter().chain("_".chars()).rev().collect() -} - -impl Value { - pub(crate) fn from_heapcell( - machine: &mut Machine, - heap_cell: HeapCellValue, - var_names: &mut IndexMap, - ) -> Self { - // Adapted from MachineState::read_term_from_heap - let mut term_stack = vec![]; - let iter = stackful_post_order_iter::( - &mut machine.machine_st.heap, - &mut machine.machine_st.stack, - heap_cell, - ); - - let mut anon_count: usize = 0; - let var_ptr_cmp = |a, b| match a { - Var::Named(name_a) => match b { - Var::Named(name_b) => name_a.cmp(&name_b), - _ => Ordering::Less, - }, - _ => match b { - Var::Named(_) => Ordering::Greater, - _ => Ordering::Equal, - }, - }; - - for addr in iter { - let addr = unmark_cell_bits!(addr); - - read_heap_cell!(addr, - (HeapCellValueTag::Lis) => { - let tail = term_stack.pop().unwrap(); - let head = term_stack.pop().unwrap(); - - let list = match tail { - Value::Atom(atom) if atom == "[]" => match head { - Value::Atom(ref a) if a.chars().collect::>().len() == 1 => { - // Handle lists of char as strings - Value::String(a.to_string()) - } - _ => Value::List(vec![head]), - }, - Value::List(elems) if elems.is_empty() => match head { - Value::Atom(ref a) if a.chars().collect::>().len() == 1 => { - // Handle lists of char as strings - Value::String(a.to_string()) - }, - _ => Value::List(vec![head]), - }, - Value::List(mut elems) => { - elems.insert(0, head); - Value::List(elems) - }, - Value::String(mut elems) => match head { - Value::Atom(ref a) if a.chars().collect::>().len() == 1 => { - // Handle lists of char as strings - elems.insert(0, a.chars().next().unwrap()); - Value::String(elems) - }, - _ => { - let mut elems: Vec = elems - .chars() - .map(|x| Value::Atom(x.into())) - .collect(); - elems.insert(0, head); - Value::List(elems) - } - }, - _ => { - Value::Structure(".".into(), vec![head, tail]) - } - }; - term_stack.push(list); - } - (HeapCellValueTag::Var | HeapCellValueTag::AttrVar | HeapCellValueTag::StackVar) => { - let var = var_names.get(&addr).map(|x| x.borrow().clone()); - match var { - Some(Var::Named(name)) => term_stack.push(Value::Var(name)), - _ => { - let anon_name = loop { - // Generate a name for the anonymous variable - let anon_name = count_to_letter_code(anon_count); - - // Find if this name is already being used - var_names.sort_by(|_, a, _, b| { - var_ptr_cmp(a.borrow().clone(), b.borrow().clone()) - }); - let binary_result = var_names.binary_search_by(|_,a| { - let var_ptr = Var::Named(anon_name.clone()); - var_ptr_cmp(a.borrow().clone(), var_ptr.clone()) - }); - - match binary_result { - Ok(_) => anon_count += 1, // Name already used - Err(_) => { - // Name not used, assign it to this variable - let var_ptr = VarPtr::from(Var::Named(anon_name.clone())); - var_names.insert(addr, var_ptr); - break anon_name; - }, - } - }; - term_stack.push(Value::Var(anon_name)); - }, - } - } - (HeapCellValueTag::F64, f) => { - term_stack.push(Value::Float(*f)); - } - (HeapCellValueTag::Char, c) => { - term_stack.push(Value::Atom(c.into())); - } - (HeapCellValueTag::Fixnum, n) => { - term_stack.push(Value::Integer(n.into())); - } - (HeapCellValueTag::Cons) => { - match Number::try_from(addr) { - Ok(Number::Integer(i)) => term_stack.push(Value::Integer((*i).clone())), - Ok(Number::Rational(r)) => term_stack.push(Value::Rational((*r).clone())), - _ => {} - } - } - (HeapCellValueTag::CStr, s) => { - term_stack.push(Value::String(s.as_str().to_string())); - } - (HeapCellValueTag::Atom, (name, arity)) => { - //let h = iter.focus().value() as usize; - //let mut arity = arity; - - // Not sure why/if this is needed. - // Might find out with better testing later. - /* - if iter.heap.len() > h + arity + 1 { - let value = iter.heap[h + arity + 1]; - - if let Some(idx) = get_structure_index(value) { - // in the second condition, arity == 0, - // meaning idx cannot pertain to this atom - // if it is the direct subterm of a larger - // structure. - if arity > 0 || !iter.direct_subterm_of_str(h) { - term_stack.push( - Term::Literal(Cell::default(), Literal::CodeIndex(idx)) - ); - - arity += 1; - } - } - } - */ - - if arity == 0 { - let atom_name = name.as_str().to_string(); - if atom_name == "[]" { - term_stack.push(Value::List(vec![])); - } else { - term_stack.push(Value::Atom(atom_name)); - } - } else { - let subterms = term_stack - .drain(term_stack.len() - arity ..) - .collect(); - - term_stack.push(Value::Structure(name.as_str().to_string(), subterms)); - } - } - (HeapCellValueTag::PStr, atom) => { - let tail = term_stack.pop().unwrap(); - - match tail { - Value::Atom(atom) => { - if atom == "[]" { - term_stack.push(Value::String(atom.as_str().to_string())); - } - }, - Value::List(l) => { - let mut list: Vec = atom - .as_str() - .to_string() - .chars() - .map(|x| Value::Atom(x.to_string())) - .collect(); - list.extend(l.into_iter()); - term_stack.push(Value::List(list)); - }, - _ => { - let mut list: Vec = atom - .as_str() - .to_string() - .chars() - .map(|x| Value::Atom(x.to_string())) - .collect(); - - let mut partial_list = Value::Structure( - ".".into(), - vec![ - list.pop().unwrap(), - tail, - ], - ); - - while let Some(last) = list.pop() { - partial_list = Value::Structure( - ".".into(), - vec![ - last, - partial_list, - ], - ); - } - - term_stack.push(partial_list); - } - } - } - // I dont know if this is needed here. - /* - (HeapCellValueTag::PStrLoc, h) => { - let atom = cell_as_atom_cell!(iter.heap[h]).get_name(); - let tail = term_stack.pop().unwrap(); - - term_stack.push(Term::PartialString( - Cell::default(), - atom.as_str().to_owned(), - Box::new(tail), - )); - } - */ - _ => { - } - ); - } - - debug_assert_eq!(term_stack.len(), 1); - term_stack.pop().unwrap() - } -} - -impl From> for QueryMatch { - fn from(bindings: BTreeMap<&str, Value>) -> Self { - QueryMatch { - bindings: bindings - .into_iter() - .map(|(k, v)| (k.to_string(), v)) - .collect::>(), - } - } -} - -impl From> for QueryMatch { - fn from(bindings: BTreeMap) -> Self { - QueryMatch { bindings } - } -} - -impl From> for QueryResolution { - fn from(query_result_lines: Vec) -> Self { - // If there is only one line, and it is true or false, return that. - if query_result_lines.len() == 1 { - match query_result_lines[0].clone() { - QueryResolutionLine::True => return QueryResolution::True, - QueryResolutionLine::False => return QueryResolution::False, - _ => {} - } - } - - // If there is only one line, and it is an empty match, return false. - if query_result_lines.len() == 1 { - if let QueryResolutionLine::Match(m) = query_result_lines[0].clone() { - if m.is_empty() { - return QueryResolution::False; - } - } - } - - // If there is at least one line with true and no matches, return true. - if query_result_lines - .iter() - .any(|l| l == &QueryResolutionLine::True) - && !query_result_lines - .iter() - .any(|l| matches!(l, QueryResolutionLine::Match(_))) - { - return QueryResolution::True; - } - - // If there is at least one match, return all matches. - let all_matches = query_result_lines - .into_iter() - .filter(|l| matches!(l, QueryResolutionLine::Match(_))) - .map(|l| match l { - QueryResolutionLine::Match(m) => QueryMatch::from(m), - _ => unreachable!(), - }) - .collect::>(); - - if !all_matches.is_empty() { - return QueryResolution::Matches(all_matches); - } - - QueryResolution::False - } -} - -impl FromIterator for QueryResolution { - fn from_iter>(iter: I) -> Self { - // TODO: Probably a good idea to implement From> based on this - // instead. - iter.into_iter().collect::>().into() - } -} - -fn split_response_string(input: &str) -> Vec { - let mut level_bracket = 0; - let mut level_parenthesis = 0; - let mut in_double_quotes = false; - let mut in_single_quotes = false; - let mut start = 0; - let mut result = Vec::new(); - - for (i, c) in input.chars().enumerate() { - match c { - '[' => level_bracket += 1, - ']' => level_bracket -= 1, - '(' => level_parenthesis += 1, - ')' => level_parenthesis -= 1, - '"' => in_double_quotes = !in_double_quotes, - '\'' => in_single_quotes = !in_single_quotes, - ',' if level_bracket == 0 - && level_parenthesis == 0 - && !in_double_quotes - && !in_single_quotes => - { - result.push(input[start..i].trim().to_string()); - start = i + 1; - } - _ => {} - } - } - - result.push(input[start..].trim().to_string()); - result -} - -fn split_key_value_pairs(input: &str) -> Vec<(String, String)> { - let items = split_response_string(input); - let mut result = Vec::new(); - - for item in items { - let parts: Vec<&str> = item.splitn(2, '=').collect(); - if parts.len() == 2 { - let key = parts[0].trim().to_string(); - let value = parts[1].trim().to_string(); - result.push((key, value)); - } - } - - result -} - -fn parse_prolog_response(input: &str) -> HashMap { - let mut map: HashMap = HashMap::new(); - // Use regex to match strings including commas inside them - for result in split_key_value_pairs(input) { - let key = result.0; - let value = result.1; - // cut off at given characters/strings: - let value = value.split('\n').next().unwrap().to_string(); - let value = value.split(' ').next().unwrap().to_string(); - let value = value.split('\t').next().unwrap().to_string(); - let value = value.split("error").next().unwrap().to_string(); - map.insert(key, value); - } - - map -} - -impl TryFrom for QueryResolutionLine { - type Error = (); - fn try_from(string: String) -> Result { - match string.as_str() { - "true" => Ok(QueryResolutionLine::True), - "false" => Ok(QueryResolutionLine::False), - _ => Ok(QueryResolutionLine::Match( - parse_prolog_response(&string) - .iter() - .map(|(k, v)| -> Result<(String, Value), ()> { - let key = k.to_string(); - let value = v.to_string(); - Ok((key, Value::try_from(value)?)) - }) - .filter_map(Result::ok) - .collect::>(), - )), - } - } -} - -fn split_nested_list(input: &str) -> Vec { - let mut level = 0; - let mut start = 0; - let mut result = Vec::new(); - - for (i, c) in input.chars().enumerate() { - match c { - '[' => level += 1, - ']' => level -= 1, - ',' if level == 0 => { - result.push(input[start..i].trim().to_string()); - start = i + 1; - } - _ => {} - } - } - - result.push(input[start..].trim().to_string()); - result -} - -impl TryFrom for Value { - type Error = (); - fn try_from(string: String) -> Result { - let trimmed = string.trim(); - - if let Ok(float_value) = string.parse::() { - Ok(Value::Float(OrderedFloat(float_value))) - } else if let Ok(int_value) = string.parse::() { - Ok(Value::Integer(int_value.into())) - } else if trimmed.starts_with('\'') && trimmed.ends_with('\'') - || trimmed.starts_with('"') && trimmed.ends_with('"') - { - Ok(Value::String(trimmed[1..trimmed.len() - 1].into())) - } else if trimmed.starts_with('[') && trimmed.ends_with(']') { - let split = split_nested_list(&trimmed[1..trimmed.len() - 1]); - - let values = split - .into_iter() - .map(Value::try_from) - .collect::, _>>()?; - - Ok(Value::List(values)) - } else if trimmed.starts_with('{') && trimmed.ends_with('}') { - let iter = trimmed[1..trimmed.len() - 1].split(','); - let mut values = vec![]; - - for value in iter { - let items: Vec<_> = value.split(':').collect(); - if items.len() == 2 { - let _key = items[0].to_string(); - let value = items[1].to_string(); - values.push(Value::try_from(value)?); - } - } - - Ok(Value::Structure("{}".into(), values)) - } else if trimmed.starts_with("<<") && trimmed.ends_with(">>") { - let iter = trimmed[2..trimmed.len() - 2].split(','); - let mut values = vec![]; - - for value in iter { - let items: Vec<_> = value.split(':').collect(); - if items.len() == 2 { - let _key = items[0].to_string(); - let value = items[1].to_string(); - values.push(Value::try_from(value)?); - } - } - - Ok(Value::Structure("<<>>".into(), values)) - } else if !trimmed.contains(',') && !trimmed.contains('\'') && !trimmed.contains('"') { - Ok(Value::String(trimmed.into())) - } else { - Err(()) - } - } -} - -impl From<&str> for Value { - fn from(str: &str) -> Self { - Value::String(str.to_string()) - } -} diff --git a/tests/scryer/helper.rs b/tests/scryer/helper.rs index 696d2b12a..bb6402a1a 100644 --- a/tests/scryer/helper.rs +++ b/tests/scryer/helper.rs @@ -26,8 +26,8 @@ impl Expectable for &[u8] { /// Tests whether the file can be successfully loaded /// and produces the expected output during it pub(crate) fn load_module_test(file: &str, expected: T) { - use scryer_prolog::Machine; + use scryer_prolog::MachineBuilder; - let mut wam = Machine::with_test_streams(); + let mut wam = MachineBuilder::default().build(); expected.assert_eq(wam.test_load_file(file).as_slice()); }