Merge pull request #2582 from bakaq/rework_library_interface

Rework library interface
This commit is contained in:
Mark Thom
2024-12-13 21:46:43 -07:00
committed by GitHub
12 changed files with 1452 additions and 1632 deletions

View File

@@ -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<LeafAnswer>) -> Vec<LeafAnswer> {
run()
}

View File

@@ -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<LeafAnswer> {
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::<Result<Vec<_>, _>>()
.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::<Result<_, _>>()
.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 {

View File

@@ -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))
})
}

View File

@@ -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,
}
pub enum StreamConfig {
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,
}
}
}
#[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<Cow<'static, str>>) -> 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")
}

View File

@@ -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<HeapCellValue, VarPtr>,
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<QueryResolutionLine, String>;
fn next(&mut self) -> Option<Self::Item> {
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::<Vec<String>>()
.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<String, Value> = 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()),
}
),]))
);
}
}

View File

@@ -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<String, Term>,
//residual_goals: Vec<Term>,
},
}
impl LeafAnswer {
/// Creates a leaf answer with no residual goals.
pub fn from_bindings<S: Into<String>>(bindings: impl IntoIterator<Item = (S, Term)>) -> 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<Term>),
/// A Prolog compound term.
Compound(String, Vec<Term>),
/// A Prolog variable.
Var(String),
}
impl Term {
/// Creates an integer term.
pub fn integer(value: impl Into<Integer>) -> Self {
Term::Integer(value.into())
}
/// Creates a rational term.
pub fn rational(value: impl Into<Rational>) -> Self {
Term::Rational(value.into())
}
/// Creates a float term.
pub fn float(value: impl Into<f64>) -> Self {
Term::Float(value.into())
}
/// Creates an atom term.
pub fn atom(value: impl Into<String>) -> 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<String>) -> Self {
Term::String(value.into())
}
/// Creates a list term.
pub fn list(value: impl IntoIterator<Item = Term>) -> Self {
Term::List(value.into_iter().collect())
}
/// Creates a compound term.
pub fn compound(functor: impl Into<String>, args: impl IntoIterator<Item = Term>) -> Self {
Term::Compound(functor.into(), args.into_iter().collect())
}
/// Creates a variable.
pub fn variable(value: impl Into<String>) -> Self {
Term::Var(value.into())
}
/// Creates a conjunction, giving the atom `true` if empty.
pub fn conjunction(value: impl IntoIterator<Item = Term>) -> Self {
Term::try_conjunction(value).unwrap_or(Term::atom("true"))
}
/// Creates a conjunction, giving `None` if empty.
pub fn try_conjunction(value: impl IntoIterator<Item = Term>) -> Option<Self> {
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<Item = Term>) -> Self {
Term::try_disjunction(value).unwrap_or(Term::atom("false"))
}
/// Creates a disjunction, giving `None` if empty.
pub fn try_disjunction(value: impl IntoIterator<Item = Term>) -> Option<Self> {
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<HeapCellValue, VarPtr>,
) -> Self {
// Adapted from MachineState::read_term_from_heap
let mut term_stack = vec![];
let iter = stackful_post_order_iter::<NonListElider>(
&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::<Vec<_>>().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::<Vec<_>>().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::<Vec<_>>().len() == 1 => {
// Handle lists of char as strings
elems.insert(0, a.chars().next().unwrap());
Term::String(elems)
},
_ => {
let mut elems: Vec<Term> = 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<Term> = 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<Term> = 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<HeapCellValue, VarPtr>,
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<LeafAnswer, Term>;
fn next(&mut self) -> Option<Self::Item> {
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<String, Term> = 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<String>) {
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<String>) {
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<String>) -> 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,
}
}
}

View File

