Add raw API for custom syntax.

This commit is contained in:
Stephen Chung 2020-10-25 21:57:18 +08:00
parent f670d55871
commit b607a3a9ba
11 changed files with 298 additions and 176 deletions

View File

@ -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<Option<ImmutableString>, 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<Option<ImmutableString>, 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.

View File

@ -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.

View File

@ -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.

View File

@ -164,14 +164,8 @@ impl Engine {
#[cfg(not(feature = "no_object"))]
#[inline(always)]
pub fn register_type_with_name<T: Variant + Clone>(&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::<T>().into(), name.into());
self.type_names.insert(type_name::<T>().into(), name.into());
self
}

View File

@ -512,14 +512,14 @@ pub struct Engine {
pub(crate) module_resolver: Option<Box<dyn ModuleResolver>>,
/// A hashmap mapping type names to pretty-print names.
pub(crate) type_names: Option<HashMap<String, String>>,
pub(crate) type_names: HashMap<String, String>,
/// A hashset containing symbols to disable.
pub(crate) disabled_symbols: Option<HashSet<String>>,
pub(crate) disabled_symbols: HashSet<String>,
/// A hashset containing custom keywords and precedence to recognize.
pub(crate) custom_keywords: Option<HashMap<String, u8>>,
pub(crate) custom_keywords: HashMap<String, Option<u8>>,
/// Custom syntax.
pub(crate) custom_syntax: Option<HashMap<String, CustomSyntax>>,
pub(crate) custom_syntax: HashMap<ImmutableString, CustomSyntax>,
/// Callback closure for resolving variable access.
pub(crate) resolve_var: Option<OnVarCallback>,
@ -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))
}

View File

@ -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()

View File

@ -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<Expr, ParseError> {
@ -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::<StaticVec<_>>())
.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,
);
}
_ => (),
}
}
_ => (),
}

View File

@ -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)
}

View File

@ -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<Dynamic, Box<EvalAltResult>> + Send + Sync;
/// A general expression parsing trait object.
#[cfg(not(feature = "sync"))]
pub type FnCustomSyntaxParse = dyn Fn(&[&String]) -> Result<Option<ImmutableString>, ParseError>;
/// A general expression parsing trait object.
#[cfg(feature = "sync")]
pub type FnCustomSyntaxParse =
dyn Fn(&[&String]) -> Result<Option<ImmutableString>, 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<String>,
pub parse: Box<FnCustomSyntaxParse>,
pub func: Shared<FnCustomSyntaxEval>,
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<ImmutableString> = 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<ImmutableString>,
parse: impl Fn(&[&String]) -> Result<Option<ImmutableString>, ParseError> + SendSync + 'static,
new_vars: isize,
func: impl Fn(&mut EvalContext, &[Expression]) -> Result<Dynamic, Box<EvalAltResult>>
+ SendSync
+ 'static,
) -> &mut Self {
let syntax = CustomSyntax {
segments,
parse: Box::new(parse),
func: (Box::new(func) as Box<FnCustomSyntaxEval>).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
}
}

View File

@ -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<String, u8>>) -> 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<Self::Item> {
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 {

View File

@ -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<EvalAltResult>> {
@ -19,10 +19,10 @@ fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
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<EvalAltResult>> {
assert_eq!(
engine.eval::<INT>(
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<EvalAltResult>> {
.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<EvalAltResult>> {
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::<INT>("hello world")?, 123);
assert_eq!(engine.eval::<INT>("hello kitty")?, 42);
assert_eq!(
*engine.compile("hello hey").expect_err("should error").0,
ParseErrorType::BadInput("hey".to_string())
);
Ok(())