From ebffbf0f9873c60264a693450b9e6d7005f75a4b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 10 Jul 2020 22:01:47 +0800 Subject: [PATCH] Refine docs and add custom syntax. --- Cargo.toml | 2 +- README.md | 2 +- doc/src/SUMMARY.md | 2 +- doc/src/about/features.md | 3 +- doc/src/engine/custom-syntax.md | 281 +++++++++++++++++++++++++++++++- doc/src/engine/dsl.md | 10 +- doc/src/language/loop.md | 4 +- src/engine.rs | 19 ++- src/fn_native.rs | 1 + src/lib.rs | 1 + src/optimize.rs | 6 +- src/parser.rs | 38 +++-- src/syntax.rs | 33 +++- tests/syntax.rs | 40 +++-- 14 files changed, 391 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6d48bc8d..99da6522 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] -default = [] +default = ["internals"] plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync diff --git a/README.md b/README.md index 285abe90..fb4acca1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Features * Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run. * [Function overloading](https://schungx.github.io/rhai/language/overload.html). * [Operator overloading](https://schungx.github.io/rhai/rust/operators.html). -* Support for use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html) - [disabling keywords/operators](https://schungx.github.io/rhai/engine/disable.html), [custom operators](https://schungx.github.io/rhai/engine/custom-op.html). +* Support for use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html) - [disabling keywords/operators](https://schungx.github.io/rhai/engine/disable.html), [custom operators](https://schungx.github.io/rhai/engine/custom-op.html) and extending the language with [custom syntax](https://schungx.github.io/rhai/engine/custom-syntax.html). * Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html). * Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index b436efb9..e344c306 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -108,7 +108,7 @@ The Rhai Scripting Language 5. [Use as DSL](engine/dsl.md) 1. [Disable Keywords and/or Operators](engine/disable.md) 2. [Custom Operators](engine/custom-op.md) - 3. [Custom Syntax](engine/custom-syntax.md) + 3. [Extending with Custom Syntax](engine/custom-syntax.md) 6. [Eval Statement](language/eval.md) 9. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 88f585f0..13b2e4c1 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -67,4 +67,5 @@ Flexible * Surgically [disable keywords and operators] to restrict the language. -* Use as a [DSL] by [disabling keywords/operators][disable keywords and operators], [custom operators] and defining [custom syntax]. +* Use as a [DSL] by [disabling keywords/operators][disable keywords and operators], [custom operators] + and extending the language with [custom syntax]. diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md index fe3f399c..542efaa5 100644 --- a/doc/src/engine/custom-syntax.md +++ b/doc/src/engine/custom-syntax.md @@ -1,5 +1,282 @@ -Custom Syntax -============= +Extending Rhai with Custom Syntax +================================ {{#include ../links.md}} + +For the ultimate advantageous, there is a built-in facility to _extend_ the Rhai language +with custom-defined _syntax_. + +But before going off to define the next weird statement type, heed this warning: + + +Don't Do It™ +------------ + +Stick with standard language syntax as much as possible. + +Having to learn Rhai is bad enough, no sane user would ever want to learn _yet_ another +obscure language syntax just to do something. + +Try to use [custom operators] first. Defining a custom syntax should be considered a _last resort_. + + +Where This Might Be Useful +------------------------- + +* Where an operation is used a _LOT_ and a custom syntax saves a lot of typing. + +* Where a custom syntax _significantly_ simplifies the code and _significantly_ enhances understanding of the code's intent. + +* Where certain logic cannot be easily encapsulated inside a function. This is usually the case where _closures_ are required, because Rhai does not have closures. + +* Where you just want to confuse your user and make their lives miserable, because you can. + + +Step One - Start With `internals` +-------------------------------- + +Since a custom syntax taps deeply into the `AST` and evaluation process of the `Engine`, +the [`internals`] feature must be on in order to expose these necessary internal data structures. + +Beware that Rhai internal data structures are _volatile_ and may change without warning. + +Caveat emptor. + + +Step Two - Design The Syntax +--------------------------- + +A custom syntax is simply a list of symbols. + +These symbol types can be used: + +* Standard [keywords]({{rootUrl}}/appendix/keywords.md) + +* Standard [operators]({{rootUrl}}/appendix/operators.md#operators). + +* Reserved [symbols]({{rootUrl}}/appendix/operators.md#symbols). + +* Identifiers following the [variable] naming rules. + +* `$expr$` - any valid expression, statement or statement block. + +* `$block$` - any valid statement block (i.e. must be enclosed by `'{'` .. `'}'`). + +* `$ident$` - any [variable] name. + +### The First Symbol Must be a Keyword + +There is no specific limit on the combination and sequencing of each symbol type, +except the _first_ symbol which must be a [custom keyword]. + +It _cannot_ be a [built-in keyword]({{rootUrl}}/appendix/keywords.md). + +However, it _may_ be a built-in keyword that has been [disabled][disable keywords and operators]. + +### The First Symbol Must be Unique + +Rhai uses the _first_ symbol as a clue to parse custom syntax. + +Therefore, at any one time, there can only be _one_ custom syntax starting with each unique symbol. + +Any new custom syntax definition using the same first symbol simply _overwrites_ the previous one. + +### Example + +```rust +exec $ident$ <- $expr$ : $block$ +``` + +The above syntax is made up of a stream of symbols: + +| Position | Input | Symbol | Description | +| :------: | :---: | :-------: | -------------------------------------------------------------------------------------------------------- | +| 1 | | `exec` | custom keyword | +| 2 | 1 | `$ident$` | a variable name | +| 3 | | `<-` | the left-arrow symbol (which is a [reserved symbol]({{rootUrl}}/appendix/operators.md#symbols) in Rhai). | +| 4 | 2 | `$expr$` | an expression, which may be enclosed with `{` .. `}`, or not. | +| 5 | | `:` | the colon symbol | +| 6 | 3 | `$block$` | a statement block, which must be enclosed with `{` .. `}`. | + +This syntax matches the following sample code and generates three inputs (one for each non-keyword): + +```rust +// Assuming the 'exec' custom syntax implementation declares the variable 'hello': +let x = exec hello <- foo(1, 2) : { + hello += bar(hello); + baz(hello); + }; + +print(x); // variable 'x' has a value returned by the custom syntax + +print(hello); // variable declared by a custom syntax persists! +``` + + +Step Three - Implementation +-------------------------- + +Any custom syntax must include an _implementation_ of it. + +### Function Signature + +The function signature of an implementation is: + +```rust +Fn( + engine: &Engine, + scope: &mut Scope, + mods: &mut Imports, + state: &mut State, + lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, + inputs: &[Expr], + level: usize +) -> Result> +``` + +where: + +* `engine : &Engine` - reference to the current [`Engine`]. +* `scope : &mut Scope` - mutable reference to the current [`Scope`]; variables can be added to it. +* `mods : &mut Imports` - mutable reference to the current collection of imported [`Module`]'s; **do not touch**. +* `state : &mut State` - mutable reference to the current evaluation state; **do not touch**. +* `lib : &Module` - reference to the current collection of script-defined functions. +* `this_ptr : &mut Option<&mut Dynamic>` - mutable reference to the current binding of the `this` pointer; **do not touch**. +* `inputs : &[Expr]` - a list of input expression trees. +* `level : usize` - the current function call level. + +There are a lot of parameters, most of which should not be touched or Bad Things Happen™. +They represent the running _content_ of a script evaluation and should simply be passed +straight-through the the [`Engine`]. + +### Access Arguments + +The most important argument is `inputs` where the matched identifiers (`$ident$`), expressions/statements (`$expr$`) +and statement blocks (`$block$) are provided. + +To access a particular argument, use the following patterns: + +| Argument type | Pattern (`n` = slot in `inputs`) | Result type | Description | +| :-----------: | ---------------------------------------- | :---------: | ------------------ | +| `$ident$` | `inputs[n].get_variable_name().unwrap()` | `&str` | name of a variable | +| `$expr$` | `inputs.get(n).unwrap()` | `Expr` | an expression tree | +| `$block$` | `inputs.get(n).unwrap()` | `Expr` | an expression tree | + +### Evaluate an Expression Tree + +Use the `engine::eval_expression_tree` method to evaluate an expression tree. + +```rust +let expr = inputs.get(0).unwrap(); +let result = engine.eval_expression_tree(scope, mods, state, lib, this_ptr, expr, level)?; +``` + +As can be seem above, most arguments are simply passed straight-through to `engine::eval_expression_tree`. + +### Declare Variables + +New variables maybe declared (usually with a variable name that is passed in via `$ident$). + +It can simply be pushed into the [`scope`]. + +However, beware that all new variables must be declared _prior_ to evaluating any expression tree. +In other words, any `scope.push(...)` calls must come _before_ any `engine::eval_expression_tree(...)` calls. + +```rust +let var_name = inputs[0].get_variable_name().unwrap().to_string(); +let expr = inputs.get(1).unwrap(); + +scope.push(var_name, 0 as INT); // do this BEFORE engine.eval_expression_tree! + +let result = engine.eval_expression_tree(scope, mods, state, lib, this_ptr, expr, level)?; +``` + + +Step Four - Register the Custom Syntax +------------------------------------- + +Use `Engine::register_custom_syntax` to register a custom syntax. + +Again, beware that the _first_ symbol must be unique. If there already exists a custom syntax starting +with that symbol, the previous syntax will be overwritten. + +The syntax is passed simply as a slice of `&str`. + +```rust +// Custom syntax implementation +fn implementation_func( + engine: &Engine, + scope: &mut Scope, + mods: &mut Imports, + state: &mut State, + lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, + inputs: &[Expr], + level: usize +) -> Result> { + let var_name = inputs[0].get_variable_name().unwrap().to_string(); + let stmt = inputs.get(1).unwrap(); + let condition = inputs.get(2).unwrap(); + + // Push one new variable into the 'scope' BEFORE 'eval_expression_tree' + scope.push(var_name, 0 as INT); + + loop { + // Evaluate the statement block + engine.eval_expression_tree(scope, mods, state, lib, this_ptr, stmt, level)?; + + // Evaluate the condition expression + let stop = !engine + .eval_expression_tree(scope, mods, state, lib, this_ptr, condition, level)? + .as_bool() + .map_err(|_| EvalAltResult::ErrorBooleanArgMismatch( + "do-while".into(), expr.position() + ))?; + + if stop { + break; + } + } + + Ok(().into()) +} + +// Register the custom syntax (sample): do |x| -> { x += 1 } while x < 0; +engine.register_custom_syntax( + &[ "do", "|", "$ident$", "|", "->", "$block$", "while", "$expr$" ], // the custom syntax + 1, // the number of new variables declared within this custom syntax + implementation_func +)?; +``` + + +Step Five - Disable Unneeded Statement Types +------------------------------------------- + +When a DSL needs a custom syntax, most likely than not it is extremely specialized. +Therefore, many statement types actually may not make sense under the same usage scenario. + +So, while at it, better [disable][disable keywords and operators] those built-in keywords +and operators that should not be used by the user. The would leave only the bare minimum +language surface exposed, together with the custom syntax that is tailor-designed for +the scenario. + +A keyword or operator that is disabled can still be used in a custom syntax. + +In an extreme case, it is possible to disable _every_ keyword in the language, leaving only +custom syntax (plus possibly expressions). But again, Don't Do It™ - unless you are certain +of what you're doing. + + +Step Six - Document +------------------- + +For custom syntax, documentation is crucial. + +Make sure there are _lots_ of examples for users to follow. + + +Step Seven - Profit! +-------------------- diff --git a/doc/src/engine/dsl.md b/doc/src/engine/dsl.md index adc04e44..5534f243 100644 --- a/doc/src/engine/dsl.md +++ b/doc/src/engine/dsl.md @@ -56,13 +56,17 @@ essentially custom statement types. The [`internals`] feature is needed to be able to define [custom syntax] in Rhai. -For example: +For example, the following is a SQL like syntax for some obscure DSL operation: ```rust let table = [..., ..., ..., ...]; -// Syntax = "select" $ident$ $ident$ "from" $expr$ "->" $ident$ ":" $expr$ -let total = select sum price from table -> row : row.weight > 50; +// Syntax = "calculate" $ident$ $ident$ "from" $expr$ "->" $ident$ ":" $expr$ +let total = calculate sum price from table -> row : row.weight > 50; + +// Note: There is nothing special about the use of symbols; to make it look exactly like SQL: +// Syntax = "SELECT" $ident$ "(" $ident$ ")" "FROM" $expr$ "AS" $ident$ "WHERE" $expr$ +let total = SELECT sum(price) FROM table AS row WHERE row.weight > 50; ``` After registering this custom syntax with Rhai, it can be used anywhere inside a script as diff --git a/doc/src/language/loop.md b/doc/src/language/loop.md index e625082a..3fd9b5fb 100644 --- a/doc/src/language/loop.md +++ b/doc/src/language/loop.md @@ -3,9 +3,9 @@ Infinite `loop` {{#include ../links.md}} -Infinite loops follow C syntax. +Infinite loops follow Rust syntax. -Like C, `continue` can be used to skip to the next iteration, by-passing all following statements; +Like Rust, `continue` can be used to skip to the next iteration, by-passing all following statements; `break` can be used to break out of the loop unconditionally. ```rust diff --git a/src/engine.rs b/src/engine.rs index 1fcae581..dfded485 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -11,13 +11,15 @@ use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, St use crate::r#unsafe::unsafe_cast_var_name_to_lifetime; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::syntax::CustomSyntax; use crate::token::Position; use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; +#[cfg(feature = "internals")] +use crate::syntax::CustomSyntax; + use crate::stdlib::{ any::{type_name, TypeId}, borrow::Cow, @@ -85,9 +87,12 @@ pub const FN_GET: &str = "get$"; pub const FN_SET: &str = "set$"; pub const FN_IDX_GET: &str = "index$get$"; pub const FN_IDX_SET: &str = "index$set$"; + +#[cfg(feature = "internals")] pub const MARKER_EXPR: &str = "$expr$"; -pub const MARKER_STMT: &str = "$stmt$"; +#[cfg(feature = "internals")] pub const MARKER_BLOCK: &str = "$block$"; +#[cfg(feature = "internals")] pub const MARKER_IDENT: &str = "$ident$"; /// A type specifying the method of chaining. @@ -279,6 +284,7 @@ pub struct Engine { /// A hashset containing custom keywords and precedence to recognize. pub(crate) custom_keywords: Option>, /// Custom syntax. + #[cfg(feature = "internals")] pub(crate) custom_syntax: Option>, /// Callback closure for implementing the `print` command. @@ -329,6 +335,8 @@ impl Default for Engine { type_names: None, disabled_symbols: None, custom_keywords: None, + + #[cfg(feature = "internals")] custom_syntax: None, // default print/debug implementations @@ -562,6 +570,8 @@ impl Engine { type_names: None, disabled_symbols: None, custom_keywords: None, + + #[cfg(feature = "internals")] custom_syntax: None, print: Box::new(|_| {}), @@ -1667,14 +1677,14 @@ impl Engine { } } - /// Evaluate an expression inside an AST. + /// Evaluate an expression tree. /// /// ## WARNING - Low Level API /// /// This function is very low level. It evaluates an expression from an AST. #[cfg(feature = "internals")] #[deprecated(note = "this method is volatile and may change")] - pub fn eval_expr_from_ast( + pub fn eval_expression_tree( &self, scope: &mut Scope, mods: &mut Imports, @@ -2118,6 +2128,7 @@ impl Engine { Expr::False(_) => Ok(false.into()), Expr::Unit(_) => Ok(().into()), + #[cfg(feature = "internals")] Expr::Custom(x) => { let func = (x.0).1.as_ref(); let exprs = (x.0).0.as_ref(); diff --git a/src/fn_native.rs b/src/fn_native.rs index 70594208..399d1746 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,3 +1,4 @@ +//! Module containing interfaces with native-Rust functions. use crate::any::Dynamic; use crate::engine::Engine; use crate::module::Module; diff --git a/src/lib.rs b/src/lib.rs index b6e73b49..6e319260 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,6 +91,7 @@ mod scope; mod serde; mod settings; mod stdlib; +#[cfg(feature = "internals")] mod syntax; mod token; mod r#unsafe; diff --git a/src/optimize.rs b/src/optimize.rs index 0fad8450..ed07d1fb 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -2,10 +2,13 @@ use crate::any::Dynamic; use crate::calc_fn_hash; use crate::engine::{Engine, Imports, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, KEYWORD_TYPE_OF}; use crate::module::Module; -use crate::parser::{map_dynamic_to_expr, CustomExpr, Expr, ReturnType, ScriptFnDef, Stmt, AST}; +use crate::parser::{map_dynamic_to_expr, Expr, ReturnType, ScriptFnDef, Stmt, AST}; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; use crate::utils::StaticVec; +#[cfg(feature = "internals")] +use crate::parser::CustomExpr; + use crate::stdlib::{ boxed::Box, iter::empty, @@ -599,6 +602,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { } // Custom syntax + #[cfg(feature = "internals")] Expr::Custom(x) => Expr::Custom(Box::new(( CustomExpr( (x.0).0.into_iter().map(|expr| optimize_expr(expr, state)).collect(), diff --git a/src/parser.rs b/src/parser.rs index c32a919a..4df5b799 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,19 +2,23 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; -use crate::engine::{ - make_getter, make_setter, Engine, KEYWORD_THIS, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT, - MARKER_STMT, -}; +use crate::engine::{make_getter, make_setter, Engine, KEYWORD_THIS}; use crate::error::{LexError, ParseError, ParseErrorType}; -use crate::fn_native::Shared; use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::syntax::FnCustomSyntaxEval; use crate::token::{Position, Token, TokenStream}; use crate::utils::{StaticVec, StraightHasherBuilder}; +#[cfg(feature = "internals")] +use crate::engine::{MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; + +#[cfg(feature = "internals")] +use crate::fn_native::Shared; + +#[cfg(feature = "internals")] +use crate::syntax::FnCustomSyntaxEval; + use crate::stdlib::{ borrow::Cow, boxed::Box, @@ -574,8 +578,10 @@ impl Stmt { } #[derive(Clone)] +#[cfg(feature = "internals")] pub struct CustomExpr(pub StaticVec, pub Shared); +#[cfg(feature = "internals")] impl fmt::Debug for CustomExpr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) @@ -647,6 +653,7 @@ pub enum Expr { /// () Unit(Position), /// Custom syntax + #[cfg(feature = "internals")] Custom(Box<(CustomExpr, Position)>), } @@ -743,6 +750,7 @@ impl Expr { Self::Dot(x) | Self::Index(x) => x.0.position(), + #[cfg(feature = "internals")] Self::Custom(x) => x.1, } } @@ -776,6 +784,8 @@ impl Expr { Self::Assignment(x) => x.3 = new_pos, Self::Dot(x) => x.2 = new_pos, Self::Index(x) => x.2 = new_pos, + + #[cfg(feature = "internals")] Self::Custom(x) => x.1 = new_pos, } @@ -881,6 +891,7 @@ impl Expr { _ => false, }, + #[cfg(feature = "internals")] Self::Custom(_) => false, } } @@ -897,6 +908,14 @@ impl Expr { _ => self, } } + + #[cfg(feature = "internals")] + pub fn get_variable_name(&self) -> Option<&str> { + match self { + Self::Variable(x) => Some((x.0).0.as_str()), + _ => None, + } + } } /// Consume a particular token, checking that it is the expected one. @@ -2046,6 +2065,7 @@ fn parse_expr( settings.ensure_level_within_max_limit(state.max_expr_depth)?; // Check if it is a custom syntax. + #[cfg(feature = "internals")] if let Some(ref custom) = state.engine.custom_syntax { let (token, pos) = input.peek().unwrap(); let token_pos = *pos; @@ -2085,12 +2105,6 @@ fn parse_expr( (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }, MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?), - MARKER_STMT => { - let stmt = parse_stmt(input, state, lib, settings)? - .unwrap_or_else(|| Stmt::Noop(settings.pos)); - let pos = stmt.position(); - exprs.push(Expr::Stmt(Box::new((stmt, pos)))) - } MARKER_BLOCK => { let stmt = parse_block(input, state, lib, settings)?; let pos = stmt.position(); diff --git a/src/syntax.rs b/src/syntax.rs index e1a05786..2fea999e 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -1,5 +1,8 @@ +//! Module containing implementation for custom syntax. +#![cfg(feature = "internals")] + use crate::any::Dynamic; -use crate::engine::{Engine, Imports, State, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT, MARKER_STMT}; +use crate::engine::{Engine, Imports, State, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; use crate::error::LexError; use crate::fn_native::{SendSync, Shared}; use crate::module::Module; @@ -57,7 +60,7 @@ impl fmt::Debug for CustomSyntax { } impl Engine { - pub fn add_custom_syntax + ToString>( + pub fn register_custom_syntax + ToString>( &mut self, value: &[S], scope_delta: isize, @@ -83,12 +86,28 @@ impl Engine { for s in value { let seg = match s.as_ref() { // Markers not in first position - MARKER_EXPR | MARKER_STMT | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => { - s.to_string() - } + MARKER_EXPR | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => s.to_string(), // Standard symbols not in first position - s if !segments.is_empty() && Token::lookup_from_syntax(s).is_some() => s.into(), - // Custom keyword + s if !segments.is_empty() && Token::lookup_from_syntax(s).is_some() => { + if self + .disabled_symbols + .as_ref() + .map(|d| d.contains(s)) + .unwrap_or(false) + { + // If symbol is disabled, make it a custom keyword + if self.custom_keywords.is_none() { + self.custom_keywords = Some(Default::default()); + } + + if !self.custom_keywords.as_ref().unwrap().contains_key(s) { + self.custom_keywords.as_mut().unwrap().insert(s.into(), 0); + } + } + + s.into() + } + // Identifier s if is_valid_identifier(s.chars()) => { if self.custom_keywords.is_none() { self.custom_keywords = Some(Default::default()); diff --git a/tests/syntax.rs b/tests/syntax.rs index f12a0919..501c90b3 100644 --- a/tests/syntax.rs +++ b/tests/syntax.rs @@ -7,9 +7,16 @@ use rhai::{ fn test_custom_syntax() -> Result<(), Box> { let mut engine = Engine::new(); + // Disable 'while' and make sure it still works with custom syntax + engine.disable_symbol("while"); + engine.consume("while false {}").expect_err("should error"); + engine.consume("let while = 0")?; + engine - .add_custom_syntax( - &["do", "$ident$", "$block$", "while", "$expr$"], + .register_custom_syntax( + &[ + "do", "|", "$ident$", "|", "->", "$block$", "while", "$expr$", + ], 1, |engine: &Engine, scope: &mut Scope, @@ -17,22 +24,19 @@ fn test_custom_syntax() -> Result<(), Box> { state: &mut EvalState, lib: &Module, this_ptr: &mut Option<&mut Dynamic>, - exprs: &[Expr], + inputs: &[Expr], level: usize| { - let var_name = match exprs.get(0).unwrap() { - Expr::Variable(s) => (s.0).0.clone(), - _ => unreachable!(), - }; - let stmt = exprs.get(1).unwrap(); - let expr = exprs.get(2).unwrap(); + let var_name = inputs[0].get_variable_name().unwrap().to_string(); + let stmt = inputs.get(1).unwrap(); + let expr = inputs.get(2).unwrap(); scope.push(var_name, 0 as INT); loop { - engine.eval_expr_from_ast(scope, mods, state, lib, this_ptr, stmt, level)?; + engine.eval_expression_tree(scope, mods, state, lib, this_ptr, stmt, level)?; if !engine - .eval_expr_from_ast(scope, mods, state, lib, this_ptr, expr, level)? + .eval_expression_tree(scope, mods, state, lib, this_ptr, expr, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch( @@ -50,20 +54,24 @@ fn test_custom_syntax() -> Result<(), Box> { ) .unwrap(); - assert!(matches!( - *engine.add_custom_syntax(&["!"], 0, |_, _, _, _, _, _, _, _| Ok(().into())).expect_err("should error"), - LexError::ImproperSymbol(s) if s == "!" - )); + // 'while' is now a custom keyword so this it can no longer be a variable + engine.consume("let while = 0").expect_err("should error"); assert_eq!( engine.eval::( r" - do x { x += 1 } while x < 42; + do |x| -> { x += 1 } while x < 42; x " )?, 42 ); + // The first symbol must be an identifier + assert!(matches!( + *engine.register_custom_syntax(&["!"], 0, |_, _, _, _, _, _, _, _| Ok(().into())).expect_err("should error"), + LexError::ImproperSymbol(s) if s == "!" + )); + Ok(()) }