├── .github └── FUNDING.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── drama.rs ├── groceries.rs └── hats.rs └── src └── lib.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: bvssvni 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "monotonic_solver" 3 | version = "0.5.0" 4 | authors = ["Sven Nilsen "] 5 | keywords = ["monotonic", "solver", "theorem", "proving", "reasoning"] 6 | description = "A monotonic solver designed to be easy to use with Rust enum expressions" 7 | license = "MIT" 8 | readme = "README.md" 9 | repository = "https://github.com/advancedresearch/monotonic_solver.git" 10 | homepage = "https://github.com/advancedresearch/monotonic_solver" 11 | documentation = "https://docs.rs/monotonic_solver" 12 | categories = ["algorithms", "science"] 13 | 14 | [dependencies] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 advancedresearch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monotonic-Solver 2 | A monotonic solver designed to be easy to use with Rust enum expressions 3 | 4 | This can be used to: 5 | 6 | - Research modeling of common sense for artificial intelligence 7 | - Test inference rules when studying logic and languages 8 | - Generate story plots 9 | - Search and extract data 10 | 11 | Used in [Avalog](https://github.com/advancedresearch/avalog), 12 | an experimental implementation of Avatar Logic with a Prolog-like syntax. 13 | 14 | Blog posts: 15 | 16 | - [2017-07-25 New Library for Automated Monotonic Theorem Proving](https://github.com/advancedresearch/advancedresearch.github.io/blob/master/blog/2017-07-25-new-library-for-automated-monotonic-theorem-proving.md) 17 | 18 | The advantage of this library design is the ease-of-use for prototyping. 19 | In a few hours, one can test a new idea for modeling common sense. 20 | 21 | Here is an example of program output (from "examples/drama.rs"): 22 | 23 | ```text 24 | Bob murdered Alice with a gun 25 | Bob shot Alice with a gun 26 | Bob pulled the trigger of the gun 27 | Bob aimed the gun at Alice 28 | ``` 29 | 30 | This is a program that generates drama story plots. 31 | The solver starts with the ending and work backwards to the beginning. 32 | 33 | - Start: "Bob murdered Alice with a gun" 34 | - Goal: "Bob aimed the gun at Alice". 35 | 36 | You can follow the reasoning step-by-step, 37 | printed out as sentences in natural language or code. 38 | 39 | 40 | When using this story plot for writing, you might do something like this: 41 | 42 | ```text 43 | Bob picked up the gun and aimed it Alice. 44 | "I hate you!" he cried. 45 | "Wait, I can explain..." Alice raised her hands. 46 | A loud bang. 47 | Bob realized in the same moment what he did. 48 | Something he never would believe if anyone had told him as a child. 49 | He was now a murderer. 50 | ``` 51 | 52 | This particular program reasons backwards in time to take advantage of monotonic logic. 53 | It helps to avoid explosive combinatorics of possible worlds. 54 | 55 | Technically, this solver can also be used when multiple contradicting facts lead 56 | to the same goal. 57 | The alternative histories, that do not lead to a goal, are erased when 58 | the solver reduces the proof after finding a solution. 59 | 60 | ### Example 61 | 62 | Here is the full source code of a "examples/groceries.rs" that figures out which fruits 63 | a person will buy from the available food and taste preferences. 64 | 65 | ```rust 66 | extern crate monotonic_solver; 67 | 68 | use monotonic_solver::{search, Solver}; 69 | 70 | use Expr::*; 71 | use Fruit::*; 72 | use Taste::*; 73 | use Person::*; 74 | 75 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 76 | pub enum Person { 77 | Hannah, 78 | Peter, 79 | Clara, 80 | } 81 | 82 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 83 | pub enum Taste { 84 | Sweet, 85 | Sour, 86 | Bitter, 87 | NonSour, 88 | } 89 | 90 | impl Taste { 91 | fn likes(&self, fruit: Fruit) -> bool { 92 | *self == Sweet && fruit.is_sweet() || 93 | *self == Sour && fruit.is_sour() || 94 | *self == Bitter && fruit.is_bitter() || 95 | *self == NonSour && !fruit.is_sour() 96 | } 97 | } 98 | 99 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 100 | pub enum Fruit { 101 | Apple, 102 | Grape, 103 | Lemon, 104 | Orange, 105 | } 106 | 107 | impl Fruit { 108 | fn is_sweet(&self) -> bool { 109 | match *self {Orange | Apple => true, Grape | Lemon => false} 110 | } 111 | 112 | fn is_sour(&self) -> bool { 113 | match *self {Lemon | Orange => true, Apple | Grape => false} 114 | } 115 | 116 | fn is_bitter(&self) -> bool { 117 | match *self {Grape | Lemon => true, Apple | Orange => false} 118 | } 119 | } 120 | 121 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 122 | pub enum Expr { 123 | ForSale(Fruit), 124 | Preference(Person, Taste, Taste), 125 | Buy(Person, Fruit), 126 | } 127 | 128 | fn infer(solver: Solver, story: &[Expr]) -> Option { 129 | for expr in story { 130 | if let &Preference(x, taste1, taste2) = expr { 131 | for expr2 in story { 132 | if let &ForSale(y) = expr2 { 133 | // Both tastes must be satisfied for the fruit. 134 | if taste1.likes(y) && taste2.likes(y) { 135 | let new_expr = Buy(x, y); 136 | if solver.can_add(&new_expr) {return Some(new_expr)}; 137 | } 138 | } 139 | } 140 | } 141 | } 142 | None 143 | } 144 | 145 | fn main() { 146 | let start = vec![ 147 | ForSale(Orange), 148 | ForSale(Grape), 149 | ForSale(Apple), 150 | ForSale(Lemon), 151 | Preference(Hannah, Sour, Bitter), 152 | Preference(Peter, Sour, Sweet), 153 | Preference(Peter, NonSour, Bitter), 154 | Preference(Clara, NonSour, Sweet), 155 | ]; 156 | let order_constraints = vec![ 157 | // Peter likes grape better than orange. 158 | (Buy(Peter, Grape), Buy(Peter, Orange)), 159 | ]; 160 | 161 | // Look up what this person will buy. 162 | let person = Peter; 163 | 164 | let (res, _) = search( 165 | &start, 166 | |expr| if let &Buy(x, y) = expr {if x == person {Some(y)} else {None}} else {None}, 167 | Some(1000), // max proof size. 168 | &[], 169 | &order_constraints, 170 | infer, 171 | ); 172 | println!("{:?} will buy:", person); 173 | for r in res { 174 | println!("- {:?}", r); 175 | } 176 | } 177 | ``` 178 | 179 | When you run this program, it will output: 180 | 181 | ```text 182 | Peter will buy: 183 | - Grape 184 | - Orange 185 | ``` 186 | 187 | This is what Peter will buy. 188 | 189 | Notice the following kinds of constraints: 190 | 191 | - People prefer some fruits above others 192 | - A fruit can give multiple tasting experiences 193 | - All tasting experiences must be satisfied for people to buy the fruit 194 | - Not all kinds of fruits are available all the time 195 | - People's preferences are combinations of tasting experiences 196 | - People might change preferences over time 197 | 198 | When you start to code a new idea, you might only know vaguely 199 | what the solver should do. Experiment! 200 | 201 | ### Design 202 | 203 | A monotonic solver is an automatic theorem prover that finds proofs using 204 | forward-only search. The word "monotonic" means additional facts do not cancel 205 | the truth value of previously added facts. 206 | 207 | This theorem prover is designed to work on AST (Abstract Syntax Tree) 208 | described with Rust enums. 209 | The API is low level to allow precise control over performance, 210 | by taking advantage of `HashSet` cache for inferred facts and filtering. 211 | 212 | - `solve_and_reduce` is most commonly used, because it first finds a proof 213 | and then removes all facts that are inferred but irrelevant. 214 | - `solve` is used to show exhaustive search for facts, for e.g. debugging. 215 | 216 | The API is able to simplify the proof without knowing anything explicit 217 | about the rules, because it reasons counter-factually afterwards by modifying the filter. 218 | After finding a solution, it tests each fact one by one, by re-solving the problem, starting with the latest added facts and moving to the beginning, to solve the implicit dependencies. 219 | All steps in the new solution must exist in the old solution. 220 | Since this can happen many times, it is important to take advantage of the cache. 221 | 222 | Each fact can only be added once to the solution. 223 | It is therefore not necessary a good algorithm to use on long chains of events. 224 | A more applicable area is modeling of common sense for short activities. 225 | 226 | This is the recommended way of using this library: 227 | 228 | 1. Model common sense for a restricted domain of reasoning 229 | 2. Wrap the solver and constraints in an understandable programming interface 230 | 231 | The purpose is use a handful of facts to infer a few additional facts. 232 | In many applications, such additional facts can be critical, 233 | because they might seem so obvious to the user that they are not even mentioned. 234 | 235 | It can also be used to speed up productivity when serial thinking is required. 236 | Human brains are not that particularly good at this kind of reasoning, at least not compared to a computer. 237 | 238 | 239 | The challenge is to encode the rules required to make the computer an efficient reasoner. 240 | This is why this library focuses on ease-of-use in a way that is familiar to Rust programmers, so multiple designs can be tested and compared with short iteration cycles. 241 | 242 | ### Usage 243 | 244 | There are two modes supported by this library: Solving and searching. 245 | 246 | - In solving mode, you specify a goal and the solver tries to find a proof. 247 | - In searching mode, you specify a pattern and extract some data. 248 | 249 | The solver requires 5 things: 250 | 251 | 1. A list of start facts. 252 | 2. A list of goal facts. 253 | 3. A list of filtered facts. 254 | 4. A list of order-constraints. 255 | 5. A function pointer to the inference algorithm. 256 | 257 | Start facts are the initial conditions that trigger the search through rules. 258 | 259 | Goal facts decides when the search terminates. 260 | 261 | Filtered facts are blocked from being added to the solution. 262 | This can be used as feedback to the algorithm when a wrong assumption is made. 263 | 264 | Order-constraints are used when facts represents events. 265 | It is a list of tuples of the form `(A, B)` which controls the ordering of events. 266 | The event `B` is added to the internal filter temporarily until event `A` 267 | has happened. 268 | 269 | The search requires 6 things (similar to solver except no goal is required): 270 | 271 | 1. A list of start facts. 272 | 2. A matching pattern to extract data. 273 | 3. A maximum size of proof to avoid running out of memory. 274 | 4. A list of filtered facts. 275 | 5. A list of order-constraints. 276 | 6. A function pointer to the inference algorithm. 277 | 278 | It is common to set up the inference algorithm in this pattern: 279 | 280 | ```ignore 281 | fn infer(solver: Solver, story: &[Expr]) -> Option { 282 | let places = &[ 283 | University, CoffeeBar 284 | ]; 285 | 286 | for expr in story { 287 | if let &HadChild {father, mother, ..} = expr { 288 | let new_expr = Married {man: father, woman: mother}; 289 | if solver.can_add(&new_expr) {return Some(new_expr);} 290 | } 291 | 292 | if let &Married {man, woman} = expr { 293 | let new_expr = FellInLove {man, woman}; 294 | if solver.can_add(&new_expr) {return Some(new_expr);} 295 | } 296 | 297 | ... 298 | } 299 | None 300 | } 301 | ``` 302 | 303 | The `solver.can_add` call checks whether the fact is already inferred. 304 | It is also common to create lists of items to iterate over, 305 | and use it in combination with the cache to improve performance of lookups. 306 | -------------------------------------------------------------------------------- /examples/drama.rs: -------------------------------------------------------------------------------- 1 | #![feature(box_patterns)] 2 | 3 | extern crate monotonic_solver; 4 | 5 | use std::fmt; 6 | 7 | use monotonic_solver::{solve_and_reduce, Solver}; 8 | 9 | use Person::*; 10 | use Expr::*; 11 | use Place::*; 12 | use Weapon::*; 13 | 14 | /// People in the story. 15 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 16 | pub enum Person { 17 | Alice, 18 | Bob, 19 | Cecil, 20 | Dan, 21 | Erica, 22 | Filip, 23 | Gaia, 24 | Hans, 25 | Ida, 26 | Joel, 27 | Kitty, 28 | Lamar, 29 | Monica, 30 | Nils, 31 | } 32 | 33 | impl Person { 34 | pub fn male(&self) -> bool { 35 | match *self { 36 | Alice | Cecil | Erica | Gaia | Ida | Kitty | Monica => false, 37 | Bob | Dan | Filip | Hans | Joel | Lamar | Nils => true 38 | } 39 | } 40 | } 41 | 42 | impl fmt::Display for Person { 43 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 44 | write!(fmt, "{:?}", self) 45 | } 46 | } 47 | 48 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 49 | pub enum Place { 50 | University, 51 | CoffeeBar, 52 | } 53 | 54 | impl fmt::Display for Place { 55 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 56 | write!(fmt, "{}", match *self { 57 | University => "university", 58 | CoffeeBar => "coffee bar", 59 | }) 60 | } 61 | } 62 | 63 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 64 | pub enum Weapon { 65 | Knife, 66 | Gun, 67 | } 68 | 69 | impl Weapon { 70 | fn shoots(&self) -> bool { 71 | match *self { 72 | Knife => false, 73 | Gun => true, 74 | } 75 | } 76 | } 77 | 78 | impl fmt::Display for Weapon { 79 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 80 | write!(fmt, "{}", match *self { 81 | Knife => "knife", 82 | Gun => "gun", 83 | }) 84 | } 85 | } 86 | 87 | /// Event expressions in the story. 88 | #[derive(Clone, PartialEq, Eq, Hash, Debug)] 89 | pub enum Expr { 90 | HadChild {father: Person, mother: Person, child: Person}, 91 | Married {man: Person, woman: Person}, 92 | FellInLove {man: Person, woman: Person}, 93 | RomanticDinner {man: Person, woman: Person}, 94 | MetEachOther {man: Person, woman: Person, at: Place}, 95 | WasWorking {person: Person, place: Place}, 96 | GotJob {person: Person, place: Place}, 97 | AppliedForJob {person: Person, place: Place}, 98 | DiedOfCancer(Person), 99 | HadToStayInBedBecauseOfIllness(Person), 100 | GotCancer(Person), 101 | GotSick(Person), 102 | FeltIll(Person), 103 | DiedInCarAccident(Person), 104 | DroveCarToWork(Person), 105 | OwnedCar(Person), 106 | PurchasedCar(Person), 107 | SawAffordableCar(Person), 108 | WasLookingForCar(Person), 109 | NeededCar(Person), 110 | Murdered {murderer: Person, victim: Person, weapon: Weapon}, 111 | Shot {attacker: Person, target: Person, weapon: Weapon}, 112 | PulledTrigger {person: Person, weapon: Weapon}, 113 | Aimed {aimer: Person, target: Person, weapon: Weapon}, 114 | } 115 | 116 | impl fmt::Display for Expr { 117 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 118 | match *self { 119 | HadChild {father, mother, child} => 120 | write!(fmt, "{} and {} gave birth to {}", father, mother, child)?, 121 | Married {man, woman} => write!(fmt, "{} married {}", man, woman)?, 122 | FellInLove {man, woman} => write!(fmt, "{} and {} fell in love", man, woman)?, 123 | RomanticDinner {man, woman} => 124 | write!(fmt, "{} took {} out for a romantic dinner", man, woman)?, 125 | MetEachOther {man, woman, at} => 126 | write!(fmt, "{} and {} met each other at the {}", man, woman, at)?, 127 | WasWorking {person, place} => 128 | write!(fmt, "{} was working at the {}", person, place)?, 129 | GotJob {person, place} => 130 | write!(fmt, "{} got job at the {}", person, place)?, 131 | AppliedForJob {person, place} => 132 | write!(fmt, "{} applied for job at the {}", person, place)?, 133 | DiedOfCancer(person) => write!(fmt, "{} died of cancer", person)?, 134 | HadToStayInBedBecauseOfIllness(person) => 135 | write!(fmt, "{} has to stay in bed because of illness", person)?, 136 | GotCancer(person) => write!(fmt, "{} got cancer", person)?, 137 | GotSick(person) => write!(fmt, "{} got sick", person)?, 138 | FeltIll(person) => write!(fmt, "{} felt ill", person)?, 139 | DiedInCarAccident(person) => write!(fmt, "{} died in car accident", person)?, 140 | DroveCarToWork(person) => write!(fmt, "{} drove the car to work", person)?, 141 | OwnedCar(person) => write!(fmt, "{} owned a car", person)?, 142 | PurchasedCar(person) => write!(fmt, "{} purchased a car", person)?, 143 | SawAffordableCar(person) => { 144 | write!(fmt, "{} saw a car {} could affort", person, 145 | if person.male() {"he"} else {"she"})? 146 | } 147 | WasLookingForCar(person) => write!(fmt, "{} was looking for a car", person)?, 148 | NeededCar(person) => write!(fmt, "{} needed a car", person)?, 149 | Murdered {murderer, victim, weapon} => 150 | write!(fmt, "{} murdered {} with a {}", murderer, victim, weapon)?, 151 | Shot {attacker, target, weapon} => 152 | write!(fmt, "{} shot {} with a {}", attacker, target, weapon)?, 153 | Aimed {aimer, target, weapon} => 154 | write!(fmt, "{} aimed the {} at {}", aimer, weapon, target)?, 155 | PulledTrigger {person, weapon} => 156 | write!(fmt, "{} pulled the trigger of the {}", person, weapon)?, 157 | } 158 | Ok(()) 159 | } 160 | } 161 | 162 | fn infer(solver: Solver, story: &[Expr]) -> Option { 163 | /* 164 | let people = &[ 165 | Alice, Bob, Cecil, Dan, Erica, Filip, 166 | Gaia, Hans, Ida, Joel, Kitty, Lamar, 167 | Monica, Nils 168 | ]; 169 | */ 170 | let places = &[ 171 | University, CoffeeBar 172 | ]; 173 | 174 | for expr in story { 175 | if let &HadChild {father, mother, ..} = expr { 176 | let new_expr = Married {man: father, woman: mother}; 177 | if solver.can_add(&new_expr) {return Some(new_expr);} 178 | } 179 | 180 | if let &Married {man, woman} = expr { 181 | let new_expr = FellInLove {man, woman}; 182 | if solver.can_add(&new_expr) {return Some(new_expr);} 183 | } 184 | 185 | if let &FellInLove {man, woman} = expr { 186 | let new_expr = RomanticDinner {man, woman}; 187 | if solver.can_add(&new_expr) {return Some(new_expr);} 188 | 189 | for place in places { 190 | if solver.cache.contains(&WasWorking {person: man, place: *place}) || 191 | solver.cache.contains(&WasWorking {person: woman, place: *place}) { 192 | let new_expr = MetEachOther {man, woman, at: *place}; 193 | if solver.can_add(&new_expr) {return Some(new_expr);} 194 | } 195 | } 196 | } 197 | 198 | if let &WasWorking {person, place} = expr { 199 | let new_expr = GotJob {person, place}; 200 | if solver.can_add(&new_expr) {return Some(new_expr);} 201 | } 202 | 203 | if let &GotJob {person, place} = expr { 204 | let new_expr = AppliedForJob {person, place}; 205 | if solver.can_add(&new_expr) {return Some(new_expr);} 206 | } 207 | 208 | if let &DiedOfCancer(person) = expr { 209 | let new_expr = GotCancer(person); 210 | if solver.can_add(&new_expr) {return Some(new_expr);} 211 | 212 | let new_expr = HadToStayInBedBecauseOfIllness(person); 213 | if solver.can_add(&new_expr) {return Some(new_expr);} 214 | } 215 | 216 | if let &GotCancer(person) = expr { 217 | let new_expr = GotSick(person); 218 | if solver.can_add(&new_expr) {return Some(new_expr);} 219 | } 220 | 221 | if let &HadToStayInBedBecauseOfIllness(person) = expr { 222 | let new_expr = GotSick(person); 223 | if solver.can_add(&new_expr) {return Some(new_expr);} 224 | } 225 | 226 | if let &GotSick(person) = expr { 227 | let new_expr = FeltIll(person); 228 | if solver.can_add(&new_expr) {return Some(new_expr);} 229 | } 230 | 231 | if let &WasWorking {person, ..} = expr { 232 | if solver.cache.contains(&DiedInCarAccident(person)) { 233 | let new_expr = DroveCarToWork(person); 234 | if solver.can_add(&new_expr) {return Some(new_expr);} 235 | } 236 | } 237 | 238 | if let &DroveCarToWork(person) = expr { 239 | let new_expr = OwnedCar(person); 240 | if solver.can_add(&new_expr) {return Some(new_expr);} 241 | } 242 | 243 | if let &OwnedCar(person) = expr { 244 | let new_expr = PurchasedCar(person); 245 | if solver.can_add(&new_expr) {return Some(new_expr);} 246 | } 247 | 248 | if let &PurchasedCar(person) = expr { 249 | let new_expr = SawAffordableCar(person); 250 | if solver.can_add(&new_expr) {return Some(new_expr);} 251 | } 252 | 253 | if let &SawAffordableCar(person) = expr { 254 | let new_expr = WasLookingForCar(person); 255 | if solver.can_add(&new_expr) {return Some(new_expr);} 256 | } 257 | 258 | if let &WasLookingForCar(person) = expr { 259 | let new_expr = NeededCar(person); 260 | if solver.can_add(&new_expr) {return Some(new_expr);} 261 | } 262 | 263 | if let &Murdered {murderer, victim, weapon} = expr { 264 | if weapon.shoots() { 265 | let new_expr = Shot {attacker: murderer, target: victim, weapon}; 266 | if solver.can_add(&new_expr) {return Some(new_expr);} 267 | } 268 | } 269 | 270 | if let &Shot {attacker, target, weapon} = expr { 271 | let new_expr = PulledTrigger {person: attacker, weapon}; 272 | if solver.can_add(&new_expr) {return Some(new_expr);} 273 | 274 | if solver.cache.contains(&PulledTrigger {person: attacker, weapon}) { 275 | let new_expr = Aimed {aimer: attacker, target, weapon}; 276 | if solver.can_add(&new_expr) {return Some(new_expr);} 277 | } 278 | } 279 | } 280 | None 281 | } 282 | 283 | fn main() { 284 | // Events are reversed to take advantage of monotonic logic. 285 | let start = vec![ 286 | // DiedInCarAccident(Bob), 287 | // HadChild {father: Bob, mother: Alice, child: Dan}, 288 | // WasWorking {person: Bob, place: University}, 289 | Murdered {murderer: Bob, victim: Alice, weapon: Gun}, 290 | ]; 291 | let goal = vec![ 292 | // MetEachOther {man: Bob, woman: Alice, at: University}, 293 | // AppliedForJob {person: Bob, place: University}, 294 | // HadToStayInBedBecauseOfIllness(Bob), 295 | // WasLookingForCar(Bob), 296 | // Shot {attacker: Bob, target: Alice, weapon: Gun}, 297 | Aimed {aimer: Bob, target: Alice, weapon: Gun}, 298 | ]; 299 | let filter = vec![]; 300 | let order_constraints = vec![]; 301 | let (solution, status) = solve_and_reduce(&start, &goal, None, &filter, 302 | &order_constraints, infer); 303 | 304 | // for expr in solution.iter().rev() { 305 | for expr in solution { 306 | println!("{}", expr); 307 | } 308 | 309 | if status.is_err() { 310 | println!("Could not solve"); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /examples/groceries.rs: -------------------------------------------------------------------------------- 1 | extern crate monotonic_solver; 2 | 3 | use monotonic_solver::{search, Solver}; 4 | 5 | use Expr::*; 6 | use Fruit::*; 7 | use Taste::*; 8 | use Person::*; 9 | 10 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 11 | pub enum Person { 12 | Hannah, 13 | Peter, 14 | Clara, 15 | } 16 | 17 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 18 | pub enum Taste { 19 | Sweet, 20 | Sour, 21 | Bitter, 22 | NonSour, 23 | } 24 | 25 | impl Taste { 26 | fn likes(&self, fruit: Fruit) -> bool { 27 | *self == Sweet && fruit.is_sweet() || 28 | *self == Sour && fruit.is_sour() || 29 | *self == Bitter && fruit.is_bitter() || 30 | *self == NonSour && !fruit.is_sour() 31 | } 32 | } 33 | 34 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 35 | pub enum Fruit { 36 | Apple, 37 | Grape, 38 | Lemon, 39 | Orange, 40 | } 41 | 42 | impl Fruit { 43 | fn is_sweet(&self) -> bool { 44 | match *self {Orange | Apple => true, Grape | Lemon => false} 45 | } 46 | 47 | fn is_sour(&self) -> bool { 48 | match *self {Lemon | Orange => true, Apple | Grape => false} 49 | } 50 | 51 | fn is_bitter(&self) -> bool { 52 | match *self {Grape | Lemon => true, Apple | Orange => false} 53 | } 54 | } 55 | 56 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 57 | pub enum Expr { 58 | ForSale(Fruit), 59 | Preference(Person, Taste, Taste), 60 | Buy(Person, Fruit), 61 | } 62 | 63 | fn infer(solver: Solver, story: &[Expr]) -> Option { 64 | for expr in story { 65 | if let &Preference(x, taste1, taste2) = expr { 66 | for expr2 in story { 67 | if let &ForSale(y) = expr2 { 68 | // Both tastes must be satisfied for the fruit. 69 | if taste1.likes(y) && taste2.likes(y) { 70 | let new_expr = Buy(x, y); 71 | if solver.can_add(&new_expr) {return Some(new_expr)}; 72 | } 73 | } 74 | } 75 | } 76 | } 77 | None 78 | } 79 | 80 | fn main() { 81 | let start = vec![ 82 | ForSale(Orange), 83 | ForSale(Grape), 84 | ForSale(Apple), 85 | ForSale(Lemon), 86 | Preference(Hannah, Sour, Bitter), 87 | Preference(Peter, Sour, Sweet), 88 | Preference(Peter, NonSour, Bitter), 89 | Preference(Clara, NonSour, Sweet), 90 | ]; 91 | let order_constraints = vec![ 92 | // Peter likes grape better than orange. 93 | (Buy(Peter, Grape), Buy(Peter, Orange)), 94 | ]; 95 | 96 | // Look up what this person will buy. 97 | let person = Peter; 98 | 99 | let (res, _) = search( 100 | &start, 101 | |expr| if let &Buy(x, y) = expr {if x == person {Some(y)} else {None}} else {None}, 102 | Some(1000), // max proof size. 103 | &[], 104 | &order_constraints, 105 | infer, 106 | ); 107 | println!("{:?} will buy:", person); 108 | for r in res { 109 | println!("- {:?}", r); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /examples/hats.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This example shows how to use combine a monotonic solver 4 | with a mutable world. 5 | 6 | - 3 people can wear 3 hats 7 | - The same hat can not be worn by two people at the same time 8 | - Two people can swap hats 9 | - The hat worn by a person is tracked 10 | 11 | Some expressions are used as instructions to manipulate the world. 12 | If any instruction is invalid, no new inferences are made to stop the solver. 13 | New inferences are made from the world after execution. 14 | 15 | The solver can only reason about a single timeline. 16 | This is because it requires N worlds to reason about N timelines. 17 | 18 | The world is constructed from scratch and checked for each new inference. 19 | Not an ideal design, but for simple worlds this is fast enough. 20 | In a real world application, e.g. game programming, one would use a such 21 | solver to test the game logic, which does not need optimization. 22 | 23 | */ 24 | 25 | extern crate monotonic_solver; 26 | 27 | use monotonic_solver::{solve, solve_and_reduce, Solver}; 28 | 29 | use Expr::*; 30 | use Person::*; 31 | 32 | /// List of people. 33 | #[repr(u8)] 34 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 35 | pub enum Person { 36 | Alice = 0, 37 | Bob = 1, 38 | Carl = 2, 39 | } 40 | 41 | impl Person { 42 | /// Converts from a number. 43 | pub fn from(val: u8) -> Option { 44 | match val { 45 | 0 => Some(Alice), 46 | 1 => Some(Bob), 47 | 2 => Some(Carl), 48 | _ => None, 49 | } 50 | } 51 | } 52 | 53 | /// List of hats. 54 | #[repr(u8)] 55 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 56 | pub enum Hat { 57 | Blue, 58 | Green, 59 | Yellow, 60 | } 61 | 62 | impl Hat { 63 | pub fn from(val: u8) -> Option { 64 | match val { 65 | 0 => Some(Hat::Blue), 66 | 1 => Some(Hat::Green), 67 | 2 => Some(Hat::Yellow), 68 | _ => None, 69 | } 70 | } 71 | } 72 | 73 | pub struct World { 74 | pub wears: [Option; 3], 75 | pub worn: [[bool; 3]; 3], 76 | } 77 | 78 | impl World { 79 | pub fn new() -> World { 80 | World { 81 | wears: [None; 3], 82 | worn: [[false; 3]; 3], 83 | } 84 | } 85 | 86 | /// Returns whether a hat is weared by anyone. 87 | pub fn is_hat_free(&self, hat: Hat) -> bool { 88 | for i in 0..self.wears.len() { 89 | if self.wears[i] == Some(hat) {return false}; 90 | } 91 | return true; 92 | } 93 | 94 | pub fn update_worn(&mut self) { 95 | for i in 0..self.wears.len() { 96 | if let Some(hat) = self.wears[i] { 97 | self.worn[i][hat as usize] = true; 98 | } 99 | } 100 | } 101 | } 102 | 103 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 104 | pub enum Expr { 105 | TakesOn(Person, Hat), 106 | TakesOff(Person, Hat), 107 | TakesOffHat(Person), 108 | SwapHats(Person, Person), 109 | TakeAllOff, 110 | /// The story works out. 111 | Sound, 112 | Wears(Person, Hat), 113 | WearsNoHat(Person), 114 | HaveWorn(Person, Hat), 115 | } 116 | 117 | fn infer(solver: Solver, story: &[Expr]) -> Option { 118 | let ref mut world = World::new(); 119 | 120 | // Execute expressions on world. 121 | for expr in story { 122 | if let TakesOn(a, hat) = *expr { 123 | if !world.is_hat_free(hat) {return None}; 124 | 125 | if world.wears[a as usize].is_some() { 126 | return None; 127 | } else { 128 | world.wears[a as usize] = Some(hat); 129 | } 130 | } 131 | 132 | if let TakesOff(a, hat) = *expr { 133 | if world.wears[a as usize].is_none() { 134 | return None; 135 | } else if world.wears[a as usize].unwrap() != hat { 136 | return None; 137 | } else { 138 | world.wears[a as usize] = None; 139 | } 140 | } 141 | 142 | if let TakesOffHat(a) = *expr { 143 | world.wears[a as usize] = None; 144 | } 145 | 146 | if let SwapHats(a, b) = *expr { 147 | world.wears.swap(a as usize, b as usize); 148 | } 149 | 150 | if let TakeAllOff = *expr { 151 | for i in 0..world.wears.len() { 152 | world.wears[i] = None; 153 | } 154 | } 155 | 156 | world.update_worn(); 157 | } 158 | 159 | for i in 0..world.wears.len() { 160 | if let Some(person) = Person::from(i as u8) { 161 | if let Some(hat) = world.wears[i] { 162 | let new_expr = Wears(person, hat); 163 | if solver.can_add(&new_expr) {return Some(new_expr)}; 164 | } else { 165 | let new_expr = WearsNoHat(person); 166 | if solver.can_add(&new_expr) {return Some(new_expr)}; 167 | } 168 | } 169 | } 170 | 171 | for i in 0..world.worn.len() { 172 | for j in 0..world.worn[i].len() { 173 | if world.worn[i][j] { 174 | if let Some(person) = Person::from(i as u8) { 175 | if let Some(hat) = Hat::from(j as u8) { 176 | let new_expr = HaveWorn(person, hat); 177 | if solver.can_add(&new_expr) {return Some(new_expr)}; 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | Some(Sound) 185 | } 186 | 187 | pub fn trivial() -> (Vec, Vec) { 188 | ( 189 | vec![ 190 | TakesOn(Alice, Hat::Blue), 191 | ], 192 | vec![ 193 | Sound, 194 | ] 195 | ) 196 | } 197 | 198 | pub fn hats_colliding() -> (Vec, Vec) { 199 | ( 200 | vec![ 201 | TakesOn(Alice, Hat::Blue), 202 | TakesOn(Alice, Hat::Green), 203 | ], 204 | vec![ 205 | Sound, 206 | ] 207 | ) 208 | } 209 | 210 | pub fn take_on_off() -> (Vec, Vec) { 211 | ( 212 | vec![ 213 | TakesOn(Alice, Hat::Blue), 214 | TakesOff(Alice, Hat::Blue), 215 | ], 216 | vec![ 217 | Sound, 218 | ] 219 | ) 220 | } 221 | 222 | pub fn take_off_wrong() -> (Vec, Vec) { 223 | ( 224 | vec![ 225 | TakesOn(Alice, Hat::Blue), 226 | TakesOff(Alice, Hat::Green), 227 | ], 228 | vec![ 229 | Sound, 230 | ] 231 | ) 232 | } 233 | 234 | pub fn take_off_empty() -> (Vec, Vec) { 235 | ( 236 | vec![ 237 | TakesOff(Alice, Hat::Green), 238 | ], 239 | vec![ 240 | Sound, 241 | ] 242 | ) 243 | } 244 | 245 | pub fn bob_and_alice_wears_different_hats() -> (Vec, Vec) { 246 | ( 247 | vec![ 248 | TakesOn(Alice, Hat::Green), 249 | TakesOn(Bob, Hat::Blue), 250 | ], 251 | vec![ 252 | Sound, 253 | ] 254 | ) 255 | } 256 | 257 | pub fn alice_and_bob_tries_to_put_on_same_hat() -> (Vec, Vec) { 258 | ( 259 | vec![ 260 | TakesOn(Alice, Hat::Green), 261 | TakesOn(Bob, Hat::Green), 262 | ], 263 | vec![ 264 | Sound, 265 | ] 266 | ) 267 | } 268 | 269 | /// Checks a list of tests. 270 | pub fn check(fs: &[(fn() -> (Vec, Vec), bool)]) { 271 | for (i, &(f, ok)) in fs.iter().enumerate() { 272 | let (start, goal) = f(); 273 | let order_constraints = vec![]; 274 | 275 | // Use `solve` because it's faster than reduction. 276 | let (_, status) = solve( 277 | &start, 278 | &goal, 279 | None, 280 | &[], 281 | &order_constraints, 282 | infer, 283 | ); 284 | if status.is_ok() != ok { 285 | panic!("Failed check `{}`", i); 286 | } 287 | } 288 | } 289 | 290 | pub fn test() -> (Vec, Vec) { 291 | ( 292 | vec![ 293 | TakesOn(Alice, Hat::Green), 294 | TakesOn(Bob, Hat::Blue), 295 | TakesOn(Carl, Hat::Yellow), 296 | SwapHats(Alice, Carl), 297 | TakesOffHat(Alice), 298 | ], 299 | vec![ 300 | WearsNoHat(Alice), 301 | Wears(Bob, Hat::Blue), 302 | Wears(Carl, Hat::Green), 303 | HaveWorn(Alice, Hat::Green), 304 | HaveWorn(Alice, Hat::Yellow), 305 | Sound, 306 | ] 307 | ) 308 | } 309 | 310 | fn main() { 311 | check(&[ 312 | (trivial, true), 313 | (hats_colliding, false), 314 | (take_on_off, true), 315 | (take_off_wrong, false), 316 | (take_off_empty, false), 317 | (bob_and_alice_wears_different_hats, true), 318 | (alice_and_bob_tries_to_put_on_same_hat, false), 319 | ]); 320 | 321 | let (start, goal) = test(); 322 | let order_constraints = vec![ 323 | ]; 324 | 325 | let (res, status) = solve_and_reduce( 326 | &start, 327 | &goal, 328 | None, 329 | &[], 330 | &order_constraints, 331 | infer, 332 | ); 333 | if status.is_ok() { 334 | println!("OK"); 335 | } else { 336 | println!("ERROR"); 337 | } 338 | for r in res { 339 | println!("{:?}", r); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | //! # Monotonic-Solver 4 | //! A monotonic solver designed to be easy to use with Rust enum expressions 5 | //! 6 | //! This can be used to: 7 | //! 8 | //! - Research modeling of common sense for artificial intelligence 9 | //! - Test inference rules when studying logic and languages 10 | //! - Generate story plots 11 | //! - Search and extract data 12 | //! 13 | //! Used in [Avalog](https://github.com/advancedresearch/avalog), 14 | //! an experimental implementation of Avatar Logic with a Prolog-like syntax. 15 | //! 16 | //! Blog posts: 17 | //! 18 | //! - [2017-07-25 New Library for Automated Monotonic Theorem Proving](https://github.com/advancedresearch/advancedresearch.github.io/blob/master/blog/2017-07-25-new-library-for-automated-monotonic-theorem-proving.md) 19 | //! 20 | //! The advantage of this library design is the ease-of-use for prototyping. 21 | //! In a few hours, one can test a new idea for modeling common sense. 22 | //! 23 | //! Here is an example of program output (from "examples/drama.rs"): 24 | //! 25 | //! ```text 26 | //! Bob murdered Alice with a gun 27 | //! Bob shot Alice with a gun 28 | //! Bob pulled the trigger of the gun 29 | //! Bob aimed the gun at Alice 30 | //! ``` 31 | //! 32 | //! This is a program that generates drama story plots. 33 | //! The solver starts with the ending and work backwards to the beginning. 34 | //! 35 | //! - Start: "Bob murdered Alice with a gun" 36 | //! - Goal: "Bob aimed the gun at Alice". 37 | //! 38 | //! You can follow the reasoning step-by-step, 39 | //! printed out as sentences in natural language or code. 40 | //! 41 | //! 42 | //! When using this story plot for writing, you might do something like this: 43 | //! 44 | //! ```text 45 | //! Bob picked up the gun and aimed it Alice. 46 | //! "I hate you!" he cried. 47 | //! "Wait, I can explain..." Alice raised her hands. 48 | //! A loud bang. 49 | //! Bob realized in the same moment what he did. 50 | //! Something he never would believe if anyone had told him as a child. 51 | //! He was now a murderer. 52 | //! ``` 53 | //! 54 | //! This particular program reasons backwards in time to take advantage of monotonic logic. 55 | //! It helps to avoid explosive combinatorics of possible worlds. 56 | //! 57 | //! Technically, this solver can also be used when multiple contradicting facts lead 58 | //! to the same goal. 59 | //! The alternative histories, that do not lead to a goal, are erased when 60 | //! the solver reduces the proof after finding a solution. 61 | //! 62 | //! ### Example 63 | //! 64 | //! Here is the full source code of a "examples/groceries.rs" that figures out which fruits 65 | //! a person will buy from the available food and taste preferences. 66 | //! 67 | //! ```rust 68 | //! extern crate monotonic_solver; 69 | //! 70 | //! use monotonic_solver::{search, Solver}; 71 | //! 72 | //! use Expr::*; 73 | //! use Fruit::*; 74 | //! use Taste::*; 75 | //! use Person::*; 76 | //! 77 | //! #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 78 | //! pub enum Person { 79 | //! Hannah, 80 | //! Peter, 81 | //! Clara, 82 | //! } 83 | //! 84 | //! #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 85 | //! pub enum Taste { 86 | //! Sweet, 87 | //! Sour, 88 | //! Bitter, 89 | //! NonSour, 90 | //! } 91 | //! 92 | //! impl Taste { 93 | //! fn likes(&self, fruit: Fruit) -> bool { 94 | //! *self == Sweet && fruit.is_sweet() || 95 | //! *self == Sour && fruit.is_sour() || 96 | //! *self == Bitter && fruit.is_bitter() || 97 | //! *self == NonSour && !fruit.is_sour() 98 | //! } 99 | //! } 100 | //! 101 | //! #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 102 | //! pub enum Fruit { 103 | //! Apple, 104 | //! Grape, 105 | //! Lemon, 106 | //! Orange, 107 | //! } 108 | //! 109 | //! impl Fruit { 110 | //! fn is_sweet(&self) -> bool { 111 | //! match *self {Orange | Apple => true, Grape | Lemon => false} 112 | //! } 113 | //! 114 | //! fn is_sour(&self) -> bool { 115 | //! match *self {Lemon | Orange => true, Apple | Grape => false} 116 | //! } 117 | //! 118 | //! fn is_bitter(&self) -> bool { 119 | //! match *self {Grape | Lemon => true, Apple | Orange => false} 120 | //! } 121 | //! } 122 | //! 123 | //! #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 124 | //! pub enum Expr { 125 | //! ForSale(Fruit), 126 | //! Preference(Person, Taste, Taste), 127 | //! Buy(Person, Fruit), 128 | //! } 129 | //! 130 | //! fn infer(solver: Solver, story: &[Expr]) -> Option { 131 | //! for expr in story { 132 | //! if let &Preference(x, taste1, taste2) = expr { 133 | //! for expr2 in story { 134 | //! if let &ForSale(y) = expr2 { 135 | //! // Both tastes must be satisfied for the fruit. 136 | //! if taste1.likes(y) && taste2.likes(y) { 137 | //! let new_expr = Buy(x, y); 138 | //! if solver.can_add(&new_expr) {return Some(new_expr)}; 139 | //! } 140 | //! } 141 | //! } 142 | //! } 143 | //! } 144 | //! None 145 | //! } 146 | //! 147 | //! fn main() { 148 | //! let start = vec![ 149 | //! ForSale(Orange), 150 | //! ForSale(Grape), 151 | //! ForSale(Apple), 152 | //! ForSale(Lemon), 153 | //! Preference(Hannah, Sour, Bitter), 154 | //! Preference(Peter, Sour, Sweet), 155 | //! Preference(Peter, NonSour, Bitter), 156 | //! Preference(Clara, NonSour, Sweet), 157 | //! ]; 158 | //! let order_constraints = vec![ 159 | //! // Peter likes grape better than orange. 160 | //! (Buy(Peter, Grape), Buy(Peter, Orange)), 161 | //! ]; 162 | //! 163 | //! // Look up what this person will buy. 164 | //! let person = Peter; 165 | //! 166 | //! let (res, _) = search( 167 | //! &start, 168 | //! |expr| if let &Buy(x, y) = expr {if x == person {Some(y)} else {None}} else {None}, 169 | //! Some(1000), // max proof size. 170 | //! &[], 171 | //! &order_constraints, 172 | //! infer, 173 | //! ); 174 | //! println!("{:?} will buy:", person); 175 | //! for r in res { 176 | //! println!("- {:?}", r); 177 | //! } 178 | //! } 179 | //! ``` 180 | //! 181 | //! When you run this program, it will output: 182 | //! 183 | //! ```text 184 | //! Peter will buy: 185 | //! - Grape 186 | //! - Orange 187 | //! ``` 188 | //! 189 | //! This is what Peter will buy. 190 | //! 191 | //! Notice the following kinds of constraints: 192 | //! 193 | //! - People prefer some fruits above others 194 | //! - A fruit can give multiple tasting experiences 195 | //! - All tasting experiences must be satisfied for people to buy the fruit 196 | //! - Not all kinds of fruits are available all the time 197 | //! - People's preferences are combinations of tasting experiences 198 | //! - People might change preferences over time 199 | //! 200 | //! When you start to code a new idea, you might only know vaguely 201 | //! what the solver should do. Experiment! 202 | //! 203 | //! ### Design 204 | //! 205 | //! A monotonic solver is an automatic theorem prover that finds proofs using 206 | //! forward-only search. The word "monotonic" means additional facts do not cancel 207 | //! the truth value of previously added facts. 208 | //! 209 | //! This theorem prover is designed to work on AST (Abstract Syntax Tree) 210 | //! described with Rust enums. 211 | //! The API is low level to allow precise control over performance, 212 | //! by taking advantage of `HashSet` cache for inferred facts and filtering. 213 | //! 214 | //! - `solve_and_reduce` is most commonly used, because it first finds a proof 215 | //! and then removes all facts that are inferred but irrelevant. 216 | //! - `solve` is used to show exhaustive search for facts, for e.g. debugging. 217 | //! 218 | //! The API is able to simplify the proof without knowing anything explicit 219 | //! about the rules, because it reasons counter-factually afterwards by modifying the filter. 220 | //! After finding a solution, it tests each fact one by one, by re-solving the problem, starting with the latest added facts and moving to the beginning, to solve the implicit dependencies. 221 | //! All steps in the new solution must exist in the old solution. 222 | //! Since this can happen many times, it is important to take advantage of the cache. 223 | //! 224 | //! Each fact can only be added once to the solution. 225 | //! It is therefore not necessary a good algorithm to use on long chains of events. 226 | //! A more applicable area is modeling of common sense for short activities. 227 | //! 228 | //! This is the recommended way of using this library: 229 | //! 230 | //! 1. Model common sense for a restricted domain of reasoning 231 | //! 2. Wrap the solver and constraints in an understandable programming interface 232 | //! 233 | //! The purpose is use a handful of facts to infer a few additional facts. 234 | //! In many applications, such additional facts can be critical, 235 | //! because they might seem so obvious to the user that they are not even mentioned. 236 | //! 237 | //! It can also be used to speed up productivity when serial thinking is required. 238 | //! Human brains are not that particularly good at this kind of reasoning, at least not compared to a computer. 239 | //! 240 | //! 241 | //! The challenge is to encode the rules required to make the computer an efficient reasoner. 242 | //! This is why this library focuses on ease-of-use in a way that is familiar to Rust programmers, so multiple designs can be tested and compared with short iteration cycles. 243 | //! 244 | //! ### Usage 245 | //! 246 | //! There are two modes supported by this library: Solving and searching. 247 | //! 248 | //! - In solving mode, you specify a goal and the solver tries to find a proof. 249 | //! - In searching mode, you specify a pattern and extract some data. 250 | //! 251 | //! The solver requires 5 things: 252 | //! 253 | //! 1. A list of start facts. 254 | //! 2. A list of goal facts. 255 | //! 3. A list of filtered facts. 256 | //! 4. A list of order-constraints. 257 | //! 5. A function pointer to the inference algorithm. 258 | //! 259 | //! Start facts are the initial conditions that trigger the search through rules. 260 | //! 261 | //! Goal facts decides when the search terminates. 262 | //! 263 | //! Filtered facts are blocked from being added to the solution. 264 | //! This can be used as feedback to the algorithm when a wrong assumption is made. 265 | //! 266 | //! Order-constraints are used when facts represents events. 267 | //! It is a list of tuples of the form `(A, B)` which controls the ordering of events. 268 | //! The event `B` is added to the internal filter temporarily until event `A` 269 | //! has happened. 270 | //! 271 | //! The search requires 6 things (similar to solver except no goal is required): 272 | //! 273 | //! 1. A list of start facts. 274 | //! 2. A matching pattern to extract data. 275 | //! 3. A maximum size of proof to avoid running out of memory. 276 | //! 4. A list of filtered facts. 277 | //! 5. A list of order-constraints. 278 | //! 6. A function pointer to the inference algorithm. 279 | //! 280 | //! It is common to set up the inference algorithm in this pattern: 281 | //! 282 | //! ```ignore 283 | //! fn infer(solver: Solver, story: &[Expr]) -> Option { 284 | //! let places = &[ 285 | //! University, CoffeeBar 286 | //! ]; 287 | //! 288 | //! for expr in story { 289 | //! if let &HadChild {father, mother, ..} = expr { 290 | //! let new_expr = Married {man: father, woman: mother}; 291 | //! if solver.can_add(&new_expr) {return Some(new_expr);} 292 | //! } 293 | //! 294 | //! if let &Married {man, woman} = expr { 295 | //! let new_expr = FellInLove {man, woman}; 296 | //! if solver.can_add(&new_expr) {return Some(new_expr);} 297 | //! } 298 | //! 299 | //! ... 300 | //! } 301 | //! None 302 | //! } 303 | //! ``` 304 | //! 305 | //! The `solver.can_add` call checks whether the fact is already inferred. 306 | //! It is also common to create lists of items to iterate over, 307 | //! and use it in combination with the cache to improve performance of lookups. 308 | 309 | use std::hash::Hash; 310 | use std::collections::HashSet; 311 | 312 | /// Stores solver error. 313 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 314 | pub enum Error { 315 | /// Failed to reach the goal. 316 | Failure, 317 | /// Reached maximum proof size limit. 318 | MaxSize, 319 | } 320 | 321 | /// Solver argument to inference function. 322 | pub struct Solver<'a, T, A = ()> { 323 | /// A hash set used check whether a fact has been inferred. 324 | pub cache: &'a HashSet, 325 | /// A filter cache to filter out facts deliberately. 326 | /// 327 | /// This is used to e.g. reduce proofs automatically. 328 | pub filter_cache: &'a HashSet, 329 | /// Stores acceleration data structures, reused monotonically. 330 | pub accelerator: &'a mut A, 331 | } 332 | 333 | impl<'a, T, A> Solver<'a, T, A> where T: Hash + Eq { 334 | /// Returns `true` if new expression can be added to facts. 335 | pub fn can_add(&self, new_expr: &T) -> bool { 336 | !self.cache.contains(new_expr) && 337 | !self.filter_cache.contains(new_expr) 338 | } 339 | } 340 | 341 | /// Solves without reducing. 342 | pub fn solve_with_accelerator( 343 | start: &[T], 344 | goal: &[T], 345 | max_size: Option, 346 | filter: &[T], 347 | order_constraints: &[(T, T)], 348 | infer: fn(Solver, story: &[T]) -> Option, 349 | accelerator: &mut A, 350 | ) -> (Vec, Result<(), Error>) { 351 | let mut cache = HashSet::new(); 352 | for s in start { 353 | cache.insert(s.clone()); 354 | } 355 | let mut filter_cache: HashSet = HashSet::new(); 356 | for f in filter { 357 | filter_cache.insert(f.clone()); 358 | } 359 | let mut res: Vec = start.into(); 360 | loop { 361 | if goal.iter().all(|e| res.iter().any(|f| e == f)) { 362 | break; 363 | } 364 | if let Some(n) = max_size { 365 | if res.len() >= n {return (res, Err(Error::MaxSize))}; 366 | } 367 | 368 | // Modify filter to prevent violation of order-constraints. 369 | let mut added_to_filter = vec![]; 370 | for (i, &(ref a, ref b)) in order_constraints.iter().enumerate() { 371 | if !cache.contains(a) && !filter_cache.contains(b) { 372 | added_to_filter.push(i); 373 | } 374 | } 375 | for &i in &added_to_filter { 376 | filter_cache.insert(order_constraints[i].1.clone()); 377 | } 378 | 379 | let expr = if let Some(expr) = infer(Solver { 380 | cache: &cache, 381 | filter_cache: &filter_cache, 382 | accelerator, 383 | }, &res) { 384 | expr 385 | } else { 386 | return (res, Err(Error::Failure)); 387 | }; 388 | res.push(expr.clone()); 389 | cache.insert(expr); 390 | 391 | // Revert filter. 392 | for &i in &added_to_filter { 393 | filter_cache.remove(&order_constraints[i].1); 394 | } 395 | } 396 | (res, Ok(())) 397 | } 398 | 399 | /// Solves without reducing. 400 | pub fn solve( 401 | start: &[T], 402 | goal: &[T], 403 | max_size: Option, 404 | filter: &[T], 405 | order_constraints: &[(T, T)], 406 | infer: fn(Solver, story: &[T]) -> Option 407 | ) -> (Vec, Result<(), Error>) { 408 | solve_with_accelerator(start, goal, max_size, filter, order_constraints, infer, &mut ()) 409 | } 410 | 411 | 412 | /// Solves and reduces the proof to those steps that are necessary. 413 | /// 414 | /// Uses an accelerator constructor initalized from start and goal. 415 | pub fn solve_and_reduce_with_accelerator( 416 | start: &[T], 417 | goal: &[T], 418 | mut max_size: Option, 419 | filter: &[T], 420 | order_constraints: &[(T, T)], 421 | infer: fn(Solver, story: &[T]) -> Option, 422 | accelerator: fn(&[T], &[T]) -> A, 423 | ) -> (Vec, Result<(), Error>) { 424 | let (mut res, status) = solve_with_accelerator(start, goal, max_size, filter, 425 | order_constraints, infer, &mut accelerator(start, goal)); 426 | if status.is_err() {return (res, status)}; 427 | 428 | // Check that every step is necessary. 429 | max_size = Some(res.len()); 430 | let mut new_filter: Vec = filter.into(); 431 | loop { 432 | let old_len = res.len(); 433 | for i in (0..res.len()).rev() { 434 | if goal.iter().any(|e| e == &res[i]) {continue;} 435 | 436 | new_filter.push(res[i].clone()); 437 | 438 | if let (solution, Ok(())) = solve_with_accelerator(start, goal, max_size, 439 | &new_filter, order_constraints, infer, &mut accelerator(start, goal)) { 440 | if solution.len() < res.len() && 441 | solution.iter().all(|e| res.iter().any(|f| e == f)) 442 | { 443 | max_size = Some(solution.len()); 444 | res = solution; 445 | break; 446 | } 447 | } 448 | 449 | new_filter.pop(); 450 | } 451 | 452 | if res.len() == old_len {break;} 453 | } 454 | 455 | (res, Ok(())) 456 | } 457 | 458 | /// Solves and reduces the proof to those steps that are necessary. 459 | pub fn solve_and_reduce( 460 | start: &[T], 461 | goal: &[T], 462 | max_size: Option, 463 | filter: &[T], 464 | order_constraints: &[(T, T)], 465 | infer: fn(Solver, story: &[T]) -> Option 466 | ) -> (Vec, Result<(), Error>) { 467 | solve_and_reduce_with_accelerator(start, goal, max_size, filter, 468 | order_constraints, infer, |_, _| ()) 469 | } 470 | 471 | /// Searches for matches by a pattern. 472 | /// 473 | /// - `pat` specifies the map and acceptance criteria 474 | /// - `max_size` specifies the maximum size of proof 475 | /// 476 | /// Returns `Ok` if all rules where exausted. 477 | /// Returns `Err` if the maximum size of proof was exceeded. 478 | pub fn search_with_accelerator( 479 | start: &[T], 480 | pat: F, 481 | max_size: Option, 482 | filter: &[T], 483 | order_constraints: &[(T, T)], 484 | infer: fn(Solver, story: &[T]) -> Option, 485 | accelerator: &mut A, 486 | ) -> (Vec, Result<(), Error>) 487 | where T: Clone + PartialEq + Eq + Hash, 488 | F: Fn(&T) -> Option 489 | { 490 | let mut cache = HashSet::new(); 491 | for s in start { 492 | cache.insert(s.clone()); 493 | } 494 | let mut filter_cache: HashSet = HashSet::new(); 495 | for f in filter { 496 | filter_cache.insert(f.clone()); 497 | } 498 | let mut res: Vec = start.into(); 499 | let mut matches: Vec = vec![]; 500 | 501 | for expr in start { 502 | if let Some(val) = (pat)(expr) { 503 | matches.push(val); 504 | } 505 | } 506 | 507 | loop { 508 | if let Some(n) = max_size { 509 | if res.len() >= n {break}; 510 | } 511 | 512 | // Modify filter to prevent violating of order-constraints. 513 | let mut added_to_filter = vec![]; 514 | for (i, &(ref a, ref b)) in order_constraints.iter().enumerate() { 515 | if !cache.contains(a) && !filter_cache.contains(b) { 516 | added_to_filter.push(i); 517 | } 518 | } 519 | for &i in &added_to_filter { 520 | filter_cache.insert(order_constraints[i].1.clone()); 521 | } 522 | 523 | let expr = if let Some(expr) = infer(Solver { 524 | cache: &cache, 525 | filter_cache: &filter_cache, 526 | accelerator, 527 | }, &res) { 528 | expr 529 | } else { 530 | return (matches, Ok(())); 531 | }; 532 | res.push(expr.clone()); 533 | 534 | if let Some(val) = (pat)(&expr) { 535 | matches.push(val); 536 | } 537 | 538 | cache.insert(expr); 539 | 540 | // Revert filter. 541 | for &i in &added_to_filter { 542 | filter_cache.remove(&order_constraints[i].1); 543 | } 544 | 545 | } 546 | (matches, Err(Error::MaxSize)) 547 | } 548 | 549 | /// Searches for matches by a pattern. 550 | /// 551 | /// - `pat` specifies the map and acceptance criteria 552 | /// - `max_size` specifies the maximum size of proof 553 | /// 554 | /// Returns `Ok` if all rules where exausted. 555 | /// Returns `Err` if the maximum size of proof was exceeded. 556 | pub fn search( 557 | start: &[T], 558 | pat: F, 559 | max_size: Option, 560 | filter: &[T], 561 | order_constraints: &[(T, T)], 562 | infer: fn(Solver, story: &[T]) -> Option 563 | ) -> (Vec, Result<(), Error>) 564 | where T: Clone + PartialEq + Eq + Hash, 565 | F: Fn(&T) -> Option 566 | { 567 | search_with_accelerator(start, pat, max_size, filter, order_constraints, infer, &mut ()) 568 | } 569 | --------------------------------------------------------------------------------