quad_compat_rhai/
custom_syntax.rs

1//! Module implementing custom syntax for [`Engine`].
2
3use crate::ast::Expr;
4use crate::engine::EvalContext;
5use crate::func::native::SendSync;
6use crate::r#unsafe::unsafe_try_cast;
7use crate::tokenizer::{is_valid_identifier, Token};
8use crate::types::dynamic::Variant;
9use crate::{
10    Engine, Identifier, ImmutableString, LexError, ParseError, Position, RhaiResult, Shared,
11    StaticVec, INT,
12};
13#[cfg(feature = "no_std")]
14use std::prelude::v1::*;
15use std::{any::TypeId, ops::Deref};
16
17/// Collection of special markers for custom syntax definition.
18pub mod markers {
19    /// Special marker for matching an expression.
20    pub const CUSTOM_SYNTAX_MARKER_EXPR: &str = "$expr$";
21    /// Special marker for matching a statements block.
22    pub const CUSTOM_SYNTAX_MARKER_BLOCK: &str = "$block$";
23    /// Special marker for matching an identifier.
24    pub const CUSTOM_SYNTAX_MARKER_IDENT: &str = "$ident$";
25    /// Special marker for matching a single symbol.
26    pub const CUSTOM_SYNTAX_MARKER_SYMBOL: &str = "$symbol$";
27    /// Special marker for matching a string literal.
28    pub const CUSTOM_SYNTAX_MARKER_STRING: &str = "$string$";
29    /// Special marker for matching an integer number.
30    pub const CUSTOM_SYNTAX_MARKER_INT: &str = "$int$";
31    /// Special marker for matching a floating-point number.
32    #[cfg(not(feature = "no_float"))]
33    pub const CUSTOM_SYNTAX_MARKER_FLOAT: &str = "$float$";
34    /// Special marker for matching a boolean value.
35    pub const CUSTOM_SYNTAX_MARKER_BOOL: &str = "$bool$";
36    /// Special marker for identifying the custom syntax variant.
37    pub const CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT: &str = "$$";
38}
39
40/// A general expression evaluation trait object.
41#[cfg(not(feature = "sync"))]
42pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression]) -> RhaiResult;
43/// A general expression evaluation trait object.
44#[cfg(feature = "sync")]
45pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression]) -> RhaiResult + Send + Sync;
46
47/// A general expression parsing trait object.
48#[cfg(not(feature = "sync"))]
49pub type FnCustomSyntaxParse =
50    dyn Fn(&[ImmutableString], &str) -> Result<Option<ImmutableString>, ParseError>;
51/// A general expression parsing trait object.
52#[cfg(feature = "sync")]
53pub type FnCustomSyntaxParse =
54    dyn Fn(&[ImmutableString], &str) -> Result<Option<ImmutableString>, ParseError> + Send + Sync;
55
56/// An expression sub-tree in an [`AST`][crate::AST].
57#[derive(Debug, Clone)]
58pub struct Expression<'a>(&'a Expr);
59
60impl<'a> From<&'a Expr> for Expression<'a> {
61    #[inline(always)]
62    fn from(expr: &'a Expr) -> Self {
63        Self(expr)
64    }
65}
66
67impl Expression<'_> {
68    /// If this expression is a variable name, return it.  Otherwise [`None`].
69    #[inline(always)]
70    #[must_use]
71    pub fn get_variable_name(&self) -> Option<&str> {
72        self.0.get_variable_name(true)
73    }
74    /// Get the position of this expression.
75    #[inline(always)]
76    #[must_use]
77    pub const fn position(&self) -> Position {
78        self.0.position()
79    }
80    /// Get the value of this expression if it is a literal constant.
81    /// Supports [`INT`][crate::INT], [`FLOAT`][crate::FLOAT], `()`, `char`, `bool` and
82    /// [`ImmutableString`][crate::ImmutableString].
83    ///
84    /// Returns [`None`] also if the constant is not of the specified type.
85    #[inline]
86    #[must_use]
87    pub fn get_literal_value<T: Variant>(&self) -> Option<T> {
88        // Coded this way in order to maximally leverage potentials for dead-code removal.
89
90        if TypeId::of::<T>() == TypeId::of::<INT>() {
91            return match self.0 {
92                Expr::IntegerConstant(x, _) => unsafe_try_cast(*x).ok(),
93                _ => None,
94            };
95        }
96        #[cfg(not(feature = "no_float"))]
97        if TypeId::of::<T>() == TypeId::of::<crate::FLOAT>() {
98            return match self.0 {
99                Expr::FloatConstant(x, _) => unsafe_try_cast(*x).ok(),
100                _ => None,
101            };
102        }
103        if TypeId::of::<T>() == TypeId::of::<char>() {
104            return match self.0 {
105                Expr::CharConstant(x, _) => unsafe_try_cast(*x).ok(),
106                _ => None,
107            };
108        }
109        if TypeId::of::<T>() == TypeId::of::<ImmutableString>() {
110            return match self.0 {
111                Expr::StringConstant(x, _) => unsafe_try_cast(x.clone()).ok(),
112                _ => None,
113            };
114        }
115        if TypeId::of::<T>() == TypeId::of::<bool>() {
116            return match self.0 {
117                Expr::BoolConstant(x, _) => unsafe_try_cast(*x).ok(),
118                _ => None,
119            };
120        }
121        if TypeId::of::<T>() == TypeId::of::<()>() {
122            return match self.0 {
123                Expr::Unit(_) => unsafe_try_cast(()).ok(),
124                _ => None,
125            };
126        }
127        None
128    }
129}
130
131impl AsRef<Expr> for Expression<'_> {
132    #[inline(always)]
133    fn as_ref(&self) -> &Expr {
134        &self.0
135    }
136}
137
138impl Deref for Expression<'_> {
139    type Target = Expr;
140
141    #[inline(always)]
142    fn deref(&self) -> &Self::Target {
143        &self.0
144    }
145}
146
147impl EvalContext<'_, '_, '_, '_, '_, '_, '_, '_> {
148    /// Evaluate an [expression tree][Expression].
149    ///
150    /// # WARNING - Low Level API
151    ///
152    /// This function is very low level.  It evaluates an expression from an [`AST`][crate::AST].
153    #[inline(always)]
154    pub fn eval_expression_tree(&mut self, expr: &Expression) -> RhaiResult {
155        self.engine.eval_expr(
156            self.scope,
157            self.mods,
158            self.state,
159            self.lib,
160            self.this_ptr,
161            expr,
162            self.level,
163        )
164    }
165}
166
167/// Definition of a custom syntax definition.
168pub struct CustomSyntax {
169    /// A parsing function to return the next token in a custom syntax based on the
170    /// symbols parsed so far.
171    pub parse: Box<FnCustomSyntaxParse>,
172    /// Custom syntax implementation function.
173    pub func: Shared<FnCustomSyntaxEval>,
174    /// Any variables added/removed in the scope?
175    pub scope_may_be_changed: bool,
176}
177
178impl Engine {
179    /// Register a custom syntax with the [`Engine`].
180    ///
181    /// * `symbols` holds a slice of strings that define the custom syntax.  
182    /// * `scope_may_be_changed` specifies variables _may_ be added/removed by this custom syntax.
183    /// * `func` is the implementation function.
184    ///
185    /// ## Note on `symbols`
186    ///
187    /// * Whitespaces around symbols are stripped.
188    /// * Symbols that are all-whitespace or empty are ignored.
189    /// * If `symbols` does not contain at least one valid token, then the custom syntax registration
190    ///   is simply ignored.
191    ///
192    /// ## Note on `scope_may_be_changed`
193    ///
194    /// If `scope_may_be_changed` is `true`, then _size_ of the current [`Scope`][crate::Scope]
195    /// _may_ be modified by this custom syntax.
196    ///
197    /// Adding new variables and/or removing variables count.
198    ///
199    /// Simply modifying the values of existing variables does NOT count, as the _size_ of the
200    /// current [`Scope`][crate::Scope] is unchanged, so `false` should be passed.
201    ///
202    /// Replacing one variable with another (i.e. adding a new variable and removing one variable at
203    /// the same time so that the total _size_ of the [`Scope`][crate::Scope] is unchanged) also
204    /// does NOT count, so `false` should be passed.
205    pub fn register_custom_syntax<S: AsRef<str> + Into<Identifier>>(
206        &mut self,
207        symbols: &[S],
208        scope_may_be_changed: bool,
209        func: impl Fn(&mut EvalContext, &[Expression]) -> RhaiResult + SendSync + 'static,
210    ) -> Result<&mut Self, ParseError> {
211        use markers::*;
212
213        let mut segments = StaticVec::<ImmutableString>::new();
214
215        for s in symbols {
216            let s = s.as_ref().trim();
217
218            // Skip empty symbols
219            if s.is_empty() {
220                continue;
221            }
222
223            let token = Token::lookup_from_syntax(s);
224
225            let seg = match s {
226                // Markers not in first position
227                CUSTOM_SYNTAX_MARKER_IDENT
228                | CUSTOM_SYNTAX_MARKER_SYMBOL
229                | CUSTOM_SYNTAX_MARKER_EXPR
230                | CUSTOM_SYNTAX_MARKER_BLOCK
231                | CUSTOM_SYNTAX_MARKER_BOOL
232                | CUSTOM_SYNTAX_MARKER_INT
233                | CUSTOM_SYNTAX_MARKER_STRING
234                    if !segments.is_empty() =>
235                {
236                    s.into()
237                }
238                // Markers not in first position
239                #[cfg(not(feature = "no_float"))]
240                CUSTOM_SYNTAX_MARKER_FLOAT if !segments.is_empty() => s.into(),
241                // Standard or reserved keyword/symbol not in first position
242                _ if !segments.is_empty() && token.is_some() => {
243                    // Make it a custom keyword/symbol if it is disabled or reserved
244                    if (self.disabled_symbols.contains(s)
245                        || token.map_or(false, |v| v.is_reserved()))
246                        && !self.custom_keywords.contains_key(s)
247                    {
248                        self.custom_keywords.insert(s.into(), None);
249                    }
250                    s.into()
251                }
252                // Standard keyword in first position but not disabled
253                _ if segments.is_empty()
254                    && token.as_ref().map_or(false, |v| v.is_standard_keyword())
255                    && !self.disabled_symbols.contains(s) =>
256                {
257                    return Err(LexError::ImproperSymbol(
258                        s.to_string(),
259                        format!(
260                            "Improper symbol for custom syntax at position #{}: '{}'",
261                            segments.len() + 1,
262                            s
263                        ),
264                    )
265                    .into_err(Position::NONE));
266                }
267                // Identifier in first position
268                _ if segments.is_empty() && is_valid_identifier(s.chars()) => {
269                    // Make it a custom keyword/symbol if it is disabled or reserved
270                    if self.disabled_symbols.contains(s) || token.map_or(false, |v| v.is_reserved())
271                    {
272                        if !self.custom_keywords.contains_key(s) {
273                            self.custom_keywords.insert(s.into(), None);
274                        }
275                    }
276                    s.into()
277                }
278                // Anything else is an error
279                _ => {
280                    return Err(LexError::ImproperSymbol(
281                        s.to_string(),
282                        format!(
283                            "Improper symbol for custom syntax at position #{}: '{}'",
284                            segments.len() + 1,
285                            s
286                        ),
287                    )
288                    .into_err(Position::NONE));
289                }
290            };
291
292            segments.push(seg);
293        }
294
295        // If the syntax has no symbols, just ignore the registration
296        if segments.is_empty() {
297            return Ok(self);
298        }
299
300        // The first keyword is the discriminator
301        let key = segments[0].clone();
302
303        self.register_custom_syntax_raw(
304            key,
305            // Construct the parsing function
306            move |stream, _| {
307                if stream.len() >= segments.len() {
308                    Ok(None)
309                } else {
310                    Ok(Some(segments[stream.len()].clone()))
311                }
312            },
313            scope_may_be_changed,
314            func,
315        );
316
317        Ok(self)
318    }
319    /// Register a custom syntax with the [`Engine`].
320    ///
321    /// # WARNING - Low Level API
322    ///
323    /// This function is very low level.
324    ///
325    /// * `scope_may_be_changed` specifies variables have been added/removed by this custom syntax.
326    /// * `parse` is the parsing function.
327    /// * `func` is the implementation function.
328    ///
329    /// All custom keywords used as symbols must be manually registered via [`Engine::register_custom_operator`].
330    /// Otherwise, they won't be recognized.
331    ///
332    /// # Implementation Function Signature
333    ///
334    /// The implementation function has the following signature:
335    ///
336    /// > `Fn(symbols: &[ImmutableString], look_ahead: &str) -> Result<Option<ImmutableString>, ParseError>`
337    ///
338    /// where:
339    /// * `symbols`: a slice of symbols that have been parsed so far, possibly containing `$expr$` and/or `$block$`;
340    ///   `$ident$` and other literal markers are replaced by the actual text
341    /// * `look_ahead`: a string slice containing the next symbol that is about to be read
342    ///
343    /// ## Return value
344    ///
345    /// * `Ok(None)`: parsing complete and there are no more symbols to match.
346    /// * `Ok(Some(symbol))`: the next symbol to match, which can also be `$expr$`, `$ident$` or `$block$`.
347    /// * `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`].
348    pub fn register_custom_syntax_raw(
349        &mut self,
350        key: impl Into<Identifier>,
351        parse: impl Fn(&[ImmutableString], &str) -> Result<Option<ImmutableString>, ParseError>
352            + SendSync
353            + 'static,
354        scope_may_be_changed: bool,
355        func: impl Fn(&mut EvalContext, &[Expression]) -> RhaiResult + SendSync + 'static,
356    ) -> &mut Self {
357        self.custom_syntax.insert(
358            key.into(),
359            CustomSyntax {
360                parse: Box::new(parse),
361                func: (Box::new(func) as Box<FnCustomSyntaxEval>).into(),
362                scope_may_be_changed,
363            }
364            .into(),
365        );
366        self
367    }
368}