@@ -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::<Result<_, _>>().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::<Result<_, _>>().unwrap();
assert_eq!(complete_answer, [LeafAnswer::True],);
let query = r#"triple("x","y","z")."#;
let complete_answer: Vec<_> = machine.run_query(query).collect::<Result<_, _>>().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<Vec<_>, _> = 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::<Result<_, _>>()
.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::<Result<_, _>>()
.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::<Result<_, _>>()
.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::<Result<_, _>>()
.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::<Result<_, _>>()
.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::<Result<_, _>>().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::<Result<_, _>>().unwrap();
assert_eq!(complete_answer, [LeafAnswer::True],);
let query = r#"triple("x","y","z")."#;
let complete_answer: Vec<_> = machine.run_query(query).collect::<Result<_, _>>().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::<Result<_, _>>().unwrap();
assert_eq!(complete_answer, [LeafAnswer::False],);
let query = r#"triple("a","new","b")."#;
let complete_answer: Vec<_> = machine.run_query(query).collect::<Result<_, _>>().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::<Result<_, _>>().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::<Result<_, _>>().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::<Result<_, _>>().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::<Result<_, _>>().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::<Result<_, _>>().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::<Result<_, _>>().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::<Result<_, _>>().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<Vec<_>, _> = 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::<Result<_, _>>().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::<Result<_, _>>().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::<Result<_, _>>().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::<Result<_, _>>().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::<Result<_, _>>().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::<Result<_, _>>()
.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::<Result<_, _>>()
.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")))]
);
}

View File

@@ -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<u8> {
let stream = Stream::from_owned_string(
std::fs::read_to_string(AsRef::<std::path::Path>::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<u8> {
let stream = Stream::from_owned_string(code.to_owned(), &mut self.machine_st.arena);

View File

@@ -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()

View File

@@ -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<QueryResolution, String>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryResolution {
True,
False,
Matches(Vec<QueryMatch>),
}
fn write_prolog_value_as_json<W: Write>(
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::<serde_json::Value>(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<W: std::fmt::Write>(
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<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryResolutionLine {
True,
False,
Match(BTreeMap<String, Value>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
Integer(Integer),
Rational(Rational),
Float(OrderedFloat<f64>),
Atom(String),
String(String),
List(Vec<Value>),
Structure(String, Vec<Value>),
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<HeapCellValue, VarPtr>,
) -> Self {
// Adapted from MachineState::read_term_from_heap
let mut term_stack = vec![];
let iter = stackful_post_order_iter::<NonListElider>(
&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::<Vec<_>>().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::<Vec<_>>().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::<Vec<_>>().len() == 1 => {
// Handle lists of char as strings
elems.insert(0, a.chars().next().unwrap());
Value::String(elems)
},
_ => {
let mut elems: Vec<Value> = 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<Value> = 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<Value> = 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<BTreeMap<&str, Value>> for QueryMatch {
fn from(bindings: BTreeMap<&str, Value>) -> Self {
QueryMatch {
bindings: bindings
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect::<BTreeMap<_, _>>(),
}
}
}
impl From<BTreeMap<String, Value>> for QueryMatch {
fn from(bindings: BTreeMap<String, Value>) -> Self {
QueryMatch { bindings }
}
}
impl From<Vec<QueryResolutionLine>> for QueryResolution {
fn from(query_result_lines: Vec<QueryResolutionLine>) -> 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::<Vec<_>>();
if !all_matches.is_empty() {
return QueryResolution::Matches(all_matches);
}
QueryResolution::False
}
}
impl FromIterator<QueryResolutionLine> for QueryResolution {
fn from_iter<I: IntoIterator<Item = QueryResolutionLine>>(iter: I) -> Self {
// TODO: Probably a good idea to implement From<Vec<QueryResolutionLine>> based on this
// instead.
iter.into_iter().collect::<Vec<_>>().into()
}
}
fn split_response_string(input: &str) -> Vec<String> {
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<String, String> {
let mut map: HashMap<String, String> = 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<String> for QueryResolutionLine {
type Error = ();
fn try_from(string: String) -> Result<Self, Self::Error> {
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::<BTreeMap<_, _>>(),
)),
}
}
}
fn split_nested_list(input: &str) -> Vec<String> {
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<String> for Value {
type Error = ();
fn try_from(string: String) -> Result<Self, Self::Error> {
let trimmed = string.trim();
if let Ok(float_value) = string.parse::<f64>() {
Ok(Value::Float(OrderedFloat(float_value)))
} else if let Ok(int_value) = string.parse::<i128>() {
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::<Result<Vec<_>, _>>()?;
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())
}
}

View File

@@ -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<T: Expectable>(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());
}