Add state to custom syntax.

This commit is contained in:
Stephen Chung 2022-09-12 12:03:32 +08:00
parent ea828262ba
commit c1ae9e0405
7 changed files with 112 additions and 28 deletions

View File

@ -1,6 +1,18 @@
Rhai Release Notes Rhai Release Notes
================== ==================
Version 1.11.0
==============
New features
------------
### Custom syntax with state
* [`Engine::register_custom_syntax_with_state_raw`] is added. The custom syntax parser and implementation functions take on an additional parameter that holds a user-defined custom _state_ which should substantially simplify writing some custom parsers.
* [`Engine::register_custom_syntax_raw`] is deprecated.
Version 1.10.0 Version 1.10.0
============== ==============

View File

@ -7,8 +7,8 @@ use crate::parser::ParseResult;
use crate::tokenizer::{is_valid_identifier, Token}; use crate::tokenizer::{is_valid_identifier, Token};
use crate::types::dynamic::Variant; use crate::types::dynamic::Variant;
use crate::{ use crate::{
reify, Engine, EvalContext, Identifier, ImmutableString, LexError, Position, RhaiResult, reify, Dynamic, Engine, EvalContext, Identifier, ImmutableString, LexError, Position,
StaticVec, RhaiResult, StaticVec,
}; };
use std::ops::Deref; use std::ops::Deref;
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
@ -39,19 +39,21 @@ pub mod markers {
/// A general expression evaluation trait object. /// A general expression evaluation trait object.
#[cfg(not(feature = "sync"))] #[cfg(not(feature = "sync"))]
pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression]) -> RhaiResult; pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression], &Dynamic) -> RhaiResult;
/// A general expression evaluation trait object. /// A general expression evaluation trait object.
#[cfg(feature = "sync")] #[cfg(feature = "sync")]
pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression]) -> RhaiResult + Send + Sync; pub type FnCustomSyntaxEval =
dyn Fn(&mut EvalContext, &[Expression], &Dynamic) -> RhaiResult + Send + Sync;
/// A general expression parsing trait object. /// A general expression parsing trait object.
#[cfg(not(feature = "sync"))] #[cfg(not(feature = "sync"))]
pub type FnCustomSyntaxParse = pub type FnCustomSyntaxParse =
dyn Fn(&[ImmutableString], &str) -> ParseResult<Option<ImmutableString>>; dyn Fn(&[ImmutableString], &str, &mut Dynamic) -> ParseResult<Option<ImmutableString>>;
/// A general expression parsing trait object. /// A general expression parsing trait object.
#[cfg(feature = "sync")] #[cfg(feature = "sync")]
pub type FnCustomSyntaxParse = pub type FnCustomSyntaxParse = dyn Fn(&[ImmutableString], &str, &mut Dynamic) -> ParseResult<Option<ImmutableString>>
dyn Fn(&[ImmutableString], &str) -> ParseResult<Option<ImmutableString>> + Send + Sync; + Send
+ Sync;
/// An expression sub-tree in an [`AST`][crate::AST]. /// An expression sub-tree in an [`AST`][crate::AST].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -297,10 +299,10 @@ impl Engine {
// The first keyword is the discriminator // The first keyword is the discriminator
let key = segments[0].clone(); let key = segments[0].clone();
self.register_custom_syntax_raw( self.register_custom_syntax_with_state_raw(
key, key,
// Construct the parsing function // Construct the parsing function
move |stream, _| { move |stream, _, _| {
if stream.len() >= segments.len() { if stream.len() >= segments.len() {
Ok(None) Ok(None)
} else { } else {
@ -308,12 +310,12 @@ impl Engine {
} }
}, },
scope_may_be_changed, scope_may_be_changed,
func, move |context, expressions, _| func(context, expressions),
); );
Ok(self) Ok(self)
} }
/// Register a custom syntax with the [`Engine`]. /// Register a custom syntax with the [`Engine`] with custom user-defined state.
/// ///
/// Not available under `no_custom_syntax`. /// Not available under `no_custom_syntax`.
/// ///
@ -328,30 +330,31 @@ impl Engine {
/// All custom keywords used as symbols must be manually registered via [`Engine::register_custom_operator`]. /// All custom keywords used as symbols must be manually registered via [`Engine::register_custom_operator`].
/// Otherwise, they won't be recognized. /// Otherwise, they won't be recognized.
/// ///
/// # Implementation Function Signature /// # Parsing Function Signature
/// ///
/// The implementation function has the following signature: /// The parsing function has the following signature:
/// ///
/// `Fn(symbols: &[ImmutableString], look_ahead: &str) -> Result<Option<ImmutableString>, ParseError>` /// `Fn(symbols: &[ImmutableString], look_ahead: &str, state: &mut Dynamic) -> Result<Option<ImmutableString>, ParseError>`
/// ///
/// where: /// where:
/// * `symbols`: a slice of symbols that have been parsed so far, possibly containing `$expr$` and/or `$block$`; /// * `symbols`: a slice of symbols that have been parsed so far, possibly containing `$expr$` and/or `$block$`;
/// `$ident$` and other literal markers are replaced by the actual text /// `$ident$` and other literal markers are replaced by the actual text
/// * `look_ahead`: a string slice containing the next symbol that is about to be read /// * `look_ahead`: a string slice containing the next symbol that is about to be read
/// * `state`: a [`Dynamic`] value that contains a user-defined state
/// ///
/// ## Return value /// ## Return value
/// ///
/// * `Ok(None)`: parsing complete and there are no more symbols to match. /// * `Ok(None)`: parsing complete and there are no more symbols to match.
/// * `Ok(Some(symbol))`: the next symbol to match, which can also be `$expr$`, `$ident$` or `$block$`. /// * `Ok(Some(symbol))`: the next symbol to match, which can also be `$expr$`, `$ident$` or `$block$`.
/// * `Err(ParseError)`: error that is reflected back to the [`Engine`], normally `ParseError(ParseErrorType::BadInput(LexError::ImproperSymbol(message)), Position::NONE)` to indicate a syntax error, but it can be any [`ParseError`][crate::ParseError]. /// * `Err(ParseError)`: error that is reflected back to the [`Engine`], normally `ParseError(ParseErrorType::BadInput(LexError::ImproperSymbol(message)), Position::NONE)` to indicate a syntax error, but it can be any [`ParseError`][crate::ParseError].
pub fn register_custom_syntax_raw( pub fn register_custom_syntax_with_state_raw(
&mut self, &mut self,
key: impl Into<Identifier>, key: impl Into<Identifier>,
parse: impl Fn(&[ImmutableString], &str) -> ParseResult<Option<ImmutableString>> parse: impl Fn(&[ImmutableString], &str, &mut Dynamic) -> ParseResult<Option<ImmutableString>>
+ SendSync + SendSync
+ 'static, + 'static,
scope_may_be_changed: bool, scope_may_be_changed: bool,
func: impl Fn(&mut EvalContext, &[Expression]) -> RhaiResult + SendSync + 'static, func: impl Fn(&mut EvalContext, &[Expression], &Dynamic) -> RhaiResult + SendSync + 'static,
) -> &mut Self { ) -> &mut Self {
self.custom_syntax.insert( self.custom_syntax.insert(
key.into(), key.into(),

View File

@ -258,6 +258,68 @@ impl Engine {
) -> &mut Self { ) -> &mut Self {
self.register_indexer_set(set_fn) self.register_indexer_set(set_fn)
} }
/// Register a custom syntax with the [`Engine`].
///
/// Not available under `no_custom_syntax`.
///
/// # Deprecated
///
/// This method is deprecated.
/// Use [`register_custom_syntax_with_state_raw`][Engine::register_custom_syntax_with_state_raw] instead.
///
/// This method will be removed in the next major version.
///
/// # WARNING - Low Level API
///
/// This function is very low level.
///
/// * `scope_may_be_changed` specifies variables have been added/removed by this custom syntax.
/// * `parse` is the parsing function.
/// * `func` is the implementation function.
///
/// All custom keywords used as symbols must be manually registered via [`Engine::register_custom_operator`].
/// Otherwise, they won't be recognized.
///
/// # Parsing Function Signature
///
/// The parsing function has the following signature:
///
/// `Fn(symbols: &[ImmutableString], look_ahead: &str) -> Result<Option<ImmutableString>, ParseError>`
///
/// where:
/// * `symbols`: a slice of symbols that have been parsed so far, possibly containing `$expr$` and/or `$block$`;
/// `$ident$` and other literal markers are replaced by the actual text
/// * `look_ahead`: a string slice containing the next symbol that is about to be read
///
/// ## Return value
///
/// * `Ok(None)`: parsing complete and there are no more symbols to match.
/// * `Ok(Some(symbol))`: the next symbol to match, which can also be `$expr$`, `$ident$` or `$block$`.
/// * `Err(ParseError)`: error that is reflected back to the [`Engine`], normally `ParseError(ParseErrorType::BadInput(LexError::ImproperSymbol(message)), Position::NONE)` to indicate a syntax error, but it can be any [`ParseError`][crate::ParseError].
#[deprecated(
since = "1.11.0",
note = "use `register_custom_syntax_with_state_raw` instead"
)]
#[inline(always)]
#[cfg(not(feature = "no_custom_syntax"))]
pub fn register_custom_syntax_raw(
&mut self,
key: impl Into<Identifier>,
parse: impl Fn(&[ImmutableString], &str) -> crate::parser::ParseResult<Option<ImmutableString>>
+ crate::func::SendSync
+ 'static,
scope_may_be_changed: bool,
func: impl Fn(&mut crate::EvalContext, &[crate::Expression]) -> RhaiResult
+ crate::func::SendSync
+ 'static,
) -> &mut Self {
self.register_custom_syntax_with_state_raw(
key,
move |keywords, look_ahead, _| parse(keywords, look_ahead),
scope_may_be_changed,
move |context, expressions, _| func(context, expressions),
)
}
} }
impl Dynamic { impl Dynamic {

View File

@ -61,6 +61,8 @@ pub struct CustomExpr {
pub inputs: StaticVec<Expr>, pub inputs: StaticVec<Expr>,
/// List of tokens actually parsed. /// List of tokens actually parsed.
pub tokens: StaticVec<ImmutableString>, pub tokens: StaticVec<ImmutableString>,
/// State value.
pub state: Dynamic,
/// Is the current [`Scope`][crate::Scope] possibly modified by this custom statement /// Is the current [`Scope`][crate::Scope] possibly modified by this custom statement
/// (e.g. introducing a new variable)? /// (e.g. introducing a new variable)?
pub scope_may_be_changed: bool, pub scope_may_be_changed: bool,

View File

@ -596,7 +596,7 @@ impl Engine {
let mut context = let mut context =
EvalContext::new(self, scope, global, Some(caches), lib, this_ptr, level); EvalContext::new(self, scope, global, Some(caches), lib, this_ptr, level);
let result = (custom_def.func)(&mut context, &expressions); let result = (custom_def.func)(&mut context, &expressions, &custom.state);
self.check_return_value(result, expr.start_position()) self.check_return_value(result, expr.start_position())
} }

View File

@ -2477,6 +2477,7 @@ impl Engine {
state.stack.push(marker, ()); state.stack.push(marker, ());
} }
let mut user_state = Dynamic::UNIT;
let parse_func = &*syntax.parse; let parse_func = &*syntax.parse;
let mut required_token: ImmutableString = key.into(); let mut required_token: ImmutableString = key.into();
@ -2488,7 +2489,7 @@ impl Engine {
settings.pos = *fwd_pos; settings.pos = *fwd_pos;
let settings = settings.level_up(); let settings = settings.level_up();
required_token = match parse_func(&segments, &*fwd_token.syntax()) { required_token = match parse_func(&segments, &*fwd_token.syntax(), &mut user_state) {
Ok(Some(seg)) Ok(Some(seg))
if seg.starts_with(CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT) if seg.starts_with(CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT)
&& seg.len() > CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT.len() => && seg.len() > CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT.len() =>
@ -2624,6 +2625,7 @@ impl Engine {
crate::ast::CustomExpr { crate::ast::CustomExpr {
inputs, inputs,
tokens, tokens,
state: user_state,
scope_may_be_changed: syntax.scope_may_be_changed, scope_may_be_changed: syntax.scope_may_be_changed,
self_terminated, self_terminated,
} }

View File

@ -254,14 +254,17 @@ fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
fn test_custom_syntax_raw() -> Result<(), Box<EvalAltResult>> { fn test_custom_syntax_raw() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_custom_syntax_raw( engine.register_custom_syntax_with_state_raw(
"hello", "hello",
|stream, _| match stream.len() { |stream, _, state| match stream.len() {
0 => unreachable!(), 0 => unreachable!(),
1 => Ok(Some("$ident$".into())), 1 => Ok(Some("$ident$".into())),
2 => match stream[1].as_str() { 2 => match stream[1].as_str() {
"world" => Ok(Some("$$hello".into())), "world" => Ok(Some("$$hello".into())),
"kitty" => Ok(None), "kitty" => {
*state = (42 as INT).into();
Ok(None)
}
s => Err(LexError::ImproperSymbol(s.to_string(), String::new()) s => Err(LexError::ImproperSymbol(s.to_string(), String::new())
.into_err(Position::NONE) .into_err(Position::NONE)
.into()), .into()),
@ -269,7 +272,7 @@ fn test_custom_syntax_raw() -> Result<(), Box<EvalAltResult>> {
_ => unreachable!(), _ => unreachable!(),
}, },
true, true,
|context, inputs| { |context, inputs, state| {
context.scope_mut().push("foo", 999 as INT); context.scope_mut().push("foo", 999 as INT);
Ok(match inputs[0].get_string_value().unwrap() { Ok(match inputs[0].get_string_value().unwrap() {
@ -277,14 +280,14 @@ fn test_custom_syntax_raw() -> Result<(), Box<EvalAltResult>> {
if inputs if inputs
.last() .last()
.unwrap() .unwrap()
.get_literal_value::<ImmutableString>() .get_string_value()
.map_or(false, |s| s == "$$hello") => .map_or(false, |s| s == "$$hello") =>
{ {
0 as INT 0 as INT
} }
"world" => 123 as INT, "world" => 123 as INT,
"kitty" if inputs.len() > 1 => 999 as INT, "kitty" if inputs.len() > 1 => 999 as INT,
"kitty" => 42 as INT, "kitty" => state.as_int().unwrap(),
_ => unreachable!(), _ => unreachable!(),
} }
.into()) .into())
@ -313,9 +316,9 @@ fn test_custom_syntax_raw() -> Result<(), Box<EvalAltResult>> {
fn test_custom_syntax_raw2() -> Result<(), Box<EvalAltResult>> { fn test_custom_syntax_raw2() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_custom_syntax_raw( engine.register_custom_syntax_with_state_raw(
"#", "#",
|symbols, lookahead| match symbols.len() { |symbols, lookahead, _| match symbols.len() {
1 if lookahead == "-" => Ok(Some("$symbol$".into())), 1 if lookahead == "-" => Ok(Some("$symbol$".into())),
1 => Ok(Some("$int$".into())), 1 => Ok(Some("$int$".into())),
2 if symbols[1] == "-" => Ok(Some("$int$".into())), 2 if symbols[1] == "-" => Ok(Some("$int$".into())),
@ -324,7 +327,7 @@ fn test_custom_syntax_raw2() -> Result<(), Box<EvalAltResult>> {
_ => unreachable!(), _ => unreachable!(),
}, },
false, false,
move |_, inputs| { move |_, inputs, _| {
let id = if inputs.len() == 2 { let id = if inputs.len() == 2 {
-inputs[1].get_literal_value::<INT>().unwrap() -inputs[1].get_literal_value::<INT>().unwrap()
} else { } else {