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}