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}