Files
scryer-prolog/src/machine/lib_machine.rs
2023-08-24 18:10:47 +02:00

303 lines
9.5 KiB
Rust

use std::collections::BTreeSet;
use super::{
Machine, MachineConfig, QueryResult, QueryResolutionLine,
Atom, AtomCell, HeapCellValue, HeapCellValueTag,
streams::Stream
};
impl Machine {
pub fn new_lib() -> Self {
Machine::new(MachineConfig::in_memory().with_toplevel(include_str!("../lib_toplevel.pl")))
}
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!(self.machine_st.atom_tbl.build_with(module_name));
self.run_module_predicate(atom!("loader"), (atom!("consult_stream"), 2));
}
pub fn run_query(&mut self, query: String) -> QueryResult {
let input = format!("{}", query);
//println!("Running query: {}", input);
self.set_user_input(input);
self.run_top_level(atom!("$toplevel"), (atom!("run_input_once"), 0));
self.parse_output()
}
pub fn parse_output(&self) -> QueryResult {
let output = self.get_user_output().trim().to_string();
//println!("Output: {}", output);
if output.starts_with("error(") {
Err(output)
} else {
// Remove duplicate lines
Ok(output
// 1. Split into disjunct matches
.split(";")
.map(|s| s.trim())
// 2. Dedupe through Set
.collect::<BTreeSet<&str>>()
.iter()
.cloned()
// 3. Back to Vec
.collect::<Vec<&str>>()
.iter()
// 4. Trim and remove empty lines
.map(|s| s.trim())
.map(|s| if s.ends_with(".") { s[..s.len()-1].to_string() } else { s.to_string() })
.filter(|s| !s.is_empty())
// 5. Parse into QueryResolutionLine
.map(QueryResolutionLine::try_from)
// 6. Remove lines that couldn't be parsed, so we still keep the ones they did
.filter_map(Result::ok)
.collect::<Vec<QueryResolutionLine>>()
.into())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::machine::{QueryMatch, Value, QueryResolution};
#[tokio::test]
async 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)
);
}
#[tokio::test]
async 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)."))
);
}
#[tokio::test]
async 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::from("c"),
"Actions" => Value::from("[{action: \"addLink\", source: \"this\", predicate: \"todo://state\", target: \"todo://ready\"}]"),
}),
]))
);
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::from("xyz"),
"Actions" => Value::from("[{action: \"addLink\", source: \"this\", predicate: \"recipe://title\", target: \"literal://string:Meta%20Muffins\"}]"),
}),
]))
);
let result = machine.run_query(String::from("subject_class(Class, _)."));
assert_eq!(
result,
Ok(QueryResolution::Matches(vec![
QueryMatch::from(btreemap! {
"Class" => Value::from("Recipe")
}),
QueryMatch::from(btreemap! {
"Class" => Value::from("Todo")
}),
]))
);
}
#[tokio::test]
async 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::from([Value::from("1"), Value::from("2"), Value::from("3")]))
}),
]))
);
}
#[tokio::test]
async 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)
);
}
#[ignore = "fails on windows"]
#[tokio::test]
async fn stress_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;
// 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 block.starts_with("query") {
// Extract the query from the block
let query = &block[5..];
i += 1;
println!("query #{}: {}", i, query);
// Parse and execute the query
let result = machine.run_query(query.to_string());
assert!(result.is_ok());
// Print the result
println!("{:?}", result);
} else if block.starts_with("consult") {
// Extract the code from the block
let code = &block[7..];
println!("load code: {}", code);
// Load the code into the machine
machine.consult_module_string("facts", code.to_string());
}
}
}
}