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. * `$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, 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 except the _first_ symbol which must be a custom keyword that follows the naming rules
of [variables]. of [variables].
The first symbol also cannot be a reserved [keyword], unless that keyword The first symbol also cannot be a normal or reserved [keyword].
has been [disabled][disable keywords and operators]. In other words, any valid identifier that is not a [keyword] will work fine.
In other words, any valid identifier that is not an active [keyword] will work fine.
### The First Symbol Must be Unique ### The First Symbol Must be Unique
@ -127,6 +125,8 @@ where:
* `inputs: &[Expression]` - a list of input expression trees. * `inputs: &[Expression]` - a list of input expression trees.
* Return value - result of evaluating the custom syntax expression.
### Access Arguments ### Access Arguments
The most important argument is `inputs` where the matched identifiers (`$ident$`), expressions/statements (`$expr$`) The most important argument is `inputs` where the matched identifiers (`$ident$`), expressions/statements (`$expr$`)
@ -215,9 +215,9 @@ fn implementation_func(
Ok(().into()) 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( 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 1, // the number of new variables declared within this custom syntax
implementation_func implementation_func
)?; )?;
@ -252,3 +252,70 @@ Make sure there are _lots_ of examples for users to follow.
Step Six - Profit! 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(); let mut engine = Engine::new();
// Register a variable resolver. // Register a variable resolver.
engine.on_var(|name, index, scope, context| { engine.on_var(|name, index, context| {
match name { match name {
"MYSTIC_NUMBER" => Ok(Some((42 as INT).into())), "MYSTIC_NUMBER" => Ok(Some((42 as INT).into())),
// Override a variable - make it not found even if it exists! // 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()) EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::none())
)), )),
// Silently maps 'chameleon' into 'innocent'. // 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()) EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::none())
)), )),
// Return Ok(None) to continue with the normal variable resolution process. // 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. * `args: &mut [&mut Dynamic]` - a slice containing `&mut` references to [`Dynamic`] values.
The slice is guaranteed to contain enough arguments _of the correct types_. 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). 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 Therefore, it is unnecessary to ever mutate any argument except the first one, as all mutations
will be on the cloned copy. will be on the cloned copy.

View File

@ -164,14 +164,8 @@ impl Engine {
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
#[inline(always)] #[inline(always)]
pub fn register_type_with_name<T: Variant + Clone>(&mut self, name: &str) -> &mut Self { 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 // Add the pretty-print type name into the map
self.type_names self.type_names.insert(type_name::<T>().into(), name.into());
.as_mut()
.unwrap()
.insert(type_name::<T>().into(), name.into());
self self
} }

View File

@ -512,14 +512,14 @@ pub struct Engine {
pub(crate) module_resolver: Option<Box<dyn ModuleResolver>>, pub(crate) module_resolver: Option<Box<dyn ModuleResolver>>,
/// A hashmap mapping type names to pretty-print names. /// 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. /// 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. /// 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. /// Custom syntax.
pub(crate) custom_syntax: Option<HashMap<String, CustomSyntax>>, pub(crate) custom_syntax: HashMap<ImmutableString, CustomSyntax>,
/// Callback closure for resolving variable access. /// Callback closure for resolving variable access.
pub(crate) resolve_var: Option<OnVarCallback>, pub(crate) resolve_var: Option<OnVarCallback>,
@ -658,10 +658,10 @@ impl Engine {
#[cfg(any(feature = "no_std", target_arch = "wasm32",))] #[cfg(any(feature = "no_std", target_arch = "wasm32",))]
module_resolver: None, module_resolver: None,
type_names: None, type_names: Default::default(),
disabled_symbols: None, disabled_symbols: Default::default(),
custom_keywords: None, custom_keywords: Default::default(),
custom_syntax: None, custom_syntax: Default::default(),
// variable resolver // variable resolver
resolve_var: None, resolve_var: None,
@ -715,10 +715,10 @@ impl Engine {
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
module_resolver: None, module_resolver: None,
type_names: None, type_names: Default::default(),
disabled_symbols: None, disabled_symbols: Default::default(),
custom_keywords: None, custom_keywords: Default::default(),
custom_syntax: None, custom_syntax: Default::default(),
resolve_var: None, resolve_var: None,
@ -2274,8 +2274,8 @@ impl Engine {
#[inline(always)] #[inline(always)]
pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str { pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str {
self.type_names self.type_names
.as_ref() .get(name)
.and_then(|t| t.get(name).map(String::as_str)) .map(String::as_str)
.unwrap_or_else(|| map_std_type_name(name)) .unwrap_or_else(|| map_std_type_name(name))
} }

View File

@ -1055,7 +1055,7 @@ impl Engine {
} else { } else {
// If the first argument is a variable, and there is no curried arguments, convert to method-call style // 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 // 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(...) // func(x, ...) -> x.func(...)
arg_values = args_expr arg_values = args_expr
.iter() .iter()

View File

@ -2549,9 +2549,17 @@ fn parse_binary_op(
let mut root = lhs; let mut root = lhs;
loop { loop {
let (current_op, _) = input.peek().unwrap(); let (current_op, current_pos) = input.peek().unwrap();
let custom = state.engine.custom_keywords.as_ref(); let precedence = if let Token::Custom(c) = current_op {
let precedence = current_op.precedence(custom); // 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(); let bind_right = current_op.is_bind_right();
// Bind left to the parent lhs expression if precedence is higher // 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 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 // Bind to right if the next operator has higher precedence
// If same precedence, then check if the operator binds right // 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)? make_dot_expr(current_lhs, rhs, pos)?
} }
Token::Custom(s) Token::Custom(s) if state.engine.custom_keywords.contains_key(&s) => {
if state
.engine
.custom_keywords
.as_ref()
.map(|c| c.contains_key(&s))
.unwrap_or(false) =>
{
// Accept non-native functions for custom operators // Accept non-native functions for custom operators
let op = (op.0, false, op.2, op.3); let op = (op.0, false, op.2, op.3);
Expr::FnCall(Box::new((op, None, hash, args, None))) Expr::FnCall(Box::new((op, None, hash, args, None)))
@ -2665,12 +2676,12 @@ fn parse_binary_op(
} }
/// Parse a custom syntax. /// Parse a custom syntax.
fn parse_custom( fn parse_custom_syntax(
input: &mut TokenStream, input: &mut TokenStream,
state: &mut ParseState, state: &mut ParseState,
lib: &mut FunctionsLib, lib: &mut FunctionsLib,
mut settings: ParseSettings, mut settings: ParseSettings,
key: &str, key: String,
syntax: &CustomSyntax, syntax: &CustomSyntax,
pos: Position, pos: Position,
) -> Result<Expr, ParseError> { ) -> 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; 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(); let settings = settings.level_up();
match segment.as_str() { match token.as_str() {
MARKER_IDENT => match input.next().unwrap() { MARKER_IDENT => match input.next().unwrap() {
(Token::Identifier(s), pos) => { (Token::Identifier(s), pos) => {
segments.push(s.to_string());
exprs.push(Expr::Variable(Box::new(((s, pos), None, 0, None)))); exprs.push(Expr::Variable(Box::new(((s, pos), None, 0, None))));
} }
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { (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)), (_, 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 => { MARKER_BLOCK => {
let stmt = parse_block(input, state, lib, settings)?; let stmt = parse_block(input, state, lib, settings)?;
let pos = stmt.position(); 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() { s => match input.peek().unwrap() {
(Token::LexError(err), pos) => return Err(err.into_err(*pos)),
(t, _) if t.syntax().as_ref() == s => { (t, _) if t.syntax().as_ref() == s => {
input.next().unwrap(); segments.push(input.next().unwrap().0.syntax().into_owned());
} }
(_, pos) => { (_, pos) => {
return Err(PERR::MissingToken( return Err(PERR::MissingToken(
s.to_string(), s.to_string(),
format!("for '{}' expression", key), format!("for '{}' expression", segments[0]),
) )
.into_err(*pos)) .into_err(*pos))
} }
@ -2745,16 +2774,22 @@ fn parse_expr(
settings.ensure_level_within_max_limit(state.max_expr_depth)?; settings.ensure_level_within_max_limit(state.max_expr_depth)?;
// Check if it is a custom syntax. // 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) = input.peek().unwrap();
let token_pos = *pos; let token_pos = *pos;
match token { match token {
Token::Custom(key) if custom.contains_key(key) => { Token::Custom(key) | Token::Reserved(key) | Token::Identifier(key) => {
let custom = custom.get_key_value(key).unwrap(); match state.engine.custom_syntax.get_key_value(key) {
let (key, syntax) = custom; Some((key, syntax)) => {
input.next().unwrap(); let key = key.to_string();
return parse_custom(input, state, lib, settings, key, syntax, token_pos); 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)] #[inline(always)]
pub fn disable_symbol(&mut self, symbol: &str) -> &mut Self { pub fn disable_symbol(&mut self, symbol: &str) -> &mut Self {
if self.disabled_symbols.is_none() { self.disabled_symbols.insert(symbol.into());
self.disabled_symbols = Some(Default::default());
}
self.disabled_symbols
.as_mut()
.unwrap()
.insert(symbol.into());
self self
} }
@ -291,28 +283,14 @@ impl Engine {
// Standard identifiers, reserved keywords and custom keywords are OK // Standard identifiers, reserved keywords and custom keywords are OK
None | Some(Token::Reserved(_)) | Some(Token::Custom(_)) => (), None | Some(Token::Reserved(_)) | Some(Token::Custom(_)) => (),
// Disabled keywords are also OK // Disabled keywords are also OK
Some(token) Some(token) if !self.disabled_symbols.contains(token.syntax().as_ref()) => (),
if !self
.disabled_symbols
.as_ref()
.map(|d| d.contains(token.syntax().as_ref()))
.unwrap_or(false) =>
{
()
}
// Active standard keywords cannot be made custom // Active standard keywords cannot be made custom
Some(_) => return Err(format!("'{}' is a reserved keyword", keyword).into()), Some(_) => return Err(format!("'{}' is a reserved keyword", keyword).into()),
} }
// Add to custom keywords // Add to custom keywords
if self.custom_keywords.is_none() {
self.custom_keywords = Some(Default::default());
}
self.custom_keywords self.custom_keywords
.as_mut() .insert(keyword.into(), Some(precedence));
.unwrap()
.insert(keyword.into(), precedence);
Ok(self) Ok(self)
} }

View File

@ -7,13 +7,10 @@ use crate::fn_native::{SendSync, Shared};
use crate::parser::Expr; use crate::parser::Expr;
use crate::result::EvalAltResult; use crate::result::EvalAltResult;
use crate::token::{is_valid_identifier, Position, Token}; use crate::token::{is_valid_identifier, Position, Token};
use crate::utils::ImmutableString;
use crate::StaticVec; use crate::StaticVec;
use crate::stdlib::{ use crate::stdlib::{boxed::Box, format, string::ToString};
boxed::Box,
fmt, format,
string::{String, ToString},
};
/// A general expression evaluation trait object. /// A general expression evaluation trait object.
#[cfg(not(feature = "sync"))] #[cfg(not(feature = "sync"))]
@ -24,6 +21,14 @@ pub type FnCustomSyntaxEval =
pub type FnCustomSyntaxEval = pub type FnCustomSyntaxEval =
dyn Fn(&mut EvalContext, &[Expression]) -> Result<Dynamic, Box<EvalAltResult>> + Send + Sync; 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. /// An expression sub-tree in an AST.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct Expression<'a>(&'a Expr); pub struct Expression<'a>(&'a Expr);
@ -76,20 +81,12 @@ impl EvalContext<'_, '_, '_, '_, '_, '_, '_, '_, '_> {
} }
} }
#[derive(Clone)]
pub struct CustomSyntax { pub struct CustomSyntax {
pub segments: StaticVec<String>, pub parse: Box<FnCustomSyntaxParse>,
pub func: Shared<FnCustomSyntaxEval>, pub func: Shared<FnCustomSyntaxEval>,
pub scope_delta: isize, 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 { impl Engine {
/// Register a custom syntax with the `Engine`. /// Register a custom syntax with the `Engine`.
/// ///
@ -106,7 +103,7 @@ impl Engine {
) -> Result<&mut Self, ParseError> { ) -> Result<&mut Self, ParseError> {
let keywords = keywords.as_ref(); let keywords = keywords.as_ref();
let mut segments: StaticVec<_> = Default::default(); let mut segments: StaticVec<ImmutableString> = Default::default();
for s in keywords { for s in keywords {
let s = s.as_ref().trim(); let s = s.as_ref().trim();
@ -119,47 +116,40 @@ impl Engine {
let seg = match s { let seg = match s {
// Markers not in first position // Markers not in first position
MARKER_EXPR | 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 // Standard or reserved keyword/symbol not in first position
s if !segments.is_empty() && Token::lookup_from_syntax(s).is_some() => { 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 // Make it a custom keyword/symbol
// or a reserved keyword/operator. if !self.custom_keywords.contains_key(s) {
if self self.custom_keywords.insert(s.into(), None);
.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);
}
} }
s.into() s.into()
} }
// Identifier // Standard keyword in first position
s if is_valid_identifier(s.chars()) => { s if segments.is_empty()
if self.custom_keywords.is_none() { && Token::lookup_from_syntax(s)
self.custom_keywords = Some(Default::default()); .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() s.into()
} }
// Anything else is an error // Anything else is an error
_ => { _ => {
return Err(LexError::ImproperSymbol(format!( return Err(LexError::ImproperSymbol(format!(
"Improper symbol for custom syntax: '{}'", "Improper symbol for custom syntax at position #{}: '{}'",
segments.len() + 1,
s s
)) ))
.into_err(Position::none()) .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 // If the syntax has no keywords, just ignore the registration
@ -175,24 +165,54 @@ impl Engine {
return Ok(self); return Ok(self);
} }
// Remove the first keyword as the discriminator // The first keyword is the discriminator
let key = segments.remove(0); 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 { let syntax = CustomSyntax {
segments, parse: Box::new(parse),
func: (Box::new(func) as Box<FnCustomSyntaxEval>).into(), func: (Box::new(func) as Box<FnCustomSyntaxEval>).into(),
scope_delta: new_vars, scope_delta: new_vars,
}; };
if self.custom_syntax.is_none() { self.custom_syntax.insert(key.into(), syntax);
self.custom_syntax = Some(Default::default()); self
}
self.custom_syntax
.as_mut()
.unwrap()
.insert(key, syntax.into());
Ok(self)
} }
} }

View File

@ -18,9 +18,7 @@ use crate::parser::FLOAT;
use crate::stdlib::{ use crate::stdlib::{
borrow::Cow, borrow::Cow,
boxed::Box, boxed::Box,
char, char, fmt, format,
collections::HashMap,
fmt, format,
iter::Peekable, iter::Peekable,
str::{Chars, FromStr}, str::{Chars, FromStr},
string::{String, ToString}, string::{String, ToString},
@ -610,7 +608,7 @@ impl Token {
} }
/// Get the precedence number of the 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::*; use Token::*;
match self { match self {
@ -639,9 +637,6 @@ impl Token {
Period => 240, Period => 240,
// Custom operators
Custom(s) => custom.map_or(0, |c| *c.get(s).unwrap()),
_ => 0, _ => 0,
} }
} }
@ -692,7 +687,7 @@ impl Token {
Import | Export | As => true, Import | Export | As => true,
True | False | Let | Const | If | Else | While | Loop | For | In | Continue | Break True | False | Let | Const | If | Else | While | Loop | For | In | Continue | Break
| Return | Throw => true, | Return | Throw | Try | Catch => true,
_ => false, _ => false,
} }
@ -1647,16 +1642,12 @@ impl<'a> Iterator for TokenIterator<'a, '_> {
type Item = (Token, Position); type Item = (Token, Position);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let token = match ( let token = match get_next_token(&mut self.stream, &mut self.state, &mut self.pos) {
get_next_token(&mut self.stream, &mut self.state, &mut self.pos),
self.engine.disabled_symbols.as_ref(),
self.engine.custom_keywords.as_ref(),
) {
// {EOF} // {EOF}
(None, _, _) => None, None => None,
// Reserved keyword/symbol // Reserved keyword/symbol
(Some((Token::Reserved(s), pos)), disabled, custom) => Some((match Some((Token::Reserved(s), pos)) => Some((match
(s.as_str(), custom.map(|c| c.contains_key(&s)).unwrap_or(false)) (s.as_str(), self.engine.custom_keywords.contains_key(&s))
{ {
("===", false) => Token::LexError(Box::new(LERR::ImproperSymbol( ("===", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
"'===' is not a valid operator. This is not JavaScript! Should it be '=='?".to_string(), "'===' 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) format!("'{}' is a reserved symbol", token)
))), ))),
// Reserved keyword that is not custom and disabled. // 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) format!("reserved symbol '{}' is disabled", token)
))), ))),
// Reserved keyword/operator that is not custom. // Reserved keyword/operator that is not custom.
(_, false) => Token::Reserved(s), (_, false) => Token::Reserved(s),
}, pos)), }, pos)),
// Custom keyword // 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)) Some((Token::Custom(s), pos))
} }
// Custom standard keyword - must be disabled // Custom standard keyword - must be disabled
(Some((token, pos)), Some(disabled), Some(custom)) Some((token, pos)) if token.is_keyword() && self.engine.custom_keywords.contains_key(token.syntax().as_ref()) => {
if token.is_keyword() && custom.contains_key(token.syntax().as_ref()) => if self.engine.disabled_symbols.contains(token.syntax().as_ref()) {
{
if disabled.contains(token.syntax().as_ref()) {
// Disabled standard keyword // Disabled standard keyword
Some((Token::Custom(token.syntax().into()), pos)) Some((Token::Custom(token.syntax().into()), pos))
} else { } else {
@ -1714,21 +1703,17 @@ impl<'a> Iterator for TokenIterator<'a, '_> {
} }
} }
// Disabled operator // Disabled operator
(Some((token, pos)), Some(disabled), _) Some((token, pos)) if token.is_operator() && self.engine.disabled_symbols.contains(token.syntax().as_ref()) => {
if token.is_operator() && disabled.contains(token.syntax().as_ref()) =>
{
Some(( Some((
Token::LexError(Box::new(LexError::UnexpectedInput(token.syntax().into()))), Token::LexError(Box::new(LexError::UnexpectedInput(token.syntax().into()))),
pos, pos,
)) ))
} }
// Disabled standard keyword // Disabled standard keyword
(Some((token, pos)), Some(disabled), _) Some((token, pos)) if token.is_keyword() && self.engine.disabled_symbols.contains(token.syntax().as_ref()) => {
if token.is_keyword() && disabled.contains(token.syntax().as_ref()) =>
{
Some((Token::Reserved(token.syntax().into()), pos)) Some((Token::Reserved(token.syntax().into()), pos))
} }
(r, _, _) => r, r => r,
}; };
match token { 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] #[test]
fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> { fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
@ -19,10 +19,10 @@ fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
engine.register_custom_syntax( engine.register_custom_syntax(
&[ &[
"do", "|", "$ident$", "|", "->", "$block$", "while", "$expr$", "exec", "|", "$ident$", "|", "->", "$block$", "while", "$expr$",
], ],
1, 1,
|context: &mut EvalContext, inputs: &[Expression]| { |context, inputs| {
let var_name = inputs[0].get_variable_name().unwrap().to_string(); let var_name = inputs[0].get_variable_name().unwrap().to_string();
let stmt = inputs.get(1).unwrap(); let stmt = inputs.get(1).unwrap();
let condition = inputs.get(2).unwrap(); let condition = inputs.get(2).unwrap();
@ -58,7 +58,7 @@ fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
assert_eq!( assert_eq!(
engine.eval::<INT>( engine.eval::<INT>(
r" r"
do |x| -> { x += 1 } while x < 42; exec |x| -> { x += 1 } while x < 42;
x x
" "
)?, )?,
@ -71,7 +71,48 @@ fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
.register_custom_syntax(&["!"], 0, |_, _| Ok(().into())) .register_custom_syntax(&["!"], 0, |_, _| Ok(().into()))
.expect_err("should error") .expect_err("should error")
.0, .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(()) Ok(())