From b607a3a9ba6ab8235d25d43987c2a9f6a98683ce Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 25 Oct 2020 21:57:18 +0800 Subject: [PATCH] Add raw API for custom syntax. --- doc/src/engine/custom-syntax.md | 81 ++++++++++++++++-- doc/src/engine/var.md | 4 +- doc/src/rust/register-raw.md | 2 + src/api.rs | 8 +- src/engine.rs | 28 +++---- src/fn_call.rs | 2 +- src/parser.rs | 87 ++++++++++++++------ src/settings.rs | 28 +------ src/syntax.rs | 140 ++++++++++++++++++-------------- src/token.rs | 43 ++++------ tests/syntax.rs | 51 ++++++++++-- 11 files changed, 298 insertions(+), 176 deletions(-) diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md index 84463935..0cb3bf5b 100644 --- a/doc/src/engine/custom-syntax.md +++ b/doc/src/engine/custom-syntax.md @@ -54,16 +54,14 @@ These symbol types can be used: * `$ident$` - any [variable] name. -### The First Symbol Must be a Keyword +### The First Symbol Must be an Identifier There is no specific limit on the combination and sequencing of each symbol type, except the _first_ symbol which must be a custom keyword that follows the naming rules of [variables]. -The first symbol also cannot be a reserved [keyword], unless that keyword -has been [disabled][disable keywords and operators]. - -In other words, any valid identifier that is not an active [keyword] will work fine. +The first symbol also cannot be a normal or reserved [keyword]. +In other words, any valid identifier that is not a [keyword] will work fine. ### The First Symbol Must be Unique @@ -127,6 +125,8 @@ where: * `inputs: &[Expression]` - a list of input expression trees. +* Return value - result of evaluating the custom syntax expression. + ### Access Arguments The most important argument is `inputs` where the matched identifiers (`$ident$`), expressions/statements (`$expr$`) @@ -215,9 +215,9 @@ fn implementation_func( Ok(().into()) } -// Register the custom syntax (sample): do |x| -> { x += 1 } while x < 0; +// Register the custom syntax (sample): exec |x| -> { x += 1 } while x < 0; engine.register_custom_syntax( - &[ "do", "|", "$ident$", "|", "->", "$block$", "while", "$expr$" ], // the custom syntax + &[ "exec", "|", "$ident$", "|", "->", "$block$", "while", "$expr$" ], // the custom syntax 1, // the number of new variables declared within this custom syntax implementation_func )?; @@ -252,3 +252,70 @@ Make sure there are _lots_ of examples for users to follow. Step Six - Profit! ------------------ + + +Really Advanced - Low Level Custom Syntax API +-------------------------------------------- + +Sometimes it is desirable to have multiple custom syntax starting with the +same symbol. This is especially common for _command-style_ syntax where the +second symbol calls a particular command: + +```rust +// The following simulates a command-style syntax, all starting with 'perform'. +perform action 42; // Perform a system action with a parameter +perform update system; // Update the system +perform check all; // Check all system settings +perform cleanup; // Clean up the system +perform add something; // Add something to the system +perform remove something; // Delete something from the system +``` + +For even more flexibility, there is a _low level_ API for custom syntax that +allows the registration of an entire mini-parser. + +Use `Engine::register_custom_syntax_raw` to register a custom syntax _parser_ +together with the implementation function: + +```rust +// Register the custom syntax (sample): exec |x| -> { x += 1 } while x < 0; +engine.register_custom_syntax_raw( + "exec", + |stream| match stream.len() { + 1 => Ok(Some("|".into())), + 2 => Ok(Some("$ident$".into())), + 3 => Ok(Some("|".into())), + 4 => Ok(Some("->".into())), + 5 => Ok(Some("$block$".into())), + 6 => Ok(Some("while".into())), + 7 => Ok(Some("$expr$".into())), + 8 => Ok(None) + _ => unreachable!(), + } + 1, // the number of new variables declared within this custom syntax + implementation_func // implementation function as above +)?; +``` + +### Function Signature + +The custom syntax parser has the following signature: + +> `Fn(stream: &[&String]) -> Result, ParseError>` + +where: + +* `stream: &[&String]` - a slice of symbols that have been parsed so far, perhaps containing the following: + * `$expr$` - an expression + * `$block$` - a statement block + +### Return Value + +The return value is `Result, ParseError>` where: + +* `Ok(None)` - parsing complete and there are no more symbols to match. + +* `Ok(Some(symbol))` - next symbol to match. + +* `Err(ParseError)` - error is reflected back to the [`Engine`]. + Normally this is `ParseErrorType::ImproperSymbol` to indicate that there is a syntax error, but it can be any error. diff --git a/doc/src/engine/var.md b/doc/src/engine/var.md index 270f122e..85267783 100644 --- a/doc/src/engine/var.md +++ b/doc/src/engine/var.md @@ -16,7 +16,7 @@ To do so, provide a closure to the [`Engine`] via the `Engine::on_var` method: let mut engine = Engine::new(); // Register a variable resolver. -engine.on_var(|name, index, scope, context| { +engine.on_var(|name, index, context| { match name { "MYSTIC_NUMBER" => Ok(Some((42 as INT).into())), // Override a variable - make it not found even if it exists! @@ -24,7 +24,7 @@ engine.on_var(|name, index, scope, context| { EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::none()) )), // Silently maps 'chameleon' into 'innocent'. - "chameleon" => scope.get_value("innocent").map(Some).ok_or_else(|| Box::new( + "chameleon" => context.scope.get_value("innocent").map(Some).ok_or_else(|| Box::new( EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::none()) )), // Return Ok(None) to continue with the normal variable resolution process. diff --git a/doc/src/rust/register-raw.md b/doc/src/rust/register-raw.md index 8e848734..694e6612 100644 --- a/doc/src/rust/register-raw.md +++ b/doc/src/rust/register-raw.md @@ -76,6 +76,8 @@ where: * `args: &mut [&mut Dynamic]` - a slice containing `&mut` references to [`Dynamic`] values. The slice is guaranteed to contain enough arguments _of the correct types_. +* Return value - result of the function call. + Remember, in Rhai, all arguments _except_ the _first_ one are always passed by _value_ (i.e. cloned). Therefore, it is unnecessary to ever mutate any argument except the first one, as all mutations will be on the cloned copy. diff --git a/src/api.rs b/src/api.rs index cfb1199a..bfa0abfa 100644 --- a/src/api.rs +++ b/src/api.rs @@ -164,14 +164,8 @@ impl Engine { #[cfg(not(feature = "no_object"))] #[inline(always)] pub fn register_type_with_name(&mut self, name: &str) -> &mut Self { - if self.type_names.is_none() { - self.type_names = Some(Default::default()); - } // Add the pretty-print type name into the map - self.type_names - .as_mut() - .unwrap() - .insert(type_name::().into(), name.into()); + self.type_names.insert(type_name::().into(), name.into()); self } diff --git a/src/engine.rs b/src/engine.rs index e7de12bd..d1107fa3 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -512,14 +512,14 @@ pub struct Engine { pub(crate) module_resolver: Option>, /// A hashmap mapping type names to pretty-print names. - pub(crate) type_names: Option>, + pub(crate) type_names: HashMap, /// A hashset containing symbols to disable. - pub(crate) disabled_symbols: Option>, + pub(crate) disabled_symbols: HashSet, /// A hashset containing custom keywords and precedence to recognize. - pub(crate) custom_keywords: Option>, + pub(crate) custom_keywords: HashMap>, /// Custom syntax. - pub(crate) custom_syntax: Option>, + pub(crate) custom_syntax: HashMap, /// Callback closure for resolving variable access. pub(crate) resolve_var: Option, @@ -658,10 +658,10 @@ impl Engine { #[cfg(any(feature = "no_std", target_arch = "wasm32",))] module_resolver: None, - type_names: None, - disabled_symbols: None, - custom_keywords: None, - custom_syntax: None, + type_names: Default::default(), + disabled_symbols: Default::default(), + custom_keywords: Default::default(), + custom_syntax: Default::default(), // variable resolver resolve_var: None, @@ -715,10 +715,10 @@ impl Engine { #[cfg(not(feature = "no_module"))] module_resolver: None, - type_names: None, - disabled_symbols: None, - custom_keywords: None, - custom_syntax: None, + type_names: Default::default(), + disabled_symbols: Default::default(), + custom_keywords: Default::default(), + custom_syntax: Default::default(), resolve_var: None, @@ -2274,8 +2274,8 @@ impl Engine { #[inline(always)] pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str { self.type_names - .as_ref() - .and_then(|t| t.get(name).map(String::as_str)) + .get(name) + .map(String::as_str) .unwrap_or_else(|| map_std_type_name(name)) } diff --git a/src/fn_call.rs b/src/fn_call.rs index fda09ff2..246f3060 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -1055,7 +1055,7 @@ impl Engine { } else { // If the first argument is a variable, and there is no curried arguments, convert to method-call style // in order to leverage potential &mut first argument and avoid cloning the value - if args_expr[0].get_variable_access(false).is_some() && curry.is_empty() { + if curry.is_empty() && args_expr[0].get_variable_access(false).is_some() { // func(x, ...) -> x.func(...) arg_values = args_expr .iter() diff --git a/src/parser.rs b/src/parser.rs index 50eb0454..c13a1390 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2549,9 +2549,17 @@ fn parse_binary_op( let mut root = lhs; loop { - let (current_op, _) = input.peek().unwrap(); - let custom = state.engine.custom_keywords.as_ref(); - let precedence = current_op.precedence(custom); + let (current_op, current_pos) = input.peek().unwrap(); + let precedence = if let Token::Custom(c) = current_op { + // Custom operators + if let Some(Some(p)) = state.engine.custom_keywords.get(c) { + *p + } else { + return Err(PERR::Reserved(c.clone()).into_err(*current_pos)); + } + } else { + current_op.precedence() + }; let bind_right = current_op.is_bind_right(); // Bind left to the parent lhs expression if precedence is higher @@ -2574,7 +2582,17 @@ fn parse_binary_op( let rhs = parse_unary(input, state, lib, settings)?; - let next_precedence = input.peek().unwrap().0.precedence(custom); + let (next_op, next_pos) = input.peek().unwrap(); + let next_precedence = if let Token::Custom(c) = next_op { + // Custom operators + if let Some(Some(p)) = state.engine.custom_keywords.get(c) { + *p + } else { + return Err(PERR::Reserved(c.clone()).into_err(*next_pos)); + } + } else { + next_op.precedence() + }; // Bind to right if the next operator has higher precedence // If same precedence, then check if the operator binds right @@ -2646,14 +2664,7 @@ fn parse_binary_op( make_dot_expr(current_lhs, rhs, pos)? } - Token::Custom(s) - if state - .engine - .custom_keywords - .as_ref() - .map(|c| c.contains_key(&s)) - .unwrap_or(false) => - { + Token::Custom(s) if state.engine.custom_keywords.contains_key(&s) => { // Accept non-native functions for custom operators let op = (op.0, false, op.2, op.3); Expr::FnCall(Box::new((op, None, hash, args, None))) @@ -2665,12 +2676,12 @@ fn parse_binary_op( } /// Parse a custom syntax. -fn parse_custom( +fn parse_custom_syntax( input: &mut TokenStream, state: &mut ParseState, lib: &mut FunctionsLib, mut settings: ParseSettings, - key: &str, + key: String, syntax: &CustomSyntax, pos: Position, ) -> Result { @@ -2691,13 +2702,26 @@ fn parse_custom( _ => (), } - for segment in syntax.segments.iter() { + let mut segments: StaticVec<_> = Default::default(); + segments.push(key); + + loop { settings.pos = input.peek().unwrap().1; + + let token = if let Some(seg) = (syntax.parse)(&segments.iter().collect::>()) + .map_err(|err| err.0.into_err(settings.pos))? + { + seg + } else { + break; + }; + let settings = settings.level_up(); - match segment.as_str() { + match token.as_str() { MARKER_IDENT => match input.next().unwrap() { (Token::Identifier(s), pos) => { + segments.push(s.to_string()); exprs.push(Expr::Variable(Box::new(((s, pos), None, 0, None)))); } (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { @@ -2705,20 +2729,25 @@ fn parse_custom( } (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }, - MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?), + MARKER_EXPR => { + exprs.push(parse_expr(input, state, lib, settings)?); + segments.push(MARKER_EXPR.to_string()); + } MARKER_BLOCK => { let stmt = parse_block(input, state, lib, settings)?; let pos = stmt.position(); - exprs.push(Expr::Stmt(Box::new((stmt, pos)))) + exprs.push(Expr::Stmt(Box::new((stmt, pos)))); + segments.push(MARKER_BLOCK.to_string()); } s => match input.peek().unwrap() { + (Token::LexError(err), pos) => return Err(err.into_err(*pos)), (t, _) if t.syntax().as_ref() == s => { - input.next().unwrap(); + segments.push(input.next().unwrap().0.syntax().into_owned()); } (_, pos) => { return Err(PERR::MissingToken( s.to_string(), - format!("for '{}' expression", key), + format!("for '{}' expression", segments[0]), ) .into_err(*pos)) } @@ -2745,16 +2774,22 @@ fn parse_expr( settings.ensure_level_within_max_limit(state.max_expr_depth)?; // Check if it is a custom syntax. - if let Some(ref custom) = state.engine.custom_syntax { + if !state.engine.custom_syntax.is_empty() { let (token, pos) = input.peek().unwrap(); let token_pos = *pos; match token { - Token::Custom(key) if custom.contains_key(key) => { - let custom = custom.get_key_value(key).unwrap(); - let (key, syntax) = custom; - input.next().unwrap(); - return parse_custom(input, state, lib, settings, key, syntax, token_pos); + Token::Custom(key) | Token::Reserved(key) | Token::Identifier(key) => { + match state.engine.custom_syntax.get_key_value(key) { + Some((key, syntax)) => { + let key = key.to_string(); + input.next().unwrap(); + return parse_custom_syntax( + input, state, lib, settings, key, syntax, token_pos, + ); + } + _ => (), + } } _ => (), } diff --git a/src/settings.rs b/src/settings.rs index bd23114a..ce89c3ad 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -240,15 +240,7 @@ impl Engine { /// ``` #[inline(always)] pub fn disable_symbol(&mut self, symbol: &str) -> &mut Self { - if self.disabled_symbols.is_none() { - self.disabled_symbols = Some(Default::default()); - } - - self.disabled_symbols - .as_mut() - .unwrap() - .insert(symbol.into()); - + self.disabled_symbols.insert(symbol.into()); self } @@ -291,28 +283,14 @@ impl Engine { // Standard identifiers, reserved keywords and custom keywords are OK None | Some(Token::Reserved(_)) | Some(Token::Custom(_)) => (), // Disabled keywords are also OK - Some(token) - if !self - .disabled_symbols - .as_ref() - .map(|d| d.contains(token.syntax().as_ref())) - .unwrap_or(false) => - { - () - } + Some(token) if !self.disabled_symbols.contains(token.syntax().as_ref()) => (), // Active standard keywords cannot be made custom Some(_) => return Err(format!("'{}' is a reserved keyword", keyword).into()), } // Add to custom keywords - if self.custom_keywords.is_none() { - self.custom_keywords = Some(Default::default()); - } - self.custom_keywords - .as_mut() - .unwrap() - .insert(keyword.into(), precedence); + .insert(keyword.into(), Some(precedence)); Ok(self) } diff --git a/src/syntax.rs b/src/syntax.rs index c5e9d138..5096547f 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -7,13 +7,10 @@ use crate::fn_native::{SendSync, Shared}; use crate::parser::Expr; use crate::result::EvalAltResult; use crate::token::{is_valid_identifier, Position, Token}; +use crate::utils::ImmutableString; use crate::StaticVec; -use crate::stdlib::{ - boxed::Box, - fmt, format, - string::{String, ToString}, -}; +use crate::stdlib::{boxed::Box, format, string::ToString}; /// A general expression evaluation trait object. #[cfg(not(feature = "sync"))] @@ -24,6 +21,14 @@ pub type FnCustomSyntaxEval = pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression]) -> Result> + Send + Sync; +/// A general expression parsing trait object. +#[cfg(not(feature = "sync"))] +pub type FnCustomSyntaxParse = dyn Fn(&[&String]) -> Result, ParseError>; +/// A general expression parsing trait object. +#[cfg(feature = "sync")] +pub type FnCustomSyntaxParse = + dyn Fn(&[&String]) -> Result, ParseError> + Send + Sync; + /// An expression sub-tree in an AST. #[derive(Debug, Clone, Hash)] pub struct Expression<'a>(&'a Expr); @@ -76,20 +81,12 @@ impl EvalContext<'_, '_, '_, '_, '_, '_, '_, '_, '_> { } } -#[derive(Clone)] pub struct CustomSyntax { - pub segments: StaticVec, + pub parse: Box, pub func: Shared, pub scope_delta: isize, } -impl fmt::Debug for CustomSyntax { - #[inline(always)] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.segments, f) - } -} - impl Engine { /// Register a custom syntax with the `Engine`. /// @@ -106,7 +103,7 @@ impl Engine { ) -> Result<&mut Self, ParseError> { let keywords = keywords.as_ref(); - let mut segments: StaticVec<_> = Default::default(); + let mut segments: StaticVec = Default::default(); for s in keywords { let s = s.as_ref().trim(); @@ -119,47 +116,40 @@ impl Engine { let seg = match s { // Markers not in first position MARKER_EXPR | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => s.to_string(), - // Standard symbols not in first position + // Standard or reserved keyword/symbol not in first position s if !segments.is_empty() && Token::lookup_from_syntax(s).is_some() => { - // Make it a custom keyword/operator if it is a disabled standard keyword/operator - // or a reserved keyword/operator. - if self - .disabled_symbols - .as_ref() - .map(|d| d.contains(s)) - .unwrap_or(false) - || Token::lookup_from_syntax(s) - .map(|token| token.is_reserved()) - .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); - } + // Make it a custom keyword/symbol + if !self.custom_keywords.contains_key(s) { + self.custom_keywords.insert(s.into(), None); } - s.into() } - // Identifier - s if is_valid_identifier(s.chars()) => { - if self.custom_keywords.is_none() { - self.custom_keywords = Some(Default::default()); + // Standard keyword in first position + s if segments.is_empty() + && Token::lookup_from_syntax(s) + .map(|v| v.is_keyword() || v.is_reserved()) + .unwrap_or(false) => + { + return Err(LexError::ImproperSymbol(format!( + "Improper symbol for custom syntax at position #{}: '{}'", + segments.len() + 1, + s + )) + .into_err(Position::none()) + .into()); + } + // Identifier in first position + s if segments.is_empty() && is_valid_identifier(s.chars()) => { + if !self.custom_keywords.contains_key(s) { + self.custom_keywords.insert(s.into(), None); } - - if !self.custom_keywords.as_ref().unwrap().contains_key(s) { - self.custom_keywords.as_mut().unwrap().insert(s.into(), 0); - } - s.into() } // Anything else is an error _ => { return Err(LexError::ImproperSymbol(format!( - "Improper symbol for custom syntax: '{}'", + "Improper symbol for custom syntax at position #{}: '{}'", + segments.len() + 1, s )) .into_err(Position::none()) @@ -167,7 +157,7 @@ impl Engine { } }; - segments.push(seg); + segments.push(seg.into()); } // If the syntax has no keywords, just ignore the registration @@ -175,24 +165,54 @@ impl Engine { return Ok(self); } - // Remove the first keyword as the discriminator - let key = segments.remove(0); + // The first keyword is the discriminator + let key = segments[0].clone(); + self.register_custom_syntax_raw( + key, + // Construct the parsing function + move |stream| { + if stream.len() >= segments.len() { + Ok(None) + } else { + Ok(Some(segments[stream.len()].clone())) + } + }, + new_vars, + func, + ); + + Ok(self) + } + + /// Register a custom syntax with the `Engine`. + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. + /// + /// * `new_vars` is the number of new variables declared by this custom syntax, or the number of variables removed (if negative). + /// * `parse` is the parsing function. + /// * `func` is the implementation function. + /// + /// All custom keywords must be manually registered via `Engine::register_custom_operator`. + /// Otherwise, custom keywords won't be recognized. + pub fn register_custom_syntax_raw( + &mut self, + key: impl Into, + parse: impl Fn(&[&String]) -> Result, ParseError> + SendSync + 'static, + new_vars: isize, + func: impl Fn(&mut EvalContext, &[Expression]) -> Result> + + SendSync + + 'static, + ) -> &mut Self { let syntax = CustomSyntax { - segments, + parse: Box::new(parse), func: (Box::new(func) as Box).into(), scope_delta: new_vars, }; - if self.custom_syntax.is_none() { - self.custom_syntax = Some(Default::default()); - } - - self.custom_syntax - .as_mut() - .unwrap() - .insert(key, syntax.into()); - - Ok(self) + self.custom_syntax.insert(key.into(), syntax); + self } } diff --git a/src/token.rs b/src/token.rs index 3a27c4cf..82d7c611 100644 --- a/src/token.rs +++ b/src/token.rs @@ -18,9 +18,7 @@ use crate::parser::FLOAT; use crate::stdlib::{ borrow::Cow, boxed::Box, - char, - collections::HashMap, - fmt, format, + char, fmt, format, iter::Peekable, str::{Chars, FromStr}, string::{String, ToString}, @@ -610,7 +608,7 @@ impl Token { } /// Get the precedence number of the token. - pub fn precedence(&self, custom: Option<&HashMap>) -> u8 { + pub fn precedence(&self) -> u8 { use Token::*; match self { @@ -639,9 +637,6 @@ impl Token { Period => 240, - // Custom operators - Custom(s) => custom.map_or(0, |c| *c.get(s).unwrap()), - _ => 0, } } @@ -692,7 +687,7 @@ impl Token { Import | Export | As => true, True | False | Let | Const | If | Else | While | Loop | For | In | Continue | Break - | Return | Throw => true, + | Return | Throw | Try | Catch => true, _ => false, } @@ -1647,16 +1642,12 @@ impl<'a> Iterator for TokenIterator<'a, '_> { type Item = (Token, Position); fn next(&mut self) -> Option { - let token = match ( - get_next_token(&mut self.stream, &mut self.state, &mut self.pos), - self.engine.disabled_symbols.as_ref(), - self.engine.custom_keywords.as_ref(), - ) { + let token = match get_next_token(&mut self.stream, &mut self.state, &mut self.pos) { // {EOF} - (None, _, _) => None, + None => None, // Reserved keyword/symbol - (Some((Token::Reserved(s), pos)), disabled, custom) => Some((match - (s.as_str(), custom.map(|c| c.contains_key(&s)).unwrap_or(false)) + Some((Token::Reserved(s), pos)) => Some((match + (s.as_str(), self.engine.custom_keywords.contains_key(&s)) { ("===", false) => Token::LexError(Box::new(LERR::ImproperSymbol( "'===' is not a valid operator. This is not JavaScript! Should it be '=='?".to_string(), @@ -1691,21 +1682,19 @@ impl<'a> Iterator for TokenIterator<'a, '_> { format!("'{}' is a reserved symbol", token) ))), // Reserved keyword that is not custom and disabled. - (token, false) if disabled.map(|d| d.contains(token)).unwrap_or(false) => Token::LexError(Box::new(LERR::ImproperSymbol( + (token, false) if self.engine.disabled_symbols.contains(token) => Token::LexError(Box::new(LERR::ImproperSymbol( format!("reserved symbol '{}' is disabled", token) ))), // Reserved keyword/operator that is not custom. (_, false) => Token::Reserved(s), }, pos)), // Custom keyword - (Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => { + Some((Token::Identifier(s), pos)) if self.engine.custom_keywords.contains_key(&s) => { Some((Token::Custom(s), pos)) } // Custom standard keyword - must be disabled - (Some((token, pos)), Some(disabled), Some(custom)) - if token.is_keyword() && custom.contains_key(token.syntax().as_ref()) => - { - if disabled.contains(token.syntax().as_ref()) { + Some((token, pos)) if token.is_keyword() && self.engine.custom_keywords.contains_key(token.syntax().as_ref()) => { + if self.engine.disabled_symbols.contains(token.syntax().as_ref()) { // Disabled standard keyword Some((Token::Custom(token.syntax().into()), pos)) } else { @@ -1714,21 +1703,17 @@ impl<'a> Iterator for TokenIterator<'a, '_> { } } // Disabled operator - (Some((token, pos)), Some(disabled), _) - if token.is_operator() && disabled.contains(token.syntax().as_ref()) => - { + Some((token, pos)) if token.is_operator() && self.engine.disabled_symbols.contains(token.syntax().as_ref()) => { Some(( Token::LexError(Box::new(LexError::UnexpectedInput(token.syntax().into()))), pos, )) } // Disabled standard keyword - (Some((token, pos)), Some(disabled), _) - if token.is_keyword() && disabled.contains(token.syntax().as_ref()) => - { + Some((token, pos)) if token.is_keyword() && self.engine.disabled_symbols.contains(token.syntax().as_ref()) => { Some((Token::Reserved(token.syntax().into()), pos)) } - (r, _, _) => r, + r => r, }; match token { diff --git a/tests/syntax.rs b/tests/syntax.rs index c0433e9e..447e7126 100644 --- a/tests/syntax.rs +++ b/tests/syntax.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult, EvalContext, Expression, ParseErrorType, INT}; +use rhai::{Engine, EvalAltResult, ParseError, ParseErrorType, Position, INT}; #[test] fn test_custom_syntax() -> Result<(), Box> { @@ -19,10 +19,10 @@ fn test_custom_syntax() -> Result<(), Box> { engine.register_custom_syntax( &[ - "do", "|", "$ident$", "|", "->", "$block$", "while", "$expr$", + "exec", "|", "$ident$", "|", "->", "$block$", "while", "$expr$", ], 1, - |context: &mut EvalContext, inputs: &[Expression]| { + |context, inputs| { let var_name = inputs[0].get_variable_name().unwrap().to_string(); let stmt = inputs.get(1).unwrap(); let condition = inputs.get(2).unwrap(); @@ -58,7 +58,7 @@ fn test_custom_syntax() -> Result<(), Box> { assert_eq!( engine.eval::( r" - do |x| -> { x += 1 } while x < 42; + exec |x| -> { x += 1 } while x < 42; x " )?, @@ -71,7 +71,48 @@ fn test_custom_syntax() -> Result<(), Box> { .register_custom_syntax(&["!"], 0, |_, _| Ok(().into())) .expect_err("should error") .0, - ParseErrorType::BadInput("Improper symbol for custom syntax: '!'".to_string()) + ParseErrorType::BadInput( + "Improper symbol for custom syntax at position #1: '!'".to_string() + ) + ); + + Ok(()) +} + +#[test] +fn test_custom_syntax_raw() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine.register_custom_syntax_raw( + "hello", + |stream| match stream.len() { + 0 => unreachable!(), + 1 => Ok(Some("$ident$".into())), + 2 => match stream[1].as_str() { + "world" | "kitty" => Ok(None), + s => Err(ParseError( + Box::new(ParseErrorType::BadInput(s.to_string())), + Position::none(), + )), + }, + _ => unreachable!(), + }, + 0, + |_, inputs| { + Ok(match inputs[0].get_variable_name().unwrap() { + "world" => 123 as INT, + "kitty" => 42 as INT, + _ => unreachable!(), + } + .into()) + }, + ); + + assert_eq!(engine.eval::("hello world")?, 123); + assert_eq!(engine.eval::("hello kitty")?, 42); + assert_eq!( + *engine.compile("hello hey").expect_err("should error").0, + ParseErrorType::BadInput("hey".to_string()) ); Ok(())