├── Throne.png ├── .gitignore ├── static └── index.html ├── benches ├── wood.throne ├── spaceopera.throne ├── benchmark.rs └── increment.throne ├── js └── index.js ├── package.json ├── webpack.config.js ├── src ├── core.rs ├── throne.pest ├── ffi.rs ├── lib.rs ├── string_cache.rs ├── rule.rs ├── update.rs ├── context.rs ├── wasm.rs ├── state.rs ├── token.rs ├── parser.rs └── matching.rs ├── LICENSE ├── Cargo.toml ├── examples ├── blocks.rs └── blocks.throne ├── todo.org └── README.md /Throne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-mw/throne/HEAD/Throne.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | 5 | .DS_Store 6 | 7 | package-lock.json 8 | yarn.lock 9 | dist/ 10 | pkg/ 11 | node_modules/ -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | throne 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /benches/wood.throne: -------------------------------------------------------------------------------- 1 | at 1 1 wood . at 1 1 wood . at 1 1 wood . at 1 1 wood . at 1 1 wood 2 | 3 | #update: { 4 | at X Y wood . + X 1 X' . + Y 1 Y' . + X'' 1 X . + Y'' 1 Y = at X' Y' fire . at X' Y'' fire . at X'' Y' fire . at X'' Y'' fire 5 | at X Y fire . + X 1 X' = at' X' Y fire 6 | at X Y fire . + Y 1 Y' = at' X Y' fire 7 | () = #process 8 | } 9 | 10 | #process: { 11 | at' X Y I = at X Y I 12 | () = () 13 | } -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | import("../pkg/index.js") 2 | .then(module => { 3 | module.init(); 4 | var text = ` 5 | at 0 0 wood . at 0 1 wood . at 1 1 wood . at 0 1 fire . #update 6 | #update: { 7 | at X Y wood . at X Y fire = at X Y fire 8 | () = #spread 9 | } 10 | #spread . $at X Y fire . + X 1 X' . + Y' 1 Y = at X' Y fire . at X Y' fire 11 | `; 12 | var context = module.Context.from_text(text); 13 | context.update(); 14 | context.print(); 15 | 16 | console.log(context.get_state()); 17 | }) 18 | .catch(console.error); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Tobias Mansfield-Williams ", 3 | "name": "throne", 4 | "version": "0.3.0", 5 | "scripts": { 6 | "build": "rimraf dist pkg && webpack", 7 | "start": "rimraf dist pkg && webpack-dev-server --open -d", 8 | "test": "cargo test && wasm-pack test --headless" 9 | }, 10 | "devDependencies": { 11 | "@wasm-tool/wasm-pack-plugin": "^1.1.0", 12 | "copy-webpack-plugin": "^5.0.3", 13 | "webpack": "^4.42.0", 14 | "webpack-cli": "^3.3.3", 15 | "webpack-dev-server": "^3.7.1", 16 | "rimraf": "^3.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); 4 | 5 | const dist = path.resolve(__dirname, "dist"); 6 | 7 | module.exports = { 8 | mode: "production", 9 | entry: { 10 | index: "./js/index.js" 11 | }, 12 | output: { 13 | path: dist, 14 | filename: "[name].js" 15 | }, 16 | devServer: { 17 | contentBase: dist, 18 | }, 19 | plugins: [ 20 | new CopyPlugin([ 21 | path.resolve(__dirname, "static") 22 | ]), 23 | 24 | new WasmPackPlugin({ 25 | crateDirectory: __dirname, 26 | }), 27 | ] 28 | }; 29 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | use crate::matching::*; 2 | use crate::rule::Rule; 3 | use crate::state::State; 4 | use crate::string_cache::Atom; 5 | use crate::update::SideInput; 6 | 7 | use rand::{self, rngs::SmallRng}; 8 | 9 | use std::vec::Vec; 10 | 11 | /// Stores the [State] and [Rules](Rule) for a [Context](crate::context::Context). 12 | #[derive(Clone)] 13 | pub struct Core { 14 | pub state: State, 15 | pub rules: Vec, 16 | pub executed_rule_ids: Vec, 17 | pub(crate) rule_repeat_count: usize, 18 | pub(crate) rng: SmallRng, 19 | pub(crate) qui_atom: Atom, 20 | } 21 | 22 | impl Core { 23 | pub fn rule_matches_state( 24 | &self, 25 | rule: &Rule, 26 | mut side_input: F, 27 | ) -> Result 28 | where 29 | F: SideInput, 30 | { 31 | Ok(rule_matches_state(rule, &mut self.state.clone(), &mut side_input)?.is_some()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tobias Mansfield-Williams 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 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "throne" 3 | version = "0.4.1" 4 | description = "Scripting language for game prototyping and story logic" 5 | authors = ["Tobias Mansfield-Williams "] 6 | repository = "https://github.com/t-mw/throne" 7 | readme = "README.md" 8 | keywords = ["script", "scripting", "game", "language"] 9 | categories = ["game-development"] 10 | license = "MIT" 11 | edition = "2018" 12 | exclude = ["todo.org"] 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | 17 | [dependencies] 18 | itertools = "0.10" 19 | lazy_static = "1.4" 20 | pest = "2.1" 21 | pest_derive = "2.1" 22 | rand = { version = "0.8", features = ["small_rng"] } 23 | regex = "1.4" 24 | 25 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 26 | criterion = "0.3" 27 | minifb = "0.19" 28 | pretty_assertions = "0.7" 29 | 30 | [target.wasm32-unknown-unknown.dependencies] 31 | console_error_panic_hook = "0.1" 32 | getrandom = { version = "0.2", features = ["js"] } 33 | js-sys = "0.3" 34 | wasm-bindgen = "0.2" 35 | 36 | [target.wasm32-unknown-unknown.dev-dependencies] 37 | wasm-bindgen-test = "0.3" 38 | 39 | [[bench]] 40 | name = "benchmark" 41 | harness = false 42 | -------------------------------------------------------------------------------- /src/throne.pest: -------------------------------------------------------------------------------- 1 | WHITESPACE = _{ " " } 2 | COMMENT = _{ ("//" ~ (!NEWLINE ~ ANY)*) | ("/*" ~ (!"*/" ~ ANY)* ~ "*/") } 3 | atom = @{ (ASCII_ALPHANUMERIC | "_" | "-")+ ~ "'"* } 4 | atom_var = @{ ASCII_ALPHA_UPPER ~ (ASCII_ALPHA_UPPER | ASCII_DIGIT | "_" | "-")* ~ "'"* } 5 | string = { "\"" ~ (!"\"" ~ ANY)* ~ "\"" } 6 | wildcard = @{ "_" ~ atom? } 7 | qui = { "qui" } 8 | 9 | // list <= and >= first, to give precendence over < and > 10 | prefix = { !(wildcard | string | atom_var | atom) ~ ("+" | "-" | "%" | "<=" | ">=" | "<" | ">" | "#" | "==") } 11 | 12 | phrase_compound = { prefix? ~ ((wildcard | string | atom_var | atom)+ | "(" ~ phrase_compound+ ~ ")") } 13 | phrase = !{ phrase_compound+ } 14 | 15 | stage_phrase = ${ "#" ~ phrase } 16 | copy_phrase = ${ "$" ~ phrase } 17 | side_phrase = ${ "^" ~ phrase } 18 | negate_phrase = ${ "!" ~ phrase } 19 | nonconsuming_phrase = ${ "?" ~ phrase } 20 | backwards_phrase = ${ "<<" ~ phrase } 21 | 22 | compiler_block = _{ "[" ~ (compiler_enable_unused_warnings | compiler_disable_unused_warnings) ~ "]" } 23 | compiler_enable_unused_warnings = { "enable-unused-warnings" } 24 | compiler_disable_unused_warnings = { "disable-unused-warnings" } 25 | 26 | state_phrase = { stage_phrase | phrase } 27 | state = { state_phrase ~ (NEWLINE? ~ "." ~ NEWLINE? ~ state_phrase)* } 28 | 29 | input_phrase = { qui | stage_phrase | copy_phrase | side_phrase | negate_phrase | backwards_phrase | nonconsuming_phrase | phrase } 30 | inputs = { input_phrase ~ (NEWLINE? ~ "." ~ NEWLINE? ~ input_phrase)* } 31 | output_phrase = { qui | stage_phrase | side_phrase | phrase } 32 | outputs = { output_phrase ~ (NEWLINE? ~ "." ~ NEWLINE? ~ output_phrase)* } 33 | rule = { inputs ~ NEWLINE? ~ "=" ~ NEWLINE? ~ outputs } 34 | 35 | backwards_def_phrase = { stage_phrase | side_phrase | negate_phrase | nonconsuming_phrase | phrase } 36 | backwards_def = { backwards_phrase ~ (NEWLINE? ~ "." ~ NEWLINE? ~ backwards_def_phrase)* } 37 | 38 | prefix_block = _{ "{" ~ NEWLINE* ~ rule ~ (NEWLINE+ ~ rule)* ~ NEWLINE* ~ "}" } 39 | prefixed = { inputs ~ ":" ~ prefix_block } 40 | 41 | file = { 42 | SOI ~ 43 | (NEWLINE* ~ (prefixed | rule | backwards_def | state | compiler_block))* ~ 44 | NEWLINE* ~ EOI 45 | } 46 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | use crate::context::Context; 2 | use crate::string_cache::Atom; 3 | use crate::token::Phrase; 4 | 5 | use std::ffi::CStr; 6 | use std::mem::transmute; 7 | use std::os::raw::{c_char, c_void}; 8 | use std::slice; 9 | 10 | #[repr(C)] 11 | pub struct CRule { 12 | id: i32, 13 | } 14 | 15 | #[no_mangle] 16 | pub extern "C" fn throne_context_create_from_text(string_ptr: *const c_char) -> *mut Context { 17 | let cstr = unsafe { CStr::from_ptr(string_ptr) }; 18 | unsafe { transmute(Box::new(Context::from_text(cstr.to_str().unwrap()))) } 19 | } 20 | 21 | #[no_mangle] 22 | pub extern "C" fn throne_context_destroy(context: *mut Context) { 23 | let _drop: Box = unsafe { transmute(context) }; 24 | } 25 | 26 | #[no_mangle] 27 | pub extern "C" fn throne_update(context: *mut Context) { 28 | let context = unsafe { &mut *context }; 29 | context.update().unwrap(); 30 | } 31 | 32 | #[no_mangle] 33 | pub extern "C" fn throne_context_string_to_atom( 34 | context: *mut Context, 35 | string_ptr: *const c_char, 36 | ) -> Atom { 37 | let context = unsafe { &mut *context }; 38 | let cstr = unsafe { CStr::from_ptr(string_ptr) }; 39 | context.str_to_atom(cstr.to_str().unwrap()) 40 | } 41 | 42 | #[no_mangle] 43 | pub extern "C" fn throne_context_find_matching_rules( 44 | context: *mut Context, 45 | side_input: extern "C" fn(p: *const Atom, p_len: usize, data: *mut c_void) -> bool, 46 | side_input_data: *mut c_void, 47 | result_ptr: *mut CRule, 48 | result_len: usize, 49 | ) -> usize { 50 | let context = unsafe { &mut *context }; 51 | let result = unsafe { slice::from_raw_parts_mut(result_ptr, result_len) }; 52 | 53 | let mut side_input_p = vec![]; 54 | 55 | let rules = context 56 | .find_matching_rules(|p: &Phrase| { 57 | side_input_p.clear(); 58 | side_input_p.extend(p.iter().map(|t| t.atom)); 59 | 60 | if side_input(side_input_p.as_ptr(), side_input_p.len(), side_input_data) { 61 | Some(vec![]) 62 | } else { 63 | None 64 | } 65 | }) 66 | .unwrap(); 67 | 68 | let len = rules.len().min(result_len); 69 | 70 | for i in 0..len { 71 | result[i] = CRule { id: rules[i].id }; 72 | } 73 | 74 | return len; 75 | } 76 | -------------------------------------------------------------------------------- /benches/spaceopera.throne: -------------------------------------------------------------------------------- 1 | (note 0 12 mid) . (selected-instrument 2) . (note 1 4 last) . (note 2 7 mid) . (level-instruments 2 (o (o (o (o (x (o (x (o (o (o (x (o (o (o x))))))))))))))) . (level-instruments 1 (x (o (x (o x))))) . (note 2 5 first) . (note 2 11 mid) . (note 0 6 mid) . (current-beat 9) . (note 1 0 first) . (note 0 13 last) . (note 0 1 first) . (level-instruments 0 (x (o (o (o (o (x (o (o (x (o (o (x x))))))))))))) . (note 2 15 last) . (note 1 2 mid) . (note 0 9 mid) . (level-instrument-count 3) 2 | 3 | #input-place BEATPOS: { 4 | $selected-instrument INSTRUMENT . () = #clear INSTRUMENT (#input-place-after-clear BEATPOS) 5 | } 6 | 7 | #input-place-after-clear BEATPOS: { 8 | $selected-instrument INSTRUMENT . $level-instruments INSTRUMENT NOTES . !placed INSTRUMENT = place INSTRUMENT NOTES BEATPOS first . placed INSTRUMENT 9 | place INSTRUMENT (x NOTES) POS DESC . + 1 POS POS2 . % POS2 16 POS3 = place INSTRUMENT NOTES POS3 mid . note-tmp INSTRUMENT POS DESC 10 | place INSTRUMENT (o NOTES) POS DESC . !note-tmp INSTRUMENT EXISTINGPOS EXISTINGDESC = place INSTRUMENT NOTES POS DESC 11 | place INSTRUMENT (o NOTES) POS DESC . + 1 POS POS2 . % POS2 16 POS3 . $note-tmp INSTRUMENT EXISTINGPOS EXISTINGDESC = place INSTRUMENT NOTES POS3 DESC 12 | place INSTRUMENT x POS DESC = note-tmp INSTRUMENT POS last 13 | place INSTRUMENT o POS DESC = () 14 | () = #clean-placed 15 | } 16 | 17 | #clean-placed: { 18 | placed INSTRUMENT = () 19 | () = () 20 | } 21 | 22 | #input-change-left . selected-instrument I . + I2 1 I . $level-instrument-count N . % I2 N I3 = selected-instrument I3 23 | #input-change-right . selected-instrument I . + I 1 I2 . $level-instrument-count N . % I2 N I3 = selected-instrument I3 24 | 25 | $current-beat BEAT . $note INSTRUMENT1 BEAT DESC1 . $note-tmp INSTRUMENT2 BEAT DESC2 = #clear INSTRUMENT2 #dummy . ^collide BEAT 26 | $current-beat BEAT . !note INSTRUMENT1 BEAT DESC1 . $note-tmp INSTRUMENT2 BEAT last = #set-tmp INSTRUMENT2 27 | 28 | $note-tmp INSTRUMENT1 BEAT DESC1 . $note-tmp INSTRUMENT2 BEAT DESC2 . !clearing-tmp-collision = #clear INSTRUMENT2 #cleared-tmp-collision . clearing-tmp-collision . ^collide BEAT 29 | #cleared-tmp-collision . clearing-tmp-collision = () 30 | 31 | #set-beat BEAT . current-beat EXISTINGBEAT = current-beat BEAT 32 | 33 | #set-tmp INSTRUMENT: { 34 | note-tmp INSTRUMENT POS DESC = note INSTRUMENT POS DESC 35 | () = () 36 | } 37 | 38 | #clear INSTRUMENT RETURN: { 39 | note INSTRUMENT POS DESC = () 40 | note-tmp INSTRUMENT POS DESC = () 41 | () = RETURN 42 | } 43 | 44 | #dummy = () -------------------------------------------------------------------------------- /benches/benchmark.rs: -------------------------------------------------------------------------------- 1 | extern crate throne; 2 | 3 | #[macro_use] 4 | extern crate criterion; 5 | #[macro_use] 6 | extern crate lazy_static; 7 | 8 | use criterion::Criterion; 9 | 10 | fn criterion_benchmark(c: &mut Criterion) { 11 | c.bench_function("update/context1", |b| { 12 | b.iter_with_setup( 13 | || { 14 | // only parse once, otherwise benchmark is affected 15 | lazy_static! { 16 | static ref CONTEXT: throne::Context = 17 | throne::Context::from_text(include_str!("wood.throne")) 18 | .unwrap() 19 | .with_test_rng(); 20 | } 21 | 22 | CONTEXT.clone() 23 | }, 24 | |mut context| { 25 | context.append_state("#update"); 26 | throne::update(&mut context.core, |_: &throne::Phrase| None).unwrap(); 27 | }, 28 | ) 29 | }); 30 | 31 | c.bench_function("update/context2", |b| { 32 | b.iter_with_setup( 33 | || { 34 | // only parse once, otherwise benchmark is affected 35 | lazy_static! { 36 | static ref CONTEXT: throne::Context = 37 | throne::Context::from_text(include_str!("spaceopera.throne")) 38 | .unwrap() 39 | .with_test_rng(); 40 | } 41 | 42 | CONTEXT.clone() 43 | }, 44 | |mut context| { 45 | throne::update(&mut context.core, |_: &throne::Phrase| None).unwrap(); 46 | }, 47 | ) 48 | }); 49 | 50 | c.bench_function("update/context3", |b| { 51 | b.iter_with_setup( 52 | || { 53 | // only parse once, otherwise benchmark is affected 54 | lazy_static! { 55 | static ref CONTEXT: throne::Context = 56 | throne::Context::from_text(include_str!("increment.throne")) 57 | .unwrap() 58 | .with_test_rng(); 59 | } 60 | 61 | CONTEXT.clone() 62 | }, 63 | |mut context| { 64 | context.append_state("#increment"); 65 | throne::update(&mut context.core, |_: &throne::Phrase| None).unwrap(); 66 | }, 67 | ) 68 | }); 69 | } 70 | 71 | criterion_group!(benches, criterion_benchmark); 72 | criterion_main!(benches); 73 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Throne is a scripting language for game prototyping and story logic. 2 | //! 3 | //! Documentation for learning the language itself can be found on [GitHub](https://github.com/t-mw/throne#reference). 4 | //! 5 | //! # Example 6 | //! 7 | //! ``` 8 | //! use throne::{ContextBuilder, tokenize}; 9 | //! 10 | //! // Write your script text inline or in an external file included with `include_str!(..)` 11 | //! let script = r#" 12 | //! Mary is sister of David 13 | //! Sarah is child of Mary 14 | //! Tom is child of David 15 | //! 16 | //! CHILD is child of PARENT . AUNT is sister of PARENT . 17 | //! COUSIN is child of AUNT = COUSIN is cousin of CHILD 18 | //! "#; 19 | //! 20 | //! // Build the Throne context using your script text to define the initial state and rules 21 | //! let mut context = ContextBuilder::new() 22 | //! .text(script) 23 | //! .build() 24 | //! .unwrap_or_else(|e| panic!("Failed to build Throne context: {}", e)); 25 | //! 26 | //! // Execute an update step 27 | //! context.update().unwrap_or_else(|e| panic!("Throne context update failed: {}", e)); 28 | //! 29 | //! // Fetch the updated state 30 | //! let state = context.core.state.get_all(); 31 | //! 32 | //! // Convert a string to a Throne phrase 33 | //! let expected_state_phrase = tokenize("Sarah is cousin of Tom", &mut context.string_cache); 34 | //! 35 | //! assert_eq!(state, vec![expected_state_phrase]); 36 | //! ``` 37 | 38 | #[macro_use] 39 | extern crate lazy_static; 40 | extern crate pest; 41 | #[macro_use] 42 | extern crate pest_derive; 43 | extern crate rand; 44 | extern crate regex; 45 | 46 | mod context; 47 | mod core; 48 | #[cfg(not(target_arch = "wasm32"))] 49 | mod ffi; 50 | mod matching; 51 | mod parser; 52 | mod rule; 53 | mod state; 54 | mod string_cache; 55 | #[cfg(test)] 56 | mod tests; 57 | pub mod token; 58 | mod update; 59 | #[cfg(target_arch = "wasm32")] 60 | mod wasm; 61 | 62 | pub use crate::context::{Context, ContextBuilder}; 63 | pub use crate::core::Core; 64 | #[cfg(not(target_arch = "wasm32"))] 65 | #[doc(hidden)] 66 | pub use crate::ffi::*; 67 | pub use crate::rule::Rule; 68 | pub use crate::state::{PhraseId, State}; 69 | pub use crate::string_cache::{Atom, StringCache}; 70 | pub use crate::token::{tokenize, Phrase, PhraseGroup, PhraseString, Token}; 71 | pub use crate::update::{update, SideInput}; 72 | #[cfg(target_arch = "wasm32")] 73 | pub use crate::wasm::*; 74 | 75 | pub mod errors { 76 | pub use crate::matching::ExcessivePermutationError; 77 | pub use crate::parser::Error as ParserError; 78 | pub use crate::update::{Error as UpdateError, RuleRepeatError}; 79 | } 80 | -------------------------------------------------------------------------------- /src/string_cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::convert::TryInto; 3 | use std::i32; 4 | 5 | /// References a string or integer stored in a [StringCache]. 6 | #[repr(C)] 7 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] 8 | pub struct Atom { 9 | is_integer: bool, 10 | v: u32, 11 | } 12 | 13 | /// Stores the mappings between a set of [Atoms](Atom) and the primitives that they reference. 14 | #[derive(Clone, Debug)] 15 | pub struct StringCache { 16 | atom_to_str: Vec, 17 | str_to_atom: HashMap, 18 | pub(crate) wildcard_counter: i32, 19 | } 20 | 21 | impl StringCache { 22 | pub(crate) fn new() -> StringCache { 23 | StringCache { 24 | atom_to_str: vec![], 25 | str_to_atom: HashMap::new(), 26 | wildcard_counter: 0, 27 | } 28 | } 29 | 30 | /// Returns an [Atom] referencing the provided `string`, defining a new [Atom] if necessary. 31 | /// 32 | /// The string referenced by the returned [Atom] can be retrieved using [Self::atom_to_str()], 33 | /// unless the string could be converted to an integer in which case [Self::atom_to_integer()] 34 | /// must be used. 35 | pub fn str_to_atom(&mut self, string: &str) -> Atom { 36 | use std::str::FromStr; 37 | 38 | if let Some(n) = i32::from_str(string).ok() { 39 | return StringCache::integer_to_atom(n); 40 | } 41 | 42 | if let Some(atom) = self.str_to_existing_atom(string) { 43 | return atom; 44 | } 45 | 46 | let idx = self.atom_to_str.len().try_into().unwrap(); 47 | let atom = Atom { 48 | is_integer: false, 49 | v: idx, 50 | }; 51 | 52 | self.atom_to_str.push(string.to_string()); 53 | self.str_to_atom.insert(string.to_string(), atom); 54 | 55 | atom 56 | } 57 | 58 | /// Returns an [Atom] referencing the provided `string`, only if an [Atom] was previously defined for the string. 59 | pub fn str_to_existing_atom(&self, string: &str) -> Option { 60 | self.str_to_atom.get(string).cloned() 61 | } 62 | 63 | /// Returns an [Atom] referencing the provided `integer`. 64 | pub fn integer_to_atom(integer: i32) -> Atom { 65 | Atom { 66 | is_integer: true, 67 | v: integer as u32, 68 | } 69 | } 70 | 71 | /// Returns the string referenced by the provided `atom`. 72 | pub fn atom_to_str(&self, atom: Atom) -> Option<&str> { 73 | if atom.is_integer { 74 | None 75 | } else { 76 | Some(&self.atom_to_str[atom.v as usize]) 77 | } 78 | } 79 | 80 | /// Returns the integer referenced by the provided `atom`. 81 | pub fn atom_to_integer(atom: Atom) -> Option { 82 | if atom.is_integer { 83 | Some(atom.v as i32) 84 | } else { 85 | None 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/rule.rs: -------------------------------------------------------------------------------- 1 | use crate::string_cache::StringCache; 2 | use crate::token::{phrase_to_string, PhraseGroup, Token}; 3 | 4 | use std::marker::PhantomData; 5 | 6 | /// Represents a Throne rule. 7 | /// 8 | /// A `Rule` is uniquely identified by its `id`. 9 | /// Each input and output for a `Rule` is a [Phrase](crate::Phrase). 10 | #[derive(Clone, Debug, Eq, PartialEq)] 11 | pub struct Rule { 12 | pub id: i32, 13 | pub inputs: Vec>, 14 | pub outputs: Vec>, 15 | pub(crate) input_phrase_group_counts: Vec, 16 | pub source_span: LineColSpan, 17 | marker: PhantomData<()>, 18 | } 19 | 20 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 21 | pub struct LineColSpan { 22 | pub line_start: usize, 23 | pub line_end: usize, 24 | pub col_start: usize, 25 | pub col_end: usize, 26 | } 27 | 28 | impl Rule { 29 | pub fn to_string(&self, string_cache: &StringCache) -> String { 30 | rule_to_string_common( 31 | self.id.to_string(), 32 | &self.inputs, 33 | &self.outputs, 34 | string_cache, 35 | ) 36 | } 37 | } 38 | 39 | #[derive(Clone, Debug)] 40 | pub struct RuleBuilder { 41 | pub inputs: Vec>, 42 | pub outputs: Vec>, 43 | input_phrase_group_counts: Option>, 44 | source_span: LineColSpan, 45 | } 46 | 47 | impl RuleBuilder { 48 | pub fn new( 49 | inputs: Vec>, 50 | outputs: Vec>, 51 | source_span: LineColSpan, 52 | ) -> Self { 53 | RuleBuilder { 54 | inputs, 55 | outputs, 56 | input_phrase_group_counts: None, 57 | source_span, 58 | } 59 | } 60 | 61 | pub(crate) fn input_phrase_group_counts(mut self, v: Vec) -> Self { 62 | self.input_phrase_group_counts = Some(v); 63 | self 64 | } 65 | 66 | pub fn build(mut self, id: i32) -> Rule { 67 | let input_phrase_group_counts = self 68 | .input_phrase_group_counts 69 | .take() 70 | .unwrap_or_else(|| self.inputs.iter().map(|p| p.groups().count()).collect()); 71 | Rule { 72 | id, 73 | inputs: self.inputs, 74 | outputs: self.outputs, 75 | input_phrase_group_counts, 76 | source_span: self.source_span, 77 | marker: PhantomData, 78 | } 79 | } 80 | 81 | pub fn to_string(&self, string_cache: &StringCache) -> String { 82 | rule_to_string_common("?".to_string(), &self.inputs, &self.outputs, string_cache) 83 | } 84 | } 85 | 86 | fn rule_to_string_common( 87 | id_str: String, 88 | inputs: &[Vec], 89 | outputs: &[Vec], 90 | string_cache: &StringCache, 91 | ) -> String { 92 | let inputs = inputs 93 | .iter() 94 | .map(|p| phrase_to_string(p, string_cache)) 95 | .collect::>() 96 | .join(" . "); 97 | 98 | let outputs = outputs 99 | .iter() 100 | .map(|p| phrase_to_string(p, string_cache)) 101 | .collect::>() 102 | .join(" . "); 103 | 104 | format!("{:5}: {} = {}", id_str, inputs, outputs) 105 | } 106 | -------------------------------------------------------------------------------- /benches/increment.throne: -------------------------------------------------------------------------------- 1 | quality foo 1 2 | 3 | #increment . quality foo1 A . + A 1 B = quality foo1 B 4 | #increment . quality foo2 A . + A 1 B = quality foo2 B 5 | #increment . quality foo3 A . + A 1 B = quality foo3 B 6 | #increment . quality foo4 A . + A 1 B = quality foo4 B 7 | #increment . quality foo5 A . + A 1 B = quality foo5 B 8 | #increment . quality foo6 A . + A 1 B = quality foo6 B 9 | #increment . quality foo7 A . + A 1 B = quality foo7 B 10 | #increment . quality foo8 A . + A 1 B = quality foo8 B 11 | #increment . quality foo9 A . + A 1 B = quality foo9 B 12 | #increment . quality foo10 A . + A 1 B = quality foo10 B 13 | #increment . quality foo11 A . + A 1 B = quality foo11 B 14 | #increment . quality foo12 A . + A 1 B = quality foo12 B 15 | #increment . quality foo13 A . + A 1 B = quality foo13 B 16 | #increment . quality foo14 A . + A 1 B = quality foo14 B 17 | #increment . quality foo15 A . + A 1 B = quality foo15 B 18 | #increment . quality foo16 A . + A 1 B = quality foo16 B 19 | #increment . quality foo17 A . + A 1 B = quality foo17 B 20 | #increment . quality foo18 A . + A 1 B = quality foo18 B 21 | #increment . quality foo19 A . + A 1 B = quality foo19 B 22 | #increment . quality foo20 A . + A 1 B = quality foo20 B 23 | #increment . quality foo21 A . + A 1 B = quality foo21 B 24 | #increment . quality foo22 A . + A 1 B = quality foo22 B 25 | #increment . quality foo23 A . + A 1 B = quality foo23 B 26 | #increment . quality foo24 A . + A 1 B = quality foo24 B 27 | #increment . quality foo25 A . + A 1 B = quality foo25 B 28 | #increment . quality foo26 A . + A 1 B = quality foo26 B 29 | #increment . quality foo A . + A 1 B = quality foo B 30 | #increment . quality foo28 A . + A 1 B = quality foo28 B 31 | #increment . quality foo29 A . + A 1 B = quality foo29 B 32 | #increment . quality foo30 A . + A 1 B = quality foo30 B 33 | #increment . quality foo31 A . + A 1 B = quality foo31 B 34 | #increment . quality foo32 A . + A 1 B = quality foo32 B 35 | #increment . quality foo33 A . + A 1 B = quality foo33 B 36 | #increment . quality foo34 A . + A 1 B = quality foo34 B 37 | #increment . quality foo35 A . + A 1 B = quality foo35 B 38 | #increment . quality foo36 A . + A 1 B = quality foo36 B 39 | #increment . quality foo37 A . + A 1 B = quality foo37 B 40 | #increment . quality foo38 A . + A 1 B = quality foo38 B 41 | #increment . quality foo39 A . + A 1 B = quality foo39 B 42 | #increment . quality foo40 A . + A 1 B = quality foo40 B 43 | #increment . quality foo41 A . + A 1 B = quality foo41 B 44 | #increment . quality foo42 A . + A 1 B = quality foo42 B 45 | #increment . quality foo43 A . + A 1 B = quality foo43 B 46 | #increment . quality foo44 A . + A 1 B = quality foo44 B 47 | #increment . quality foo45 A . + A 1 B = quality foo45 B 48 | #increment . quality foo46 A . + A 1 B = quality foo46 B 49 | #increment . quality foo47 A . + A 1 B = quality foo47 B 50 | #increment . quality foo48 A . + A 1 B = quality foo48 B 51 | #increment . quality foo49 A . + A 1 B = quality foo49 B 52 | #increment . quality foo50 A . + A 1 B = quality foo50 B 53 | #increment . quality foo51 A . + A 1 B = quality foo51 B 54 | #increment . quality foo52 A . + A 1 B = quality foo52 B 55 | #increment . quality foo53 A . + A 1 B = quality foo53 B 56 | #increment . quality foo54 A . + A 1 B = quality foo54 B 57 | #increment . quality foo55 A . + A 1 B = quality foo55 B 58 | #increment . quality foo56 A . + A 1 B = quality foo56 B 59 | #increment . quality foo57 A . + A 1 B = quality foo57 B 60 | #increment . quality foo58 A . + A 1 B = quality foo58 B 61 | #increment . quality foo59 A . + A 1 B = quality foo59 B 62 | #increment . quality foo60 A . + A 1 B = quality foo60 B 63 | #increment . quality foo61 A . + A 1 B = quality foo61 B 64 | -------------------------------------------------------------------------------- /examples/blocks.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | fn main() { 3 | example::main(); 4 | } 5 | 6 | #[cfg(target_arch = "wasm32")] 7 | fn main() { 8 | println!("This example is not supported on WebAssembly"); 9 | } 10 | 11 | #[cfg(not(target_arch = "wasm32"))] 12 | mod example { 13 | extern crate lazy_static; 14 | 15 | extern crate minifb; 16 | extern crate pest; 17 | extern crate pest_derive; 18 | extern crate rand; 19 | extern crate regex; 20 | 21 | use minifb::{Key, KeyRepeat, Window, WindowOptions}; 22 | 23 | use std::{thread, time}; 24 | 25 | const WIDTH: usize = 100; 26 | const HEIGHT: usize = 200; 27 | 28 | pub fn main() { 29 | let mut window = Window::new( 30 | "Test - ESC to exit", 31 | WIDTH, 32 | HEIGHT, 33 | WindowOptions::default(), 34 | ) 35 | .unwrap_or_else(|e| { 36 | panic!("{}", e); 37 | }); 38 | 39 | let mut context = throne::ContextBuilder::new() 40 | .text(include_str!("blocks.throne")) 41 | .build() 42 | .unwrap_or_else(|e| panic!("{}", e)); 43 | 44 | let kd = context.str_to_atom("key-down"); 45 | let ku = context.str_to_atom("key-up"); 46 | let kp = context.str_to_atom("key-pressed"); 47 | let left = context.str_to_atom("left"); 48 | let right = context.str_to_atom("right"); 49 | let up = context.str_to_atom("up"); 50 | let down = context.str_to_atom("down"); 51 | 52 | while window.is_open() && !window.is_key_down(Key::Escape) { 53 | context.push_state("#update"); 54 | // context.print(); 55 | 56 | let string_to_key = |s: &throne::Atom| match s { 57 | s if *s == left => Some(Key::Left), 58 | s if *s == right => Some(Key::Right), 59 | s if *s == up => Some(Key::Up), 60 | s if *s == down => Some(Key::Down), 61 | _ => None, 62 | }; 63 | 64 | context 65 | .update_with_side_input(|p: &throne::Phrase| { 66 | if p.len() != 2 { 67 | return None; 68 | } 69 | 70 | match &p[0].atom { 71 | a if *a == kd => string_to_key(&p[1].atom).and_then(|k| { 72 | if window.is_key_down(k) { 73 | Some(p.to_vec()) 74 | } else { 75 | None 76 | } 77 | }), 78 | a if *a == ku => string_to_key(&p[1].atom).and_then(|k| { 79 | if !window.is_key_down(k) { 80 | Some(p.to_vec()) 81 | } else { 82 | None 83 | } 84 | }), 85 | a if *a == kp => string_to_key(&p[1].atom).and_then(|k| { 86 | if window.is_key_pressed(k, KeyRepeat::Yes) { 87 | Some(p.to_vec()) 88 | } else { 89 | None 90 | } 91 | }), 92 | _ => None, 93 | } 94 | }) 95 | .unwrap(); 96 | 97 | let mut buffer: Vec = vec![0; WIDTH * HEIGHT]; 98 | 99 | let is_valid_pos = |x, y| x < WIDTH as i32 && y < HEIGHT as i32; 100 | 101 | for phrase_id in context.core.state.iter() { 102 | let p = context.core.state.get(phrase_id); 103 | 104 | match ( 105 | p.get(0).and_then(|t| t.as_str(&context.string_cache)), 106 | p.get(2).and_then(|t| t.as_integer()), 107 | p.get(3).and_then(|t| t.as_integer()), 108 | p.get(4).and_then(|t| t.as_integer()), 109 | ) { 110 | (Some("block-falling"), Some(x), Some(y), _) 111 | | (Some("block-set"), _, Some(x), Some(y)) => { 112 | let color = 0x00b27474; 113 | 114 | let x0 = x * 10; 115 | let x1 = x0 + 10; 116 | 117 | let y0 = y * 10; 118 | let y1 = y0 + 10; 119 | 120 | for y in y0..y1 { 121 | for x in x0..x1 { 122 | if is_valid_pos(x, y) { 123 | let idx = x + WIDTH as i32 * (HEIGHT as i32 - 1 - y); 124 | buffer[idx as usize] = color; 125 | } 126 | } 127 | } 128 | } 129 | _ => (), 130 | } 131 | } 132 | 133 | // We unwrap here as we want this code to exit if it fails. Real applications may want to handle this in a different way 134 | window.update_with_buffer(&buffer, WIDTH, HEIGHT).unwrap(); 135 | 136 | thread::sleep(time::Duration::from_millis(33)); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /examples/blocks.throne: -------------------------------------------------------------------------------- 1 | // initialize state 2 | fall-timer 5 . default-fall-timer 5 . block-id 0 . falling-shape-id 0 . max-width 10 . max-height 20 3 | 4 | // spawn a shape that will fall, if one doesn't already exist 5 | #update . !shape _X _Y _BLOCKS . falling-shape-id ID . + ID 1 ID' = falling-shape-id ID' . #new-shape 6 | #update . $shape _X _Y _BLOCKS = #input-u 7 | 8 | // define available shapes and where they spawn 9 | #new-shape . $max-height H = new-shape 4 H ((block -1 0) ((block 0 0) ((block 1 0) ((block 0 1) (cons))))) . #shape-to-blocks (#input-lr) 10 | #new-shape . $max-height H = new-shape 4 H ((block 0 -2) ((block 0 -1) ((block 0 0) ((block 0 1) (cons))))) . #shape-to-blocks (#input-lr) 11 | #new-shape . $max-height H = new-shape 4 H ((block 0 -1) ((block 0 0) ((block 0 1) ((block 1 1) (cons))))) . #shape-to-blocks (#input-lr) 12 | 13 | // this stage is the beginning of the sequence of stages that places the blocks defined by a shape. 14 | // placing these blocks is either the result of a new shape being spawned at the top of the screen, 15 | // or an existing falling shape being moved or rotated. in the second case we move and rotate by 16 | // clearing any existing blocks and spawning a new set of blocks at updated positions. 17 | #shape-to-blocks RETURN: { 18 | // clear existing falling blocks 19 | block-falling _ID _X _Y = () 20 | // prepare to spawn new blocks defined by the shape 21 | $new-shape _X _Y BLOCKS . !BLOCKS = BLOCKS 22 | () = #shape-to-blocks-create RETURN 23 | } 24 | 25 | // this stage spawns 'falling' blocks defined by the shape 26 | #shape-to-blocks-create RETURN: { 27 | $new-shape X Y _ . (block DX DY) BLOCK . block-id ID . + ID 1 ID' . + X DX X' . + Y DY Y' = block-falling ID X' Y' . block-id ID' . BLOCK 28 | () = #shape-to-blocks-check RETURN 29 | } 30 | 31 | // this stage aborts the placement of blocks if any constraint is violated 32 | #shape-to-blocks-check RETURN . block-falling _ X Y . $block-set _ _ X Y = #shape-to-blocks-fail RETURN 33 | #shape-to-blocks-check RETURN . block-falling _ X _ . < X 0 = #shape-to-blocks-fail RETURN 34 | #shape-to-blocks-check RETURN . block-falling _ X _ . $max-width W . >= X W = #shape-to-blocks-fail RETURN 35 | #shape-to-blocks-check RETURN . () = #shape-to-blocks-ok RETURN 36 | 37 | // in this stage placement of blocks succeeded, so the new shape becomes a falling shape 38 | #shape-to-blocks-ok RETURN: { 39 | shape _ _ _ = () 40 | new-shape X Y BLOCKS . () = shape X Y BLOCKS . RETURN 41 | } 42 | 43 | // in this stage placement of blocks from the new shape failed, so we return to the previous falling shape 44 | #shape-to-blocks-fail RETURN: { 45 | new-shape _ _ _ = () 46 | $shape X Y BLOCKS . () = new-shape X Y BLOCKS . #shape-to-blocks RETURN 47 | } 48 | 49 | // rotate the shape if the up arrow key is pressed 50 | #input-u . ^key-pressed up = #rotate-shape 51 | #input-u . () = #input-lr 52 | #rotate-shape: { 53 | $shape X Y BLOCKS . !new-shape X Y _ = new-shape X Y BLOCKS . new-blocks (cons) 54 | new-shape X Y ((block DX DY) BLOCKS) . new-blocks BLOCKS2 . + DX2 DX 0 = new-shape X Y BLOCKS . new-blocks ((block DY DX2) BLOCKS2) 55 | new-shape X Y _ . new-blocks BLOCKS . () = new-shape X Y BLOCKS . #shape-to-blocks (#input-d) 56 | } 57 | 58 | // move the shape horizontally if the left or right arrow key is pressed 59 | #input-lr . ^key-pressed left . $shape X Y BLOCKS . - X 1 X' = new-shape X' Y BLOCKS . #shape-to-blocks (#input-d) 60 | #input-lr . ^key-pressed right . $shape X Y BLOCKS . + X 1 X' = new-shape X' Y BLOCKS . #shape-to-blocks (#input-d) 61 | #input-lr . () = #input-d 62 | 63 | // move the shape down faster than normal if the down arrow key is pressed 64 | #input-d: { 65 | ^key-down down . default-fall-timer 5 . fall-timer _ = default-fall-timer 1 . fall-timer 0 66 | ^key-up down . default-fall-timer 1 . fall-timer _ = default-fall-timer 5 . fall-timer 0 67 | () = #collision 68 | } 69 | 70 | // if any falling blocks are about to collide with any set blocks or the bottom of the screen, the 71 | // falling blocks should become set blocks. 72 | #collision: { 73 | block-falling ID X Y . + Y' 1 Y . $block-set _ _ X Y' = block-setting ID X Y 74 | block-falling ID X Y . + Y' 1 Y . < Y' 0 = block-setting ID X Y 75 | $block-setting _ _ _ . block-falling ID X' Y' = block-setting ID X' Y' 76 | $block-setting _ _ _ . shape _ _ _ = () 77 | () = #set 78 | } 79 | #set: { 80 | block-setting ID X Y . $falling-shape-id SHAPE_ID = block-set ID SHAPE_ID X Y 81 | $max-width W . () = #score-x . score-counter W 0 82 | } 83 | 84 | // mark completed rows for clearing 85 | #score-x . score-counter X Y . + X' 1 X . $block-set _ _ X' Y = score-counter X' Y . #score-x 86 | #score-x . score-counter 0 Y = #clear . clear-y Y 87 | #score-x . $score-counter _ _ . () = #score-y 88 | #score-y . score-counter _ Y . + Y 1 Y' . $max-width W . $max-height H . < Y' H = score-counter W Y' . #score-x 89 | #score-y . score-counter _ _ . () = #fall-tick 90 | 91 | // this stage clears blocks in any completed rows 92 | #clear: { 93 | $clear-y Y . block-set _ _ _ Y = () 94 | block-clear-move _ = () 95 | () = #clear-move 96 | } 97 | 98 | // this stage moves down any blocks hanging in space as a result of clearing completed rows 99 | #clear-move: { 100 | $clear-y Y . block-set ID SHAPE_ID X Y' . !block-clear-move ID . > Y' Y . - Y' 1 Y'' = block-set ID SHAPE_ID X Y'' . block-clear-move ID 101 | $max-width W . clear-y _ . () = #score-x . score-counter W 0 102 | } 103 | 104 | // move blocks down every TIMER frames 105 | #fall-tick . fall-timer TIMER . >= TIMER 0 . + TIMER2 1 TIMER . >= TIMER2 0 = fall-timer TIMER2 . #clean 106 | #fall-tick . fall-timer TIMER . >= TIMER 0 . + TIMER2 1 TIMER . < TIMER2 0 . $default-fall-timer D = fall-timer D . #fall 107 | #fall . shape X Y BLOCKS . + Y' 1 Y = new-shape X Y' BLOCKS . #shape-to-blocks #clean 108 | #fall . () = #clean 109 | 110 | #clean: { 111 | cons = () 112 | } -------------------------------------------------------------------------------- /src/update.rs: -------------------------------------------------------------------------------- 1 | use crate::core::Core; 2 | use crate::matching; 3 | use crate::rule::Rule; 4 | use crate::state::State; 5 | use crate::token::*; 6 | 7 | use rand::seq::SliceRandom; 8 | 9 | use std::fmt; 10 | 11 | const RULE_REPEAT_LIMIT: usize = 2000; 12 | 13 | #[derive(Debug)] 14 | pub enum Error { 15 | RuleRepeatError(RuleRepeatError), 16 | ExcessivePermutationError(matching::ExcessivePermutationError), 17 | } 18 | 19 | impl Error { 20 | pub fn rule<'a>(&self, rules: &'a [Rule]) -> Option<&'a Rule> { 21 | let rule_id = match self { 22 | Self::RuleRepeatError(e) => e.rule_id, 23 | Self::ExcessivePermutationError(e) => e.rule_id, 24 | }; 25 | rules.iter().find(|r| r.id == rule_id) 26 | } 27 | } 28 | 29 | impl std::error::Error for Error {} 30 | 31 | impl fmt::Display for Error { 32 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 33 | match self { 34 | Self::RuleRepeatError(e) => { 35 | write!(f, "{}", e) 36 | } 37 | Self::ExcessivePermutationError(e) => { 38 | write!(f, "{}", e) 39 | } 40 | } 41 | } 42 | } 43 | 44 | impl From for Error { 45 | fn from(e: RuleRepeatError) -> Self { 46 | Error::RuleRepeatError(e) 47 | } 48 | } 49 | 50 | impl From for Error { 51 | fn from(e: matching::ExcessivePermutationError) -> Self { 52 | Error::ExcessivePermutationError(e) 53 | } 54 | } 55 | 56 | #[derive(Debug)] 57 | pub struct RuleRepeatError { 58 | pub rule_id: i32, 59 | } 60 | 61 | impl std::error::Error for RuleRepeatError {} 62 | 63 | impl fmt::Display for RuleRepeatError { 64 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 65 | write!( 66 | f, 67 | "Script execution was abandoned since rule {} appears to be repeating infinitely.", 68 | self.rule_id 69 | ) 70 | } 71 | } 72 | 73 | // https://stackoverflow.com/questions/44246722/is-there-any-way-to-create-an-alias-of-a-specific-fnmut 74 | pub trait SideInput: FnMut(&Phrase) -> Option> {} 75 | impl SideInput for F where F: FnMut(&Phrase) -> Option> {} 76 | 77 | /// A free function equivalent to [Context::update_with_side_input](crate::Context::update_with_side_input) if required to avoid lifetime conflicts. 78 | pub fn update(core: &mut Core, mut side_input: F) -> Result<(), Error> 79 | where 80 | F: SideInput, 81 | { 82 | let state = &mut core.state; 83 | let rules = &mut core.rules; 84 | let executed_rule_ids = &mut core.executed_rule_ids; 85 | let rng = &mut core.rng; 86 | 87 | core.rule_repeat_count = 0; 88 | executed_rule_ids.clear(); 89 | 90 | // shuffle state so that a given rule with multiple potential 91 | // matches does not always match the same permutation of state. 92 | state.shuffle(rng); 93 | 94 | // shuffle rules so that each has an equal chance of selection. 95 | rules.shuffle(rng); 96 | 97 | // change starting rule on each iteration to introduce randomness. 98 | let mut start_rule_idx = 0; 99 | 100 | let mut quiescence = false; 101 | 102 | loop { 103 | let mut matching_rule = None; 104 | let mut rule_output_phrase_group_counts = vec![]; 105 | 106 | if quiescence { 107 | state.push_with_metadata(vec![Token::new_atom(core.qui_atom, 1, 1)], 1); 108 | } 109 | 110 | for i in 0..rules.len() { 111 | let rule = &rules[(start_rule_idx + i) % rules.len()]; 112 | 113 | if let Some(result) = matching::rule_matches_state(&rule, state, &mut side_input)? { 114 | matching_rule = Some(result.rule); 115 | rule_output_phrase_group_counts = result.output_phrase_group_counts; 116 | break; 117 | } 118 | } 119 | 120 | start_rule_idx += 1; 121 | 122 | if quiescence { 123 | quiescence = false; 124 | 125 | if matching_rule.is_none() { 126 | let qui_atom = core.qui_atom; 127 | assert!( 128 | state 129 | .iter() 130 | .enumerate() 131 | .filter(|&(_, p)| state.get(p)[0].atom == qui_atom) 132 | .map(|(i, _)| i) 133 | .collect::>() 134 | == vec![state.len() - 1], 135 | "expected 1 * () at end of state" 136 | ); 137 | 138 | let idx = state.len() - 1; 139 | state.remove_idx(idx); 140 | state.clear_removed_tokens(); 141 | 142 | return Ok(()); 143 | } 144 | } 145 | 146 | if let Some(ref matching_rule) = matching_rule { 147 | if let Some(previously_executed_rule_id) = executed_rule_ids.last() { 148 | if matching_rule.id == *previously_executed_rule_id { 149 | core.rule_repeat_count += 1; 150 | if core.rule_repeat_count > RULE_REPEAT_LIMIT { 151 | Err(RuleRepeatError { 152 | rule_id: matching_rule.id, 153 | })?; 154 | } 155 | } else { 156 | core.rule_repeat_count = 0; 157 | } 158 | } 159 | 160 | executed_rule_ids.push(matching_rule.id); 161 | assert!(execute_rule( 162 | matching_rule, 163 | state, 164 | Some(&rule_output_phrase_group_counts) 165 | )); 166 | } else { 167 | quiescence = true; 168 | } 169 | } 170 | } 171 | 172 | pub(crate) fn execute_rule( 173 | rule: &Rule, 174 | state: &mut State, 175 | rule_output_phrase_group_counts: Option<&[usize]>, 176 | ) -> bool { 177 | let inputs = &rule.inputs; 178 | let outputs = &rule.outputs; 179 | 180 | for input in inputs { 181 | if input[0].is_consuming { 182 | if !state.remove_phrase(input) { 183 | return false; 184 | } 185 | } 186 | } 187 | 188 | outputs.iter().enumerate().for_each(|(output_i, output)| { 189 | if let Some(group_counts) = rule_output_phrase_group_counts { 190 | state.push_with_metadata(output.clone(), group_counts[output_i]); 191 | } else { 192 | state.push(output.clone()); 193 | } 194 | }); 195 | 196 | true 197 | } 198 | -------------------------------------------------------------------------------- /todo.org: -------------------------------------------------------------------------------- 1 | *** TODO syntax to match rule only once per update / permanently. or even make matching once per update the default. 2 | - option to scope permanence by a variable 3 | - #stage: { input 3= output } evaluates max 3 times within stage, becomes: 4 | #stage: { 5 | // must be structured to keep behavior of random rule selection 6 | !tmp-counter-123 _ . input = output . tmp-counter-123 2 7 | tmp-counter-123 N . > N 0 . - N 1 N' . input = output . tmp-counter-123 N' 8 | () = #tmp-cleanup 9 | } 10 | #tmp-cleanup: { tmp-counter-123 _ = () } 11 | - could also allow integer variables to be used instead of constant e.g. #stage: { $count N . input N= output } 12 | *** TODO lispy key properties to enable pattern matching or syntax sugar for getting + modifying values by key in a record of form (record (k1 v1) (k2 v2) ...) 13 | e.g to modify player (name N) (x 10) (y 20) 14 | // This rule... 15 | player :x X . + X 1 X' = player :x X' 16 | // ...would be syntactic sugar for: 17 | player ...O1 (x X) ...O2 . + X 1 X' = player ...O1 (x X') ...O2 18 | // if there’s more than one ‘player’ on each side of the rule the verbose syntax is needed to disambiguate. 19 | // NB: the ... proposal doesnt handle multiple unordered properties well 20 | *** TODO make $ remove instead of preserve, since remove is less common and this makes the stage behavior (#asdf:) consistent with other syntax 21 | *** TODO support custom backwards predicate (<<) under stage label 22 | *** TODO decide on consistent syntax for arguments e.g. keywords? 23 | *** TODO arbitrary math expressions e.g. 'expr 2 + A / 3 + 1.5 = B' 24 | *** TODO replace !== backwards predicate with ability to use '!' anywhere in a phrase e.g. !== A B would become == A !B 25 | *** TODO ability to arbitrarily nest 'stage' scopes 26 | *** TODO ability to put rule output in 'stage' scopes e.g. in1 . in2 = out1 { subset = () } === in1 . in2 . subset = out1 27 | *** TODO ability to put state in 'stage' scopes e.g. #stage { some-state } becomes #stage . !some-state = some-state 28 | *** TODO reduce serialization boilerplate with either serde or generated code (like with pest) 29 | *** TODO allow output in custom backwards predicate, i.e. output is appended to rule 30 | *** TODO test doing initial matching through ecs instead of first atoms (e.g. https://github.com/TomGillen/legion) 31 | - requires a macro to generate rust structs for throne identifiers 32 | - an item of state is then e.g. &[FooAtom::N1, Var::N2, BarAtom::N3, etc..] 33 | - possible matching rules are found using a component query 34 | *** TODO support marking state phrases immutable in debug builds, logging warning when the state is consumed by a rule. or do compile-time check. 35 | *** TODO support matching phrases while binding to them, to avoid retyping them in output e.g. (foo A)@FOO . bar = FOO 36 | *** TODO look at prolog optimizations 37 | - could we compile each rule down to a specialized routine with a stack for variable matches to replace test_inputs_with_permutation 38 | - bin each phrase by (atom, atom position) and variable name to speed up matching 39 | *** TODO add syntax for matching on const lists i.e. (FOO | BAR) matched against (a b c): FOO = a, BAR = (b c) 40 | *** TODO ink -> throne converter 41 | *** TODO syntax for inserting the result of an input match directly into output 42 | - e.g. foo FOO = `foo capitalized is` (^capitalize FOO (pattern: [Option<&Atom>; N], match_pattern_length: bool) -> Vec<&[Token]> { ... }` 52 | - replace find_phrase* variants too 53 | *** TODO measure performance with https://github.com/bodil/smartstring 54 | *** TODO add examples 55 | - [X] Conway's game of life 56 | - [ ] Chess 57 | - [ ] Tic tac toe 58 | - [ ] Procedural generationn 59 | *** TODO test with https://github.com/yuulive/mo 60 | *** TODO reduce permutation space beyond first atom 61 | - a X . a X = ... or a X . b X = ... with a (0..N) and b (0..N) triggers an O(N^2) search on each update. 62 | *** TODO support backwards predicates in any order 63 | - currently backwards predicates are evaluated left to right in two passes, so > 2 backwards predicates in the wrong order will fail matching e.g. + C 3 D . % B 2 C . + A 1 B = ... 64 | - backwards predicates need to be evaluated in order based on variable dependencies. 65 | - could extend ordering based on dependencies to matching in general, including side predicates and normal state matches, to reduce permutations. 66 | *** TODO try https://twitter.com/tomaka17/status/1391052081272967170 67 | - "you might be able to save a lot of hashmap lookups if you replace a `HashMap` with a `HashMap` and a `Slab`. This might be very useful if K is something heavy such as a `String`" 68 | *** DONE support quiescence rule under stage label i.e. don't copy left-hand stage for quiescence rule 69 | CLOSED: [2021-04-30 Fri 11:16] 70 | *** DONE replace #foo -> stage foo, because # does not have special effects like other symbols 71 | CLOSED: [2021-04-29 Thu 15:24] 72 | *** DONE syntax for scheduling some output of a rule to be deleted at the end of the update 73 | CLOSED: [2021-05-08 Sat 03:24] 74 | - left up to embedder. 75 | *** DONE make () = () optional in prefixed blocks 76 | CLOSED: [2021-05-04 Tue 19:55] 77 | *** DONE detect infinite loops 78 | CLOSED: [2021-05-03 Mon 13:17] 79 | *** DONE selectively disable warnings 80 | CLOSED: [2020-01-24 Fri 14:24] 81 | *** DONE wildcard variable: _ 82 | *** DONE support defining own backwards predicates: 83 | - defined as rule without '=': { 26 | text: &'a str, 27 | string_cache: StringCache, 28 | rng: Option<&'a mut SmallRng>, 29 | } 30 | 31 | impl<'a> ContextBuilder<'a> { 32 | pub fn new() -> Self { 33 | ContextBuilder { 34 | text: "", 35 | string_cache: StringCache::new(), 36 | rng: None, 37 | } 38 | } 39 | 40 | /// Sets the script text used to define the initial state and rules of the [Context]. 41 | pub fn text(mut self, text: &'a str) -> Self { 42 | self.text = text; 43 | self 44 | } 45 | 46 | /// Sets the [StringCache] used for the [Context]. 47 | pub fn string_cache(mut self, string_cache: StringCache) -> Self { 48 | self.string_cache = string_cache; 49 | self 50 | } 51 | 52 | /// Sets the random number generator used for the [Context]. 53 | /// 54 | /// Defaults to `rand::rngs::SmallRng::from_rng(&mut rand::thread_rng())`. 55 | pub fn rng(mut self, rng: &'a mut SmallRng) -> Self { 56 | self.rng = Some(rng); 57 | self 58 | } 59 | 60 | /// Builds the [Context] using the provided script text to define the initial state and rules. 61 | /// Returns an error if the script text could not be parsed. 62 | pub fn build(self) -> Result { 63 | Context::new( 64 | self.text, 65 | self.string_cache, 66 | self.rng.unwrap_or(&mut default_rng()), 67 | ) 68 | } 69 | } 70 | 71 | fn default_rng() -> SmallRng { 72 | // NB: update doc for ContextBuilder::rng if this changes 73 | SmallRng::from_rng(&mut thread_rng()).unwrap() 74 | } 75 | 76 | /// Stores the [State], [Rules](Rule) and [Atom] mappings for a Throne script. 77 | /// 78 | /// Create a new `Context` using a [ContextBuilder]. 79 | #[derive(Clone)] 80 | #[non_exhaustive] 81 | pub struct Context { 82 | pub core: Core, 83 | pub string_cache: StringCache, 84 | } 85 | 86 | impl Context { 87 | pub(crate) fn from_text(text: &str) -> Result { 88 | ContextBuilder::new().text(text).build() 89 | } 90 | 91 | #[cfg(test)] 92 | pub(crate) fn from_text_rng(text: &str, rng: &mut SmallRng) -> Result { 93 | ContextBuilder::new().text(text).rng(rng).build() 94 | } 95 | 96 | fn new( 97 | text: &str, 98 | mut string_cache: StringCache, 99 | rng: &mut SmallRng, 100 | ) -> Result { 101 | let result = parser::parse(text, &mut string_cache, rng)?; 102 | 103 | let mut state = State::new(); 104 | for phrase in result.state.into_iter() { 105 | state.push(phrase); 106 | } 107 | 108 | let qui_atom = string_cache.str_to_atom(parser::QUI); 109 | 110 | Ok(Context { 111 | core: Core { 112 | state, 113 | rules: result.rules, 114 | executed_rule_ids: vec![], 115 | rule_repeat_count: 0, 116 | rng: rng.clone(), 117 | qui_atom, 118 | }, 119 | string_cache, 120 | }) 121 | } 122 | 123 | /// Executes any [Rule] that matches the current [State] until the set of matching rules is exhausted. 124 | pub fn update(&mut self) -> Result<(), update::Error> { 125 | self.update_with_side_input(|_: &Phrase| None) 126 | } 127 | 128 | /// Equivalent to [Context::update()], but accepts a callback to respond to `^` predicates. 129 | pub fn update_with_side_input(&mut self, side_input: F) -> Result<(), update::Error> 130 | where 131 | F: SideInput, 132 | { 133 | update(&mut self.core, side_input) 134 | } 135 | 136 | /// Executes a specific [Rule]. 137 | /// 138 | /// Returns `true` if the [Rule] was successfully executed or `false` if some of its inputs could not be matched to the current [State]. 139 | pub fn execute_rule(&mut self, rule: &Rule) -> bool { 140 | update::execute_rule(rule, &mut self.core.state, None) 141 | } 142 | 143 | /// Returns the set of [Rules](Rule) that may be executed in the next update. 144 | pub fn find_matching_rules( 145 | &self, 146 | mut side_input: F, 147 | ) -> Result, ExcessivePermutationError> 148 | where 149 | F: SideInput, 150 | { 151 | let state = &mut self.core.state.clone(); 152 | 153 | let mut rules = vec![]; 154 | for rule in &self.core.rules { 155 | if let Some(matching_rule) = 156 | rule_matches_state(&rule, state, &mut side_input)?.map(|result| result.rule) 157 | { 158 | rules.push(matching_rule); 159 | } 160 | } 161 | 162 | Ok(rules) 163 | } 164 | 165 | /// Alias for [StringCache::str_to_atom]. 166 | pub fn str_to_atom(&mut self, string: &str) -> Atom { 167 | self.string_cache.str_to_atom(string) 168 | } 169 | 170 | /// Alias for [StringCache::str_to_existing_atom]. 171 | pub fn str_to_existing_atom(&self, string: &str) -> Option { 172 | self.string_cache.str_to_existing_atom(string) 173 | } 174 | 175 | /// Alias for [StringCache::atom_to_str]. 176 | pub fn atom_to_str(&self, atom: Atom) -> Option<&str> { 177 | self.string_cache.atom_to_str(atom) 178 | } 179 | 180 | /// Alias for [StringCache::atom_to_integer]. 181 | pub fn atom_to_integer(&self, atom: Atom) -> Option { 182 | StringCache::atom_to_integer(atom) 183 | } 184 | 185 | #[cfg(test)] 186 | pub fn with_test_rng(mut self) -> Context { 187 | self.core.rng = crate::tests::test_rng(); 188 | self 189 | } 190 | 191 | /// Converts the provided text to a [Phrase] and adds it to the context's [State]. 192 | pub fn push_state(&mut self, phrase_text: &str) { 193 | self.core 194 | .state 195 | .push(tokenize(phrase_text, &mut self.string_cache)); 196 | } 197 | 198 | /// Copies the state from another `Context` to this one. 199 | pub fn extend_state_from_context(&mut self, other: &Context) { 200 | for phrase_id in other.core.state.iter() { 201 | let phrase = other.core.state.get(phrase_id); 202 | let new_phrase = phrase 203 | .iter() 204 | .map(|t| { 205 | if StringCache::atom_to_integer(t.atom).is_some() { 206 | t.clone() 207 | } else { 208 | let string = other 209 | .string_cache 210 | .atom_to_str(t.atom) 211 | .expect(&format!("missing token: {:?}", t)); 212 | 213 | let mut new_token = t.clone(); 214 | new_token.atom = self.string_cache.str_to_atom(string); 215 | new_token 216 | } 217 | }) 218 | .collect(); 219 | self.core.state.push(new_phrase); 220 | } 221 | } 222 | 223 | /// Prints a representation of the `Context` to the console. 224 | pub fn print(&self) { 225 | println!("{}", self); 226 | } 227 | } 228 | 229 | impl fmt::Display for Context { 230 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 231 | let state = state::state_to_string(&self.core.state, &self.string_cache); 232 | 233 | let mut rules = self 234 | .core 235 | .rules 236 | .iter() 237 | .map(|r| r.to_string(&self.string_cache)) 238 | .collect::>(); 239 | rules.sort(); 240 | 241 | write!( 242 | f, 243 | "state:\n{}\nrules:\n{}\n{}", 244 | state, 245 | rules.join("\n"), 246 | if self.core.executed_rule_ids.is_empty() { 247 | "no rules were executed in the previous update".to_string() 248 | } else { 249 | format!( 250 | "rule ids executed in the previous update:\n{}", 251 | self.core.executed_rule_ids.iter().join(", ") 252 | ) 253 | } 254 | ) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/wasm.rs: -------------------------------------------------------------------------------- 1 | use crate::context::Context as ThroneContext; 2 | use crate::parser; 3 | use crate::rule::{self, Rule}; 4 | use crate::string_cache::{Atom, StringCache}; 5 | use crate::token::{Phrase, PhraseGroup, Token}; 6 | use crate::update::{self, update}; 7 | 8 | use wasm_bindgen::prelude::*; 9 | 10 | use std::collections::hash_map::DefaultHasher; 11 | use std::hash::{Hash, Hasher}; 12 | 13 | #[wasm_bindgen] 14 | pub fn init() { 15 | std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 16 | } 17 | 18 | #[wasm_bindgen] 19 | extern "C" { 20 | #[wasm_bindgen(js_namespace = console)] 21 | fn log(s: &str); 22 | } 23 | 24 | #[wasm_bindgen] 25 | pub struct Context { 26 | throne_context: ThroneContext, 27 | } 28 | 29 | impl From for JsValue { 30 | fn from(e: parser::Error) -> Self { 31 | let js_error = js_sys::Error::new(&format!("{}", e)); 32 | js_sys::Object::define_property( 33 | &js_error, 34 | &JsValue::from("throne_span"), 35 | js_sys::Object::try_from(&JsValue::from(LineColSpanDescriptor { 36 | value: e.pest.line_col.into(), 37 | })) 38 | .unwrap(), 39 | ); 40 | js_error.into() 41 | } 42 | } 43 | 44 | fn js_from_update_result(result: Result<(), update::Error>, rules: &[Rule]) -> Result<(), JsValue> { 45 | match result { 46 | Err(e) => { 47 | let js_error = js_sys::Error::new(&format!("{}", e)); 48 | if let Some(rule_source_span) = e.rule(rules).map(|r| r.source_span) { 49 | js_sys::Object::define_property( 50 | &js_error, 51 | &JsValue::from("throne_span"), 52 | js_sys::Object::try_from(&JsValue::from(LineColSpanDescriptor { 53 | value: rule_source_span.into(), 54 | })) 55 | .unwrap(), 56 | ); 57 | } 58 | Err(js_error.into()) 59 | } 60 | Ok(()) => Ok(()), 61 | } 62 | } 63 | 64 | #[wasm_bindgen] 65 | struct LineColSpanDescriptor { 66 | pub value: LineColSpan, 67 | } 68 | 69 | #[wasm_bindgen] 70 | #[derive(Copy, Clone)] 71 | pub struct LineColSpan { 72 | pub line_start: usize, 73 | pub line_end: usize, 74 | pub col_start: usize, 75 | pub col_end: usize, 76 | } 77 | 78 | impl From for LineColSpan { 79 | fn from(line_col: pest::error::LineColLocation) -> Self { 80 | match line_col { 81 | pest::error::LineColLocation::Pos((line, col)) => LineColSpan { 82 | line_start: line, 83 | line_end: line, 84 | col_start: col, 85 | col_end: col, 86 | }, 87 | pest::error::LineColLocation::Span((line_start, col_start), (line_end, col_end)) => { 88 | LineColSpan { 89 | line_start, 90 | line_end, 91 | col_start, 92 | col_end, 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | impl From for LineColSpan { 100 | fn from(span: rule::LineColSpan) -> Self { 101 | LineColSpan { 102 | line_start: span.line_start, 103 | line_end: span.line_end, 104 | col_start: span.col_start, 105 | col_end: span.col_end, 106 | } 107 | } 108 | } 109 | 110 | #[wasm_bindgen] 111 | impl Context { 112 | pub fn from_text(text: &str) -> Result { 113 | Ok(Context { 114 | throne_context: ThroneContext::from_text(text)?, 115 | }) 116 | } 117 | 118 | pub fn push_state(&mut self, text: &str) { 119 | self.throne_context.push_state(text); 120 | } 121 | 122 | pub fn remove_state_by_first_atom(&mut self, text: &str) { 123 | let atom = self.throne_context.str_to_atom(text); 124 | self.throne_context 125 | .core 126 | .state 127 | .remove_pattern([Some(atom)], false); 128 | } 129 | 130 | pub fn update_with_side_input( 131 | &mut self, 132 | side_input: Option, 133 | ) -> Result<(), JsValue> { 134 | if let Some(side_input) = side_input { 135 | let core = &mut self.throne_context.core; 136 | let string_cache = &mut self.throne_context.string_cache; 137 | let side_input = |phrase: &Phrase| { 138 | let js_phrase = js_value_from_phrase(phrase, string_cache); 139 | let result = side_input.call1(&JsValue::null(), &js_phrase); 140 | match result { 141 | Ok(v) => { 142 | if js_sys::Array::is_array(&v) { 143 | let arr = js_sys::Array::from(&v); 144 | let mut out = vec![phrase[0].clone()]; 145 | for item in arr.iter().skip(1) { 146 | if let Some(s) = item.as_string() { 147 | out.push(Token::new(&s, 0, 0, string_cache)); 148 | } else if let Some(n) = item.as_f64() { 149 | out.push(Token::new_integer(n as i32, 0, 0)); 150 | } else { 151 | return None; 152 | } 153 | } 154 | Some(out.normalize()) 155 | } else { 156 | None 157 | } 158 | } 159 | Err(_) => None, 160 | } 161 | }; 162 | js_from_update_result(update(core, side_input), &core.rules) 163 | } else { 164 | js_from_update_result( 165 | self.throne_context.update(), 166 | &self.throne_context.core.rules, 167 | ) 168 | } 169 | } 170 | 171 | pub fn get_state(&self) -> JsValue { 172 | let string_cache = &self.throne_context.string_cache; 173 | let js_phrases = self 174 | .throne_context 175 | .core 176 | .state 177 | .get_all() 178 | .iter() 179 | .map(|phrase| js_value_from_phrase(phrase, string_cache)) 180 | .collect::(); 181 | JsValue::from(js_phrases) 182 | } 183 | 184 | pub fn get_state_hashes(&self) -> JsValue { 185 | let js_hashes = self 186 | .throne_context 187 | .core 188 | .state 189 | .get_all() 190 | .iter() 191 | .map(|phrase| { 192 | let mut hasher = DefaultHasher::new(); 193 | phrase.hash(&mut hasher); 194 | JsValue::from(hasher.finish().to_string()) 195 | }) 196 | .collect::(); 197 | JsValue::from(js_hashes) 198 | } 199 | 200 | pub fn print(&self) { 201 | log(&format!("{}", self.throne_context)); 202 | } 203 | } 204 | 205 | fn js_value_from_phrase(phrase: &Phrase, string_cache: &StringCache) -> JsValue { 206 | let mut result = vec![]; 207 | for group in phrase.groups() { 208 | if group.len() == 1 { 209 | result.push(js_value_from_atom(group[0].atom, string_cache)); 210 | } else { 211 | result.push(js_value_from_phrase(&group.normalize(), string_cache)); 212 | } 213 | } 214 | 215 | JsValue::from(result.iter().collect::()) 216 | } 217 | 218 | fn js_value_from_atom(atom: Atom, string_cache: &StringCache) -> JsValue { 219 | if let Some(string) = string_cache.atom_to_str(atom) { 220 | JsValue::from(string) 221 | } else if let Some(n) = StringCache::atom_to_integer(atom) { 222 | JsValue::from(n) 223 | } else { 224 | JsValue::null() 225 | } 226 | } 227 | 228 | #[cfg(test)] 229 | mod tests { 230 | use wasm_bindgen_test::wasm_bindgen_test; 231 | 232 | use super::*; 233 | use crate::token::tokenize; 234 | 235 | #[wasm_bindgen_test] 236 | fn test_js_value_from_phrase_nested() { 237 | let mut string_cache = StringCache::new(); 238 | let phrase = tokenize("t1 (t21 (t221 t222 t223) t23) t3", &mut string_cache); 239 | let js_phrase = js_value_from_phrase(&phrase, &string_cache); 240 | assert_eq!( 241 | format!("{:?}", js_phrase), 242 | r#"JsValue(["t1", ["t21", ["t221", "t222", "t223"], "t23"], "t3"])"# 243 | ); 244 | } 245 | 246 | #[wasm_bindgen_test] 247 | fn test_js_value_from_phrase_nested2() { 248 | let mut string_cache = StringCache::new(); 249 | let phrase = tokenize("t1 (t21 (t221 t222 t223))", &mut string_cache); 250 | log(&format!("{:#?}", phrase)); 251 | let js_phrase = js_value_from_phrase(&phrase, &string_cache); 252 | assert_eq!( 253 | format!("{:?}", js_phrase), 254 | r#"JsValue(["t1", ["t21", ["t221", "t222", "t223"]]])"# 255 | ); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Throne](Throne.png) 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/throne.svg)](https://crates.io/crates/throne) 4 | [![Docs Status](https://docs.rs/throne/badge.svg)](https://docs.rs/throne) 5 | 6 | A scripting language for game prototyping and story logic: 7 | 8 | ``` 9 | // Declare the initial state as 'phrases', with one phrase per line. 10 | Mary is sister of David 11 | Sarah is child of Mary 12 | Tom is child of David 13 | 14 | // Define rules with the format: INPUT = OUTPUT. 15 | CHILD is child of PARENT . AUNT is sister of PARENT . 16 | COUSIN is child of AUNT = COUSIN is cousin of CHILD 17 | 18 | // The final state will be: 19 | // Sarah is cousin of Tom 20 | ``` 21 | 22 | ## Motivation 23 | 24 | The original inspiration for Throne comes from languages used to author interactive fiction, such as [Inform](http://inform7.com/). 25 | As described in [this](https://brunodias.dev/2017/05/05/inform-prototyping.html) article, Inform can be used to prototype games from genres besides interactive fiction. By defining gameplay through rules, some of the verbosity of general purpose programming languages can be avoided. However, Inform and other interactive fiction authoring systems are too slow to execute their rules in every frame of a real-time gameplay loop and are difficult to embed in an existing engine. 26 | 27 | Throne allows gameplay logic to be defined through rules and so provides some of the benefits of a rule-based language like Inform, but is also fast to execute and easy to embed in an existing engine. Its rule syntax and mechanics began as simplified versions of those found in the [Ceptre](https://www.cs.cmu.edu/~cmartens/ceptre.pdf) programming language, which was the main influence for the design of Throne. 28 | 29 | ## Examples 30 | - [throne-playground](https://github.com/t-mw/throne-playground) - a web-based editor for Throne, made possible by compiling Throne to WebAssembly. 31 | - [blocks](examples/blocks.throne) - a simple tile matching game run with `cargo run --example blocks`. 32 | - [storylets-rs](https://github.com/t-mw/storylets-rs) - A storylet-based narrative engine for games. 33 | 34 | ## Reference 35 | 36 | Rules are of the format `INPUT = OUTPUT`, where `INPUT` and `OUTPUT` are lists that use period (`.`) as a separator between items: 37 | - `INPUT` is a list of one or more conditions that must pass for the rule to be executed. The conditions can either be state [phrases](#phrases) that must exist or [predicates](#predicates) that must evaluate to true. Any [matching](#matching) state phrases are consumed by the rule on execution. 38 | - `OUTPUT` is a list of state phrases that will be generated by the rule if it is executed. 39 | - Identifiers in rules that use only uppercase letters (`CHILD`, `PARENT`, `AUNT` and `COUSIN` in the snippet above) are variables that will be assigned when the rule is executed. 40 | 41 | Evaluating a Throne script involves executing any rule that matches the current state until the set of matching rules is exhausted. Rules are executed in a random order and may be executed more than once. 42 | 43 | ### Phrases 44 | 45 | The building blocks of a Throne script are phrases, which are collections of arbitrary character sequences (referred to in this section as *atoms*) separated by spaces. In other words, a phrase is a list of atoms separated by spaces. 46 | 47 | A list of phrases completely defines the state of a Throne script. Combined with [predicates](#predicates), lists of phrases also completely define the inputs and outputs of the rules in a Throne script. 48 | 49 | The following are examples of valid phrases: 50 | 51 | | Example | Notes | 52 | | --- | --- | 53 | | `1 2-abC Def_3` | ASCII alphanumeric characters, optionally mixed with dashes and underscores, are the simplest way to define a phrase. This phrase contains three atoms. | 54 | | `"!complex$" "1 2"` | Atoms containing non-ASCII alphanumeric characters, or that should include whitespace, must be double-quoted. This phrase contains two atoms. | 55 | | `foo MATCH_1ST AND-2ND` | Atoms that contain only uppercase ASCII alphanumeric characters, optionally mixed with dashes and underscores, define a variable when included in a rule's list of inputs. This phrase contains three atoms, the last two of which are variables. | 56 | | this (is (a nested) phrase) | Surrounding a set of atoms with parentheses causes them to be nested within the phrase. This affects variable [matching](#matching). | 57 | 58 | ### Predicates 59 | 60 | The following predicates can be used as one of the items in a rule's list of inputs: 61 | 62 | | Syntax | Effect | Example | 63 | | --- | --- | --- | 64 | | `+ X Y Z` | Matches when the sum of `X` and `Y` equals `Z` | `+ HEALTH 10 HEALED` | 65 | | `- X Y Z` | Matches when the sum of `Y` and `Z` equals `X` | `- HEALTH 10 DAMAGED` | 66 | | `< X Y` | Matches when `X` is less than `Y` | `< MONEY 0` | 67 | | `> X Y` | Matches when `X` is greater than `Y` | `> MONEY 0` | 68 | | `<= X Y` | Matches when `X` is less than or equal to `Y` | `<= MONEY 0` | 69 | | `>= X Y` | Matches when `X` is greater than or equal to `Y` | `>= MONEY 0` | 70 | | `% X Y Z` | Matches when the modulo of `X` with `Y` equals `Z` | `% DEGREES 360 REM` | 71 | | `== X Y` | Matches when `X` equals `Y` | `== HEALTH 100` | 72 | | `!X` | Matches when `X` does not exist in the state | `!this does not exist` | 73 | | `^X` | Calls the host application and matches depending on the response | `^os-clock-hour 12` | 74 | 75 | When a predicate accepts two input variables, both variables must be assigned a value for the predicate to produce a match. A value is assigned either by writing a constant inline or by sharing a variable with another of the rule's inputs. 76 | When a predicate accepts three input variables and one of the variables remains unassigned, it will be assigned the expected value according to the effect of the predicate e.g. `A` will be assigned the value `8` in `+ 2 A 10`. 77 | 78 | ### Matching 79 | 80 | Before a rule is executed, each of its inputs must be successfully matched to a unique state phrase. 81 | An input matches a state phrase if they are equal after all variables have been assigned. 82 | 83 | The following are examples of potential matches: 84 | 85 | | Rule Inputs | State Phrases | Outcome | 86 | | --- | --- | --- | 87 | | `health H = ...` | `health 100` | A successful match if `H` is unassigned or was already set to `100` by another input in the rule. `H` is then assigned the value `100`. | 88 | | `health H . health H = ...` | `health 100` | A failed match because only one compatible state phrase exists. | 89 | | `health H . health H = ...` |
health 100
health 200
| A failed match because `H` cannot be set to both `100` and `200`. | 90 | | `health H1 . health H2 = ...` |
health 100
health 200
| A successful match if `H1` and `H2` are unassigned or already assigned compatible values by other inputs in the rule. `H1` is then assigned the value of `100` or `200`, and `H2` is assigned the remaining value. | 91 | | `health H1 . health H2 . + H1 10 H2 = ...` |
health 100
health 200
| A failed match because the predicate cannot be satisfied. | 92 | | `health H = ...` | `health ((min 20) (max 100))` | A successful match if `H` is unassigned or was already set to `(min 20) (max 100)` by another input in the rule. `H` is then assigned the phrase `(min 20) (max 100)`. | 93 | | `health (_ (max H)) = ...` | `health ((min 20) (max 100))` | A successful match if `H` is unassigned or was already set to `100` by another input in the rule. `H` is then assigned the value `100`. `_` is a wildcard that will match anything, in this case the phrase `min 20`. | 94 | 95 | ### Constructs 96 | 97 | Special syntax exists to make it easier to write complex rules, but in the end these constructs compile down to the simple form described in the introduction. The following table lists the available constructs: 98 | 99 | | Syntax | Effect | Example | Compiled Form | 100 | | --- | --- | --- | --- | 101 | | Input phrase prefixed with `$` | Copies the input phrase to the rule output. | `$foo = bar` | `foo = bar . foo` | 102 | | A set of rules surrounded by curly braces prefixed with `INPUT:` where `INPUT` is a list of phrases | Copies `INPUT` to each rule's inputs. |
foo . bar: {
hello = world
123 = 456
}
|
foo . bar . hello = world
foo . bar . 123 = 456
| 103 | | `<<<foo X . < |
foo X . + X 1 Y = Y
foo X . - X 1 B . % B 2 Y = Y
| 104 | 105 | ### The `()` Phrase 106 | 107 | The `()` phrase represents the absence of state. When present in a rule's list of outputs it has no effect, besides making it possible to write rules that produce no output (e.g. `foo = ()`). 108 | When present in a rule's list of inputs it has the effect of producing a match when no other rules can be matched. For example, in the following script the first rule will only ever be matched last: 109 | 110 | ``` 111 | foo // initial state 112 | () . bar = () // matched last 113 | foo = bar // matched first 114 | ``` 115 | 116 | In this way `()` can be used as a form of control flow, overriding the usual random order of rule execution. 117 | 118 | ### Stage Phrases 119 | 120 | Prefixing a phrase with `#` marks it as a 'stage' phrase. 121 | Stage phrases behave in largely the same way as normal phrases, but their presence should be used to indicate how far a script has executed within a sequence of 'stages'. A stage phrase can be included in a rule's inputs to only execute the rule within that stage of the script's execution, and included in a rule's outputs to define the transition to a new stage of the script's execution. 122 | 123 | Stage phrases only differ in their behavior to normal phrases when used as a prefix to a set of curly braces. In this case the stage phrase will be copied to not only the inputs of the rules within the braces, but also the outputs, except when a rule includes `()` as an input phrase. This makes it easy to scope execution of the prefixed set of rules to a stage and finally transition to a second stage once execution of the first stage is complete. 124 | 125 | | Example | Compiled Form | 126 | | --- | --- | 127 | |
#first-stage: {
foo = bar
() = #second-stage
}
|
#first-stage . foo = #first-stage . bar
#first-stage . () = #second-stage
| 128 | 129 | ## Build for WebAssembly 130 | 131 | 1. Run `cargo install wasm-pack` to install [wasm-pack](https://github.com/rustwasm/wasm-pack). 132 | 1. Run `npm install ; npm start` in this directory. 133 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::matching::phrase_equal; 2 | use crate::string_cache::{Atom, StringCache}; 3 | use crate::token::*; 4 | 5 | use rand::{rngs::SmallRng, seq::SliceRandom}; 6 | 7 | use std::ops::Range; 8 | 9 | /// References a [Phrase] in a [State]. 10 | #[derive(Clone, Copy, Eq, PartialEq, Debug)] 11 | pub struct PhraseId { 12 | idx: usize, 13 | rev: usize, 14 | } 15 | 16 | /// Stores a set of [Phrases](Phrase). 17 | #[derive(Clone, Debug)] 18 | pub struct State { 19 | storage: Storage, 20 | match_cache: MatchCache, 21 | scratch_state: Option, 22 | } 23 | 24 | impl State { 25 | pub(crate) fn new() -> State { 26 | State { 27 | storage: Storage::new(), 28 | match_cache: MatchCache::new(), 29 | scratch_state: None, 30 | } 31 | } 32 | 33 | /// Removes a phrase by [PhraseId]. 34 | pub fn remove(&mut self, id: PhraseId) { 35 | assert!(id.rev == self.storage.rev); 36 | self.remove_idx(id.idx); 37 | } 38 | 39 | pub(crate) fn remove_idx(&mut self, idx: usize) { 40 | assert!(!self.is_locked()); 41 | 42 | let remove_phrase = self.storage.phrase_ranges.swap_remove(idx); 43 | self.storage 44 | .removed_phrase_ranges 45 | .push(remove_phrase.token_range); 46 | 47 | self.storage.rev += 1; 48 | } 49 | 50 | /// Removes the first occurrence of `phrase` from the state. 51 | /// 52 | /// Returns `false` if the phrase could not be found. 53 | pub fn remove_phrase(&mut self, phrase: &Phrase) -> bool { 54 | let remove_idx = 55 | self.storage 56 | .phrase_ranges 57 | .iter() 58 | .position(|PhraseMetadata { token_range, .. }| { 59 | phrase_equal( 60 | &self.storage.tokens[token_range.clone()], 61 | phrase, 62 | (0, 0), 63 | (0, 0), 64 | ) 65 | }); 66 | 67 | if let Some(remove_idx) = remove_idx { 68 | self.remove_idx(remove_idx); 69 | true 70 | } else { 71 | false 72 | } 73 | } 74 | 75 | /// Removes any phrases matching the provided `pattern`. 76 | /// 77 | /// If `match_pattern_length` is `true`, only phrases matching the exact length of the provided 78 | /// `pattern` will be removed. Otherwise, phrases longer than the provided `pattern` may be removed, 79 | /// if their beginning subset matches the pattern. 80 | pub fn remove_pattern( 81 | &mut self, 82 | pattern: [Option; N], 83 | match_pattern_length: bool, 84 | ) { 85 | assert!(!self.is_locked()); 86 | 87 | let tokens = &mut self.storage.tokens; 88 | let removed_phrase_ranges = &mut self.storage.removed_phrase_ranges; 89 | let mut did_remove_tokens = false; 90 | 91 | self.storage 92 | .phrase_ranges 93 | .retain(|PhraseMetadata { token_range, .. }| { 94 | let phrase = &tokens[token_range.clone()]; 95 | if !test_phrase_pattern_match(phrase, pattern, match_pattern_length) { 96 | return true; 97 | } 98 | 99 | removed_phrase_ranges.push(token_range.clone()); 100 | did_remove_tokens = true; 101 | 102 | false 103 | }); 104 | 105 | if did_remove_tokens { 106 | self.storage.rev += 1; 107 | } 108 | } 109 | 110 | pub(crate) fn clear_removed_tokens(&mut self) { 111 | self.storage 112 | .removed_phrase_ranges 113 | .sort_unstable_by_key(|range| std::cmp::Reverse(range.start)); 114 | for remove_range in self.storage.removed_phrase_ranges.drain(..) { 115 | let remove_len = remove_range.end - remove_range.start; 116 | self.storage 117 | .tokens 118 | .drain(remove_range.start..remove_range.end); 119 | for PhraseMetadata { token_range, .. } in self.storage.phrase_ranges.iter_mut() { 120 | if token_range.start >= remove_range.end { 121 | token_range.start -= remove_len; 122 | token_range.end -= remove_len; 123 | } 124 | } 125 | } 126 | } 127 | 128 | pub(crate) fn update_cache(&mut self) { 129 | self.match_cache.update_storage(&self.storage); 130 | } 131 | 132 | pub(crate) fn match_cached_state_indices_for_rule_input( 133 | &self, 134 | input_phrase: &Phrase, 135 | input_phrase_group_count: usize, 136 | ) -> &[usize] { 137 | assert!(self.match_cache.storage_rev == self.storage.rev); 138 | debug_assert_eq!(input_phrase.groups().count(), input_phrase_group_count); 139 | self.match_cache 140 | .match_rule_input(input_phrase, input_phrase_group_count) 141 | } 142 | 143 | pub(crate) fn shuffle(&mut self, rng: &mut SmallRng) { 144 | assert!(self.scratch_state.is_none()); 145 | self.storage.phrase_ranges.shuffle(rng); 146 | self.storage.rev += 1; 147 | } 148 | 149 | /// Adds a new `phrase` to the `State` and returns a [PhraseId] referencing the newly added phrase. 150 | pub fn push(&mut self, phrase: Vec) -> PhraseId { 151 | let group_count = phrase.groups().count(); 152 | self.push_with_metadata(phrase, group_count) 153 | } 154 | 155 | pub(crate) fn push_with_metadata( 156 | &mut self, 157 | mut phrase: Vec, 158 | group_count: usize, 159 | ) -> PhraseId { 160 | let first_group_is_single_token = phrase[0].open_depth == 1; 161 | let first_atom = if first_group_is_single_token && is_concrete_pred(&phrase) { 162 | Some(phrase[0].atom) 163 | } else { 164 | None 165 | }; 166 | 167 | let start = self.storage.tokens.len(); 168 | self.storage.tokens.append(&mut phrase); 169 | let end = self.storage.tokens.len(); 170 | 171 | self.storage.phrase_ranges.push(PhraseMetadata { 172 | token_range: Range { start, end }, 173 | first_atom, 174 | group_count, 175 | }); 176 | self.storage.rev += 1; 177 | 178 | let id = PhraseId { 179 | idx: self.storage.phrase_ranges.len() - 1, 180 | rev: self.storage.rev, 181 | }; 182 | 183 | id 184 | } 185 | 186 | /// Returns the number of phrases in the `State`. 187 | pub fn len(&self) -> usize { 188 | self.storage.phrase_ranges.len() 189 | } 190 | 191 | /// Returns an iterator of references to phrases in the `State`. 192 | pub fn iter(&self) -> impl Iterator + '_ { 193 | self.storage.iter() 194 | } 195 | 196 | /// Returns the [Phrase] referenced by the provided [PhraseId]. 197 | pub fn get(&self, id: PhraseId) -> &Phrase { 198 | self.storage.get(id) 199 | } 200 | 201 | /// Constructs and returns a [Vec] of all phrases in the `State`. 202 | pub fn get_all(&self) -> Vec> { 203 | self.storage 204 | .phrase_ranges 205 | .iter() 206 | .map(|PhraseMetadata { token_range, .. }| { 207 | self.storage.tokens[token_range.clone()].to_vec() 208 | }) 209 | .collect::>() 210 | } 211 | 212 | /// Returns an iterator of references to phrases matching the provided `pattern`. 213 | /// 214 | /// If `match_pattern_length` is `true`, only phrases matching the exact length of the provided 215 | /// `pattern` will be returned. Otherwise, phrases longer than the provided `pattern` may be returned, 216 | /// if their beginning subset matches the pattern. 217 | pub fn iter_pattern( 218 | &self, 219 | pattern: [Option; N], 220 | match_pattern_length: bool, 221 | ) -> impl Iterator + '_ { 222 | self.iter().filter(move |phrase_id| { 223 | test_phrase_pattern_match(self.get(*phrase_id), pattern, match_pattern_length) 224 | }) 225 | } 226 | 227 | #[cfg(test)] 228 | pub(crate) fn from_phrases(phrases: &[Vec]) -> State { 229 | let mut state = State::new(); 230 | for p in phrases { 231 | state.push(p.clone()); 232 | } 233 | state 234 | } 235 | 236 | pub(crate) fn lock_scratch(&mut self) { 237 | self.scratch_state = Some(ScratchState { 238 | storage_phrase_ranges_len: self.storage.phrase_ranges.len(), 239 | storage_tokens_len: self.storage.tokens.len(), 240 | storage_rev: self.storage.rev, 241 | }); 242 | } 243 | 244 | pub(crate) fn unlock_scratch(&mut self) { 245 | self.reset_scratch(); 246 | self.scratch_state = None; 247 | } 248 | 249 | pub(crate) fn reset_scratch(&mut self) { 250 | let ScratchState { 251 | storage_phrase_ranges_len, 252 | storage_tokens_len, 253 | storage_rev, 254 | .. 255 | } = self.scratch_state.as_ref().expect("scratch_state"); 256 | self.storage 257 | .phrase_ranges 258 | .drain(storage_phrase_ranges_len..); 259 | self.storage.tokens.drain(storage_tokens_len..); 260 | self.storage.rev = *storage_rev; 261 | } 262 | 263 | fn is_locked(&self) -> bool { 264 | self.scratch_state.is_some() 265 | } 266 | } 267 | 268 | impl std::ops::Index for State { 269 | type Output = [Token]; 270 | 271 | fn index(&self, i: usize) -> &Phrase { 272 | self.storage.get_by_metadata(&self.storage.phrase_ranges[i]) 273 | } 274 | } 275 | 276 | impl std::fmt::Display for State { 277 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 278 | write!(f, "{:?}", self.get_all()) 279 | } 280 | } 281 | 282 | #[derive(Clone, Debug)] 283 | struct ScratchState { 284 | storage_phrase_ranges_len: usize, 285 | storage_tokens_len: usize, 286 | storage_rev: usize, 287 | } 288 | 289 | #[derive(Clone, Debug)] 290 | struct Storage { 291 | // indexes into token collection 292 | phrase_ranges: Vec, 293 | removed_phrase_ranges: Vec>, 294 | 295 | // collection of all tokens found in the state phrases 296 | tokens: Vec, 297 | 298 | // increments on mutation 299 | rev: usize, 300 | } 301 | 302 | impl Storage { 303 | fn new() -> Self { 304 | Storage { 305 | phrase_ranges: vec![], 306 | removed_phrase_ranges: vec![], 307 | tokens: vec![], 308 | rev: 0, 309 | } 310 | } 311 | 312 | fn iter<'a>(&'a self) -> impl Iterator + 'a { 313 | let rev = self.rev; 314 | self.phrase_ranges 315 | .iter() 316 | .enumerate() 317 | .map(move |(idx, _)| PhraseId { idx, rev }) 318 | } 319 | 320 | fn get(&self, id: PhraseId) -> &Phrase { 321 | assert!(id.rev == self.rev); 322 | self.get_by_metadata(&self.phrase_ranges[id.idx]) 323 | } 324 | 325 | fn get_by_metadata(&self, metadata: &PhraseMetadata) -> &Phrase { 326 | &self.tokens[metadata.token_range.clone()] 327 | } 328 | } 329 | 330 | #[derive(Clone, Debug)] 331 | struct PhraseMetadata { 332 | token_range: Range, 333 | first_atom: Option, 334 | group_count: usize, 335 | } 336 | 337 | #[derive(Clone, Debug)] 338 | struct MatchCache { 339 | first_atom_pairs: Vec<(Atom, usize)>, 340 | first_atom_indices: Vec, 341 | state_indices_by_length: Vec>, 342 | storage_rev: usize, 343 | } 344 | 345 | impl MatchCache { 346 | fn new() -> Self { 347 | MatchCache { 348 | first_atom_pairs: vec![], 349 | first_atom_indices: vec![], 350 | state_indices_by_length: vec![], 351 | storage_rev: 0, 352 | } 353 | } 354 | 355 | fn clear(&mut self) { 356 | self.first_atom_pairs.clear(); 357 | self.first_atom_indices.clear(); 358 | self.state_indices_by_length.clear(); 359 | } 360 | 361 | fn update_storage(&mut self, storage: &Storage) { 362 | if self.storage_rev == storage.rev { 363 | return; 364 | } 365 | self.storage_rev = storage.rev; 366 | 367 | self.clear(); 368 | for (s_i, phrase_metadata) in storage.phrase_ranges.iter().enumerate() { 369 | if let Some(first_atom) = phrase_metadata.first_atom { 370 | self.first_atom_pairs.push((first_atom, s_i)); 371 | } 372 | if self.state_indices_by_length.len() < phrase_metadata.group_count + 1 { 373 | self.state_indices_by_length 374 | .resize(phrase_metadata.group_count + 1, vec![]); 375 | } 376 | self.state_indices_by_length[phrase_metadata.group_count].push(s_i); 377 | } 378 | self.first_atom_pairs.sort_unstable_by(|a, b| a.0.cmp(&b.0)); 379 | for (_, s_i) in &self.first_atom_pairs { 380 | self.first_atom_indices.push(*s_i); 381 | } 382 | } 383 | 384 | fn match_rule_input(&self, input_phrase: &Phrase, input_phrase_group_count: usize) -> &[usize] { 385 | let first_group_is_single_token = input_phrase[0].open_depth == 1; 386 | if first_group_is_single_token && is_concrete_pred(input_phrase) { 387 | let input_first_atom = input_phrase[0].atom; 388 | if let Ok(idx) = self 389 | .first_atom_pairs 390 | .binary_search_by(|(atom, _)| atom.cmp(&input_first_atom)) 391 | { 392 | // binary search won't always find the first match, 393 | // so search backwards until we find it 394 | let start_idx = self 395 | .first_atom_pairs 396 | .iter() 397 | .enumerate() 398 | .rev() 399 | .skip(self.first_atom_pairs.len() - 1 - idx) 400 | .take_while(|(_, (atom, _))| *atom == input_first_atom) 401 | .last() 402 | .expect("start idx") 403 | .0; 404 | let end_idx = self 405 | .first_atom_pairs 406 | .iter() 407 | .enumerate() 408 | .skip(idx) 409 | .take_while(|(_, (atom, _))| *atom == input_first_atom) 410 | .last() 411 | .expect("end idx") 412 | .0; 413 | return &self.first_atom_indices[start_idx..end_idx + 1]; 414 | } else { 415 | return &[]; 416 | }; 417 | } 418 | 419 | if let Some(v) = &self.state_indices_by_length.get(input_phrase_group_count) { 420 | v 421 | } else { 422 | &[] 423 | } 424 | } 425 | } 426 | 427 | pub(crate) fn state_to_string(state: &State, string_cache: &StringCache) -> String { 428 | state 429 | .iter() 430 | .map(|phrase_id| state.get(phrase_id).to_string(string_cache)) 431 | .collect::>() 432 | .join("\n") 433 | } 434 | -------------------------------------------------------------------------------- /src/token.rs: -------------------------------------------------------------------------------- 1 | use crate::string_cache::*; 2 | 3 | use regex::Regex; 4 | 5 | use std::hash::Hash; 6 | 7 | pub(crate) const WILDCARD_DUMMY_PREFIX: &str = "WILDCARD"; 8 | 9 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 10 | pub enum TokenFlag { 11 | None, 12 | Variable, 13 | Side, 14 | BackwardsPred(BackwardsPred), 15 | } 16 | 17 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 18 | pub enum BackwardsPred { 19 | Plus, 20 | Minus, 21 | Lt, 22 | Gt, 23 | Lte, 24 | Gte, 25 | ModNeg, 26 | Equal, 27 | Custom, 28 | } 29 | 30 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 31 | pub struct Token { 32 | pub atom: Atom, 33 | pub is_negated: bool, 34 | pub is_consuming: bool, 35 | pub flag: TokenFlag, 36 | pub open_depth: u8, 37 | pub close_depth: u8, 38 | } 39 | 40 | fn are_var_chars(mut chars: impl Iterator) -> bool { 41 | chars 42 | .next() 43 | .map(|c| c.is_ascii_uppercase()) 44 | .unwrap_or(false) 45 | && chars.all(|c| c.is_numeric() || !c.is_ascii_lowercase()) 46 | } 47 | 48 | fn is_wildcard_token(token: &str) -> bool { 49 | if token == "_" { 50 | return true; 51 | } 52 | 53 | let mut chars = token.chars(); 54 | chars.next() == Some('_') && are_var_chars(chars) 55 | } 56 | 57 | impl Token { 58 | pub fn new( 59 | string: &str, 60 | open_depth: u8, 61 | close_depth: u8, 62 | string_cache: &mut StringCache, 63 | ) -> Token { 64 | let mut string = string; 65 | 66 | let is_consuming = if let Some('?') = string.chars().next() { 67 | string = string.get(1..).expect("get"); 68 | false 69 | } else { 70 | true 71 | }; 72 | 73 | let mut is_negated = false; 74 | let mut is_side = false; 75 | match string.chars().next() { 76 | Some('!') => { 77 | is_negated = true; 78 | string = string.get(1..).expect("get"); 79 | } 80 | Some('^') => { 81 | is_side = true; 82 | string = string.get(1..).expect("get"); 83 | } 84 | _ => {} 85 | } 86 | 87 | let is_var = are_var_chars(string.chars()); 88 | 89 | let mut chars = string.chars(); 90 | let first_char = chars.next(); 91 | let second_char = string.chars().nth(1); 92 | 93 | let backwards_pred = match (first_char.unwrap_or(' '), second_char.unwrap_or(' ')) { 94 | ('<', '<') => Some(BackwardsPred::Custom), 95 | ('%', _) => Some(BackwardsPred::ModNeg), 96 | ('<', '=') => Some(BackwardsPred::Lte), 97 | ('>', '=') => Some(BackwardsPred::Gte), 98 | ('<', _) => Some(BackwardsPred::Lt), 99 | ('>', _) => Some(BackwardsPred::Gt), 100 | ('+', _) => Some(BackwardsPred::Plus), 101 | ('-', _) => Some(BackwardsPred::Minus), 102 | ('=', '=') => Some(BackwardsPred::Equal), 103 | 104 | _ => None, 105 | }; 106 | 107 | let atom = string_cache.str_to_atom(string); 108 | 109 | let flag = match (is_var, is_side, backwards_pred) { 110 | (false, false, None) => TokenFlag::None, 111 | (true, false, None) => TokenFlag::Variable, 112 | (false, true, None) => TokenFlag::Side, 113 | (false, false, Some(v)) => TokenFlag::BackwardsPred(v), 114 | _ => unreachable!(), 115 | }; 116 | 117 | Token { 118 | atom, 119 | flag, 120 | is_negated, 121 | is_consuming, 122 | open_depth, 123 | close_depth, 124 | } 125 | } 126 | 127 | pub(crate) fn new_atom(atom: Atom, open_depth: u8, close_depth: u8) -> Token { 128 | Token { 129 | atom, 130 | flag: TokenFlag::None, 131 | is_negated: false, 132 | is_consuming: true, 133 | open_depth, 134 | close_depth, 135 | } 136 | } 137 | 138 | pub(crate) fn new_integer(n: i32, open_depth: u8, close_depth: u8) -> Token { 139 | Token { 140 | atom: StringCache::integer_to_atom(n), 141 | flag: TokenFlag::None, 142 | is_negated: false, 143 | is_consuming: true, 144 | open_depth, 145 | close_depth, 146 | } 147 | } 148 | 149 | pub fn as_str<'a>(&self, string_cache: &'a StringCache) -> Option<&'a str> { 150 | string_cache.atom_to_str(self.atom) 151 | } 152 | 153 | pub fn as_integer(&self) -> Option { 154 | StringCache::atom_to_integer(self.atom) 155 | } 156 | 157 | pub fn to_string(&self, string_cache: &StringCache) -> String { 158 | self.as_str(string_cache) 159 | .map(|s| s.to_string()) 160 | .or_else(|| self.as_integer().map(|n| n.to_string())) 161 | .expect("to_string") 162 | } 163 | } 164 | 165 | /// Converts the provided `string` to a [Phrase]. 166 | pub fn tokenize(string: &str, string_cache: &mut StringCache) -> Vec { 167 | assert!(!string.is_empty()); 168 | let mut string = string.to_string(); 169 | 170 | // create iterator for strings surrounded by backticks 171 | lazy_static! { 172 | static ref RE1: Regex = Regex::new(r#""(.*?)""#).unwrap(); 173 | } 174 | 175 | let string1 = string.clone(); 176 | let mut strings = RE1 177 | .captures_iter(&string1) 178 | .map(|c| c.get(1).expect("string_capture").as_str()); 179 | 180 | string = RE1.replace_all(&string, "\"").to_string(); 181 | 182 | // split into tokens 183 | lazy_static! { 184 | static ref RE2: Regex = Regex::new(r"\(|\)|\s+|[^\(\)\s]+").unwrap(); 185 | } 186 | 187 | let tokens = RE2 188 | .find_iter(&string) 189 | .map(|m| m.as_str()) 190 | .filter(|s| !s.trim().is_empty()) 191 | .collect::>(); 192 | 193 | let mut result = vec![]; 194 | 195 | let mut open_depth = 0; 196 | let mut close_depth = 0; 197 | 198 | for (i, token) in tokens.iter().enumerate() { 199 | if *token == "(" { 200 | open_depth += 1; 201 | continue; 202 | } 203 | 204 | if *token == ")" { 205 | continue; 206 | } 207 | 208 | for t in tokens.iter().skip(i + 1) { 209 | if *t == ")" { 210 | close_depth += 1; 211 | } else { 212 | break; 213 | } 214 | } 215 | 216 | if *token == "\"" { 217 | let atom = string_cache.str_to_atom(strings.next().expect("string")); 218 | result.push(Token::new_atom(atom, open_depth, close_depth)); 219 | } else if is_wildcard_token(token) { 220 | let var_string = format!("{}{}", WILDCARD_DUMMY_PREFIX, string_cache.wildcard_counter); 221 | string_cache.wildcard_counter += 1; 222 | 223 | result.push(Token::new( 224 | &var_string, 225 | open_depth, 226 | close_depth, 227 | string_cache, 228 | )); 229 | } else { 230 | result.push(Token::new(token, open_depth, close_depth, string_cache)); 231 | } 232 | open_depth = 0; 233 | close_depth = 0; 234 | } 235 | 236 | // phrases should always contain tokens with a depth > 0. single variable phrases are an 237 | // exception to this rule, because they should be able to match whole state phrases. 238 | if !(result.len() == 1 && is_var_token(&result[0])) { 239 | result.first_mut().unwrap().open_depth += 1; 240 | result.last_mut().unwrap().close_depth += 1; 241 | } 242 | 243 | result 244 | } 245 | 246 | /// A sequence of [Tokens](Token) representing a phrase, usually produced using [tokenize]. 247 | /// 248 | /// The owned representation of a `Phrase` is `Vec`. 249 | /// A `Phrase` occurs as a [State](crate::State) item or as an input or output item in a [Rule](crate::Rule). 250 | pub type Phrase = [Token]; 251 | pub(crate) type VecPhrase = Vec; 252 | 253 | pub trait PhraseGroup { 254 | fn groups(&self) -> PhraseGroupIterator<'_>; 255 | fn groups_at_depth(&self, depth: u8) -> PhraseGroupIterator<'_>; 256 | fn normalize(&self) -> Vec; 257 | } 258 | 259 | impl PhraseGroup for Phrase { 260 | fn groups(&self) -> PhraseGroupIterator<'_> { 261 | self.groups_at_depth(1) 262 | } 263 | 264 | fn groups_at_depth(&self, depth: u8) -> PhraseGroupIterator<'_> { 265 | PhraseGroupIterator { 266 | phrase: self, 267 | idx: 0, 268 | counter: PhraseGroupCounter::new_at_depth(depth), 269 | } 270 | } 271 | 272 | fn normalize(&self) -> Vec { 273 | let len = self.len(); 274 | if len == 0 { 275 | return vec![]; 276 | } 277 | 278 | let mut vec = self.to_vec(); 279 | 280 | let mut interior_depth: i32 = 0; 281 | let mut min_interior_depth: i32 = 0; 282 | for (i, t) in vec.iter().enumerate() { 283 | if i == 0 { 284 | interior_depth -= t.close_depth as i32; 285 | } else if i == vec.len() - 1 { 286 | interior_depth += t.open_depth as i32; 287 | } else { 288 | interior_depth += t.open_depth as i32; 289 | interior_depth -= t.close_depth as i32; 290 | } 291 | 292 | min_interior_depth = interior_depth.min(min_interior_depth); 293 | } 294 | 295 | vec[0].open_depth = 1 + (-min_interior_depth) as u8; 296 | vec[len - 1].close_depth = 1 + (interior_depth - min_interior_depth) as u8; 297 | 298 | return vec; 299 | } 300 | } 301 | 302 | pub struct PhraseGroupIterator<'a> { 303 | phrase: &'a Phrase, 304 | idx: usize, 305 | counter: PhraseGroupCounter, 306 | } 307 | 308 | impl<'a> Iterator for PhraseGroupIterator<'a> { 309 | type Item = &'a Phrase; 310 | 311 | fn next(&mut self) -> Option { 312 | for (i, t) in self.phrase.iter().enumerate().skip(self.idx) { 313 | if let Some(start_idx) = self.counter.count(t) { 314 | self.idx = i + 1; 315 | return Some(&self.phrase[start_idx..i + 1]); 316 | } 317 | } 318 | 319 | None 320 | } 321 | } 322 | 323 | pub(crate) struct PhraseGroupCounter { 324 | depth: u8, 325 | at_depth: u8, 326 | idx: usize, 327 | start_idx: Option, 328 | pub group_count: usize, 329 | } 330 | 331 | impl PhraseGroupCounter { 332 | pub(crate) fn new() -> Self { 333 | Self::new_at_depth(1) 334 | } 335 | 336 | fn new_at_depth(depth: u8) -> Self { 337 | PhraseGroupCounter { 338 | depth: 0, 339 | at_depth: depth, 340 | idx: 0, 341 | start_idx: None, 342 | group_count: 0, 343 | } 344 | } 345 | 346 | pub(crate) fn count(&mut self, token: &Token) -> Option { 347 | self.depth += token.open_depth; 348 | if self.start_idx.is_none() && self.depth >= self.at_depth { 349 | self.start_idx = Some(self.idx); 350 | } 351 | self.idx += 1; 352 | self.depth -= token.close_depth; 353 | if self.depth <= self.at_depth { 354 | if let Some(start_idx) = self.start_idx.take() { 355 | self.group_count += 1; 356 | return Some(start_idx); 357 | } 358 | } 359 | None 360 | } 361 | } 362 | 363 | #[inline] 364 | pub(crate) fn token_equal( 365 | a: &Token, 366 | b: &Token, 367 | ignore_depth: bool, 368 | a_depth_diffs: Option<(u8, u8)>, 369 | b_depth_diffs: Option<(u8, u8)>, 370 | ) -> bool { 371 | let a_depth_diffs = a_depth_diffs.unwrap_or((0, 0)); 372 | let b_depth_diffs = b_depth_diffs.unwrap_or((0, 0)); 373 | 374 | a.atom == b.atom 375 | && a.is_negated == b.is_negated 376 | && (ignore_depth 377 | || (a.open_depth as i32 - a_depth_diffs.0 as i32 378 | == b.open_depth as i32 - b_depth_diffs.0 as i32 379 | && a.close_depth as i32 - a_depth_diffs.1 as i32 380 | == b.close_depth as i32 - b_depth_diffs.1 as i32)) 381 | } 382 | 383 | pub(crate) fn is_concrete_token(token: &Token) -> bool { 384 | token.flag == TokenFlag::None 385 | } 386 | 387 | pub(crate) fn is_var_token(token: &Token) -> bool { 388 | token.flag == TokenFlag::Variable 389 | } 390 | 391 | pub(crate) fn is_backwards_pred(tokens: &Phrase) -> bool { 392 | matches!(tokens[0].flag, TokenFlag::BackwardsPred(_)) 393 | } 394 | 395 | pub(crate) fn is_side_pred(tokens: &Phrase) -> bool { 396 | tokens[0].flag == TokenFlag::Side 397 | } 398 | 399 | pub(crate) fn is_negated_pred(tokens: &Phrase) -> bool { 400 | tokens[0].is_negated 401 | } 402 | 403 | pub(crate) fn is_concrete_pred(tokens: &Phrase) -> bool { 404 | !is_negated_pred(tokens) && is_concrete_token(&tokens[0]) 405 | } 406 | 407 | pub(crate) fn is_var_pred(tokens: &Phrase) -> bool { 408 | !is_negated_pred(tokens) && is_var_token(&tokens[0]) 409 | } 410 | 411 | pub(crate) fn normalize_match_phrase( 412 | variable_token: &Token, 413 | mut match_phrase: Vec, 414 | ) -> Vec { 415 | let len = match_phrase.len(); 416 | match_phrase[0].is_negated = variable_token.is_negated; 417 | match_phrase[0].open_depth += variable_token.open_depth; 418 | match_phrase[len - 1].close_depth += variable_token.close_depth; 419 | match_phrase 420 | } 421 | 422 | pub trait PhraseString { 423 | fn to_string(&self, string_cache: &StringCache) -> String; 424 | } 425 | 426 | impl PhraseString for Phrase { 427 | fn to_string(&self, string_cache: &StringCache) -> String { 428 | phrase_to_string(self, string_cache) 429 | } 430 | } 431 | 432 | pub(crate) fn phrase_to_string(phrase: &Phrase, string_cache: &StringCache) -> String { 433 | let mut tokens = vec![]; 434 | 435 | for t in phrase { 436 | let mut string = String::new(); 437 | 438 | if let Some(s) = t.as_str(string_cache) { 439 | if s.chars().any(|c| c.is_whitespace()) { 440 | string += &format!("\"{}\"", s); 441 | } else { 442 | string += s; 443 | } 444 | } else { 445 | string += &t.as_integer().expect("integer").to_string(); 446 | } 447 | 448 | tokens.push(format!( 449 | "{}{}{}{}{}", 450 | String::from("(").repeat(t.open_depth as usize), 451 | if !t.is_consuming { "?" } else { "" }, 452 | if t.is_negated { 453 | "!" 454 | } else if t.flag == TokenFlag::Side { 455 | "^" 456 | } else { 457 | "" 458 | }, 459 | string, 460 | String::from(")").repeat(t.close_depth as usize) 461 | )); 462 | } 463 | 464 | tokens.join(" ") 465 | } 466 | 467 | pub(crate) fn test_phrase_pattern_match( 468 | phrase: &Phrase, 469 | pattern: [Option; N], 470 | match_pattern_length: bool, 471 | ) -> bool { 472 | if phrase.len() < N || (match_pattern_length && phrase.len() != N) { 473 | return false; 474 | } 475 | if pattern 476 | .iter() 477 | .enumerate() 478 | .any(|(i, atom)| atom.filter(|atom| phrase[i].atom != *atom).is_some()) 479 | { 480 | return false; 481 | } 482 | true 483 | } 484 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::matching::*; 2 | use crate::rule::{LineColSpan, Rule, RuleBuilder}; 3 | use crate::string_cache::{Atom, StringCache}; 4 | use crate::token::*; 5 | 6 | use pest::iterators::Pair; 7 | use pest::Parser; 8 | use rand::rngs::SmallRng; 9 | use rand::Rng; 10 | 11 | use std::collections::HashMap; 12 | use std::fmt; 13 | 14 | pub const QUI: &str = "qui"; 15 | 16 | pub struct ParseResult { 17 | pub rules: Vec, 18 | pub state: Vec>, 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct Error { 23 | pub pest: pest::error::Error, 24 | } 25 | 26 | impl From> for Error { 27 | fn from(pest: pest::error::Error) -> Self { 28 | Error { pest } 29 | } 30 | } 31 | 32 | impl std::error::Error for Error {} 33 | 34 | impl fmt::Display for Error { 35 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 36 | write!(f, "{}", self.pest) 37 | } 38 | } 39 | 40 | pub mod generated { 41 | #[derive(Parser)] 42 | #[grammar = "throne.pest"] 43 | pub struct Parser; 44 | } 45 | 46 | pub(crate) fn parse( 47 | text: &str, 48 | mut string_cache: &mut StringCache, 49 | rng: &mut SmallRng, 50 | ) -> Result { 51 | let text = text.replace("()", QUI); 52 | 53 | let file = generated::Parser::parse(generated::Rule::file, &text)? 54 | .next() 55 | .unwrap(); 56 | 57 | let mut state: Vec> = vec![]; 58 | let mut rule_builders: Vec<(RuleBuilder, bool)> = vec![]; 59 | let mut backwards_preds: Vec<(VecPhrase, Vec)> = vec![]; 60 | let mut enable_unused_warnings = true; 61 | 62 | for line in file.into_inner() { 63 | match line.as_rule() { 64 | generated::Rule::prefixed => { 65 | let prefix_block_source_span: LineColSpan = line.as_span().into(); 66 | 67 | let mut prefixed = line.into_inner(); 68 | let prefix_inputs_pair = prefixed.next().unwrap(); 69 | 70 | let mut any_input_qui = false; 71 | for pair in prefixed { 72 | let (r, has_input_qui) = pair_to_throne_rule( 73 | pair, 74 | Some(prefix_inputs_pair.clone()), 75 | &mut string_cache, 76 | enable_unused_warnings, 77 | ); 78 | rule_builders.push((r, enable_unused_warnings)); 79 | 80 | if has_input_qui { 81 | any_input_qui = true; 82 | } 83 | } 84 | 85 | // add PREFIX . () = () if there's no other way out from 86 | // the prefix block to avoid infinite loops. 87 | if !any_input_qui { 88 | let mut inputs = vec![]; 89 | let mut outputs = vec![]; 90 | add_prefix_inputs_pair_to_inputs_outputs( 91 | prefix_inputs_pair, 92 | &mut inputs, 93 | &mut outputs, 94 | &mut string_cache, 95 | false, 96 | ); 97 | inputs.push(tokenize(QUI, string_cache)); 98 | 99 | // source span covers entire prefix block since this rule doesn't exist in source 100 | rule_builders.push(( 101 | RuleBuilder::new(inputs, outputs, prefix_block_source_span), 102 | enable_unused_warnings, 103 | )); 104 | } 105 | } 106 | generated::Rule::backwards_def => { 107 | check_rule_variables_for_pair(line.clone(), enable_unused_warnings); 108 | 109 | let mut backwards_def = line.into_inner(); 110 | 111 | let mut first_phrase = 112 | tokenize(backwards_def.next().unwrap().as_str(), &mut string_cache); 113 | 114 | let mut var_replacements = 115 | replace_variables(&mut first_phrase, &mut string_cache, None, rng); 116 | 117 | let mut other_phrases = backwards_def 118 | .map(|phrase| tokenize(phrase.as_str(), &mut string_cache)) 119 | .collect::>(); 120 | 121 | for phrase in &mut other_phrases { 122 | var_replacements = 123 | replace_variables(phrase, &mut string_cache, Some(var_replacements), rng); 124 | } 125 | 126 | backwards_preds.push((first_phrase, other_phrases)); 127 | } 128 | generated::Rule::rule => { 129 | rule_builders.push(( 130 | pair_to_throne_rule(line, None, &mut string_cache, enable_unused_warnings).0, 131 | enable_unused_warnings, 132 | )); 133 | } 134 | generated::Rule::state => { 135 | for phrase in line.into_inner() { 136 | state.push(tokenize(phrase.as_str(), &mut string_cache)); 137 | } 138 | } 139 | generated::Rule::compiler_disable_unused_warnings => { 140 | enable_unused_warnings = false; 141 | } 142 | generated::Rule::compiler_enable_unused_warnings => { 143 | enable_unused_warnings = true; 144 | } 145 | generated::Rule::EOI => (), 146 | _ => unreachable!("{}", line), 147 | } 148 | } 149 | 150 | let mut new_rule_builders = vec![]; 151 | for (rule, enable_unused_warnings_for_rule) in &rule_builders { 152 | if let Some(mut replaced_rules) = replace_backwards_preds( 153 | &rule, 154 | &backwards_preds, 155 | &string_cache, 156 | *enable_unused_warnings_for_rule, 157 | ) { 158 | new_rule_builders.append(&mut replaced_rules); 159 | } 160 | } 161 | 162 | let new_rules = new_rule_builders 163 | .into_iter() 164 | .enumerate() 165 | .map(|(i, builder)| builder.build(i as i32)) 166 | .collect(); 167 | 168 | Ok(ParseResult { 169 | rules: new_rules, 170 | state, 171 | }) 172 | } 173 | 174 | fn pair_to_throne_rule( 175 | rule_pair: Pair, 176 | prefix_inputs_pair: Option>, 177 | string_cache: &mut StringCache, 178 | enable_unused_warnings: bool, 179 | ) -> (RuleBuilder, bool) { 180 | let rule_pair_clone = rule_pair.clone(); 181 | let source_span: LineColSpan = rule_pair.as_span().into(); 182 | 183 | let mut pairs = rule_pair.into_inner(); 184 | let inputs_pair = pairs.next().unwrap(); 185 | let outputs_pair = pairs.next().unwrap(); 186 | 187 | let mut inputs = vec![]; 188 | let mut outputs = vec![]; 189 | 190 | let input_pairs = inputs_pair.into_inner(); 191 | let has_input_qui = input_pairs.clone().any(|p| { 192 | let input_phrase = p.into_inner().next().unwrap(); 193 | input_phrase.as_rule() == generated::Rule::qui 194 | }); 195 | 196 | if let Some(prefix_inputs_pair) = prefix_inputs_pair { 197 | add_prefix_inputs_pair_to_inputs_outputs( 198 | prefix_inputs_pair, 199 | &mut inputs, 200 | &mut outputs, 201 | string_cache, 202 | !has_input_qui, 203 | ); 204 | } 205 | 206 | for p in input_pairs { 207 | let input_phrase = p.into_inner().next().unwrap(); 208 | add_input_phrase_pair_to_inputs_outputs( 209 | input_phrase, 210 | &mut inputs, 211 | &mut outputs, 212 | string_cache, 213 | false, 214 | ); 215 | } 216 | 217 | for p in outputs_pair.into_inner() { 218 | let output_phrase = p.into_inner().next().unwrap(); 219 | 220 | match output_phrase.as_rule() { 221 | generated::Rule::qui => (), 222 | _ => outputs.push(tokenize(output_phrase.as_str(), string_cache)), 223 | } 224 | } 225 | 226 | check_rule_variables( 227 | &inputs, 228 | &outputs, 229 | rule_pair_clone, 230 | enable_unused_warnings, 231 | string_cache, 232 | ); 233 | ( 234 | RuleBuilder::new(inputs, outputs, source_span), 235 | has_input_qui, 236 | ) 237 | } 238 | 239 | fn add_prefix_inputs_pair_to_inputs_outputs( 240 | prefix_inputs_pair: Pair, 241 | inputs: &mut Vec, 242 | outputs: &mut Vec, 243 | string_cache: &mut StringCache, 244 | copy_stage_phrase: bool, 245 | ) { 246 | // insert stages at beginning of rule input, so that 'first atoms' optimization is effective 247 | let prefix_input_pairs = prefix_inputs_pair.into_inner(); 248 | for p in prefix_input_pairs.clone() { 249 | let input_phrase = p.into_inner().next().unwrap(); 250 | if input_phrase.as_rule() == generated::Rule::stage_phrase { 251 | add_input_phrase_pair_to_inputs_outputs( 252 | input_phrase, 253 | inputs, 254 | outputs, 255 | string_cache, 256 | copy_stage_phrase, 257 | ); 258 | } 259 | } 260 | 261 | for p in prefix_input_pairs { 262 | let input_phrase = p.into_inner().next().unwrap(); 263 | if input_phrase.as_rule() != generated::Rule::stage_phrase { 264 | add_input_phrase_pair_to_inputs_outputs( 265 | input_phrase, 266 | inputs, 267 | outputs, 268 | string_cache, 269 | copy_stage_phrase, 270 | ); 271 | } 272 | } 273 | } 274 | 275 | fn add_input_phrase_pair_to_inputs_outputs( 276 | input_phrase: Pair, 277 | inputs: &mut Vec, 278 | outputs: &mut Vec, 279 | string_cache: &mut StringCache, 280 | copy_stage_phrase: bool, 281 | ) { 282 | match input_phrase.as_rule() { 283 | generated::Rule::copy_phrase => { 284 | let copy_phrase = tokenize( 285 | input_phrase.into_inner().next().unwrap().as_str(), 286 | string_cache, 287 | ); 288 | 289 | inputs.push(copy_phrase.clone()); 290 | outputs.push(copy_phrase); 291 | } 292 | // stage phrases have the special behavior of acting as copy phrases when used as 293 | // prefixes, except when the prefixed rule includes a qui. 294 | generated::Rule::stage_phrase => { 295 | let stage_phrase = tokenize(input_phrase.as_str(), string_cache); 296 | if copy_stage_phrase { 297 | outputs.push(stage_phrase.clone()); 298 | } 299 | inputs.push(stage_phrase); 300 | } 301 | _ => { 302 | inputs.push(tokenize(input_phrase.as_str(), string_cache)); 303 | } 304 | } 305 | } 306 | 307 | // for each backwards predicate, replace it with the corresponding phrase 308 | fn replace_backwards_preds( 309 | rule: &RuleBuilder, 310 | backwards_preds: &Vec<(VecPhrase, Vec)>, 311 | string_cache: &StringCache, 312 | enable_unused_warnings: bool, 313 | ) -> Option> { 314 | let mut backwards_preds_per_input = vec![vec![]; rule.inputs.len()]; 315 | let mut backwards_pred_pointers = vec![0; rule.inputs.len()]; 316 | 317 | for (i_i, input) in rule.inputs.iter().enumerate() { 318 | if input[0].flag == TokenFlag::BackwardsPred(BackwardsPred::Custom) { 319 | let mut matched = false; 320 | 321 | for (b_i, (first_phrase, _)) in backwards_preds.iter().enumerate() { 322 | if match_variables_twoway(input, first_phrase, &mut vec![]) { 323 | backwards_preds_per_input[i_i].push(b_i); 324 | matched = true; 325 | } 326 | } 327 | 328 | if !matched { 329 | if enable_unused_warnings { 330 | println!("WARNING: backwards predicate in rule did not match '{}'. Check that the backwards predicate is defined.", rule.to_string(string_cache)); 331 | } 332 | return None; 333 | } 334 | } 335 | } 336 | 337 | let first = backwards_preds_per_input.iter().position(|v| v.len() > 0); 338 | let last = backwards_preds_per_input 339 | .iter() 340 | .rev() 341 | .position(|v| v.len() > 0) 342 | .map(|idx| backwards_preds_per_input.len() - 1 - idx); 343 | 344 | let backwards_pred_input_range = match (first, last) { 345 | (Some(first), Some(last)) => (first, last), 346 | // no backwards predicates in rule 347 | _ => return Some(vec![rule.clone()]), 348 | }; 349 | 350 | let mut new_rule_builders = vec![]; 351 | 'outer: loop { 352 | let mut nonvariable_matches: Vec = vec![]; 353 | let mut matches: Vec = vec![]; 354 | 355 | let mut matched = true; 356 | 357 | for (i_i, input) in rule.inputs.iter().enumerate() { 358 | let backwards_preds_for_input = &backwards_preds_per_input[i_i]; 359 | if backwards_preds_for_input.len() > 0 { 360 | let backwards_pred_pointer = backwards_pred_pointers[i_i]; 361 | let (first_phrase, _) = 362 | &backwards_preds[backwards_preds_for_input[backwards_pred_pointer]]; 363 | 364 | // Match variables from rule input b.p. to b.p. definition first phrase, 365 | // ignoring matches that have already been made, unless those matches 366 | // were to non-variables in the backwards predicate definition. 367 | // 368 | // i.e. Given <>(); 392 | } 393 | } 394 | 395 | if matched { 396 | let mut new_inputs = vec![]; 397 | 398 | for (i_i, input) in rule.inputs.iter().enumerate() { 399 | let backwards_preds_for_input = &backwards_preds_per_input[i_i]; 400 | if backwards_preds_for_input.len() > 0 { 401 | let backwards_pred_pointer = backwards_pred_pointers[i_i]; 402 | let (first_phrase, other_phrases) = 403 | &backwards_preds[backwards_preds_for_input[backwards_pred_pointer]]; 404 | 405 | let complete_input_phrase = assign_vars(input, &nonvariable_matches); 406 | let mut inverse_matches = vec![]; 407 | 408 | match_variables_assuming_compatible_structure( 409 | &first_phrase, 410 | &complete_input_phrase, 411 | &mut inverse_matches, 412 | ); 413 | 414 | for phrase in other_phrases { 415 | // replace variable names in backwards predicate phrase 416 | // with variable names from original rule 417 | let complete_phrase = assign_vars(phrase, &inverse_matches); 418 | new_inputs.push(complete_phrase); 419 | } 420 | } else { 421 | new_inputs.push(assign_vars(input, &nonvariable_matches)); 422 | } 423 | } 424 | 425 | let mut new_rule = rule.clone(); 426 | 427 | new_rule.inputs = new_inputs; 428 | new_rule.outputs = rule 429 | .outputs 430 | .iter() 431 | .map(|output| assign_vars(output, &nonvariable_matches)) 432 | .collect(); 433 | new_rule_builders.push(new_rule); 434 | } 435 | 436 | // find next permutation of backwards predicates in rule 437 | for i in (backwards_pred_input_range.0..=backwards_pred_input_range.1).rev() { 438 | let backwards_preds_for_input = &backwards_preds_per_input[i]; 439 | 440 | if backwards_preds_for_input.len() == 0 { 441 | continue; 442 | } 443 | 444 | let at_end = backwards_pred_pointers[i] == backwards_preds_for_input.len() - 1; 445 | if at_end && i == backwards_pred_input_range.0 { 446 | // finished checking all permutations 447 | break 'outer; 448 | } 449 | 450 | if at_end { 451 | backwards_pred_pointers[i] = 0; 452 | } else { 453 | backwards_pred_pointers[i] += 1; 454 | break; 455 | } 456 | } 457 | } 458 | 459 | Some(new_rule_builders) 460 | } 461 | 462 | // replace all variable tokens in a phrase with unique tokens, 463 | // optionally using any existing mappings provided 464 | fn replace_variables( 465 | phrase: &mut Vec, 466 | string_cache: &mut StringCache, 467 | existing_map: Option>, 468 | rng: &mut SmallRng, 469 | ) -> HashMap { 470 | let mut existing_map = existing_map.unwrap_or(HashMap::new()); 471 | 472 | for token in phrase { 473 | if token.flag != TokenFlag::Variable { 474 | continue; 475 | } 476 | 477 | if let Some(replacement) = existing_map.get(&token.atom) { 478 | token.atom = *replacement; 479 | } else { 480 | loop { 481 | let s = string_cache.atom_to_str(token.atom).unwrap(); 482 | let replacement_s = format!("{}_BACK{}{}", s, rng.gen::(), rng.gen::()); 483 | let replacement = string_cache.str_to_atom(&replacement_s); 484 | 485 | if existing_map.contains_key(&replacement) { 486 | continue; 487 | } 488 | 489 | existing_map.insert(token.atom, replacement); 490 | token.atom = replacement; 491 | break; 492 | } 493 | } 494 | } 495 | 496 | existing_map 497 | } 498 | 499 | fn check_rule_variables( 500 | inputs: &Vec>, 501 | outputs: &Vec>, 502 | pair: Pair, 503 | enable_unused_warnings: bool, 504 | string_cache: &StringCache, 505 | ) { 506 | if !enable_unused_warnings { 507 | return; 508 | } 509 | 510 | let rule_str = pair.as_str(); 511 | 512 | let mut var_counts = HashMap::new(); 513 | for token in inputs 514 | .iter() 515 | .chain(outputs.iter()) 516 | .flatten() 517 | .filter(|t| is_var_token(t)) 518 | { 519 | let count = var_counts 520 | .entry(token.as_str(string_cache).unwrap()) 521 | .or_insert(0); 522 | *count += 1; 523 | } 524 | 525 | for (var_name, count) in &var_counts { 526 | if !var_name.starts_with(WILDCARD_DUMMY_PREFIX) && *count == 1 { 527 | println!("WARNING: {} was only used once in '{}'. Check for errors or replace with a wildcard.", var_name, rule_str); 528 | } 529 | } 530 | } 531 | 532 | fn check_rule_variables_for_pair(pair: Pair, enable_unused_warnings: bool) { 533 | if !enable_unused_warnings { 534 | return; 535 | } 536 | 537 | let rule_str = pair.as_str(); 538 | let inner = pair.into_inner(); 539 | let mut var_counts = HashMap::new(); 540 | 541 | for p in inner.flatten() { 542 | if let generated::Rule::atom_var = p.as_rule() { 543 | let count = var_counts.entry(p.as_str()).or_insert(0); 544 | *count += 1; 545 | } 546 | } 547 | 548 | for (var_name, count) in &var_counts { 549 | if !var_name.starts_with(WILDCARD_DUMMY_PREFIX) && *count == 1 { 550 | println!("WARNING: {} was only used once in '{}'. Check for errors or replace with a wildcard.", var_name, rule_str); 551 | } 552 | } 553 | } 554 | 555 | impl From> for LineColSpan { 556 | fn from(span: pest::Span) -> Self { 557 | let start_line_col = span.start_pos().line_col(); 558 | let end_line_col = span.end_pos().line_col(); 559 | LineColSpan { 560 | line_start: start_line_col.0, 561 | line_end: end_line_col.0, 562 | col_start: start_line_col.1, 563 | col_end: end_line_col.1, 564 | } 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /src/matching.rs: -------------------------------------------------------------------------------- 1 | use crate::rule::{Rule, RuleBuilder}; 2 | use crate::state::State; 3 | use crate::string_cache::Atom; 4 | use crate::token::*; 5 | use crate::update::SideInput; 6 | 7 | use std::fmt; 8 | 9 | const EXCESSIVE_PERMUTATION_LIMIT: usize = 2000; 10 | 11 | #[derive(Debug)] 12 | pub struct ExcessivePermutationError { 13 | pub rule_id: i32, 14 | } 15 | 16 | impl std::error::Error for ExcessivePermutationError {} 17 | 18 | impl fmt::Display for ExcessivePermutationError { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | write!(f, 21 | "Rule {} caused > {} state permutations to be checked. Review the complexity of the rule.", 22 | self.rule_id, EXCESSIVE_PERMUTATION_LIMIT 23 | ) 24 | } 25 | } 26 | 27 | pub fn test_match_without_variables(input_tokens: &Phrase, pred_tokens: &Phrase) -> Option { 28 | let mut matcher = NoVariablesMatcher { has_var: false }; 29 | 30 | if base_match(input_tokens, pred_tokens, &mut matcher) { 31 | Some(matcher.has_var) 32 | } else { 33 | None 34 | } 35 | } 36 | 37 | pub fn match_variables_twoway( 38 | tokens1: &Phrase, 39 | tokens2: &Phrase, 40 | existing_matches_and_result: &mut Vec, 41 | ) -> bool { 42 | let initial_matches_len = existing_matches_and_result.len(); 43 | 44 | let mut matcher = TwoWayMatcher { 45 | existing_matches_and_result, 46 | initial_matches_len, 47 | }; 48 | 49 | base_match(tokens1, tokens2, &mut matcher) 50 | } 51 | 52 | trait BaseMatcher { 53 | fn is_twoway_matcher(&self) -> bool; 54 | fn do_match(&mut self, token: &Token, phrase: &Phrase) -> bool; 55 | } 56 | 57 | struct NoVariablesMatcher { 58 | has_var: bool, 59 | } 60 | 61 | impl BaseMatcher for NoVariablesMatcher { 62 | fn is_twoway_matcher(&self) -> bool { 63 | false 64 | } 65 | 66 | fn do_match(&mut self, _token: &Token, _phrase: &Phrase) -> bool { 67 | self.has_var = true; 68 | true 69 | } 70 | } 71 | 72 | struct TwoWayMatcher<'a> { 73 | existing_matches_and_result: &'a mut Vec, 74 | initial_matches_len: usize, 75 | } 76 | 77 | impl BaseMatcher for TwoWayMatcher<'_> { 78 | fn is_twoway_matcher(&self) -> bool { 79 | true 80 | } 81 | 82 | fn do_match(&mut self, token: &Token, phrase: &Phrase) -> bool { 83 | let variable_already_matched = if let Some(ref existing_match) = self 84 | .existing_matches_and_result 85 | .iter() 86 | .find(|m| m.atom == token.atom) 87 | { 88 | if !phrase_equal( 89 | &existing_match.phrase[..], 90 | phrase, 91 | existing_match.depths, 92 | (token.open_depth, token.close_depth), 93 | ) { 94 | // this match of the variable conflicted with an existing match 95 | self.existing_matches_and_result 96 | .drain(self.initial_matches_len..); 97 | return false; 98 | } 99 | 100 | true 101 | } else { 102 | false 103 | }; 104 | 105 | if !variable_already_matched { 106 | self.existing_matches_and_result 107 | .push(Match::new(token, phrase.to_vec())); 108 | } 109 | 110 | true 111 | } 112 | } 113 | 114 | fn base_match(tokens1: &Phrase, tokens2: &Phrase, matcher: &mut impl BaseMatcher) -> bool { 115 | let mut token1_iter = tokens1.iter(); 116 | let mut token2_iter = tokens2.iter(); 117 | 118 | let mut depth1 = 0; 119 | let mut depth2 = 0; 120 | 121 | let mut token1_i = 0; 122 | let mut token2_i = 0; 123 | 124 | loop { 125 | let token1 = token1_iter.next(); 126 | let token2 = token2_iter.next(); 127 | 128 | match (token1, token2) { 129 | (None, None) => break, 130 | (Some(_), None) => return false, 131 | (None, Some(_)) => return false, 132 | (Some(token1), Some(token2)) => { 133 | depth1 += token1.open_depth; 134 | depth2 += token2.open_depth; 135 | 136 | let is_var1 = is_var_token(token1); 137 | let is_var2 = is_var_token(token2) && matcher.is_twoway_matcher(); 138 | 139 | if !is_var1 && !is_var2 { 140 | if token1.atom != token2.atom || depth1 != depth2 { 141 | return false; 142 | } 143 | 144 | depth1 -= token1.close_depth; 145 | depth2 -= token2.close_depth; 146 | } else if is_var1 && is_var2 { 147 | if depth1 != depth2 { 148 | return false; 149 | } 150 | 151 | depth1 -= token1.close_depth; 152 | depth2 -= token2.close_depth; 153 | } else if is_var1 { 154 | depth2 -= token2.close_depth; 155 | 156 | // colect tokens to assign to the input variable 157 | let start_i = token2_i; 158 | 159 | while depth1 < depth2 { 160 | if let Some(token2) = token2_iter.next() { 161 | depth2 += token2.open_depth; 162 | depth2 -= token2.close_depth; 163 | 164 | token2_i += 1; 165 | } else { 166 | return false; 167 | } 168 | } 169 | 170 | let end_i = token2_i + 1; 171 | 172 | if !matcher.do_match(token1, &tokens2[start_i..end_i]) { 173 | return false; 174 | } 175 | 176 | depth1 -= token1.close_depth; 177 | } else if is_var2 { 178 | depth1 -= token1.close_depth; 179 | 180 | // colect tokens to assign to the input variable 181 | let start_i = token1_i; 182 | 183 | while depth2 < depth1 { 184 | if let Some(token1) = token1_iter.next() { 185 | depth1 += token1.open_depth; 186 | depth1 -= token1.close_depth; 187 | 188 | token1_i += 1; 189 | } else { 190 | return false; 191 | } 192 | } 193 | 194 | let end_i = token1_i + 1; 195 | 196 | if !matcher.do_match(token2, &tokens1[start_i..end_i]) { 197 | return false; 198 | } 199 | 200 | depth2 -= token2.close_depth; 201 | } 202 | 203 | token1_i += 1; 204 | token2_i += 1; 205 | } 206 | } 207 | } 208 | 209 | true 210 | } 211 | 212 | #[derive(Clone, Eq, PartialEq, Debug)] 213 | pub struct MatchLite { 214 | pub var_atom: Atom, 215 | pub var_open_close_depth: (u8, u8), 216 | pub var_open_close_depth_norm: (u8, u8), 217 | pub state_i: usize, 218 | pub state_token_range: (usize, usize), 219 | } 220 | 221 | impl MatchLite { 222 | fn as_slice<'a>(&self, state: &'a State) -> &'a [Token] { 223 | &state[self.state_i][self.state_token_range.0..self.state_token_range.1] 224 | } 225 | 226 | pub fn to_phrase(&self, state: &State) -> Vec { 227 | let mut phrase = self.as_slice(state).to_vec(); 228 | 229 | let subset_len = phrase.len(); 230 | let source_len = state[self.state_i].len(); 231 | 232 | if subset_len < source_len { 233 | // if the phrase subset overlaps with the beginning/end of the source phrase, remove the 234 | // implicit open/close depth of the source phrase, since we are moving this subset into a 235 | // new phrase. 236 | if self.state_token_range.0 == 0 { 237 | phrase[0].open_depth -= 1; 238 | } 239 | if self.state_token_range.1 == source_len { 240 | phrase[subset_len - 1].close_depth -= 1; 241 | } 242 | } 243 | 244 | // use the variable open depth as the baseline for the new phrase subset 245 | phrase[0].open_depth -= self.var_open_close_depth_norm.0; 246 | 247 | // calculate close depth required so that sum(open_depth - close_depth) == 0 248 | let mut depth = 0; 249 | for i in 0..subset_len - 1 { 250 | depth += phrase[i].open_depth; 251 | depth -= phrase[i].close_depth; 252 | } 253 | depth += phrase[subset_len - 1].open_depth; 254 | phrase[subset_len - 1].close_depth = depth; 255 | 256 | phrase 257 | } 258 | } 259 | 260 | #[derive(Clone, Eq, PartialEq, Debug)] 261 | pub struct Match { 262 | pub atom: Atom, 263 | depths: (u8, u8), 264 | pub phrase: Vec, 265 | } 266 | 267 | impl Match { 268 | fn new(token: &Token, mut phrase: Vec) -> Match { 269 | let len = phrase.len(); 270 | let depths = (token.open_depth, token.close_depth); 271 | 272 | if len == 1 { 273 | phrase[0].open_depth = 0; 274 | phrase[0].close_depth = 0; 275 | } else { 276 | phrase[0].open_depth -= depths.0; 277 | phrase[len - 1].close_depth -= depths.1; 278 | } 279 | 280 | Match { 281 | atom: token.atom, 282 | depths, 283 | phrase: phrase, 284 | } 285 | } 286 | } 287 | 288 | pub fn match_state_variables_with_existing( 289 | input_tokens: &Phrase, 290 | state: &State, 291 | s_i: usize, 292 | existing_matches_and_result: &mut Vec, 293 | ) -> bool { 294 | if let Some(has_var) = test_match_without_variables(input_tokens, &state[s_i]) { 295 | if has_var { 296 | match_state_variables_assuming_compatible_structure( 297 | input_tokens, 298 | state, 299 | s_i, 300 | existing_matches_and_result, 301 | ) 302 | } else { 303 | true 304 | } 305 | } else { 306 | false 307 | } 308 | } 309 | 310 | pub fn match_state_variables_assuming_compatible_structure( 311 | input_tokens: &Phrase, 312 | state: &State, 313 | state_i: usize, 314 | existing_matches_and_result: &mut Vec, 315 | ) -> bool { 316 | let pred_tokens = &state[state_i]; 317 | 318 | debug_assert!(test_match_without_variables(input_tokens, pred_tokens).is_some()); 319 | 320 | let existing_matches_len = existing_matches_and_result.len(); 321 | 322 | let mut pred_token_i = 0; 323 | 324 | let mut input_depth = 0; 325 | let mut pred_depth = 0; 326 | 327 | for (token_i, token) in input_tokens.iter().enumerate() { 328 | let pred_token = &pred_tokens[pred_token_i]; 329 | pred_token_i += 1; 330 | 331 | input_depth += token.open_depth; 332 | pred_depth += pred_token.open_depth; 333 | 334 | let is_var = is_var_token(token); 335 | 336 | if is_var { 337 | pred_depth -= pred_token.close_depth; 338 | 339 | // colect tokens to assign to the input variable 340 | let start_i = pred_token_i - 1; 341 | 342 | while input_depth < pred_depth { 343 | let pred_token = &pred_tokens[pred_token_i]; 344 | pred_token_i += 1; 345 | 346 | pred_depth += pred_token.open_depth; 347 | pred_depth -= pred_token.close_depth; 348 | } 349 | 350 | let end_i = pred_token_i; 351 | 352 | let variable_already_matched = if let Some(ref existing_match) = 353 | existing_matches_and_result 354 | .iter() 355 | .find(|m| m.var_atom == token.atom) 356 | { 357 | if !phrase_equal( 358 | &existing_match.as_slice(state), 359 | &pred_tokens[start_i..end_i], 360 | existing_match.var_open_close_depth, 361 | (token.open_depth, token.close_depth), 362 | ) { 363 | // this match of the variable conflicted with an existing match 364 | existing_matches_and_result.drain(existing_matches_len..); 365 | return false; 366 | } 367 | 368 | true 369 | } else { 370 | false 371 | }; 372 | 373 | if !variable_already_matched { 374 | // remove the implicit open/close depth of the source phrase 375 | let normalized_depths = ( 376 | if token_i == 0 { 377 | token.open_depth - 1 378 | } else { 379 | token.open_depth 380 | }, 381 | if token_i == input_tokens.len() - 1 { 382 | token.close_depth - 1 383 | } else { 384 | token.close_depth 385 | }, 386 | ); 387 | let m = MatchLite { 388 | var_atom: token.atom, 389 | var_open_close_depth: (token.open_depth, token.close_depth), 390 | var_open_close_depth_norm: normalized_depths, 391 | state_i, 392 | state_token_range: (start_i, end_i), 393 | }; 394 | 395 | existing_matches_and_result.push(m); 396 | } 397 | } else { 398 | pred_depth -= pred_token.close_depth; 399 | } 400 | 401 | input_depth -= token.close_depth; 402 | } 403 | 404 | true 405 | } 406 | 407 | pub fn match_variables_assuming_compatible_structure( 408 | input_tokens: &Phrase, 409 | pred_tokens: &Phrase, 410 | existing_matches_and_result: &mut Vec, 411 | ) -> bool { 412 | assert!(test_match_without_variables(input_tokens, pred_tokens).is_some()); 413 | 414 | let existing_matches_len = existing_matches_and_result.len(); 415 | 416 | let mut pred_token_i = 0; 417 | 418 | let mut input_depth = 0; 419 | let mut pred_depth = 0; 420 | 421 | for token in input_tokens { 422 | let pred_token = &pred_tokens[pred_token_i]; 423 | pred_token_i += 1; 424 | 425 | input_depth += token.open_depth; 426 | pred_depth += pred_token.open_depth; 427 | 428 | let is_var = is_var_token(token); 429 | 430 | if is_var { 431 | pred_depth -= pred_token.close_depth; 432 | 433 | // colect tokens to assign to the input variable 434 | let start_i = pred_token_i - 1; 435 | 436 | while input_depth < pred_depth { 437 | let pred_token = &pred_tokens[pred_token_i]; 438 | pred_token_i += 1; 439 | 440 | pred_depth += pred_token.open_depth; 441 | pred_depth -= pred_token.close_depth; 442 | } 443 | 444 | let end_i = pred_token_i; 445 | 446 | let variable_already_matched = if let Some(ref existing_match) = 447 | existing_matches_and_result 448 | .iter() 449 | .find(|m| m.atom == token.atom) 450 | { 451 | if !phrase_equal( 452 | &existing_match.phrase[..], 453 | &pred_tokens[start_i..end_i], 454 | existing_match.depths, 455 | (token.open_depth, token.close_depth), 456 | ) { 457 | // this match of the variable conflicted with an existing match 458 | existing_matches_and_result.drain(existing_matches_len..); 459 | return false; 460 | } 461 | 462 | true 463 | } else { 464 | false 465 | }; 466 | 467 | if !variable_already_matched { 468 | let phrase = pred_tokens[start_i..end_i].to_vec(); 469 | existing_matches_and_result.push(Match::new(token, phrase)); 470 | } 471 | } else { 472 | pred_depth -= pred_token.close_depth; 473 | } 474 | 475 | input_depth -= token.close_depth; 476 | } 477 | 478 | true 479 | } 480 | 481 | // Checks whether the rule's forward and backward predicates match the state. 482 | // Returns a new rule with all variables resolved, with backwards/side 483 | // predicates removed. 484 | pub(crate) fn rule_matches_state( 485 | r: &Rule, 486 | state: &mut State, 487 | side_input: &mut F, 488 | ) -> Result, ExcessivePermutationError> 489 | where 490 | F: SideInput, 491 | { 492 | state.update_cache(); 493 | 494 | let inputs = &r.inputs; 495 | let outputs = &r.outputs; 496 | 497 | // per input, a list of states that could match the input 498 | let input_state_matches = if let Some(matches) = 499 | gather_potential_input_state_matches(inputs, &r.input_phrase_group_counts, state) 500 | { 501 | matches 502 | } else { 503 | return Ok(None); 504 | }; 505 | 506 | // precompute values required for deriving branch indices. 507 | let mut input_rev_permutation_counts = vec![1; input_state_matches.potential_matches.len()]; 508 | let mut permutation_count = 1; 509 | input_state_matches 510 | .potential_matches 511 | .iter() 512 | .enumerate() 513 | .rev() 514 | .for_each(|(i, InputStateMatch { states, .. })| { 515 | permutation_count *= states.len(); 516 | 517 | if i > 0 { 518 | input_rev_permutation_counts[i - 1] = permutation_count; 519 | } 520 | }); 521 | 522 | if permutation_count > EXCESSIVE_PERMUTATION_LIMIT { 523 | return Err(ExcessivePermutationError { rule_id: r.id }); 524 | } 525 | 526 | // we'll use state as a scratchpad for other token allocations 527 | state.lock_scratch(); 528 | 529 | 'outer: for p_i in 0..permutation_count { 530 | state.reset_scratch(); 531 | 532 | let mut variables_matched = input_state_matches.definite_matched_variables.clone(); 533 | 534 | if !test_inputs_with_permutation( 535 | p_i, 536 | inputs, 537 | state, 538 | &input_state_matches, 539 | &input_rev_permutation_counts, 540 | &mut variables_matched, 541 | side_input, 542 | ) { 543 | continue 'outer; 544 | } 545 | 546 | let mut forward_concrete = vec![]; 547 | let mut outputs_concrete = vec![]; 548 | 549 | let mut input_phrase_group_counts = vec![]; 550 | inputs 551 | .iter() 552 | .filter(|pred| is_concrete_pred(pred) || is_var_pred(pred)) 553 | .for_each(|v| { 554 | let mut group_counter = PhraseGroupCounter::new(); 555 | forward_concrete.push(assign_state_vars( 556 | v, 557 | state, 558 | &variables_matched, 559 | &mut group_counter, 560 | )); 561 | input_phrase_group_counts.push(group_counter.group_count); 562 | }); 563 | 564 | let mut output_phrase_group_counts = vec![]; 565 | outputs.iter().for_each(|v| { 566 | if is_side_pred(v) { 567 | let pred = 568 | assign_state_vars(v, state, &variables_matched, &mut PhraseGroupCounter::new()); 569 | side_input(&pred); 570 | } else { 571 | let mut group_counter = PhraseGroupCounter::new(); 572 | outputs_concrete.push(assign_state_vars( 573 | v, 574 | state, 575 | &variables_matched, 576 | &mut group_counter, 577 | )); 578 | output_phrase_group_counts.push(group_counter.group_count); 579 | } 580 | }); 581 | 582 | state.unlock_scratch(); 583 | 584 | return Ok(Some(RuleMatchesStateResult { 585 | rule: RuleBuilder::new(forward_concrete, outputs_concrete, r.source_span) 586 | .input_phrase_group_counts(input_phrase_group_counts) 587 | .build(r.id), 588 | output_phrase_group_counts, 589 | })); 590 | } 591 | 592 | state.unlock_scratch(); 593 | 594 | Ok(None) 595 | } 596 | 597 | #[derive(Debug)] 598 | pub(crate) struct RuleMatchesStateResult { 599 | pub rule: Rule, 600 | pub output_phrase_group_counts: Vec, 601 | } 602 | 603 | #[derive(Debug)] 604 | struct InputStateMatches { 605 | potential_matches: Vec, 606 | definite_matched_variables: Vec, 607 | initial_states_matched_bool: Vec, 608 | } 609 | 610 | #[derive(Debug)] 611 | struct InputStateMatch { 612 | i_i: usize, 613 | has_var: bool, 614 | states: Vec, 615 | } 616 | 617 | impl InputStateMatch { 618 | fn test_final_match( 619 | &self, 620 | state_match_idx: usize, 621 | inputs: &Vec>, 622 | state: &State, 623 | states_matched_bool: &mut [bool], 624 | variables_matched: &mut Vec, 625 | ) -> bool { 626 | let s_i = self.states[state_match_idx]; 627 | let input_phrase = &inputs[self.i_i]; 628 | 629 | // a previous input in the permutation has already matched the state being checked 630 | if input_phrase[0].is_consuming { 631 | if states_matched_bool[s_i] { 632 | return false; 633 | } else { 634 | states_matched_bool[s_i] = true; 635 | } 636 | } 637 | 638 | // we should know that the structures are compatible from earlier matching checks 639 | !self.has_var 640 | || match_state_variables_assuming_compatible_structure( 641 | input_phrase, 642 | state, 643 | s_i, 644 | variables_matched, 645 | ) 646 | } 647 | } 648 | 649 | fn gather_potential_input_state_matches( 650 | inputs: &Vec>, 651 | input_phrase_group_counts: &Vec, 652 | state: &State, 653 | ) -> Option { 654 | // only matches that have a structure that is compatible with the input should be returned from 655 | // this method, i.e. only variable assignments are preventing the exact matches from being known. 656 | let mut potential_matches = vec![]; // inputs that could not be inexpensively matched to a single state 657 | let mut multiple_matches = vec![]; // inputs that may yet be inexpensively matched to a single state 658 | let mut single_matches = vec![]; // inputs that have been matched to a single state 659 | 660 | for (i_i, input) in inputs.iter().enumerate() { 661 | if input.len() == 1 && is_var_pred(input) { 662 | // treat input with a single variable as a special case that can match any state 663 | let states = state.iter().enumerate().map(|(i, _)| i).collect::>(); 664 | potential_matches.push(InputStateMatch { 665 | i_i, 666 | has_var: true, 667 | states, 668 | }); 669 | continue; 670 | } 671 | 672 | if !is_concrete_pred(input) && !is_var_pred(input) { 673 | continue; 674 | } 675 | 676 | let cached_state_matches = 677 | state.match_cached_state_indices_for_rule_input(input, input_phrase_group_counts[i_i]); 678 | 679 | let mut has_var = false; 680 | let mut states = vec![]; 681 | for s_i in cached_state_matches { 682 | if let Some(match_has_var) = test_match_without_variables(input, &state[*s_i]) { 683 | if match_has_var { 684 | has_var = match_has_var; 685 | } 686 | states.push(*s_i); 687 | } 688 | } 689 | 690 | if states.len() == 0 { 691 | return None; 692 | } else if states.len() == 1 { 693 | single_matches.push(InputStateMatch { 694 | i_i, 695 | has_var, 696 | states, 697 | }); 698 | } else { 699 | multiple_matches.push(InputStateMatch { 700 | i_i, 701 | has_var, 702 | states, 703 | }); 704 | } 705 | } 706 | 707 | // immediately match phrases that could only match a single state, to 708 | // reduce number of permutations that need to be checked later on. 709 | let mut definite_matched_variables = vec![]; 710 | let mut initial_states_matched_bool = vec![false; state.len()]; 711 | 712 | for input_state_match in &single_matches { 713 | if !input_state_match.test_final_match( 714 | 0, 715 | inputs, 716 | state, 717 | &mut initial_states_matched_bool, 718 | &mut definite_matched_variables, 719 | ) { 720 | return None; 721 | } 722 | } 723 | 724 | // having gathered the variables for all initial single matches, eliminate 725 | // any other matches that have now become single matches. 726 | if definite_matched_variables.len() > 0 { 727 | for input_state_match in multiple_matches { 728 | let mut state_single_match_idx = None; 729 | 730 | if input_state_match.has_var { 731 | let input_phrase = &inputs[input_state_match.i_i]; 732 | let existing_matches_len = definite_matched_variables.len(); 733 | 734 | for (state_match_idx, s_i) in input_state_match.states.iter().enumerate() { 735 | if match_state_variables_assuming_compatible_structure( 736 | input_phrase, 737 | state, 738 | *s_i, 739 | &mut definite_matched_variables, 740 | ) { 741 | definite_matched_variables.drain(existing_matches_len..); 742 | 743 | if state_single_match_idx.is_some() { 744 | state_single_match_idx = None; 745 | break; 746 | } 747 | state_single_match_idx = Some(state_match_idx); 748 | } 749 | } 750 | } 751 | 752 | if let Some(state_single_match_idx) = state_single_match_idx { 753 | if !input_state_match.test_final_match( 754 | state_single_match_idx, 755 | inputs, 756 | state, 757 | &mut initial_states_matched_bool, 758 | &mut definite_matched_variables, 759 | ) { 760 | return None; 761 | } 762 | } else { 763 | potential_matches.push(input_state_match); 764 | } 765 | } 766 | } else { 767 | potential_matches.append(&mut multiple_matches); 768 | } 769 | 770 | // try to improve performance later during enumeration of permutations, by 771 | // causing inputs to be checked from least to most matches. 772 | potential_matches.sort_unstable_by_key(|InputStateMatch { states, .. }| states.len()); 773 | 774 | Some(InputStateMatches { 775 | potential_matches, 776 | definite_matched_variables, 777 | initial_states_matched_bool, 778 | }) 779 | } 780 | 781 | fn test_inputs_with_permutation( 782 | p_i: usize, 783 | inputs: &Vec>, 784 | state: &mut State, 785 | input_state_matches: &InputStateMatches, 786 | input_rev_permutation_counts: &[usize], 787 | variables_matched: &mut Vec, 788 | side_input: &mut impl SideInput, 789 | ) -> bool { 790 | let len = state.len(); 791 | let mut states_matched_bool = input_state_matches.initial_states_matched_bool.clone(); 792 | 793 | // iterate across the graph of permutations from root to leaf, where each 794 | // level of the tree is an input, and each branch is a match against a state. 795 | for (concrete_input_i, input_state_match) in 796 | input_state_matches.potential_matches.iter().enumerate() 797 | { 798 | let branch_idx = 799 | (p_i / input_rev_permutation_counts[concrete_input_i]) % input_state_match.states.len(); 800 | 801 | if !input_state_match.test_final_match( 802 | branch_idx, 803 | inputs, 804 | state, 805 | &mut states_matched_bool, 806 | variables_matched, 807 | ) { 808 | return false; 809 | } 810 | } 811 | 812 | // try assigning variables from backwards predicates so that they can be used in side 813 | // predicates, ignoring failures because we will check again later. 814 | for input in inputs.iter().filter(|input| is_backwards_pred(input)) { 815 | match_backwards_variables(input, state, variables_matched); 816 | } 817 | 818 | for input in inputs.iter().filter(|input| is_side_pred(input)) { 819 | if !match_side_variables(input, state, variables_matched, side_input) { 820 | return false; 821 | } 822 | } 823 | 824 | // check all backwards predicates in order, aborting if matching fails. 825 | for input in inputs.iter().filter(|input| is_backwards_pred(input)) { 826 | if !match_backwards_variables(input, state, variables_matched) { 827 | return false; 828 | } 829 | } 830 | 831 | for input in inputs.iter().filter(|input| is_negated_pred(input)) { 832 | // check negated predicates last, so that we know about all variables 833 | // from the backwards and side predicates. 834 | if state 835 | .iter() 836 | .enumerate() 837 | .filter(|&(s_i, _)| s_i < len && !states_matched_bool[s_i]) 838 | .any(|(s_i, _)| { 839 | match_state_variables_with_existing(input, state, s_i, variables_matched) 840 | }) 841 | { 842 | return false; 843 | } 844 | } 845 | 846 | true 847 | } 848 | 849 | fn match_backwards_variables( 850 | pred: &Phrase, 851 | state: &mut State, 852 | existing_matches_and_result: &mut Vec, 853 | ) -> bool { 854 | let mut group_counter = PhraseGroupCounter::new(); 855 | let pred = assign_state_vars(pred, state, existing_matches_and_result, &mut group_counter); 856 | 857 | if let Some(eval_result) = evaluate_backwards_pred(&pred) { 858 | let s_i = state.len(); 859 | state.push_with_metadata(eval_result, group_counter.group_count); 860 | 861 | match_state_variables_with_existing(&pred, state, s_i, existing_matches_and_result) 862 | } else { 863 | false 864 | } 865 | } 866 | 867 | fn match_side_variables( 868 | pred: &Phrase, 869 | state: &mut State, 870 | existing_matches_and_result: &mut Vec, 871 | side_input: &mut F, 872 | ) -> bool 873 | where 874 | F: SideInput, 875 | { 876 | let mut group_counter = PhraseGroupCounter::new(); 877 | let pred = assign_state_vars(pred, state, existing_matches_and_result, &mut group_counter); 878 | 879 | if let Some(eval_result) = side_input(&pred) { 880 | if eval_result.len() == 0 { 881 | return true; 882 | } 883 | 884 | let s_i = state.len(); 885 | state.push_with_metadata(eval_result, group_counter.group_count); 886 | 887 | match_state_variables_with_existing(&pred, state, s_i, existing_matches_and_result) 888 | } else { 889 | false 890 | } 891 | } 892 | 893 | pub(crate) fn assign_state_vars( 894 | tokens: &Phrase, 895 | state: &State, 896 | matches: &[MatchLite], 897 | group_counter: &mut PhraseGroupCounter, 898 | ) -> Vec { 899 | let mut result: Vec = vec![]; 900 | 901 | for token in tokens { 902 | if is_var_token(token) { 903 | if let Some(m) = matches.iter().find(|m| m.var_atom == token.atom) { 904 | let mut append_phrase = normalize_match_phrase(token, m.to_phrase(state)); 905 | for t in &append_phrase { 906 | group_counter.count(t); 907 | } 908 | result.append(&mut append_phrase); 909 | continue; 910 | } 911 | } 912 | 913 | result.push(token.clone()); 914 | group_counter.count(token); 915 | } 916 | 917 | // adjust depths for phrases with a single variable that matched a whole state phrase 918 | if result.len() == 1 && result[0].open_depth == 0 { 919 | result[0].open_depth = 1; 920 | result[0].close_depth = 1; 921 | group_counter.group_count += 1; 922 | } 923 | 924 | result 925 | } 926 | 927 | pub fn assign_vars(tokens: &Phrase, matches: &[Match]) -> Vec { 928 | let mut result: Vec = vec![]; 929 | 930 | for token in tokens { 931 | if is_var_token(token) { 932 | if let Some(m) = matches.iter().find(|m| m.atom == token.atom) { 933 | result.append(&mut normalize_match_phrase(token, m.phrase.clone())); 934 | continue; 935 | } 936 | } 937 | 938 | result.push(token.clone()); 939 | } 940 | 941 | // adjust depths for phrases with a single variable that matched a whole state phrase 942 | if result.len() == 1 { 943 | result[0].open_depth = 1; 944 | result[0].close_depth = 1; 945 | } 946 | 947 | result 948 | } 949 | 950 | pub fn evaluate_backwards_pred(tokens: &Phrase) -> Option> { 951 | match tokens[0].flag { 952 | TokenFlag::BackwardsPred(BackwardsPred::Plus) => { 953 | let n1 = tokens[1].as_integer(); 954 | let n2 = tokens[2].as_integer(); 955 | let n3 = tokens[3].as_integer(); 956 | 957 | match (n1, n2, n3) { 958 | (Some(v1), Some(v2), None) => Some(vec![ 959 | tokens[0].clone(), 960 | tokens[1].clone(), 961 | tokens[2].clone(), 962 | Token::new_integer(v1 + v2, 0, 1), 963 | ]), 964 | (Some(v1), None, Some(v3)) => Some(vec![ 965 | tokens[0].clone(), 966 | tokens[1].clone(), 967 | Token::new_integer(v3 - v1, 0, 0), 968 | tokens[3].clone(), 969 | ]), 970 | (None, Some(v2), Some(v3)) => Some(vec![ 971 | tokens[0].clone(), 972 | Token::new_integer(v3 - v2, 0, 0), 973 | tokens[2].clone(), 974 | tokens[3].clone(), 975 | ]), 976 | (Some(v1), Some(v2), Some(v3)) if v1 + v2 == v3 => Some(tokens.to_owned()), 977 | _ => None, 978 | } 979 | } 980 | TokenFlag::BackwardsPred(BackwardsPred::Minus) => { 981 | let n1 = tokens[1].as_integer(); 982 | let n2 = tokens[2].as_integer(); 983 | let n3 = tokens[3].as_integer(); 984 | 985 | match (n1, n2, n3) { 986 | (Some(v1), Some(v2), None) => Some(vec![ 987 | tokens[0].clone(), 988 | tokens[1].clone(), 989 | tokens[2].clone(), 990 | Token::new_integer(v1 - v2, 0, 1), 991 | ]), 992 | (Some(v1), None, Some(v3)) => Some(vec![ 993 | tokens[0].clone(), 994 | tokens[1].clone(), 995 | Token::new_integer(-v3 + v1, 0, 0), 996 | tokens[3].clone(), 997 | ]), 998 | (None, Some(v2), Some(v3)) => Some(vec![ 999 | tokens[0].clone(), 1000 | Token::new_integer(v3 + v2, 0, 0), 1001 | tokens[2].clone(), 1002 | tokens[3].clone(), 1003 | ]), 1004 | (Some(v1), Some(v2), Some(v3)) if v1 - v2 == v3 => Some(tokens.to_owned()), 1005 | _ => None, 1006 | } 1007 | } 1008 | TokenFlag::BackwardsPred(BackwardsPred::Lt) => { 1009 | let n1 = tokens[1].as_integer(); 1010 | let n2 = tokens[2].as_integer(); 1011 | 1012 | match (n1, n2) { 1013 | (Some(v1), Some(v2)) if v1 < v2 => Some(tokens.to_owned()), 1014 | _ => None, 1015 | } 1016 | } 1017 | TokenFlag::BackwardsPred(BackwardsPred::Gt) => { 1018 | let n1 = tokens[1].as_integer(); 1019 | let n2 = tokens[2].as_integer(); 1020 | 1021 | match (n1, n2) { 1022 | (Some(v1), Some(v2)) if v1 > v2 => Some(tokens.to_owned()), 1023 | _ => None, 1024 | } 1025 | } 1026 | TokenFlag::BackwardsPred(BackwardsPred::Lte) => { 1027 | let n1 = tokens[1].as_integer(); 1028 | let n2 = tokens[2].as_integer(); 1029 | 1030 | match (n1, n2) { 1031 | (Some(v1), Some(v2)) if v1 <= v2 => Some(tokens.to_owned()), 1032 | _ => None, 1033 | } 1034 | } 1035 | TokenFlag::BackwardsPred(BackwardsPred::Gte) => { 1036 | let n1 = tokens[1].as_integer(); 1037 | let n2 = tokens[2].as_integer(); 1038 | 1039 | match (n1, n2) { 1040 | (Some(v1), Some(v2)) if v1 >= v2 => Some(tokens.to_owned()), 1041 | _ => None, 1042 | } 1043 | } 1044 | TokenFlag::BackwardsPred(BackwardsPred::ModNeg) => { 1045 | let n1 = tokens[1].as_integer(); 1046 | let n2 = tokens[2].as_integer(); 1047 | let n3 = tokens[3].as_integer(); 1048 | 1049 | match (n1, n2, n3) { 1050 | (Some(v1), Some(v2), Some(v3)) => { 1051 | if v1.rem_euclid(v2) == v3 { 1052 | Some(tokens.to_owned()) 1053 | } else { 1054 | None 1055 | } 1056 | } 1057 | (Some(v1), Some(v2), None) => Some(vec![ 1058 | tokens[0].clone(), 1059 | tokens[1].clone(), 1060 | tokens[2].clone(), 1061 | Token::new_integer(v1.rem_euclid(v2), 0, 1), 1062 | ]), 1063 | _ => None, 1064 | } 1065 | } 1066 | TokenFlag::BackwardsPred(BackwardsPred::Equal) => { 1067 | let mut args = tokens.groups().skip(1); 1068 | let arg1 = args.next().expect("== : first argument missing"); 1069 | let arg2 = args.next().expect("== : second argument missing"); 1070 | if phrase_equal(arg1, arg2, (0, 0), (0, 1)) { 1071 | if tokens[0].is_negated { 1072 | None 1073 | } else { 1074 | Some(tokens.to_owned()) 1075 | } 1076 | } else { 1077 | if tokens[0].is_negated { 1078 | Some(tokens.to_owned()) 1079 | } else { 1080 | None 1081 | } 1082 | } 1083 | } 1084 | _ => unreachable!("{:?}", tokens[0].flag), 1085 | } 1086 | } 1087 | 1088 | #[inline] 1089 | pub fn phrase_equal(a: &Phrase, b: &Phrase, a_depths: (u8, u8), b_depths: (u8, u8)) -> bool { 1090 | if a.len() != b.len() { 1091 | return false; 1092 | } 1093 | 1094 | let len = b.len(); 1095 | 1096 | if len == 1 { 1097 | token_equal(&a[0], &b[0], true, None, None) 1098 | } else { 1099 | token_equal( 1100 | &a[0], 1101 | &b[0], 1102 | false, 1103 | Some((a_depths.0, 0)), 1104 | Some((b_depths.0, 0)), 1105 | ) && a 1106 | .iter() 1107 | .skip(1) 1108 | .take(len - 2) 1109 | .zip(b.iter().skip(1).take(len - 2)) 1110 | .all(|(t1, t2)| token_equal(t1, t2, false, None, None)) 1111 | && token_equal( 1112 | &a[len - 1], 1113 | &b[len - 1], 1114 | false, 1115 | Some((0, a_depths.1)), 1116 | Some((0, b_depths.1)), 1117 | ) 1118 | } 1119 | } 1120 | --------------------------------------------------------------------------------