From db343a1be60d56526798fa19ec1b366a405d817d Mon Sep 17 00:00:00 2001 From: dcz Date: Sat, 30 Nov 2024 09:48:23 +0000 Subject: [PATCH] Universe can be queried while immutable Added SymbolOverlay which contains changes needed to execute a particular query. --- examples/arithmetic.rs | 5 +- examples/repl.rs | 12 +-- examples/zebra.rs | 8 +- src/lib.rs | 4 +- src/resolve/arithmetic.rs | 22 +++-- src/search.rs | 2 + src/search/test.rs | 10 ++- src/textual.rs | 46 ++++++++-- src/textual/parser.rs | 26 ++++-- src/textual/pretty.rs | 15 ++-- src/universe.rs | 181 ++++++++++++++++++++++++++++++++------ 11 files changed, 253 insertions(+), 78 deletions(-) diff --git a/examples/arithmetic.rs b/examples/arithmetic.rs index 55ea4d7..78eeadc 100644 --- a/examples/arithmetic.rs +++ b/examples/arithmetic.rs @@ -7,7 +7,7 @@ fn main() { .unwrap(); let query = u.prepare_query("mul(A,A,B).").unwrap(); - let solutions = query_dfs(u.resolver(), &query); + let solutions = query_dfs(u.resolver(), query.query()); for solution in solutions.take(10) { println!("SOLUTION:"); @@ -16,7 +16,8 @@ fn main() { println!( " ${} = {}", var.ord(), - u.pretty().term_to_string(&term, query.scope.as_ref()) + u.pretty() + .term_to_string(&term, query.query().scope.as_ref()) ); } else { println!(""); diff --git a/examples/repl.rs b/examples/repl.rs index bb5d912..88f2992 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -9,7 +9,7 @@ use logru::resolve::{ArithmeticResolver, ResolverExt}; use logru::search::{query_dfs, Resolved, Resolver}; use logru::term_arena::{AppTerm, ArgRange}; use logru::textual::{Prettifier, TextualUniverse}; -use logru::SymbolStore; +use logru::{SymbolStorage, SymbolStore}; use rustyline::completion::Completer; use rustyline::error::ReadlineError; use rustyline::highlight::Highlighter; @@ -148,11 +148,11 @@ fn query(state: &mut AppState, args: &str) { Ok(query) => { let builtins = state .commands - .as_resolver(&state.universe.symbols, query.scope.as_ref()); + .as_resolver(&state.universe.symbols, query.query().scope.as_ref()); let resolver = builtins .or_else(&mut state.arithmetic) .or_else(state.universe.resolver()); - let mut solutions = query_dfs(resolver, &query); + let mut solutions = query_dfs(resolver, query.query()); loop { if state.interrupted.load(atomic::Ordering::SeqCst) { println!("Interrupted!"); @@ -163,7 +163,9 @@ fn query(state: &mut AppState, args: &str) { let solution = solutions.get_solution(); println!("Found solution:"); for (var, term) in solution.iter_vars() { - if let Some(name) = query.scope.as_ref().and_then(|s| s.get_name(var)) { + if let Some(name) = + query.query().scope.as_ref().and_then(|s| s.get_name(var)) + { print!(" {} = ", name); } else { print!(" _{} = ", var.ord()); @@ -174,7 +176,7 @@ fn query(state: &mut AppState, args: &str) { state .universe .pretty() - .term_to_string(&term, query.scope.as_ref()) + .term_to_string(&term, query.query().scope.as_ref()) ); } else { println!(""); diff --git a/examples/zebra.rs b/examples/zebra.rs index 8391c48..dbb15ee 100644 --- a/examples/zebra.rs +++ b/examples/zebra.rs @@ -11,7 +11,7 @@ fn main() { let query = u.prepare_query("puzzle(Houses).").unwrap(); for i in 0..repeats { - let search = logru::query_dfs(u.resolver(), &query); + let search = logru::query_dfs(u.resolver(), query.query()); let before = Instant::now(); let solutions = search.collect::>(); let duration = before.elapsed(); @@ -20,7 +20,11 @@ fn main() { if i == 0 { for var in solution.vars() { if let Some(term) = var { - println!("{}", u.pretty().term_to_string(term, query.scope.as_ref())); + println!( + "{}", + u.pretty() + .term_to_string(term, query.query().scope.as_ref()) + ); } else { println!(""); } diff --git a/src/lib.rs b/src/lib.rs index 8185a3b..43c2428 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ //! //! ``` //! use logru::ast::{self, Rule}; +//! use logru::SymbolStorage; //! //! let mut syms = logru::SymbolStore::new(); //! let mut r = logru::RuleSet::new(); @@ -84,6 +85,7 @@ //! # use logru::ast::{self, Rule}; //! # use logru::resolve::RuleResolver; //! # use logru::search::Solution; +//! # use logru::SymbolStorage; //! # let mut syms = logru::SymbolStore::new(); //! # let mut r = logru::RuleSet::new(); //! # let s = syms.get_or_insert_named("s"); @@ -154,4 +156,4 @@ pub mod universe; pub use resolve::RuleResolver; pub use search::query_dfs; -pub use universe::{RuleSet, SymbolStore}; +pub use universe::{RuleSet, SymbolStorage, SymbolStore, Symbols}; diff --git a/src/resolve/arithmetic.rs b/src/resolve/arithmetic.rs index d29597c..4bcc9fc 100644 --- a/src/resolve/arithmetic.rs +++ b/src/resolve/arithmetic.rs @@ -4,7 +4,7 @@ use std::convert::TryInto; use crate::ast::Sym; use crate::search::{Resolved, Resolver, SolutionState}; use crate::term_arena::{AppTerm, ArgRange, Term, TermId}; -use crate::SymbolStore; +use crate::universe::SymbolStorage; /// A special resolver for integer arithmetic. It provides a special predicate `is/2` which /// evaluates integer expressions. @@ -46,7 +46,7 @@ pub struct ArithmeticResolver { } impl ArithmeticResolver { - pub fn new(symbols: &mut SymbolStore) -> Self { + pub fn new(symbols: &mut T) -> Self { let exps = [ ("add", Exp::Add), ("sub", Exp::Sub), @@ -167,14 +167,12 @@ mod tests { #[test] fn simple() { - let mut tu = TextualUniverse::new(); - let query = tu + let tu = TextualUniverse::new(); + let mut query = tu .prepare_query("is(X, add(3, mul(3, sub(6, div(10, rem(10, pow(2,3))))))).") .unwrap(); - let mut results = query_dfs( - ArithmeticResolver::new(&mut tu.symbols).or_else(tu.resolver()), - &query, - ); + let resolver = ArithmeticResolver::new(&mut query.symbols_mut()); + let mut results = query_dfs(resolver.or_else(tu.resolver()), query.query()); assert_eq!(results.next(), Some(Solution(vec![Some(Term::Int(6))]))); assert!(results.next().is_none()); } @@ -193,25 +191,25 @@ mod tests { .unwrap(); { let query = tu.prepare_query("eq(add(2, 2), pow(2, 2)).").unwrap(); - let mut results = query_dfs(arith.by_ref().or_else(tu.resolver()), &query); + let mut results = query_dfs(arith.by_ref().or_else(tu.resolver()), query.query()); assert_eq!(results.next(), Some(Solution(vec![]))); assert!(results.next().is_none()); } { let query = tu.prepare_query("eq(X, pow(2, 2)).").unwrap(); - let mut results = query_dfs(arith.by_ref().or_else(tu.resolver()), &query); + let mut results = query_dfs(arith.by_ref().or_else(tu.resolver()), query.query()); assert_eq!(results.next(), Some(Solution(vec![Some(Term::Int(4))]))); assert!(results.next().is_none()); } { let query = tu.prepare_query("eq(add(2, 2), X).").unwrap(); - let mut results = query_dfs(arith.by_ref().or_else(tu.resolver()), &query); + let mut results = query_dfs(arith.by_ref().or_else(tu.resolver()), query.query()); assert_eq!(results.next(), Some(Solution(vec![Some(Term::Int(4))]))); assert!(results.next().is_none()); } { let query = tu.prepare_query("eq(2, 2).").unwrap(); - let mut results = query_dfs(arith.by_ref().or_else(tu.resolver()), &query); + let mut results = query_dfs(arith.by_ref().or_else(tu.resolver()), query.query()); assert_eq!(results.next(), Some(Solution(vec![]))); assert!(results.next().is_none()); } diff --git a/src/search.rs b/src/search.rs index dac1610..80c93d6 100644 --- a/src/search.rs +++ b/src/search.rs @@ -113,6 +113,7 @@ impl<'c> ResolveContext<'c> { } } +#[derive(Debug)] pub enum Resolved { /// The goal resolved to a single choice that was successfully applied to the state. Success, @@ -284,6 +285,7 @@ impl SolutionIter { /// # use logru::ast::{self, Rule}; /// # use logru::resolve::RuleResolver; /// # use logru::search::Step; + /// # use logru::SymbolStorage; /// # let mut syms = logru::SymbolStore::new(); /// # let mut r = logru::RuleSet::new(); /// # let s = syms.get_or_insert_named("s"); diff --git a/src/search/test.rs b/src/search/test.rs index 00e1ed8..d1db314 100644 --- a/src/search/test.rs +++ b/src/search/test.rs @@ -1,6 +1,6 @@ use super::*; use crate::textual::TextualUniverse; -use crate::{ast::*, RuleResolver, RuleSet, SymbolStore}; +use crate::{ast::*, RuleResolver, RuleSet, SymbolStorage, SymbolStore}; #[test] fn genealogy() { @@ -237,15 +237,17 @@ fn cut() { #[track_caller] fn assert_solutions(tu: &mut TextualUniverse, query: &str, solutions: &[&[Option<&str>]]) { let query = tu.prepare_query(query).unwrap(); - let rets: Vec<_> = query_dfs(tu.resolver(), &query).collect(); + let rets: Vec<_> = query_dfs(tu.resolver(), query.query()).collect(); let pretty_solutions: Vec<_> = rets .into_iter() .map(|sol| { sol.vars() .iter() .map(|var| { - var.as_ref() - .map(|term| tu.pretty().term_to_string(&term, query.scope.as_ref())) + var.as_ref().map(|term| { + tu.pretty() + .term_to_string(&term, query.query().scope.as_ref()) + }) }) .collect::>() }) diff --git a/src/textual.rs b/src/textual.rs index fafbf3c..d23b1b0 100644 --- a/src/textual.rs +++ b/src/textual.rs @@ -13,7 +13,7 @@ use crate::{ ast::Query, resolve::RuleResolver, search::{self, SolutionIter}, - universe::{RuleSet, SymbolStore}, + universe::{RuleSet, SymbolOverlay, SymbolStore}, }; pub use self::{parser::Parser, pretty::Prettifier}; @@ -64,12 +64,12 @@ pub use self::{parser::Parser, pretty::Prettifier}; /// .unwrap(); /// /// let query = u.prepare_query("mul(X,X,Y).").unwrap(); -/// let solutions = query_dfs(u.resolver(), &query); +/// let solutions = query_dfs(u.resolver(), query.query()); /// for solution in solutions.take(5) { /// println!("SOLUTION:"); /// for (var, term) in solution.iter_vars() { /// if let Some(term) = term { -/// println!(" ${} = {}", var.ord(), u.pretty().term_to_string(&term, query.scope.as_ref())); +/// println!(" ${} = {}", var.ord(), u.pretty().term_to_string(&term, query.query().scope.as_ref())); /// } else { /// println!(""); /// } @@ -100,8 +100,11 @@ impl TextualUniverse { } /// Parse a query, but do not run it. - pub fn prepare_query(&mut self, query: &str) -> Result { - Parser::new(&mut self.symbols).parse_query_str(query) + pub fn prepare_query(&self, query: &str) -> Result, ParseError> { + let symbols = SymbolOverlay::new(&self.symbols); + let mut parser = Parser::new(symbols); + let query = parser.parse_query_str(query)?; + Ok(UniverseQuery::new(parser.into_symbols(), query)) } /// Run a query against the universe using the DFS solver. @@ -115,18 +118,21 @@ impl TextualUniverse { /// thus the pretty-printer is still accessible. pub fn query_dfs(&mut self, query: &str) -> Result, ParseError> { let query = self.prepare_query(query)?; - Ok(search::query_dfs(RuleResolver::new(&self.rules), &query)) + Ok(search::query_dfs( + RuleResolver::new(&self.rules), + query.query(), + )) } // //////////////////////////////// OTHER ACCESSORS //////////////////////////////// /// Return a pretty-printer using the symbols defined in this universe. - pub fn pretty(&self) -> Prettifier { + pub fn pretty(&self) -> Prettifier { Prettifier::new(&self.symbols) } /// Return a term parser that uses the name mapping of this universe for parsing terms. - pub fn parse(&mut self) -> Parser { + pub fn parse(&mut self) -> Parser<&mut SymbolStore> { Parser::new(&mut self.symbols) } @@ -141,3 +147,27 @@ impl Default for TextualUniverse { Self::new() } } + +/// A query tied to a particular universe through symbols +pub struct UniverseQuery<'a> { + symbols: SymbolOverlay<'a>, + query: Query, +} + +impl<'a> UniverseQuery<'a> { + pub fn new(symbols: SymbolOverlay<'a>, query: Query) -> Self { + Self { symbols, query } + } + + pub fn query(&self) -> &Query { + &self.query + } + + pub fn symbols(&self) -> &SymbolOverlay<'a> { + &self.symbols + } + + pub fn symbols_mut(&mut self) -> &mut SymbolOverlay<'a> { + &mut self.symbols + } +} diff --git a/src/textual/parser.rs b/src/textual/parser.rs index 0a2f3fe..65660f8 100644 --- a/src/textual/parser.rs +++ b/src/textual/parser.rs @@ -3,7 +3,7 @@ use std::iter::Peekable; use logos::{Logos, Span, SpannedIter}; use crate::ast::{AppTerm, Query, Rule, Sym, Term, Var, VarScope}; -use crate::universe::SymbolStore; +use crate::universe::SymbolStorage; use super::lexer::Token; @@ -91,17 +91,21 @@ impl ParseErrorKind { /// A parser for terms using the Prolog-like syntax of the /// [TextualUniverse](super::TextualUniverse). -pub struct Parser<'u> { - symbols: &'u mut SymbolStore, +pub struct Parser { + symbols: T, } -impl<'a> Parser<'a> { - pub fn new(symbols: &'a mut SymbolStore) -> Self { +impl<'a, T: SymbolStorage> Parser { + pub fn new(symbols: T) -> Self { Self { symbols } } - // //////////////////////////////// PUBLIC PARSER //////////////////////////////// + /// The act of parsing creates a new set of symbols which correspond to the parsed queries. This extracts those symbols. + pub fn into_symbols(self) -> T { + self.symbols + } + // //////////////////////////////// PUBLIC PARSER //////////////////////////////// pub fn parse_query_str(&mut self, query: &str) -> Result { let mut tokens = TokenStream::new(query); let mut scope = VarScope::new(); @@ -279,13 +283,17 @@ impl<'a> Parser<'a> { mod test { use super::super::pretty; use super::*; + use crate::universe::{SymbolOverlay, SymbolStore}; + fn query_roundtrip_test(input: &str) { - let mut nu = SymbolStore::new(); - let mut p = Parser::new(&mut nu); + let mut ss = SymbolStore::new(); + let nu = SymbolOverlay::new(&mut ss); + let mut p = Parser::new(nu); let q = p.parse_query_str(input).unwrap(); - let pretty = pretty::Prettifier::new(&nu); + let symbols = p.into_symbols(); + let pretty = pretty::Prettifier::new(&symbols); let qs = pretty.query_to_string(&q); assert_eq!(qs, input); } diff --git a/src/textual/pretty.rs b/src/textual/pretty.rs index e8d3b0e..bca69e2 100644 --- a/src/textual/pretty.rs +++ b/src/textual/pretty.rs @@ -1,15 +1,16 @@ -use crate::ast::{AppTerm, Query, Rule, Term, VarScope}; - -use super::SymbolStore; +use crate::{ + ast::{AppTerm, Query, Rule, Term, VarScope}, + universe::Symbols, +}; /// A pretty-printer for terms using the Prolog-like syntax of the /// [TextualUniverse](super::TextualUniverse). -pub struct Prettifier<'u> { - universe: &'u SymbolStore, +pub struct Prettifier<'u, T: Symbols> { + universe: &'u T, } -impl<'a> Prettifier<'a> { - pub fn new(universe: &'a SymbolStore) -> Self { +impl<'a, T: Symbols> Prettifier<'a, T> { + pub fn new(universe: &'a T) -> Self { Self { universe } } diff --git a/src/universe.rs b/src/universe.rs index 308c5fd..cba9d21 100644 --- a/src/universe.rs +++ b/src/universe.rs @@ -1,6 +1,6 @@ //! # Universe //! -//! The universe consists of two main "databases" that are relevant to the set of facts, rules and +//! The universe consists of two main "databases" that are relevant to the set of facts", "databases" that are relevant to the set of facts"),rules and //! queries: //! 1. The [`SymbolStore`] is used for allocating named and unnamed [`Sym`]bols which are used as //! identifiers in the low-level representation. @@ -12,6 +12,44 @@ use crate::{ term_arena::{self, TermArena}, }; +/// A modifiable collection of symbols +pub trait SymbolStorage { + /// Return the symbol associated with the name, or allocate a fresh ID and associate it with the + /// given name. + fn get_or_insert_named(&mut self, name: &str) -> Sym; + + /// Given a list of name-value pairs, translate the names into [`Sym`]s and return a mapping + /// from symbols to values. + fn build_sym_map<'a, T>( + &mut self, + pairs: impl IntoIterator, + ) -> HashMap { + pairs + .into_iter() + .map(|(name, value)| (self.get_or_insert_named(name), value)) + .collect() + } +} + +impl SymbolStorage for &mut T +where + T: SymbolStorage, +{ + fn get_or_insert_named(&mut self, name: &str) -> Sym { + let this: &mut T = *self; // Specify the concrete type to call to avoid accidentally recursing back into this method and creating an infinite loop. + this.get_or_insert_named(name) + } +} + +/// A readable collection of symbols +pub trait Symbols { + /// Get the name of a symbol, if it has one. + fn get_symbol_name(&self, sym: Sym) -> Option<&str>; + + /// Returns the number of symbols that have been allocated in this universe. + fn num_symbols(&self) -> usize; +} + /// The symbol store is responsible for allocating unique [`Sym`]s (unique within the instance, /// there are no guardrails preventing mixing [`Sym`]s from different [`SymbolStore`]s), and keeps a /// mapping between [`Sym`]s and friendly names. @@ -40,7 +78,7 @@ use crate::{ /// # Example /// /// See the [top-level example](crate#example). -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SymbolStore { /// Mapping from names to symbols sym_by_name: HashMap, Sym>, @@ -58,30 +96,6 @@ impl SymbolStore { } } - /// Return the symbol associated with the name, or allocate a fresh ID and associate it with the - /// given name. - pub fn get_or_insert_named(&mut self, name: &str) -> Sym { - self.sym_by_name.get(name).copied().unwrap_or_else(|| { - let sym = Sym::from_ord(self.name_by_sym.len()); - let shared_name: Arc = name.into(); - self.name_by_sym.push(Some(shared_name.clone())); - self.sym_by_name.insert(shared_name, sym); - sym - }) - } - - /// Given a list of name-value pairs, translate the names into [`Sym`]s and return a mapping - /// from symbols to values. - pub fn build_sym_map<'a, T>( - &mut self, - pairs: impl IntoIterator, - ) -> HashMap { - pairs - .into_iter() - .map(|(name, value)| (self.get_or_insert_named(name), value)) - .collect() - } - /// Generate a fresh unnamed symbol ID in this universe. /// /// # Notes @@ -94,24 +108,88 @@ impl SymbolStore { self.name_by_sym.push(None); sym } +} +impl Symbols for SymbolStore { /// Get the name of a symbol, if it has one. - pub fn get_symbol_name(&self, sym: Sym) -> Option<&str> { + fn get_symbol_name(&self, sym: Sym) -> Option<&str> { self.name_by_sym.get(sym.ord()).and_then(|n| n.as_deref()) } /// Returns the number of symbols that have been allocated in this universe. - pub fn num_symbols(&self) -> usize { + fn num_symbols(&self) -> usize { self.name_by_sym.len() } } +impl<'a> SymbolStorage for SymbolStore { + /// Return the symbol associated with the name, or allocate a fresh ID and associate it with the + /// given name. + fn get_or_insert_named(&mut self, name: &str) -> Sym { + self.sym_by_name.get(name).copied().unwrap_or_else(|| { + let sym = Sym::from_ord(self.name_by_sym.len()); + let shared_name: Arc = name.into(); + self.name_by_sym.push(Some(shared_name.clone())); + self.sym_by_name.insert(shared_name, sym); + sym + }) + } +} + impl Default for SymbolStore { fn default() -> Self { Self::new() } } +/// Stores and allocates unique symbols on top of a symbol store, without modifying it. +#[derive(Debug)] +pub struct SymbolOverlay<'a> { + symbols: &'a SymbolStore, + // Sym entries stored here start at 0, but get translated on the API boundary to start at symbols.num_symbols(). + overlay: SymbolStore, +} + +impl<'a> SymbolOverlay<'a> { + pub fn new(symbols: &'a SymbolStore) -> Self { + Self { + symbols, + overlay: Default::default(), + } + } +} + +impl SymbolStorage for SymbolOverlay<'_> { + /// Return the symbol associated with the name, or allocate a fresh ID and associate it with the + /// given name. + fn get_or_insert_named(&mut self, name: &str) -> Sym { + self.symbols + .sym_by_name + .get(name) + .copied() + .unwrap_or_else(|| { + Sym::from_ord( + self.symbols.num_symbols() + self.overlay.get_or_insert_named(name).ord(), + ) + }) + } +} + +impl Symbols for SymbolOverlay<'_> { + /// Get the name of a symbol, if it has one. + fn get_symbol_name(&self, sym: Sym) -> Option<&str> { + match sym.ord().checked_sub(self.symbols.num_symbols()) { + None => self.symbols.get_symbol_name(sym), + Some(index) => self.overlay.get_symbol_name(Sym::from_ord(index)), + } + } + + /// Returns the number of symbols that have been allocated in this universe. + fn num_symbols(&self) -> usize { + self.overlay.num_symbols() + self.symbols.num_symbols() + } +} + /// Data structure holding facts and rules for use in solvers, like the built-in /// [query_dfs](crate::query_dfs). /// @@ -238,3 +316,50 @@ impl Default for RuleSet { Self::new() } } + +#[cfg(test)] +mod test { + use crate::{ast::Sym, Symbols}; + + use super::{SymbolOverlay, SymbolStorage, SymbolStore}; + + #[test] + fn overlay() { + let mut plain = SymbolStore::new(); + plain.insert_unnamed(); + plain.insert_unnamed(); + plain.insert_unnamed(); + plain.get_or_insert_named("a"); + let overlay_copy = plain.clone(); + let mut overlaid = SymbolOverlay::new(&overlay_copy); + + assert_eq!( + plain.get_or_insert_named("b"), + overlaid.get_or_insert_named("b") + ); + + assert_eq!( + plain.get_or_insert_named("c"), + overlaid.get_or_insert_named("c") + ); + + assert_eq!(plain.num_symbols(), overlaid.num_symbols()); + + assert_eq!( + plain.get_symbol_name(Sym::from_ord(3)), + overlaid.get_symbol_name(Sym::from_ord(3)) + ); + + // WARNING: those two checks are overly strict: they check that SymbolOverlay and SymbolStore assign the same numbers to additional symbols. That is not pat of the contract for an Overlay, although it makes debugging and testing easier. + // If the need arises to relax the constraint, check instead that newly added symbols don't collide with existing ones and that names can be obtained correctly. + assert_eq!( + plain.get_symbol_name(Sym::from_ord(5)), + overlaid.get_symbol_name(Sym::from_ord(5)) + ); + + assert_eq!( + plain.get_symbol_name(Sym::from_ord(99)), + overlaid.get_symbol_name(Sym::from_ord(99)) + ); + } +}