rhai/api/
custom_syntax.rs

1//! Module implementing custom syntax for [`Engine`].
2#![cfg(not(feature = "no_custom_syntax"))]
3
4use crate::ast::Expr;
5use crate::func::SendSync;
6use crate::parser::ParseResult;
7use crate::tokenizer::{is_reserved_keyword_or_symbol, is_valid_identifier, Token};
8use crate::types::dynamic::Variant;
9use crate::{
10    Dynamic, Engine, EvalContext, Identifier, ImmutableString, LexError, Position, RhaiResult,
11};
12#[cfg(feature = "no_std")]
13use std::prelude::v1::*;
14use std::{borrow::Borrow, ops::Deref};
15
16/// Collection of special markers for custom syntax definition.
17pub mod markers {
18    /// Special marker for matching an expression.
19    pub const CUSTOM_SYNTAX_MARKER_EXPR: &str = "$expr$";
20    /// Special marker for matching a statements block.
21    pub const CUSTOM_SYNTAX_MARKER_BLOCK: &str = "$block$";
22    /// Special marker for matching a statements block after the starting `{`.
23    pub const CUSTOM_SYNTAX_MARKER_INNER: &str = "$inner$";
24    /// Special marker for matching a function body.
25    #[cfg(not(feature = "no_function"))]
26    pub const CUSTOM_SYNTAX_MARKER_FUNC: &str = "$func$";
27    /// Special marker for matching a single character from the input stream.
28    pub const CUSTOM_SYNTAX_MARKER_RAW: &str = "$raw$";
29    /// Special marker for matching an identifier.
30    pub const CUSTOM_SYNTAX_MARKER_IDENT: &str = "$ident$";
31    /// Special marker for matching a single symbol.
32    pub const CUSTOM_SYNTAX_MARKER_SYMBOL: &str = "$symbol$";
33    /// Special marker for matching a single token.
34    pub const CUSTOM_SYNTAX_MARKER_TOKEN: &str = "$token$";
35    /// Special marker for matching a string literal.
36    pub const CUSTOM_SYNTAX_MARKER_STRING: &str = "$string$";
37    /// Special marker for matching an integer number.
38    pub const CUSTOM_SYNTAX_MARKER_INT: &str = "$int$";
39    /// Special marker for matching a floating-point number.
40    #[cfg(not(feature = "no_float"))]
41    pub const CUSTOM_SYNTAX_MARKER_FLOAT: &str = "$float$";
42    /// Special marker for matching a boolean value.
43    pub const CUSTOM_SYNTAX_MARKER_BOOL: &str = "$bool$";
44    /// Special marker for identifying the custom syntax variant.
45    pub const CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT: &str = "$$";
46}
47
48/// A general expression evaluation trait object.
49#[cfg(not(feature = "sync"))]
50pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression], &Dynamic) -> RhaiResult;
51/// A general expression evaluation trait object.
52#[cfg(feature = "sync")]
53pub type FnCustomSyntaxEval =
54    dyn Fn(&mut EvalContext, &[Expression], &Dynamic) -> RhaiResult + Send + Sync;
55
56/// A general expression parsing trait object.
57#[cfg(not(feature = "sync"))]
58pub type FnCustomSyntaxParse =
59    dyn Fn(&[ImmutableString], &str, &mut Dynamic) -> ParseResult<Option<ImmutableString>>;
60/// A general expression parsing trait object.
61#[cfg(feature = "sync")]
62pub type FnCustomSyntaxParse = dyn Fn(&[ImmutableString], &str, &mut Dynamic) -> ParseResult<Option<ImmutableString>>
63    + Send
64    + Sync;
65
66/// An expression sub-tree in an [`AST`][crate::AST].
67#[derive(Debug, Clone)]
68pub struct Expression<'a>(&'a Expr);
69
70impl<'a> From<&'a Expr> for Expression<'a> {
71    #[inline(always)]
72    fn from(expr: &'a Expr) -> Self {
73        Self(expr)
74    }
75}
76
77impl Expression<'_> {
78    /// Evaluate this [expression tree][Expression] within an [evaluation context][`EvalContext`].
79    ///
80    /// # WARNING - Low Level API
81    ///
82    /// This function is very low level.  It evaluates an expression from an [`AST`][crate::AST].
83    #[inline(always)]
84    pub fn eval_with_context(&self, context: &mut EvalContext) -> RhaiResult {
85        context.eval_expression_tree(self)
86    }
87    /// Evaluate this [expression tree][Expression] within an [evaluation context][`EvalContext`].
88    ///
89    /// The following option is available:
90    ///
91    /// * whether to rewind the [`Scope`][crate::Scope] after evaluation if the expression is a [`StmtBlock`][crate::ast::StmtBlock]
92    ///
93    /// # WARNING - Unstable API
94    ///
95    /// This API is volatile and may change in the future.
96    ///
97    /// # WARNING - Low Level API
98    ///
99    /// This function is _extremely_ low level.  It evaluates an expression from an [`AST`][crate::AST].
100    #[deprecated = "This API is NOT deprecated, but it is considered volatile and may change in the future."]
101    #[inline(always)]
102    pub fn eval_with_context_raw(
103        &self,
104        context: &mut EvalContext,
105        rewind_scope: bool,
106    ) -> RhaiResult {
107        #[allow(deprecated)]
108        context.eval_expression_tree_raw(self, rewind_scope)
109    }
110    /// Get the value of this expression if it is a variable name or a string constant.
111    ///
112    /// Returns [`None`] also if the constant is not of the specified type.
113    #[inline(always)]
114    #[must_use]
115    pub fn get_string_value(&self) -> Option<&str> {
116        match self.0 {
117            #[cfg(not(feature = "no_module"))]
118            Expr::Variable(x, ..) if !x.2.is_empty() => None,
119            Expr::Variable(x, ..) => Some(&x.1),
120            #[cfg(not(feature = "no_function"))]
121            Expr::ThisPtr(..) => Some(crate::engine::KEYWORD_THIS),
122            Expr::StringConstant(x, ..) => Some(x),
123            _ => None,
124        }
125    }
126    /// Get the position of this expression.
127    #[inline(always)]
128    #[must_use]
129    pub const fn position(&self) -> Position {
130        self.0.position()
131    }
132    /// Get the value of this expression if it is a literal constant.
133    ///
134    /// Supports [`INT`][crate::INT], [`FLOAT`][crate::FLOAT], `()`, `char`, `bool` and
135    /// [`ImmutableString`][crate::ImmutableString].
136    ///
137    /// Returns [`None`] also if the constant is not of the specified type.
138    #[inline]
139    #[must_use]
140    pub fn get_literal_value<T: Variant + Clone>(&self) -> Option<T> {
141        // Coded this way in order to maximally leverage potentials for dead-code removal.
142        match self.0 {
143            Expr::DynamicConstant(x, ..) => x.clone().try_cast::<T>(),
144            Expr::IntegerConstant(x, ..) => reify! { *x => Option<T> },
145
146            #[cfg(not(feature = "no_float"))]
147            Expr::FloatConstant(x, ..) => reify! { *x => Option<T> },
148
149            Expr::CharConstant(x, ..) => reify! { *x => Option<T> },
150            Expr::StringConstant(x, ..) => reify! { x.clone() => Option<T> },
151            Expr::Variable(x, ..) => reify! { x.1.clone() => Option<T> },
152            Expr::BoolConstant(x, ..) => reify! { *x => Option<T> },
153            Expr::Unit(..) => reify! { () => Option<T> },
154
155            _ => None,
156        }
157    }
158}
159
160impl Borrow<Expr> for Expression<'_> {
161    #[inline(always)]
162    fn borrow(&self) -> &Expr {
163        self.0
164    }
165}
166
167impl AsRef<Expr> for Expression<'_> {
168    #[inline(always)]
169    fn as_ref(&self) -> &Expr {
170        self.0
171    }
172}
173
174impl Deref for Expression<'_> {
175    type Target = Expr;
176
177    #[inline(always)]
178    fn deref(&self) -> &Self::Target {
179        self.0
180    }
181}
182
183/// Definition of a custom syntax definition.
184pub struct CustomSyntax {
185    /// A parsing function to return the next token in a custom syntax based on the
186    /// symbols parsed so far.
187    pub parse: Box<FnCustomSyntaxParse>,
188    /// Custom syntax implementation function.
189    pub func: Box<FnCustomSyntaxEval>,
190    /// Any variables added/removed in the scope?
191    pub scope_may_be_changed: bool,
192    /// Is look-ahead enabled when parsing this custom syntax?
193    pub use_look_ahead: bool,
194}
195
196impl Engine {
197    /// Register a custom syntax with the [`Engine`].
198    ///
199    /// Not available under `no_custom_syntax`.
200    ///
201    /// * `symbols` holds a slice of strings that define the custom syntax.
202    /// * `scope_may_be_changed` specifies variables _may_ be added/removed by this custom syntax.
203    /// * `func` is the implementation function.
204    ///
205    /// ## Note on `symbols`
206    ///
207    /// * Whitespaces around symbols are stripped.
208    /// * Symbols that are all-whitespace or empty are ignored.
209    /// * If `symbols` does not contain at least one valid token, then the custom syntax registration
210    ///   is simply ignored.
211    ///
212    /// ## Note on `scope_may_be_changed`
213    ///
214    /// If `scope_may_be_changed` is `true`, then _size_ of the current [`Scope`][crate::Scope]
215    /// _may_ be modified by this custom syntax.
216    ///
217    /// Adding new variables and/or removing variables count.
218    ///
219    /// Simply modifying the values of existing variables does NOT count, as the _size_ of the
220    /// current [`Scope`][crate::Scope] is unchanged, so `false` should be passed.
221    ///
222    /// Replacing one variable with another (i.e. adding a new variable and removing one variable at
223    /// the same time so that the total _size_ of the [`Scope`][crate::Scope] is unchanged) also
224    /// does NOT count, so `false` should be passed.
225    pub fn register_custom_syntax<S: AsRef<str> + Into<Identifier>>(
226        &mut self,
227        symbols: impl AsRef<[S]>,
228        scope_may_be_changed: bool,
229        func: impl Fn(&mut EvalContext, &[Expression]) -> RhaiResult + SendSync + 'static,
230    ) -> ParseResult<&mut Self> {
231        #[allow(clippy::wildcard_imports)]
232        use markers::*;
233
234        let mut segments = Vec::<ImmutableString>::new();
235
236        for s in symbols.as_ref() {
237            let s = s.as_ref().trim();
238
239            // Skip empty symbols
240            if s.is_empty() {
241                continue;
242            }
243
244            let token = Token::lookup_symbol_from_syntax(s).or_else(|| {
245                is_reserved_keyword_or_symbol(s)
246                    .0
247                    .then(|| Token::Reserved(Box::new(s.into())))
248            });
249
250            let seg = match s {
251                CUSTOM_SYNTAX_MARKER_RAW => {
252                    return Err(LexError::ImproperSymbol(
253                        String::new(),
254                        "`register_custom_syntax` does not support `$raw$`".to_string(),
255                    )
256                    .into_err(Position::NONE));
257                }
258
259                // Markers not in first position
260                CUSTOM_SYNTAX_MARKER_IDENT
261                | CUSTOM_SYNTAX_MARKER_SYMBOL
262                | CUSTOM_SYNTAX_MARKER_TOKEN
263                | CUSTOM_SYNTAX_MARKER_EXPR
264                | CUSTOM_SYNTAX_MARKER_BLOCK
265                | CUSTOM_SYNTAX_MARKER_BOOL
266                | CUSTOM_SYNTAX_MARKER_INT
267                | CUSTOM_SYNTAX_MARKER_STRING
268                    if !segments.is_empty() =>
269                {
270                    s.into()
271                }
272
273                // Markers not in first position
274                #[cfg(not(feature = "no_function"))]
275                CUSTOM_SYNTAX_MARKER_FUNC if !segments.is_empty() => s.into(),
276
277                // Markers not in first position
278                #[cfg(not(feature = "no_float"))]
279                CUSTOM_SYNTAX_MARKER_FLOAT if !segments.is_empty() => s.into(),
280
281                // Identifier not in first position
282                _ if !segments.is_empty() && is_valid_identifier(s) => s.into(),
283
284                // Keyword/symbol not in first position
285                _ if !segments.is_empty() && token.is_some() => {
286                    // Make it a custom keyword/symbol if it is disabled or reserved
287                    if (self.is_symbol_disabled(s)
288                        || token.as_ref().map_or(false, Token::is_reserved))
289                        && !self.custom_keywords.contains_key(s)
290                    {
291                        self.custom_keywords.insert(s.into(), None);
292                    }
293                    s.into()
294                }
295
296                // Standard keyword in first position but not disabled
297                _ if segments.is_empty()
298                    && token.as_ref().map_or(false, Token::is_standard_keyword)
299                    && !self.is_symbol_disabled(s) =>
300                {
301                    return Err(LexError::ImproperSymbol(
302                        s.to_string(),
303                        format!("Improper symbol for custom syntax at position #0: '{s}'"),
304                    )
305                    .into_err(Position::NONE));
306                }
307
308                // Identifier or symbol in first position
309                _ if segments.is_empty()
310                    && (is_valid_identifier(s) || is_reserved_keyword_or_symbol(s).0) =>
311                {
312                    // Make it a custom keyword/symbol if it is disabled or reserved
313                    if self.is_symbol_disabled(s)
314                        || (token.as_ref().map_or(false, Token::is_reserved)
315                            && !self.custom_keywords.contains_key(s))
316                    {
317                        self.custom_keywords.insert(s.into(), None);
318                    }
319                    s.into()
320                }
321
322                // Anything else is an error
323                _ => {
324                    return Err(LexError::ImproperSymbol(
325                        s.to_string(),
326                        format!(
327                            "Improper symbol for custom syntax at position #{}: '{s}'",
328                            segments.len() + 1,
329                        ),
330                    )
331                    .into_err(Position::NONE));
332                }
333            };
334
335            segments.push(seg);
336        }
337
338        // If the syntax has nothing, just ignore the registration
339        if segments.is_empty() {
340            return Ok(self);
341        }
342
343        // The first keyword/symbol is the discriminator
344        let key = segments[0].clone();
345
346        self.register_custom_syntax_with_state_raw(
347            key,
348            // Construct the parsing function
349            move |stream, _, _| match stream.len() {
350                len if len >= segments.len() => Ok(None),
351                len => Ok(Some(segments[len].clone())),
352            },
353            scope_may_be_changed,
354            move |context, expressions, _| func(context, expressions),
355        );
356
357        Ok(self)
358    }
359    /// Register a custom syntax with the [`Engine`] with custom user-defined state.
360    ///
361    /// Not available under `no_custom_syntax`.
362    ///
363    /// # WARNING - Low Level API
364    ///
365    /// This function is very low level.
366    ///
367    /// * `scope_may_be_changed` specifies variables have been added/removed by this custom syntax.
368    /// * `parse` is the parsing function.
369    /// * `func` is the implementation function.
370    ///
371    /// All custom keywords used as symbols must be manually registered via [`Engine::register_custom_operator`].
372    /// Otherwise, they won't be recognized.
373    ///
374    /// # Parsing Function Signature
375    ///
376    /// The parsing function has the following signature:
377    ///
378    /// `Fn(symbols: &[ImmutableString], look_ahead: &str, state: &mut Dynamic) -> Result<Option<ImmutableString>, ParseError>`
379    ///
380    /// where:
381    /// * `symbols`: a slice of symbols that have been parsed so far, possibly containing `$expr$` and/or `$block$`;
382    ///   `$ident$` and other literal markers are replaced by the actual text
383    /// * `look_ahead`: a string slice containing the next symbol that is about to be read
384    /// * `state`: a [`Dynamic`] value that contains a user-defined state
385    ///
386    /// ## Return value
387    ///
388    /// * `Ok(None)`: parsing complete and there are no more symbols to match.
389    /// * `Ok(Some(symbol))`: the next symbol to match, which can also be `$expr$`, `$ident$` or `$block$` etc.
390    /// * `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].
391    pub fn register_custom_syntax_with_state_raw(
392        &mut self,
393        key: impl Into<Identifier>,
394        parse: impl Fn(&[ImmutableString], &str, &mut Dynamic) -> ParseResult<Option<ImmutableString>>
395            + SendSync
396            + 'static,
397        scope_may_be_changed: bool,
398        func: impl Fn(&mut EvalContext, &[Expression], &Dynamic) -> RhaiResult + SendSync + 'static,
399    ) -> &mut Self {
400        self.custom_syntax.insert(
401            key.into(),
402            CustomSyntax {
403                parse: Box::new(parse),
404                func: Box::new(func),
405                scope_may_be_changed,
406                use_look_ahead: true,
407            }
408            .into(),
409        );
410        self
411    }
412    /// Register a custom syntax with the [`Engine`] with custom user-defined state,
413    /// but with no look-ahead.  This enables the usage of `$raw$` to process raw script
414    /// text character-by-character, by-passing the tokenizer.
415    ///
416    /// Not available under `no_custom_syntax`.
417    ///
418    /// # WARNING - Low Level API
419    ///
420    /// This function is very low level.
421    ///
422    /// * `scope_may_be_changed` specifies variables have been added/removed by this custom syntax.
423    /// * `parse` is the parsing function.
424    /// * `func` is the implementation function.
425    ///
426    /// # Parsing Function Signature
427    ///
428    /// The parsing function has the following signature:
429    ///
430    /// `Fn(symbols: &[ImmutableString], state: &mut Dynamic) -> Result<Option<ImmutableString>, ParseError>`
431    ///
432    /// where:
433    /// * `symbols`: a slice of symbols that have been parsed so far, possibly containing `$expr$` and/or `$block$`;
434    ///   `$ident$` and other literal markers are replaced by the actual text
435    /// * `state`: a [`Dynamic`] value that contains a user-defined state
436    ///
437    /// ## Return value
438    ///
439    /// * `Ok(None)`: parsing complete and there are no more symbols to match.
440    /// * `Ok(Some(symbol))`: the next symbol to match, which can also be `$expr$`, `$ident$` or `$block$` etc.
441    ///    `$raw$` can be returned such that the next character in the script text is returned without any processing.
442    /// * `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].
443    pub fn register_custom_syntax_without_look_ahead_raw(
444        &mut self,
445        key: impl Into<Identifier>,
446        parse: impl Fn(&[ImmutableString], &mut Dynamic) -> ParseResult<Option<ImmutableString>>
447            + SendSync
448            + 'static,
449        scope_may_be_changed: bool,
450        func: impl Fn(&mut EvalContext, &[Expression], &Dynamic) -> RhaiResult + SendSync + 'static,
451    ) -> &mut Self {
452        let key = key.into();
453
454        self.register_custom_syntax_with_state_raw(
455            key.clone(),
456            move |symbols, _, state| parse(symbols, state),
457            scope_may_be_changed,
458            func,
459        );
460
461        self.custom_syntax.get_mut(&key).unwrap().use_look_ahead = false;
462        self
463    }
464}