commit
540ec5df6c
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rhai"
|
||||
version = "0.17.0"
|
||||
version = "0.18.0"
|
||||
edition = "2018"
|
||||
authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"]
|
||||
description = "Embedded scripting for Rust"
|
||||
@ -73,9 +73,3 @@ optional = true
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["serde"]
|
||||
|
||||
[package.metadata.playground]
|
||||
features = ["serde"]
|
||||
|
12
README.md
12
README.md
@ -18,8 +18,8 @@ Supported targets and builds
|
||||
* WebAssembly (WASM)
|
||||
* `no-std`
|
||||
|
||||
Standard Features
|
||||
----------------
|
||||
Standard features
|
||||
-----------------
|
||||
|
||||
* Easy-to-use language similar to JavaScript+Rust with dynamic typing.
|
||||
* Tight integration with native Rust [functions](https://schungx.github.io/rhai/rust/functions.html) and [types]([#custom-types-and-methods](https://schungx.github.io/rhai/rust/custom.html)), including [getters/setters](https://schungx.github.io/rhai/rust/getters-setters.html), [methods](https://schungx.github.io/rhai/rust/custom.html) and [indexers](https://schungx.github.io/rhai/rust/indexers.html).
|
||||
@ -39,19 +39,19 @@ Standard Features
|
||||
* Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations.
|
||||
* Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html).
|
||||
|
||||
Protection Against Attacks
|
||||
-------------------------
|
||||
Protection against attacks
|
||||
--------------------------
|
||||
|
||||
* Sand-boxed - the scripting engine, if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`).
|
||||
* Rugged - protected against malicious attacks (such as [stack-overflow](https://schungx.github.io/rhai/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/rhai/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/rhai/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts.
|
||||
* Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run.
|
||||
|
||||
For Those Who Actually Want Their Own Language
|
||||
For those who actually want their own language
|
||||
---------------------------------------------
|
||||
|
||||
* Use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html).
|
||||
* Define [custom operators](https://schungx.github.io/rhai/engine/custom-op.html).
|
||||
* Restrict the language by surgically [disabling keywords and operators](https://schungx.github.io/rhai/engine/disable.html).
|
||||
* Define [custom operators](https://schungx.github.io/rhai/engine/custom-op.html).
|
||||
* Extend the language with [custom syntax](https://schungx.github.io/rhai/engine/custom-syntax.html).
|
||||
|
||||
Documentation
|
||||
|
12
RELEASES.md
12
RELEASES.md
@ -1,6 +1,17 @@
|
||||
Rhai Release Notes
|
||||
==================
|
||||
|
||||
Version 0.18.0
|
||||
==============
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
* `call` can now be called function-call style for function pointers - this is to handle builds with `no_object`.
|
||||
* Disallow many keywords as variables, such as `print`, `eval`, `call`, `this` etc.
|
||||
* `x.call(f, ...)` allows binding `x` to `this` for the function referenced by the function pointer `f`.
|
||||
|
||||
|
||||
Version 0.17.0
|
||||
==============
|
||||
|
||||
@ -31,6 +42,7 @@ New features
|
||||
This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back.
|
||||
* `Engine::disable_symbol` to surgically disable keywords and/or operators.
|
||||
* `Engine::register_custom_operator` to define a custom operator.
|
||||
* `Engine::register_custom_syntax` to define a custom syntax.
|
||||
* New low-level API `Engine::register_raw_fn` and `Engine::register_raw_fn_XXX`.
|
||||
* New low-level API `Module::set_raw_fn` mirroring `Engine::register_raw_fn`.
|
||||
* `AST::clone_functions_only`, `AST::clone_functions_only_filtered` and `AST::clone_statements_only` to clone only part of an `AST`.
|
||||
|
@ -5,6 +5,12 @@ Advanced Topics
|
||||
|
||||
This section covers advanced features such as:
|
||||
|
||||
* [Script optimization]
|
||||
* Simulated [Object Oriented Programming][OOP].
|
||||
|
||||
* The dreaded (or beloved for those with twisted tastes) [`eval`] statement
|
||||
* [`serde`] integration.
|
||||
|
||||
* [Script optimization].
|
||||
|
||||
* [Domain-Specific Languages][DSL].
|
||||
|
||||
* The dreaded (or beloved for those with twisted tastes) [`eval`] statement.
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"rootUrl": "",
|
||||
"rootUrlX": "/rhai",
|
||||
"rootUrlXX": "/rhai/vnext"
|
||||
|
@ -68,11 +68,13 @@ These symbol types can be used:
|
||||
### The First Symbol Must be a Keyword
|
||||
|
||||
There is no specific limit on the combination and sequencing of each symbol type,
|
||||
except the _first_ symbol which must be a [custom keyword].
|
||||
except the _first_ symbol which must be a custom keyword that follows the naming rules
|
||||
of [variables].
|
||||
|
||||
It _cannot_ be a [built-in keyword]({{rootUrl}}/appendix/keywords.md).
|
||||
The first symbol also cannot be a reserved [keyword], unless that keyword
|
||||
has been [disabled][disable keywords and operators].
|
||||
|
||||
However, it _may_ be a built-in keyword that has been [disabled][disable keywords and operators].
|
||||
In other words, any valid identifier that is not an active [keyword] will work fine.
|
||||
|
||||
### The First Symbol Must be Unique
|
||||
|
||||
|
@ -20,8 +20,7 @@ engine
|
||||
// The following all return parse errors.
|
||||
|
||||
engine.compile("let x = if true { 42 } else { 0 };")?;
|
||||
// ^ missing ';' after statement end
|
||||
// ^ 'if' is parsed as a variable name
|
||||
// ^ 'if' is rejected as a reserved keyword
|
||||
|
||||
engine.compile("let x = 40 + 2; x += 1;")?;
|
||||
// ^ '+=' is not recognized as an operator
|
||||
|
@ -6,7 +6,7 @@ Function Pointers
|
||||
It is possible to store a _function pointer_ in a variable just like a normal value.
|
||||
In fact, internally a function pointer simply stores the _name_ of the function as a string.
|
||||
|
||||
Call a function pointer using the `call` method, which needs to be called in method-call style.
|
||||
Call a function pointer using the `call` method.
|
||||
|
||||
|
||||
Built-in methods
|
||||
@ -40,7 +40,7 @@ func.call(1) == 42; // call a function pointer with the 'call' method
|
||||
|
||||
foo(1) == 42; // <- the above de-sugars to this
|
||||
|
||||
call(func, 1); //<- error: 'call (Fn, i64)' is not a registered function
|
||||
call(func, 1); // normal function call style also works for 'call'
|
||||
|
||||
let len = Fn("len"); // 'Fn' also works with registered native Rust functions
|
||||
|
||||
@ -66,20 +66,18 @@ Because of their dynamic nature, function pointers cannot refer to functions in
|
||||
See [function namespaces] for more details.
|
||||
|
||||
```rust
|
||||
import "foo" as f; // assume there is 'f::do_something()'
|
||||
import "foo" as f; // assume there is 'f::do_work()'
|
||||
|
||||
f::do_something(); // works!
|
||||
f::do_work(); // works!
|
||||
|
||||
let p = Fn("f::do_something");
|
||||
let p = Fn("f::do_work"); // error: invalid function name
|
||||
|
||||
p.call(); // error: function not found - 'f::do_something'
|
||||
|
||||
fn do_something_now() { // call it from a local function
|
||||
fn do_work_now() { // call it from a local function
|
||||
import "foo" as f;
|
||||
f::do_something();
|
||||
f::do_work();
|
||||
}
|
||||
|
||||
let p = Fn("do_something_now");
|
||||
let p = Fn("do_work_now");
|
||||
|
||||
p.call(); // works!
|
||||
```
|
||||
@ -134,3 +132,35 @@ let func = sign(x) + 1;
|
||||
// Dynamic dispatch
|
||||
map[func].call(42);
|
||||
```
|
||||
|
||||
|
||||
Binding the `this` Pointer
|
||||
-------------------------
|
||||
|
||||
When `call` is called as a _method_ but not on a `FnPtr` value, it is possible to dynamically dispatch
|
||||
to a function call while binding the object in the method call to the `this` pointer of the function.
|
||||
|
||||
To achieve this, pass the `FnPtr` value as the _first_ argument to `call`:
|
||||
|
||||
```rust
|
||||
fn add(x) { this += x; } // define function which uses 'this'
|
||||
|
||||
let func = Fn("add"); // function pointer to 'add'
|
||||
|
||||
func.call(1); // error: 'this' pointer is not bound
|
||||
|
||||
let x = 41;
|
||||
|
||||
func.call(x, 1); // error: function 'add (i64, i64)' not found
|
||||
|
||||
call(func, x, 1); // error: function 'add (i64, i64)' not found
|
||||
|
||||
x.call(func, 1); // 'this' is bound to 'x', dispatched to 'func'
|
||||
|
||||
x == 42;
|
||||
```
|
||||
|
||||
Beware that this only works for _method-call_ style. Normal function-call style cannot bind
|
||||
the `this` pointer (for syntactic reasons).
|
||||
|
||||
Therefore, obviously, binding the `this` pointer is unsupported under [`no_function`].
|
||||
|
@ -3,8 +3,8 @@ Maximum Size of Arrays
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
Limiting How Large Arrays Can Grow
|
||||
---------------------------------
|
||||
Limit How Large Arrays Can Grow
|
||||
------------------------------
|
||||
|
||||
Rhai by default does not limit how large an [array] can be.
|
||||
|
||||
|
@ -3,8 +3,8 @@ Maximum Call Stack Depth
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
Limiting How Stack Usage by Scripts
|
||||
----------------------------------
|
||||
Limit How Stack Usage by Scripts
|
||||
-------------------------------
|
||||
|
||||
Rhai by default limits function calls to a maximum depth of 128 levels (16 levels in debug build).
|
||||
|
||||
|
@ -3,8 +3,8 @@ Maximum Size of Object Maps
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
Limiting How Large Object Maps Can Grow
|
||||
--------------------------------------
|
||||
Limit How Large Object Maps Can Grow
|
||||
-----------------------------------
|
||||
|
||||
Rhai by default does not limit how large (i.e. the number of properties) an [object map] can be.
|
||||
|
||||
|
@ -3,8 +3,8 @@ Maximum Number of Operations
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
Limiting How Long a Script Can Run
|
||||
---------------------------------
|
||||
Limit How Long a Script Can Run
|
||||
------------------------------
|
||||
|
||||
Rhai by default does not limit how much time or CPU a script consumes.
|
||||
|
||||
|
@ -3,8 +3,8 @@ Maximum Statement Depth
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
Limiting How Deeply-Nested a Statement Can Be
|
||||
--------------------------------------------
|
||||
Limit How Deeply-Nested a Statement Can Be
|
||||
-----------------------------------------
|
||||
|
||||
Rhai by default limits statements and expressions nesting to a maximum depth of 128
|
||||
(which should be plenty) when they are at _global_ level, but only a depth of 32
|
||||
|
@ -3,8 +3,8 @@ Maximum Length of Strings
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
Limiting How Long Strings Can Grow
|
||||
---------------------------------
|
||||
Limit How Long Strings Can Grow
|
||||
------------------------------
|
||||
|
||||
Rhai by default does not limit how long a [string] can be.
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
Tracking Progress and Force-Termination
|
||||
======================================
|
||||
Track Progress and Force-Termination
|
||||
===================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
@ -31,8 +31,8 @@ Opt-Out of Features
|
||||
------------------
|
||||
|
||||
Opt out of as many features as possible, if they are not needed, to reduce code size because, remember, by default
|
||||
all code is compiled in as what a script requires cannot be predicted. If a language feature is not needed,
|
||||
omitting them via special features is a prudent strategy to optimize the build for size.
|
||||
all code is compiled into the final binary since what a script requires cannot be predicted.
|
||||
If a language feature will never be needed, omitting it is a prudent strategy to optimize the build for size.
|
||||
|
||||
Omitting arrays ([`no_index`]) yields the most code-size savings, followed by floating-point support
|
||||
([`no_float`]), checked arithmetic/script resource limits ([`unchecked`]) and finally object maps and custom types ([`no_object`]).
|
||||
|
@ -7,3 +7,9 @@ The feature [`no_std`] automatically converts the scripting engine into a `no-st
|
||||
|
||||
Usually, a `no-std` build goes hand-in-hand with [minimal builds] because typical embedded
|
||||
hardware (the primary target for `no-std`) has limited storage.
|
||||
|
||||
|
||||
Nightly Required
|
||||
----------------
|
||||
|
||||
Currently, [`no_std`] requires the nightly compiler due to the crates that it uses.
|
||||
|
@ -96,7 +96,7 @@ pub const MARKER_BLOCK: &str = "$block$";
|
||||
pub const MARKER_IDENT: &str = "$ident$";
|
||||
|
||||
#[cfg(feature = "internals")]
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct Expression<'a>(&'a Expr);
|
||||
|
||||
#[cfg(feature = "internals")]
|
||||
@ -944,7 +944,7 @@ impl Engine {
|
||||
}
|
||||
|
||||
// Has a system function an override?
|
||||
fn has_override(&self, lib: &Module, (hash_fn, hash_script): (u64, u64)) -> bool {
|
||||
fn has_override(&self, lib: &Module, hash_fn: u64, hash_script: u64) -> bool {
|
||||
// NOTE: We skip script functions for global_module and packages, and native functions for lib
|
||||
|
||||
// First check script-defined functions
|
||||
@ -986,13 +986,15 @@ impl Engine {
|
||||
|
||||
match fn_name {
|
||||
// type_of
|
||||
KEYWORD_TYPE_OF if args.len() == 1 && !self.has_override(lib, hashes) => Ok((
|
||||
self.map_type_name(args[0].type_name()).to_string().into(),
|
||||
false,
|
||||
)),
|
||||
KEYWORD_TYPE_OF if args.len() == 1 && !self.has_override(lib, hashes.0, hashes.1) => {
|
||||
Ok((
|
||||
self.map_type_name(args[0].type_name()).to_string().into(),
|
||||
false,
|
||||
))
|
||||
}
|
||||
|
||||
// Fn
|
||||
KEYWORD_FN_PTR if args.len() == 1 && !self.has_override(lib, hashes) => {
|
||||
KEYWORD_FN_PTR if args.len() == 1 && !self.has_override(lib, hashes.0, hashes.1) => {
|
||||
Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"'Fn' should not be called in method style. Try Fn(...);".into(),
|
||||
Position::none(),
|
||||
@ -1000,7 +1002,7 @@ impl Engine {
|
||||
}
|
||||
|
||||
// eval - reaching this point it must be a method-style call
|
||||
KEYWORD_EVAL if args.len() == 1 && !self.has_override(lib, hashes) => {
|
||||
KEYWORD_EVAL if args.len() == 1 && !self.has_override(lib, hashes.0, hashes.1) => {
|
||||
Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"'eval' should not be called in method style. Try eval(...);".into(),
|
||||
Position::none(),
|
||||
@ -1086,10 +1088,10 @@ impl Engine {
|
||||
let idx = idx_val.downcast_mut::<StaticVec<Dynamic>>().unwrap();
|
||||
let mut fn_name = name.as_ref();
|
||||
|
||||
// Check if it is a FnPtr call
|
||||
let (result, updated) = if fn_name == KEYWORD_FN_PTR_CALL && obj.is::<FnPtr>() {
|
||||
// FnPtr call
|
||||
// Redirect function name
|
||||
fn_name = obj.as_str().unwrap();
|
||||
let fn_name = obj.as_str().unwrap();
|
||||
// Recalculate hash
|
||||
let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty());
|
||||
// Arguments are passed as-is
|
||||
@ -1100,6 +1102,26 @@ impl Engine {
|
||||
self.exec_fn_call(
|
||||
state, lib, fn_name, *native, hash, args, false, false, def_val, level,
|
||||
)
|
||||
} else if fn_name == KEYWORD_FN_PTR_CALL && idx.len() > 0 && idx[0].is::<FnPtr>() {
|
||||
// FnPtr call on object
|
||||
// Redirect function name
|
||||
let fn_name = idx[0]
|
||||
.downcast_ref::<FnPtr>()
|
||||
.unwrap()
|
||||
.get_fn_name()
|
||||
.clone();
|
||||
// Recalculate hash
|
||||
let hash = calc_fn_hash(empty(), &fn_name, idx.len() - 1, empty());
|
||||
// Replace the first argument with the object pointer
|
||||
let mut arg_values = once(obj)
|
||||
.chain(idx.iter_mut().skip(1))
|
||||
.collect::<StaticVec<_>>();
|
||||
let args = arg_values.as_mut();
|
||||
|
||||
// Map it to name(args) in function-call style
|
||||
self.exec_fn_call(
|
||||
state, lib, &fn_name, *native, hash, args, is_ref, true, def_val, level,
|
||||
)
|
||||
} else {
|
||||
let redirected: Option<ImmutableString>;
|
||||
let mut hash = *hash;
|
||||
@ -1926,7 +1948,7 @@ impl Engine {
|
||||
let hash_fn =
|
||||
calc_fn_hash(empty(), name, 1, once(TypeId::of::<ImmutableString>()));
|
||||
|
||||
if !self.has_override(lib, (hash_fn, *hash)) {
|
||||
if !self.has_override(lib, hash_fn, *hash) {
|
||||
// Fn - only in function call style
|
||||
let expr = args_expr.get(0);
|
||||
let arg_value =
|
||||
@ -1952,7 +1974,7 @@ impl Engine {
|
||||
let hash_fn =
|
||||
calc_fn_hash(empty(), name, 1, once(TypeId::of::<ImmutableString>()));
|
||||
|
||||
if !self.has_override(lib, (hash_fn, *hash)) {
|
||||
if !self.has_override(lib, hash_fn, *hash) {
|
||||
// eval - only in function call style
|
||||
let prev_len = scope.len();
|
||||
let expr = args_expr.get(0);
|
||||
@ -1972,6 +1994,36 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle call() - Redirect function call
|
||||
let redirected;
|
||||
let mut name = name.as_ref();
|
||||
let mut args_expr = args_expr.as_ref();
|
||||
let mut hash = *hash;
|
||||
|
||||
if name == KEYWORD_FN_PTR_CALL
|
||||
&& args_expr.len() >= 1
|
||||
&& !self.has_override(lib, 0, hash)
|
||||
{
|
||||
let expr = args_expr.get(0).unwrap();
|
||||
let fn_ptr = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?;
|
||||
|
||||
if fn_ptr.is::<FnPtr>() {
|
||||
// Redirect function name
|
||||
redirected = Some(fn_ptr.cast::<FnPtr>().take_fn_name());
|
||||
name = redirected.as_ref().unwrap();
|
||||
// Skip the first argument
|
||||
args_expr = &args_expr.as_ref()[1..];
|
||||
// Recalculate hash
|
||||
hash = calc_fn_hash(empty(), name, args_expr.len(), empty());
|
||||
} else {
|
||||
return Err(Box::new(EvalAltResult::ErrorMismatchOutputType(
|
||||
self.map_type_name(type_name::<FnPtr>()).into(),
|
||||
fn_ptr.type_name().into(),
|
||||
expr.position(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Normal function call - except for Fn and eval (handled above)
|
||||
let mut arg_values: StaticVec<Dynamic>;
|
||||
let mut args: StaticVec<_>;
|
||||
@ -1983,7 +2035,7 @@ impl Engine {
|
||||
} else {
|
||||
// See if the first argument is a variable, if so, convert to method-call style
|
||||
// in order to leverage potential &mut first argument and avoid cloning the value
|
||||
match args_expr.get(0) {
|
||||
match args_expr.get(0).unwrap() {
|
||||
// func(x, ...) -> x.func(...)
|
||||
lhs @ Expr::Variable(_) => {
|
||||
arg_values = args_expr
|
||||
@ -2020,7 +2072,7 @@ impl Engine {
|
||||
|
||||
let args = args.as_mut();
|
||||
self.exec_fn_call(
|
||||
state, lib, name, *native, *hash, args, is_ref, false, def_val, level,
|
||||
state, lib, name, *native, hash, args, is_ref, false, def_val, level,
|
||||
)
|
||||
.map(|(v, _)| v)
|
||||
.map_err(|err| err.new_position(*pos))
|
||||
|
@ -98,6 +98,8 @@ pub enum ParseErrorType {
|
||||
PropertyExpected,
|
||||
/// Missing a variable name after the `let`, `const` or `for` keywords.
|
||||
VariableExpected,
|
||||
/// An identifier is a reserved keyword.
|
||||
Reserved(String),
|
||||
/// Missing an expression. Wrapped value is the expression type.
|
||||
ExprExpected(String),
|
||||
/// Defining a function `fn` in an appropriate place (e.g. inside another function).
|
||||
@ -163,6 +165,7 @@ impl ParseErrorType {
|
||||
Self::ForbiddenConstantExpr(_) => "Expecting a constant",
|
||||
Self::PropertyExpected => "Expecting name of a property",
|
||||
Self::VariableExpected => "Expecting name of a variable",
|
||||
Self::Reserved(_) => "Invalid use of reserved keyword",
|
||||
Self::ExprExpected(_) => "Expecting an expression",
|
||||
Self::FnMissingName => "Expecting name in function declaration",
|
||||
Self::FnMissingParams(_) => "Expecting parameters in function declaration",
|
||||
@ -224,6 +227,7 @@ impl fmt::Display for ParseErrorType {
|
||||
Self::LiteralTooLarge(typ, max) => {
|
||||
write!(f, "{} exceeds the maximum limit ({})", typ, max)
|
||||
}
|
||||
Self::Reserved(s) => write!(f, "'{}' is a reserved keyword", s),
|
||||
_ => f.write_str(self.desc()),
|
||||
}
|
||||
}
|
||||
|
195
src/parser.rs
195
src/parser.rs
@ -7,7 +7,7 @@ use crate::error::{LexError, ParseError, ParseErrorType};
|
||||
use crate::module::{Module, ModuleRef};
|
||||
use crate::optimize::{optimize_into_ast, OptimizationLevel};
|
||||
use crate::scope::{EntryType as ScopeEntryType, Scope};
|
||||
use crate::token::{Position, Token, TokenStream};
|
||||
use crate::token::{is_valid_identifier, Position, Token, TokenStream};
|
||||
use crate::utils::{StaticVec, StraightHasherBuilder};
|
||||
|
||||
#[cfg(feature = "internals")]
|
||||
@ -25,6 +25,7 @@ use crate::stdlib::{
|
||||
char,
|
||||
collections::HashMap,
|
||||
fmt, format,
|
||||
hash::{Hash, Hasher},
|
||||
iter::empty,
|
||||
mem,
|
||||
num::NonZeroUsize,
|
||||
@ -340,7 +341,7 @@ impl fmt::Display for FnAccess {
|
||||
}
|
||||
|
||||
/// A scripted function definition.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct ScriptFnDef {
|
||||
/// Function name.
|
||||
pub name: String,
|
||||
@ -374,7 +375,7 @@ impl fmt::Display for ScriptFnDef {
|
||||
}
|
||||
|
||||
/// `return`/`throw` statement.
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
|
||||
pub enum ReturnType {
|
||||
/// `return` statement.
|
||||
Return,
|
||||
@ -440,6 +441,8 @@ struct ParseSettings {
|
||||
pos: Position,
|
||||
/// Is the construct being parsed located at global level?
|
||||
is_global: bool,
|
||||
/// Is the construct being parsed located at function definition level?
|
||||
is_function_scope: bool,
|
||||
/// Is the current position inside a loop?
|
||||
is_breakable: bool,
|
||||
/// Is anonymous function allowed?
|
||||
@ -476,7 +479,7 @@ impl ParseSettings {
|
||||
///
|
||||
/// Each variant is at most one pointer in size (for speed),
|
||||
/// with everything being allocated together in one single tuple.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum Stmt {
|
||||
/// No-op.
|
||||
Noop(Position),
|
||||
@ -588,6 +591,13 @@ impl fmt::Debug for CustomExpr {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "internals")]
|
||||
impl Hash for CustomExpr {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// An expression.
|
||||
///
|
||||
/// Each variant is at most one pointer in size (for speed),
|
||||
@ -663,6 +673,18 @@ impl Default for Expr {
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Expr {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self::FloatConstant(x) => {
|
||||
state.write(&x.0.to_le_bytes());
|
||||
x.1.hash(state);
|
||||
}
|
||||
_ => self.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
/// Get the `Dynamic` value of a constant expression.
|
||||
///
|
||||
@ -1343,56 +1365,57 @@ fn parse_map_literal(
|
||||
eat_token(input, Token::RightBrace);
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
let (name, pos) = match input.next().unwrap() {
|
||||
(Token::Identifier(s), pos) => (s, pos),
|
||||
(Token::StringConstant(s), pos) => (s, pos),
|
||||
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
|
||||
(_, pos) if map.is_empty() => {
|
||||
return Err(PERR::MissingToken(
|
||||
Token::RightBrace.into(),
|
||||
MISSING_RBRACE.into(),
|
||||
)
|
||||
.into_err(pos))
|
||||
}
|
||||
(Token::EOF, pos) => {
|
||||
return Err(PERR::MissingToken(
|
||||
Token::RightBrace.into(),
|
||||
MISSING_RBRACE.into(),
|
||||
)
|
||||
.into_err(pos))
|
||||
}
|
||||
(_, pos) => return Err(PERR::PropertyExpected.into_err(pos)),
|
||||
};
|
||||
|
||||
match input.next().unwrap() {
|
||||
(Token::Colon, _) => (),
|
||||
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
|
||||
(_, pos) => {
|
||||
return Err(PERR::MissingToken(
|
||||
Token::Colon.into(),
|
||||
format!(
|
||||
"to follow the property '{}' in this object map literal",
|
||||
name
|
||||
),
|
||||
)
|
||||
.into_err(pos))
|
||||
}
|
||||
};
|
||||
|
||||
if state.engine.max_map_size > 0 && map.len() >= state.engine.max_map_size {
|
||||
return Err(PERR::LiteralTooLarge(
|
||||
"Number of properties in object map literal".to_string(),
|
||||
state.engine.max_map_size,
|
||||
)
|
||||
.into_err(input.peek().unwrap().1));
|
||||
}
|
||||
|
||||
let expr = parse_expr(input, state, lib, settings.level_up())?;
|
||||
map.push(((Into::<ImmutableString>::into(name), pos), expr));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let (name, pos) = match input.next().unwrap() {
|
||||
(Token::Identifier(s), pos) => (s, pos),
|
||||
(Token::StringConstant(s), pos) => (s, pos),
|
||||
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
|
||||
return Err(PERR::Reserved(s).into_err(pos));
|
||||
}
|
||||
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
|
||||
(_, pos) if map.is_empty() => {
|
||||
return Err(
|
||||
PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into())
|
||||
.into_err(pos),
|
||||
);
|
||||
}
|
||||
(Token::EOF, pos) => {
|
||||
return Err(
|
||||
PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into())
|
||||
.into_err(pos),
|
||||
);
|
||||
}
|
||||
(_, pos) => return Err(PERR::PropertyExpected.into_err(pos)),
|
||||
};
|
||||
|
||||
match input.next().unwrap() {
|
||||
(Token::Colon, _) => (),
|
||||
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
|
||||
(_, pos) => {
|
||||
return Err(PERR::MissingToken(
|
||||
Token::Colon.into(),
|
||||
format!(
|
||||
"to follow the property '{}' in this object map literal",
|
||||
name
|
||||
),
|
||||
)
|
||||
.into_err(pos))
|
||||
}
|
||||
};
|
||||
|
||||
if state.engine.max_map_size > 0 && map.len() >= state.engine.max_map_size {
|
||||
return Err(PERR::LiteralTooLarge(
|
||||
"Number of properties in object map literal".to_string(),
|
||||
state.engine.max_map_size,
|
||||
)
|
||||
.into_err(input.peek().unwrap().1));
|
||||
}
|
||||
|
||||
let expr = parse_expr(input, state, lib, settings.level_up())?;
|
||||
map.push(((Into::<ImmutableString>::into(name), pos), expr));
|
||||
|
||||
match input.peek().unwrap() {
|
||||
(Token::Comma, _) => {
|
||||
eat_token(input, Token::Comma);
|
||||
@ -1460,6 +1483,24 @@ fn parse_primary(
|
||||
let index = state.find_var(&s);
|
||||
Expr::Variable(Box::new(((s, settings.pos), None, 0, index)))
|
||||
}
|
||||
// Function call is allowed to have reserved keyword
|
||||
Token::Reserved(s) if s != KEYWORD_THIS && input.peek().unwrap().0 == Token::LeftParen => {
|
||||
Expr::Variable(Box::new(((s, settings.pos), None, 0, None)))
|
||||
}
|
||||
// Access to `this` as a variable is OK
|
||||
Token::Reserved(s) if s == KEYWORD_THIS && input.peek().unwrap().0 != Token::LeftParen => {
|
||||
if !settings.is_function_scope {
|
||||
return Err(
|
||||
PERR::BadInput(format!("'{}' can only be used in functions", s))
|
||||
.into_err(settings.pos),
|
||||
);
|
||||
} else {
|
||||
Expr::Variable(Box::new(((s, settings.pos), None, 0, None)))
|
||||
}
|
||||
}
|
||||
Token::Reserved(s) if is_valid_identifier(s.chars()) => {
|
||||
return Err(PERR::Reserved(s).into_err(settings.pos));
|
||||
}
|
||||
Token::LeftParen => parse_paren_expr(input, state, lib, settings.level_up())?,
|
||||
#[cfg(not(feature = "no_index"))]
|
||||
Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?,
|
||||
@ -1471,7 +1512,7 @@ fn parse_primary(
|
||||
_ => {
|
||||
return Err(
|
||||
PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(settings.pos)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1509,6 +1550,9 @@ fn parse_primary(
|
||||
|
||||
Expr::Variable(Box::new(((id2, pos2), modules, 0, index)))
|
||||
}
|
||||
(Token::Reserved(id2), pos2) if is_valid_identifier(id2.chars()) => {
|
||||
return Err(PERR::Reserved(id2).into_err(pos2));
|
||||
}
|
||||
(_, pos2) => return Err(PERR::VariableExpected.into_err(pos2)),
|
||||
},
|
||||
// Indexing
|
||||
@ -1538,6 +1582,7 @@ fn parse_primary(
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Make sure identifiers are valid
|
||||
Ok(root_expr)
|
||||
}
|
||||
|
||||
@ -2094,6 +2139,9 @@ fn parse_expr(
|
||||
(Token::Identifier(s), pos) => {
|
||||
exprs.push(Expr::Variable(Box::new(((s, pos), None, 0, None))));
|
||||
}
|
||||
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
|
||||
return Err(PERR::Reserved(s).into_err(pos));
|
||||
}
|
||||
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
|
||||
},
|
||||
MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?),
|
||||
@ -2103,9 +2151,6 @@ fn parse_expr(
|
||||
exprs.push(Expr::Stmt(Box::new((stmt, pos))))
|
||||
}
|
||||
s => match input.peek().unwrap() {
|
||||
(Token::Custom(custom), _) if custom == s => {
|
||||
input.next().unwrap();
|
||||
}
|
||||
(t, _) if t.syntax().as_ref() == s => {
|
||||
input.next().unwrap();
|
||||
}
|
||||
@ -2261,10 +2306,12 @@ fn parse_for(
|
||||
let name = match input.next().unwrap() {
|
||||
// Variable name
|
||||
(Token::Identifier(s), _) => s,
|
||||
// Reserved keyword
|
||||
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
|
||||
return Err(PERR::Reserved(s).into_err(pos));
|
||||
}
|
||||
// Bad identifier
|
||||
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
|
||||
// EOF
|
||||
(Token::EOF, pos) => return Err(PERR::VariableExpected.into_err(pos)),
|
||||
// Not a variable name
|
||||
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
|
||||
};
|
||||
@ -2311,20 +2358,13 @@ fn parse_let(
|
||||
// let name ...
|
||||
let (name, pos) = match input.next().unwrap() {
|
||||
(Token::Identifier(s), pos) => (s, pos),
|
||||
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
|
||||
return Err(PERR::Reserved(s).into_err(pos));
|
||||
}
|
||||
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
|
||||
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
|
||||
};
|
||||
|
||||
// Check if the name is allowed
|
||||
match name.as_str() {
|
||||
KEYWORD_THIS => {
|
||||
return Err(
|
||||
PERR::BadInput(LexError::MalformedIdentifier(name).to_string()).into_err(pos),
|
||||
)
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// let name = ...
|
||||
if match_token(input, Token::Equals)? {
|
||||
// let name = expr
|
||||
@ -2390,6 +2430,9 @@ fn parse_import(
|
||||
// import expr as name ...
|
||||
let (name, _) = match input.next().unwrap() {
|
||||
(Token::Identifier(s), pos) => (s, pos),
|
||||
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
|
||||
return Err(PERR::Reserved(s).into_err(pos));
|
||||
}
|
||||
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
|
||||
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
|
||||
};
|
||||
@ -2414,6 +2457,9 @@ fn parse_export(
|
||||
loop {
|
||||
let (id, id_pos) = match input.next().unwrap() {
|
||||
(Token::Identifier(s), pos) => (s.clone(), pos),
|
||||
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
|
||||
return Err(PERR::Reserved(s).into_err(pos));
|
||||
}
|
||||
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
|
||||
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
|
||||
};
|
||||
@ -2421,6 +2467,10 @@ fn parse_export(
|
||||
let rename = if match_token(input, Token::As)? {
|
||||
match input.next().unwrap() {
|
||||
(Token::Identifier(s), pos) => Some((s.clone(), pos)),
|
||||
(Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => {
|
||||
return Err(PERR::Reserved(s).into_err(pos));
|
||||
}
|
||||
(Token::LexError(err), pos) => return Err(err.into_err(pos)),
|
||||
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
|
||||
}
|
||||
} else {
|
||||
@ -2598,6 +2648,7 @@ fn parse_stmt(
|
||||
allow_stmt_expr: true,
|
||||
allow_anonymous_fn: true,
|
||||
is_global: false,
|
||||
is_function_scope: true,
|
||||
is_breakable: false,
|
||||
level: 0,
|
||||
pos: pos,
|
||||
@ -2695,7 +2746,11 @@ fn parse_fn(
|
||||
settings.ensure_level_within_max_limit(state.max_expr_depth)?;
|
||||
|
||||
let name = match input.next().unwrap() {
|
||||
(Token::Identifier(s), _) | (Token::Custom(s), _) => s,
|
||||
(Token::Identifier(s), _) | (Token::Custom(s), _) | (Token::Reserved(s), _)
|
||||
if s != KEYWORD_THIS && is_valid_identifier(s.chars()) =>
|
||||
{
|
||||
s
|
||||
}
|
||||
(_, pos) => return Err(PERR::FnMissingName.into_err(pos)),
|
||||
};
|
||||
|
||||
@ -2790,6 +2845,7 @@ impl Engine {
|
||||
allow_stmt_expr: false,
|
||||
allow_anonymous_fn: false,
|
||||
is_global: true,
|
||||
is_function_scope: false,
|
||||
is_breakable: false,
|
||||
level: 0,
|
||||
pos: Position::none(),
|
||||
@ -2829,6 +2885,7 @@ impl Engine {
|
||||
allow_stmt_expr: true,
|
||||
allow_anonymous_fn: true,
|
||||
is_global: true,
|
||||
is_function_scope: false,
|
||||
is_breakable: false,
|
||||
level: 0,
|
||||
pos: Position::none(),
|
||||
|
@ -2,7 +2,7 @@ use crate::engine::Engine;
|
||||
use crate::module::ModuleResolver;
|
||||
use crate::optimize::OptimizationLevel;
|
||||
use crate::packages::PackageLibrary;
|
||||
use crate::token::is_valid_identifier;
|
||||
use crate::token::{is_valid_identifier, Token};
|
||||
|
||||
use crate::stdlib::{boxed::Box, format, string::String};
|
||||
|
||||
@ -183,8 +183,7 @@ impl Engine {
|
||||
/// engine.disable_symbol("if"); // disable the 'if' keyword
|
||||
///
|
||||
/// engine.compile("let x = if true { 42 } else { 0 };")?;
|
||||
/// // ^ 'if' is parsed as a variable name
|
||||
/// // ^ missing ';' after statement end
|
||||
/// // ^ 'if' is rejected as a reserved keyword
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@ -252,6 +251,24 @@ impl Engine {
|
||||
return Err(format!("not a valid identifier: '{}'", keyword).into());
|
||||
}
|
||||
|
||||
match Token::lookup_from_syntax(keyword) {
|
||||
// Standard identifiers, reserved keywords and custom keywords are OK
|
||||
None | Some(Token::Reserved(_)) | Some(Token::Custom(_)) => (),
|
||||
// Disabled keywords are also OK
|
||||
Some(token)
|
||||
if !self
|
||||
.disabled_symbols
|
||||
.as_ref()
|
||||
.map(|d| d.contains(token.syntax().as_ref()))
|
||||
.unwrap_or(false) =>
|
||||
{
|
||||
()
|
||||
}
|
||||
// Active standard keywords cannot be made custom
|
||||
Some(_) => return Err(format!("'{}' is a reserved keyword", keyword).into()),
|
||||
}
|
||||
|
||||
// Add to custom keywords
|
||||
if self.custom_keywords.is_none() {
|
||||
self.custom_keywords = Some(Default::default());
|
||||
}
|
||||
|
@ -88,11 +88,16 @@ impl Engine {
|
||||
MARKER_EXPR | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => s.to_string(),
|
||||
// Standard symbols not in first position
|
||||
s if !segments.is_empty() && Token::lookup_from_syntax(s).is_some() => {
|
||||
// Make it a custom keyword/operator if it is a disabled standard keyword/operator
|
||||
// or a reserved keyword/operator.
|
||||
if self
|
||||
.disabled_symbols
|
||||
.as_ref()
|
||||
.map(|d| d.contains(s))
|
||||
.unwrap_or(false)
|
||||
|| Token::lookup_from_syntax(s)
|
||||
.map(|token| token.is_reserved())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// If symbol is disabled, make it a custom keyword
|
||||
if self.custom_keywords.is_none() {
|
||||
|
112
src/token.rs
112
src/token.rs
@ -1,6 +1,10 @@
|
||||
//! Main module defining the lexer and parser.
|
||||
|
||||
use crate::engine::Engine;
|
||||
use crate::engine::{
|
||||
Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_PRINT,
|
||||
KEYWORD_THIS, KEYWORD_TYPE_OF,
|
||||
};
|
||||
|
||||
use crate::error::LexError;
|
||||
use crate::parser::INT;
|
||||
use crate::utils::StaticVec;
|
||||
@ -275,8 +279,6 @@ impl Token {
|
||||
Or => "||",
|
||||
Ampersand => "&",
|
||||
And => "&&",
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
Fn => "fn",
|
||||
Continue => "continue",
|
||||
Break => "break",
|
||||
Return => "return",
|
||||
@ -297,8 +299,12 @@ impl Token {
|
||||
ModuloAssign => "%=",
|
||||
PowerOf => "~",
|
||||
PowerOfAssign => "~=",
|
||||
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
Fn => "fn",
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
Private => "private",
|
||||
|
||||
#[cfg(not(feature = "no_module"))]
|
||||
Import => "import",
|
||||
#[cfg(not(feature = "no_module"))]
|
||||
@ -355,8 +361,6 @@ impl Token {
|
||||
"||" => Or,
|
||||
"&" => Ampersand,
|
||||
"&&" => And,
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
"fn" => Fn,
|
||||
"continue" => Continue,
|
||||
"break" => Break,
|
||||
"return" => Return,
|
||||
@ -377,17 +381,30 @@ impl Token {
|
||||
"%=" => ModuloAssign,
|
||||
"~" => PowerOf,
|
||||
"~=" => PowerOfAssign,
|
||||
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
"fn" => Fn,
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
"private" => Private,
|
||||
|
||||
#[cfg(not(feature = "no_module"))]
|
||||
"import" => Import,
|
||||
#[cfg(not(feature = "no_module"))]
|
||||
"export" => Export,
|
||||
#[cfg(not(feature = "no_module"))]
|
||||
"as" => As,
|
||||
|
||||
#[cfg(feature = "no_function")]
|
||||
"fn" | "private" => Reserved(syntax.into()),
|
||||
|
||||
#[cfg(feature = "no_module")]
|
||||
"import" | "export" | "as" => Reserved(syntax.into()),
|
||||
|
||||
"===" | "!==" | "->" | "<-" | "=>" | ":=" | "::<" | "(*" | "*)" | "#" => {
|
||||
Reserved(syntax.into())
|
||||
}
|
||||
KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_TYPE_OF | KEYWORD_EVAL | KEYWORD_FN_PTR
|
||||
| KEYWORD_FN_PTR_CALL | KEYWORD_THIS => Reserved(syntax.into()),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
@ -1317,72 +1334,81 @@ impl<'a> Iterator for TokenIterator<'a, '_> {
|
||||
self.engine.disabled_symbols.as_ref(),
|
||||
self.engine.custom_keywords.as_ref(),
|
||||
) {
|
||||
// {EOF}
|
||||
(None, _, _) => None,
|
||||
(Some((Token::Reserved(s), pos)), None, None) => return Some((match s.as_str() {
|
||||
"===" => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'===' is not a valid operator. This is not JavaScript! Should it be '=='?"
|
||||
.to_string(),
|
||||
// Reserved keyword/symbol
|
||||
(Some((Token::Reserved(s), pos)), disabled, custom) => Some((match
|
||||
(s.as_str(), custom.map(|c| c.contains_key(&s)).unwrap_or(false))
|
||||
{
|
||||
("===", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'===' is not a valid operator. This is not JavaScript! Should it be '=='?".to_string(),
|
||||
))),
|
||||
"!==" => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'!==' is not a valid operator. This is not JavaScript! Should it be '!='?"
|
||||
.to_string(),
|
||||
("!==", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'!==' is not a valid operator. This is not JavaScript! Should it be '!='?".to_string(),
|
||||
))),
|
||||
"->" => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'->' is not a valid symbol. This is not C or C++!".to_string(),
|
||||
))),
|
||||
"<-" => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
("->", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'->' is not a valid symbol. This is not C or C++!".to_string()))),
|
||||
("<-", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'<-' is not a valid symbol. This is not Go! Should it be '<='?".to_string(),
|
||||
))),
|
||||
"=>" => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'=>' is not a valid symbol. This is not Rust! Should it be '>='?"
|
||||
.to_string(),
|
||||
("=>", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'=>' is not a valid symbol. This is not Rust! Should it be '>='?".to_string(),
|
||||
))),
|
||||
":=" => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"':=' is not a valid assignment operator. This is not Go! Should it be simply '='?"
|
||||
.to_string(),
|
||||
(":=", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"':=' is not a valid assignment operator. This is not Go! Should it be simply '='?".to_string(),
|
||||
))),
|
||||
"::<" => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'::<>' is not a valid symbol. This is not Rust! Should it be '::'?"
|
||||
.to_string(),
|
||||
("::<", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'::<>' is not a valid symbol. This is not Rust! Should it be '::'?".to_string(),
|
||||
))),
|
||||
"(*" | "*)" => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'(* .. *)' is not a valid comment format. This is not Pascal! Should it be '/* .. */'?"
|
||||
.to_string(),
|
||||
("(*", false) | ("*)", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'(* .. *)' is not a valid comment format. This is not Pascal! Should it be '/* .. */'?".to_string(),
|
||||
))),
|
||||
"#" => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'#' is not a valid symbol. Should it be '#{'?"
|
||||
.to_string(),
|
||||
("#", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
"'#' is not a valid symbol. Should it be '#{'?".to_string(),
|
||||
))),
|
||||
token => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
format!("'{}' is not a valid symbol.", token)
|
||||
// Reserved keyword/operator that is custom.
|
||||
(_, true) => Token::Custom(s),
|
||||
// Reserved operator that is not custom.
|
||||
(token, false) if !is_valid_identifier(token.chars()) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
format!("'{}' is a reserved symbol", token)
|
||||
))),
|
||||
// Reserved keyword that is not custom and disabled.
|
||||
(token, false) if disabled.map(|d| d.contains(token)).unwrap_or(false) => Token::LexError(Box::new(LERR::ImproperSymbol(
|
||||
format!("reserved symbol '{}' is disabled", token)
|
||||
))),
|
||||
// Reserved keyword/operator that is not custom.
|
||||
(_, false) => Token::Reserved(s),
|
||||
}, pos)),
|
||||
(r @ Some(_), None, None) => r,
|
||||
// Custom keyword
|
||||
(Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => {
|
||||
// Convert custom keywords
|
||||
Some((Token::Custom(s), pos))
|
||||
}
|
||||
(Some((token, pos)), _, Some(custom))
|
||||
if (token.is_keyword() || token.is_operator() || token.is_reserved())
|
||||
&& custom.contains_key(token.syntax().as_ref()) =>
|
||||
// Custom standard keyword - must be disabled
|
||||
(Some((token, pos)), Some(disabled), Some(custom))
|
||||
if token.is_keyword() && custom.contains_key(token.syntax().as_ref()) =>
|
||||
{
|
||||
// Convert into custom keywords
|
||||
Some((Token::Custom(token.syntax().into()), pos))
|
||||
if disabled.contains(token.syntax().as_ref()) {
|
||||
// Disabled standard keyword
|
||||
Some((Token::Custom(token.syntax().into()), pos))
|
||||
} else {
|
||||
// Active standard keyword - should never be a custom keyword!
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
// Disabled operator
|
||||
(Some((token, pos)), Some(disabled), _)
|
||||
if token.is_operator() && disabled.contains(token.syntax().as_ref()) =>
|
||||
{
|
||||
// Convert disallowed operators into lex errors
|
||||
Some((
|
||||
Token::LexError(Box::new(LexError::UnexpectedInput(token.syntax().into()))),
|
||||
pos,
|
||||
))
|
||||
}
|
||||
// Disabled standard keyword
|
||||
(Some((token, pos)), Some(disabled), _)
|
||||
if token.is_keyword() && disabled.contains(token.syntax().as_ref()) =>
|
||||
{
|
||||
// Convert disallowed keywords into identifiers
|
||||
Some((Token::Identifier(token.syntax().into()), pos))
|
||||
Some((Token::Reserved(token.syntax().into()), pos))
|
||||
}
|
||||
(r, _, _) => r,
|
||||
}
|
||||
|
@ -156,6 +156,12 @@ impl<T> Drop for StaticVec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> Hash for StaticVec<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.iter().for_each(|x| x.hash(state));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for StaticVec<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
@ -116,7 +116,7 @@ fn test_anonymous_fn() -> Result<(), Box<EvalAltResult>> {
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
fn test_fn_ptr() -> Result<(), Box<EvalAltResult>> {
|
||||
fn test_fn_ptr_raw() -> Result<(), Box<EvalAltResult>> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine.register_raw_fn(
|
||||
|
80
tests/fn_ptr.rs
Normal file
80
tests/fn_ptr.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use rhai::{Engine, EvalAltResult, RegisterFn, INT};
|
||||
|
||||
#[test]
|
||||
fn test_fn_ptr() -> Result<(), Box<EvalAltResult>> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine.register_fn("bar", |x: &mut INT, y: INT| *x += y);
|
||||
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
assert_eq!(
|
||||
engine.eval::<INT>(
|
||||
r#"
|
||||
let f = Fn("bar");
|
||||
let x = 40;
|
||||
f.call(x, 2);
|
||||
x
|
||||
"#
|
||||
)?,
|
||||
40
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
assert_eq!(
|
||||
engine.eval::<INT>(
|
||||
r#"
|
||||
let f = Fn("bar");
|
||||
let x = 40;
|
||||
x.call(f, 2);
|
||||
x
|
||||
"#
|
||||
)?,
|
||||
42
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
engine.eval::<INT>(
|
||||
r#"
|
||||
let f = Fn("bar");
|
||||
let x = 40;
|
||||
call(f, x, 2);
|
||||
x
|
||||
"#
|
||||
)?,
|
||||
42
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
assert_eq!(
|
||||
engine.eval::<INT>(
|
||||
r#"
|
||||
fn foo(x) { this += x; }
|
||||
|
||||
let f = Fn("foo");
|
||||
let x = 40;
|
||||
x.call(f, 2);
|
||||
x
|
||||
"#
|
||||
)?,
|
||||
42
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "no_function"))]
|
||||
assert!(matches!(
|
||||
*engine
|
||||
.eval::<INT>(
|
||||
r#"
|
||||
fn foo(x) { this += x; }
|
||||
|
||||
let f = Fn("foo");
|
||||
call(f, 2);
|
||||
x
|
||||
"#
|
||||
)
|
||||
.expect_err("should error"),
|
||||
EvalAltResult::ErrorInFunctionCall(fn_name, err, _) if fn_name == "foo" && matches!(*err, EvalAltResult::ErrorUnboundedThis(_))
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
@ -52,12 +52,24 @@ fn test_functions() -> Result<(), Box<EvalAltResult>> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
fn test_function_pointers() -> Result<(), Box<EvalAltResult>> {
|
||||
let engine = Engine::new();
|
||||
|
||||
assert_eq!(engine.eval::<String>(r#"type_of(Fn("abc"))"#)?, "Fn");
|
||||
|
||||
assert_eq!(
|
||||
engine.eval::<INT>(
|
||||
r#"
|
||||
fn foo(x) { 40 + x }
|
||||
|
||||
let f = Fn("foo");
|
||||
call(f, 2)
|
||||
"#
|
||||
)?,
|
||||
42
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
assert_eq!(
|
||||
engine.eval::<INT>(
|
||||
r#"
|
||||
@ -73,11 +85,13 @@ fn test_function_pointers() -> Result<(), Box<EvalAltResult>> {
|
||||
42
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
assert!(matches!(
|
||||
*engine.eval::<INT>(r#"let f = Fn("abc"); f.call(0)"#).expect_err("should error"),
|
||||
EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with("abc (")
|
||||
));
|
||||
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
assert_eq!(
|
||||
engine.eval::<INT>(
|
||||
r#"
|
||||
|
@ -1,6 +1,6 @@
|
||||
#![cfg(not(feature = "no_module"))]
|
||||
use rhai::{
|
||||
module_resolvers::StaticModuleResolver, Engine, EvalAltResult, Module, ParseError,
|
||||
module_resolvers::StaticModuleResolver, Dynamic, Engine, EvalAltResult, Module, ParseError,
|
||||
ParseErrorType, Scope, INT,
|
||||
};
|
||||
|
||||
@ -26,6 +26,7 @@ fn test_module_sub_module() -> Result<(), Box<EvalAltResult>> {
|
||||
|
||||
sub_module.set_sub_module("universe", sub_module2);
|
||||
module.set_sub_module("life", sub_module);
|
||||
module.set_var("MYSTIC_NUMBER", Dynamic::from(42 as INT));
|
||||
|
||||
assert!(module.contains_sub_module("life"));
|
||||
let m = module.get_sub_module("life").unwrap();
|
||||
@ -44,18 +45,16 @@ fn test_module_sub_module() -> Result<(), Box<EvalAltResult>> {
|
||||
let mut engine = Engine::new();
|
||||
engine.set_module_resolver(Some(resolver));
|
||||
|
||||
let mut scope = Scope::new();
|
||||
|
||||
assert_eq!(
|
||||
engine.eval_with_scope::<INT>(
|
||||
&mut scope,
|
||||
r#"import "question" as q; q::life::universe::answer + 1"#
|
||||
)?,
|
||||
engine.eval::<INT>(r#"import "question" as q; q::MYSTIC_NUMBER"#)?,
|
||||
42
|
||||
);
|
||||
assert_eq!(
|
||||
engine.eval_with_scope::<INT>(
|
||||
&mut scope,
|
||||
engine.eval::<INT>(r#"import "question" as q; q::life::universe::answer + 1"#)?,
|
||||
42
|
||||
);
|
||||
assert_eq!(
|
||||
engine.eval::<INT>(
|
||||
r#"import "question" as q; q::life::universe::inc(q::life::universe::answer)"#
|
||||
)?,
|
||||
42
|
||||
@ -221,36 +220,30 @@ fn test_module_from_ast() -> Result<(), Box<EvalAltResult>> {
|
||||
resolver2.insert("testing", module);
|
||||
engine.set_module_resolver(Some(resolver2));
|
||||
|
||||
let mut scope = Scope::new();
|
||||
|
||||
assert_eq!(
|
||||
engine.eval_with_scope::<INT>(&mut scope, r#"import "testing" as ttt; ttt::abc"#)?,
|
||||
engine.eval::<INT>(r#"import "testing" as ttt; ttt::abc"#)?,
|
||||
123
|
||||
);
|
||||
assert_eq!(
|
||||
engine.eval_with_scope::<INT>(&mut scope, r#"import "testing" as ttt; ttt::foo"#)?,
|
||||
engine.eval::<INT>(r#"import "testing" as ttt; ttt::foo"#)?,
|
||||
42
|
||||
);
|
||||
assert!(engine
|
||||
.eval_with_scope::<bool>(&mut scope, r#"import "testing" as ttt; ttt::extra::foo"#)?);
|
||||
assert!(engine.eval::<bool>(r#"import "testing" as ttt; ttt::extra::foo"#)?);
|
||||
assert_eq!(
|
||||
engine.eval_with_scope::<String>(&mut scope, r#"import "testing" as ttt; ttt::hello"#)?,
|
||||
engine.eval::<String>(r#"import "testing" as ttt; ttt::hello"#)?,
|
||||
"hello, 42 worlds!"
|
||||
);
|
||||
assert_eq!(
|
||||
engine.eval_with_scope::<INT>(&mut scope, r#"import "testing" as ttt; ttt::calc(999)"#)?,
|
||||
engine.eval::<INT>(r#"import "testing" as ttt; ttt::calc(999)"#)?,
|
||||
1000
|
||||
);
|
||||
assert_eq!(
|
||||
engine.eval_with_scope::<INT>(
|
||||
&mut scope,
|
||||
r#"import "testing" as ttt; ttt::add_len(ttt::foo, ttt::hello)"#
|
||||
)?,
|
||||
engine.eval::<INT>(r#"import "testing" as ttt; ttt::add_len(ttt::foo, ttt::hello)"#)?,
|
||||
59
|
||||
);
|
||||
assert!(matches!(
|
||||
*engine
|
||||
.eval_with_scope::<()>(&mut scope, r#"import "testing" as ttt; ttt::hidden()"#)
|
||||
.consume(r#"import "testing" as ttt; ttt::hidden()"#)
|
||||
.expect_err("should error"),
|
||||
EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "ttt::hidden"
|
||||
));
|
||||
|
@ -1,16 +1,25 @@
|
||||
#![cfg(feature = "internals")]
|
||||
use rhai::{
|
||||
Dynamic, Engine, EvalAltResult, EvalState, Expression, Imports, LexError, Module, Scope, INT,
|
||||
Dynamic, Engine, EvalAltResult, EvalState, Expression, Imports, LexError, Module, ParseError,
|
||||
ParseErrorType, Scope, INT,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine.consume("while false {}")?;
|
||||
|
||||
// Disable 'while' and make sure it still works with custom syntax
|
||||
engine.disable_symbol("while");
|
||||
engine.consume("while false {}").expect_err("should error");
|
||||
engine.consume("let while = 0")?;
|
||||
assert!(matches!(
|
||||
*engine.compile("while false {}").expect_err("should error").0,
|
||||
ParseErrorType::Reserved(err) if err == "while"
|
||||
));
|
||||
assert!(matches!(
|
||||
*engine.compile("let while = 0").expect_err("should error").0,
|
||||
ParseErrorType::Reserved(err) if err == "while"
|
||||
));
|
||||
|
||||
engine
|
||||
.register_custom_syntax(
|
||||
|
@ -7,8 +7,11 @@ fn test_tokens_disabled() {
|
||||
engine.disable_symbol("if"); // disable the 'if' keyword
|
||||
|
||||
assert!(matches!(
|
||||
*engine.compile("let x = if true { 42 } else { 0 };").expect_err("should error").0,
|
||||
ParseErrorType::MissingToken(ref token, _) if token == ";"
|
||||
*engine
|
||||
.compile("let x = if true { 42 } else { 0 };")
|
||||
.expect_err("should error")
|
||||
.0,
|
||||
ParseErrorType::Reserved(err) if err == "if"
|
||||
));
|
||||
|
||||
engine.disable_symbol("+="); // disable the '+=' operator
|
||||
|
Loading…
Reference in New Issue
Block a user