Merge pull request #187 from schungx/master

Streamline FnPtr features.
This commit is contained in:
Stephen Chung 2020-07-19 09:17:24 +08:00 committed by GitHub
commit 540ec5df6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 520 additions and 205 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "rhai" name = "rhai"
version = "0.17.0" version = "0.18.0"
edition = "2018" edition = "2018"
authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"]
description = "Embedded scripting for Rust" description = "Embedded scripting for Rust"
@ -73,9 +73,3 @@ optional = true
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant 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"]

View File

@ -18,8 +18,8 @@ Supported targets and builds
* WebAssembly (WASM) * WebAssembly (WASM)
* `no-std` * `no-std`
Standard Features Standard features
---------------- -----------------
* Easy-to-use language similar to JavaScript+Rust with dynamic typing. * 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). * 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. * 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). * 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`). * 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. * 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. * 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). * 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). * 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). * Extend the language with [custom syntax](https://schungx.github.io/rhai/engine/custom-syntax.html).
Documentation Documentation

View File

@ -1,6 +1,17 @@
Rhai Release Notes 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 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. 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::disable_symbol` to surgically disable keywords and/or operators.
* `Engine::register_custom_operator` to define a custom operator. * `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 `Engine::register_raw_fn` and `Engine::register_raw_fn_XXX`.
* New low-level API `Module::set_raw_fn` mirroring `Engine::register_raw_fn`. * 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`. * `AST::clone_functions_only`, `AST::clone_functions_only_filtered` and `AST::clone_statements_only` to clone only part of an `AST`.

View File

@ -5,6 +5,12 @@ Advanced Topics
This section covers advanced features such as: 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.

View File

@ -1,5 +1,5 @@
{ {
"version": "0.17.0", "version": "0.18.0",
"rootUrl": "", "rootUrl": "",
"rootUrlX": "/rhai", "rootUrlX": "/rhai",
"rootUrlXX": "/rhai/vnext" "rootUrlXX": "/rhai/vnext"

View File

@ -68,11 +68,13 @@ These symbol types can be used:
### The First Symbol Must be a Keyword ### The First Symbol Must be a Keyword
There is no specific limit on the combination and sequencing of each symbol type, 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 ### The First Symbol Must be Unique

View File

@ -20,8 +20,7 @@ engine
// The following all return parse errors. // The following all return parse errors.
engine.compile("let x = if true { 42 } else { 0 };")?; engine.compile("let x = if true { 42 } else { 0 };")?;
// ^ missing ';' after statement end // ^ 'if' is rejected as a reserved keyword
// ^ 'if' is parsed as a variable name
engine.compile("let x = 40 + 2; x += 1;")?; engine.compile("let x = 40 + 2; x += 1;")?;
// ^ '+=' is not recognized as an operator // ^ '+=' is not recognized as an operator

View File

@ -6,7 +6,7 @@ Function Pointers
It is possible to store a _function pointer_ in a variable just like a normal value. 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. 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 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 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 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. See [function namespaces] for more details.
```rust ```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_work_now() { // call it from a local function
fn do_something_now() { // call it from a local function
import "foo" as f; 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! p.call(); // works!
``` ```
@ -134,3 +132,35 @@ let func = sign(x) + 1;
// Dynamic dispatch // Dynamic dispatch
map[func].call(42); 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`].

View File

@ -3,8 +3,8 @@ Maximum Size of Arrays
{{#include ../links.md}} {{#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. Rhai by default does not limit how large an [array] can be.

View File

@ -3,8 +3,8 @@ Maximum Call Stack Depth
{{#include ../links.md}} {{#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). Rhai by default limits function calls to a maximum depth of 128 levels (16 levels in debug build).

View File

@ -3,8 +3,8 @@ Maximum Size of Object Maps
{{#include ../links.md}} {{#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. Rhai by default does not limit how large (i.e. the number of properties) an [object map] can be.

View File

@ -3,8 +3,8 @@ Maximum Number of Operations
{{#include ../links.md}} {{#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. Rhai by default does not limit how much time or CPU a script consumes.

View File

@ -3,8 +3,8 @@ Maximum Statement Depth
{{#include ../links.md}} {{#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 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 (which should be plenty) when they are at _global_ level, but only a depth of 32

View File

@ -3,8 +3,8 @@ Maximum Length of Strings
{{#include ../links.md}} {{#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. Rhai by default does not limit how long a [string] can be.

View File

@ -1,5 +1,5 @@
Tracking Progress and Force-Termination Track Progress and Force-Termination
====================================== ===================================
{{#include ../links.md}} {{#include ../links.md}}

View File

@ -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 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, all code is compiled into the final binary since what a script requires cannot be predicted.
omitting them via special features is a prudent strategy to optimize the build for size. 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 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`]). ([`no_float`]), checked arithmetic/script resource limits ([`unchecked`]) and finally object maps and custom types ([`no_object`]).

View File

@ -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 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. 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.

View File

@ -96,7 +96,7 @@ pub const MARKER_BLOCK: &str = "$block$";
pub const MARKER_IDENT: &str = "$ident$"; pub const MARKER_IDENT: &str = "$ident$";
#[cfg(feature = "internals")] #[cfg(feature = "internals")]
#[derive(Debug, Clone)] #[derive(Debug, Clone, Hash)]
pub struct Expression<'a>(&'a Expr); pub struct Expression<'a>(&'a Expr);
#[cfg(feature = "internals")] #[cfg(feature = "internals")]
@ -944,7 +944,7 @@ impl Engine {
} }
// Has a system function an override? // 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 // NOTE: We skip script functions for global_module and packages, and native functions for lib
// First check script-defined functions // First check script-defined functions
@ -986,13 +986,15 @@ impl Engine {
match fn_name { match fn_name {
// type_of // type_of
KEYWORD_TYPE_OF if args.len() == 1 && !self.has_override(lib, hashes) => Ok(( 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(), self.map_type_name(args[0].type_name()).to_string().into(),
false, false,
)), ))
}
// Fn // 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( Err(Box::new(EvalAltResult::ErrorRuntime(
"'Fn' should not be called in method style. Try Fn(...);".into(), "'Fn' should not be called in method style. Try Fn(...);".into(),
Position::none(), Position::none(),
@ -1000,7 +1002,7 @@ impl Engine {
} }
// eval - reaching this point it must be a method-style call // 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( Err(Box::new(EvalAltResult::ErrorRuntime(
"'eval' should not be called in method style. Try eval(...);".into(), "'eval' should not be called in method style. Try eval(...);".into(),
Position::none(), Position::none(),
@ -1086,10 +1088,10 @@ impl Engine {
let idx = idx_val.downcast_mut::<StaticVec<Dynamic>>().unwrap(); let idx = idx_val.downcast_mut::<StaticVec<Dynamic>>().unwrap();
let mut fn_name = name.as_ref(); 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>() { let (result, updated) = if fn_name == KEYWORD_FN_PTR_CALL && obj.is::<FnPtr>() {
// FnPtr call
// Redirect function name // Redirect function name
fn_name = obj.as_str().unwrap(); let fn_name = obj.as_str().unwrap();
// Recalculate hash // Recalculate hash
let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty());
// Arguments are passed as-is // Arguments are passed as-is
@ -1100,6 +1102,26 @@ impl Engine {
self.exec_fn_call( self.exec_fn_call(
state, lib, fn_name, *native, hash, args, false, false, def_val, level, 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 { } else {
let redirected: Option<ImmutableString>; let redirected: Option<ImmutableString>;
let mut hash = *hash; let mut hash = *hash;
@ -1926,7 +1948,7 @@ impl Engine {
let hash_fn = let hash_fn =
calc_fn_hash(empty(), name, 1, once(TypeId::of::<ImmutableString>())); 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 // Fn - only in function call style
let expr = args_expr.get(0); let expr = args_expr.get(0);
let arg_value = let arg_value =
@ -1952,7 +1974,7 @@ impl Engine {
let hash_fn = let hash_fn =
calc_fn_hash(empty(), name, 1, once(TypeId::of::<ImmutableString>())); 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 // eval - only in function call style
let prev_len = scope.len(); let prev_len = scope.len();
let expr = args_expr.get(0); 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) // Normal function call - except for Fn and eval (handled above)
let mut arg_values: StaticVec<Dynamic>; let mut arg_values: StaticVec<Dynamic>;
let mut args: StaticVec<_>; let mut args: StaticVec<_>;
@ -1983,7 +2035,7 @@ impl Engine {
} else { } else {
// See if the first argument is a variable, if so, convert to method-call style // 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 // 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(...) // func(x, ...) -> x.func(...)
lhs @ Expr::Variable(_) => { lhs @ Expr::Variable(_) => {
arg_values = args_expr arg_values = args_expr
@ -2020,7 +2072,7 @@ impl Engine {
let args = args.as_mut(); let args = args.as_mut();
self.exec_fn_call( 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(|(v, _)| v)
.map_err(|err| err.new_position(*pos)) .map_err(|err| err.new_position(*pos))

View File

@ -98,6 +98,8 @@ pub enum ParseErrorType {
PropertyExpected, PropertyExpected,
/// Missing a variable name after the `let`, `const` or `for` keywords. /// Missing a variable name after the `let`, `const` or `for` keywords.
VariableExpected, VariableExpected,
/// An identifier is a reserved keyword.
Reserved(String),
/// Missing an expression. Wrapped value is the expression type. /// Missing an expression. Wrapped value is the expression type.
ExprExpected(String), ExprExpected(String),
/// Defining a function `fn` in an appropriate place (e.g. inside another function). /// 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::ForbiddenConstantExpr(_) => "Expecting a constant",
Self::PropertyExpected => "Expecting name of a property", Self::PropertyExpected => "Expecting name of a property",
Self::VariableExpected => "Expecting name of a variable", Self::VariableExpected => "Expecting name of a variable",
Self::Reserved(_) => "Invalid use of reserved keyword",
Self::ExprExpected(_) => "Expecting an expression", Self::ExprExpected(_) => "Expecting an expression",
Self::FnMissingName => "Expecting name in function declaration", Self::FnMissingName => "Expecting name in function declaration",
Self::FnMissingParams(_) => "Expecting parameters in function declaration", Self::FnMissingParams(_) => "Expecting parameters in function declaration",
@ -224,6 +227,7 @@ impl fmt::Display for ParseErrorType {
Self::LiteralTooLarge(typ, max) => { Self::LiteralTooLarge(typ, max) => {
write!(f, "{} exceeds the maximum limit ({})", 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()), _ => f.write_str(self.desc()),
} }
} }

View File

@ -7,7 +7,7 @@ use crate::error::{LexError, ParseError, ParseErrorType};
use crate::module::{Module, ModuleRef}; use crate::module::{Module, ModuleRef};
use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::optimize::{optimize_into_ast, OptimizationLevel};
use crate::scope::{EntryType as ScopeEntryType, Scope}; 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}; use crate::utils::{StaticVec, StraightHasherBuilder};
#[cfg(feature = "internals")] #[cfg(feature = "internals")]
@ -25,6 +25,7 @@ use crate::stdlib::{
char, char,
collections::HashMap, collections::HashMap,
fmt, format, fmt, format,
hash::{Hash, Hasher},
iter::empty, iter::empty,
mem, mem,
num::NonZeroUsize, num::NonZeroUsize,
@ -340,7 +341,7 @@ impl fmt::Display for FnAccess {
} }
/// A scripted function definition. /// A scripted function definition.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Hash)]
pub struct ScriptFnDef { pub struct ScriptFnDef {
/// Function name. /// Function name.
pub name: String, pub name: String,
@ -374,7 +375,7 @@ impl fmt::Display for ScriptFnDef {
} }
/// `return`/`throw` statement. /// `return`/`throw` statement.
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] #[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
pub enum ReturnType { pub enum ReturnType {
/// `return` statement. /// `return` statement.
Return, Return,
@ -440,6 +441,8 @@ struct ParseSettings {
pos: Position, pos: Position,
/// Is the construct being parsed located at global level? /// Is the construct being parsed located at global level?
is_global: bool, 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 the current position inside a loop?
is_breakable: bool, is_breakable: bool,
/// Is anonymous function allowed? /// Is anonymous function allowed?
@ -476,7 +479,7 @@ impl ParseSettings {
/// ///
/// Each variant is at most one pointer in size (for speed), /// Each variant is at most one pointer in size (for speed),
/// with everything being allocated together in one single tuple. /// with everything being allocated together in one single tuple.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Hash)]
pub enum Stmt { pub enum Stmt {
/// No-op. /// No-op.
Noop(Position), 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. /// An expression.
/// ///
/// Each variant is at most one pointer in size (for speed), /// 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 { impl Expr {
/// Get the `Dynamic` value of a constant expression. /// Get the `Dynamic` value of a constant expression.
/// ///
@ -1343,24 +1365,27 @@ fn parse_map_literal(
eat_token(input, Token::RightBrace); eat_token(input, Token::RightBrace);
break; break;
} }
_ => { _ => (),
}
let (name, pos) = match input.next().unwrap() { let (name, pos) = match input.next().unwrap() {
(Token::Identifier(s), pos) => (s, pos), (Token::Identifier(s), pos) => (s, pos),
(Token::StringConstant(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)), (Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) if map.is_empty() => { (_, pos) if map.is_empty() => {
return Err(PERR::MissingToken( return Err(
Token::RightBrace.into(), PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into())
MISSING_RBRACE.into(), .into_err(pos),
) );
.into_err(pos))
} }
(Token::EOF, pos) => { (Token::EOF, pos) => {
return Err(PERR::MissingToken( return Err(
Token::RightBrace.into(), PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into())
MISSING_RBRACE.into(), .into_err(pos),
) );
.into_err(pos))
} }
(_, pos) => return Err(PERR::PropertyExpected.into_err(pos)), (_, pos) => return Err(PERR::PropertyExpected.into_err(pos)),
}; };
@ -1390,8 +1415,6 @@ fn parse_map_literal(
let expr = parse_expr(input, state, lib, settings.level_up())?; let expr = parse_expr(input, state, lib, settings.level_up())?;
map.push(((Into::<ImmutableString>::into(name), pos), expr)); map.push(((Into::<ImmutableString>::into(name), pos), expr));
}
}
match input.peek().unwrap() { match input.peek().unwrap() {
(Token::Comma, _) => { (Token::Comma, _) => {
@ -1460,6 +1483,24 @@ fn parse_primary(
let index = state.find_var(&s); let index = state.find_var(&s);
Expr::Variable(Box::new(((s, settings.pos), None, 0, index))) 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())?, Token::LeftParen => parse_paren_expr(input, state, lib, settings.level_up())?,
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?, Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?,
@ -1471,7 +1512,7 @@ fn parse_primary(
_ => { _ => {
return Err( return Err(
PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(settings.pos) 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))) 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)), (_, pos2) => return Err(PERR::VariableExpected.into_err(pos2)),
}, },
// Indexing // Indexing
@ -1538,6 +1582,7 @@ fn parse_primary(
_ => (), _ => (),
} }
// Make sure identifiers are valid
Ok(root_expr) Ok(root_expr)
} }
@ -2094,6 +2139,9 @@ fn parse_expr(
(Token::Identifier(s), pos) => { (Token::Identifier(s), pos) => {
exprs.push(Expr::Variable(Box::new(((s, pos), None, 0, None)))); 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)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
}, },
MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?), 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)))) exprs.push(Expr::Stmt(Box::new((stmt, pos))))
} }
s => match input.peek().unwrap() { s => match input.peek().unwrap() {
(Token::Custom(custom), _) if custom == s => {
input.next().unwrap();
}
(t, _) if t.syntax().as_ref() == s => { (t, _) if t.syntax().as_ref() == s => {
input.next().unwrap(); input.next().unwrap();
} }
@ -2261,10 +2306,12 @@ fn parse_for(
let name = match input.next().unwrap() { let name = match input.next().unwrap() {
// Variable name // Variable name
(Token::Identifier(s), _) => s, (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 // Bad identifier
(Token::LexError(err), pos) => return Err(err.into_err(pos)), (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 // Not a variable name
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
}; };
@ -2311,20 +2358,13 @@ fn parse_let(
// let name ... // let name ...
let (name, pos) = match input.next().unwrap() { let (name, pos) = match input.next().unwrap() {
(Token::Identifier(s), pos) => (s, pos), (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)), (Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) => return Err(PERR::VariableExpected.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 = ... // let name = ...
if match_token(input, Token::Equals)? { if match_token(input, Token::Equals)? {
// let name = expr // let name = expr
@ -2390,6 +2430,9 @@ fn parse_import(
// import expr as name ... // import expr as name ...
let (name, _) = match input.next().unwrap() { let (name, _) = match input.next().unwrap() {
(Token::Identifier(s), pos) => (s, pos), (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)), (Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
}; };
@ -2414,6 +2457,9 @@ fn parse_export(
loop { loop {
let (id, id_pos) = match input.next().unwrap() { let (id, id_pos) = match input.next().unwrap() {
(Token::Identifier(s), pos) => (s.clone(), pos), (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)), (Token::LexError(err), pos) => return Err(err.into_err(pos)),
(_, pos) => return Err(PERR::VariableExpected.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)? { let rename = if match_token(input, Token::As)? {
match input.next().unwrap() { match input.next().unwrap() {
(Token::Identifier(s), pos) => Some((s.clone(), pos)), (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)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
} }
} else { } else {
@ -2598,6 +2648,7 @@ fn parse_stmt(
allow_stmt_expr: true, allow_stmt_expr: true,
allow_anonymous_fn: true, allow_anonymous_fn: true,
is_global: false, is_global: false,
is_function_scope: true,
is_breakable: false, is_breakable: false,
level: 0, level: 0,
pos: pos, pos: pos,
@ -2695,7 +2746,11 @@ fn parse_fn(
settings.ensure_level_within_max_limit(state.max_expr_depth)?; settings.ensure_level_within_max_limit(state.max_expr_depth)?;
let name = match input.next().unwrap() { 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)), (_, pos) => return Err(PERR::FnMissingName.into_err(pos)),
}; };
@ -2790,6 +2845,7 @@ impl Engine {
allow_stmt_expr: false, allow_stmt_expr: false,
allow_anonymous_fn: false, allow_anonymous_fn: false,
is_global: true, is_global: true,
is_function_scope: false,
is_breakable: false, is_breakable: false,
level: 0, level: 0,
pos: Position::none(), pos: Position::none(),
@ -2829,6 +2885,7 @@ impl Engine {
allow_stmt_expr: true, allow_stmt_expr: true,
allow_anonymous_fn: true, allow_anonymous_fn: true,
is_global: true, is_global: true,
is_function_scope: false,
is_breakable: false, is_breakable: false,
level: 0, level: 0,
pos: Position::none(), pos: Position::none(),

View File

@ -2,7 +2,7 @@ use crate::engine::Engine;
use crate::module::ModuleResolver; use crate::module::ModuleResolver;
use crate::optimize::OptimizationLevel; use crate::optimize::OptimizationLevel;
use crate::packages::PackageLibrary; 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}; use crate::stdlib::{boxed::Box, format, string::String};
@ -183,8 +183,7 @@ impl Engine {
/// engine.disable_symbol("if"); // disable the 'if' keyword /// engine.disable_symbol("if"); // disable the 'if' keyword
/// ///
/// engine.compile("let x = if true { 42 } else { 0 };")?; /// engine.compile("let x = if true { 42 } else { 0 };")?;
/// // ^ 'if' is parsed as a variable name /// // ^ 'if' is rejected as a reserved keyword
/// // ^ missing ';' after statement end
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@ -252,6 +251,24 @@ impl Engine {
return Err(format!("not a valid identifier: '{}'", keyword).into()); 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() { if self.custom_keywords.is_none() {
self.custom_keywords = Some(Default::default()); self.custom_keywords = Some(Default::default());
} }

View File

@ -88,11 +88,16 @@ impl Engine {
MARKER_EXPR | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => s.to_string(), MARKER_EXPR | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => s.to_string(),
// Standard symbols not in first position // Standard symbols not in first position
s if !segments.is_empty() && Token::lookup_from_syntax(s).is_some() => { 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 if self
.disabled_symbols .disabled_symbols
.as_ref() .as_ref()
.map(|d| d.contains(s)) .map(|d| d.contains(s))
.unwrap_or(false) .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 symbol is disabled, make it a custom keyword
if self.custom_keywords.is_none() { if self.custom_keywords.is_none() {

View File

@ -1,6 +1,10 @@
//! Main module defining the lexer and parser. //! 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::error::LexError;
use crate::parser::INT; use crate::parser::INT;
use crate::utils::StaticVec; use crate::utils::StaticVec;
@ -275,8 +279,6 @@ impl Token {
Or => "||", Or => "||",
Ampersand => "&", Ampersand => "&",
And => "&&", And => "&&",
#[cfg(not(feature = "no_function"))]
Fn => "fn",
Continue => "continue", Continue => "continue",
Break => "break", Break => "break",
Return => "return", Return => "return",
@ -297,8 +299,12 @@ impl Token {
ModuloAssign => "%=", ModuloAssign => "%=",
PowerOf => "~", PowerOf => "~",
PowerOfAssign => "~=", PowerOfAssign => "~=",
#[cfg(not(feature = "no_function"))]
Fn => "fn",
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
Private => "private", Private => "private",
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
Import => "import", Import => "import",
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
@ -355,8 +361,6 @@ impl Token {
"||" => Or, "||" => Or,
"&" => Ampersand, "&" => Ampersand,
"&&" => And, "&&" => And,
#[cfg(not(feature = "no_function"))]
"fn" => Fn,
"continue" => Continue, "continue" => Continue,
"break" => Break, "break" => Break,
"return" => Return, "return" => Return,
@ -377,17 +381,30 @@ impl Token {
"%=" => ModuloAssign, "%=" => ModuloAssign,
"~" => PowerOf, "~" => PowerOf,
"~=" => PowerOfAssign, "~=" => PowerOfAssign,
#[cfg(not(feature = "no_function"))]
"fn" => Fn,
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
"private" => Private, "private" => Private,
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
"import" => Import, "import" => Import,
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
"export" => Export, "export" => Export,
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
"as" => As, "as" => As,
#[cfg(feature = "no_function")]
"fn" | "private" => Reserved(syntax.into()),
#[cfg(feature = "no_module")]
"import" | "export" | "as" => Reserved(syntax.into()),
"===" | "!==" | "->" | "<-" | "=>" | ":=" | "::<" | "(*" | "*)" | "#" => { "===" | "!==" | "->" | "<-" | "=>" | ":=" | "::<" | "(*" | "*)" | "#" => {
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, _ => return None,
}) })
@ -1317,72 +1334,81 @@ impl<'a> Iterator for TokenIterator<'a, '_> {
self.engine.disabled_symbols.as_ref(), self.engine.disabled_symbols.as_ref(),
self.engine.custom_keywords.as_ref(), self.engine.custom_keywords.as_ref(),
) { ) {
// {EOF}
(None, _, _) => None, (None, _, _) => None,
(Some((Token::Reserved(s), pos)), None, None) => return Some((match s.as_str() { // Reserved keyword/symbol
"===" => Token::LexError(Box::new(LERR::ImproperSymbol( (Some((Token::Reserved(s), pos)), disabled, custom) => Some((match
"'===' is not a valid operator. This is not JavaScript! Should it be '=='?" (s.as_str(), custom.map(|c| c.contains_key(&s)).unwrap_or(false))
.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( ("!==", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
"'!==' is not a valid operator. This is not JavaScript! Should it be '!='?" "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?".to_string(),
.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(), "'->' is not a valid symbol. This is not C or C++!".to_string()))),
))), ("<-", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
"<-" => Token::LexError(Box::new(LERR::ImproperSymbol(
"'<-' is not a valid symbol. This is not Go! Should it be '<='?".to_string(), "'<-' is not a valid symbol. This is not Go! Should it be '<='?".to_string(),
))), ))),
"=>" => Token::LexError(Box::new(LERR::ImproperSymbol( ("=>", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
"'=>' is not a valid symbol. This is not Rust! Should it be '>='?" "'=>' is not a valid symbol. This is not Rust! Should it be '>='?".to_string(),
.to_string(),
))), ))),
":=" => Token::LexError(Box::new(LERR::ImproperSymbol( (":=", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
"':=' is not a valid assignment operator. This is not Go! Should it be simply '='?" "':=' is not a valid assignment operator. This is not Go! Should it be simply '='?".to_string(),
.to_string(),
))), ))),
"::<" => Token::LexError(Box::new(LERR::ImproperSymbol( ("::<", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
"'::<>' is not a valid symbol. This is not Rust! Should it be '::'?" "'::<>' is not a valid symbol. This is not Rust! Should it be '::'?".to_string(),
.to_string(),
))), ))),
"(*" | "*)" => Token::LexError(Box::new(LERR::ImproperSymbol( ("(*", false) | ("*)", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
"'(* .. *)' is not a valid comment format. This is not Pascal! Should it be '/* .. */'?" "'(* .. *)' is not a valid comment format. This is not Pascal! Should it be '/* .. */'?".to_string(),
.to_string(),
))), ))),
"#" => Token::LexError(Box::new(LERR::ImproperSymbol( ("#", false) => Token::LexError(Box::new(LERR::ImproperSymbol(
"'#' is not a valid symbol. Should it be '#{'?" "'#' is not a valid symbol. Should it be '#{'?".to_string(),
.to_string(),
))), ))),
token => Token::LexError(Box::new(LERR::ImproperSymbol( // Reserved keyword/operator that is custom.
format!("'{}' is not a valid symbol.", token) (_, 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)), }, pos)),
(r @ Some(_), None, None) => r, // Custom keyword
(Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => { (Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => {
// Convert custom keywords
Some((Token::Custom(s), pos)) Some((Token::Custom(s), pos))
} }
(Some((token, pos)), _, Some(custom)) // Custom standard keyword - must be disabled
if (token.is_keyword() || token.is_operator() || token.is_reserved()) (Some((token, pos)), Some(disabled), Some(custom))
&& custom.contains_key(token.syntax().as_ref()) => if token.is_keyword() && custom.contains_key(token.syntax().as_ref()) =>
{ {
// Convert into custom keywords if disabled.contains(token.syntax().as_ref()) {
// Disabled standard keyword
Some((Token::Custom(token.syntax().into()), pos)) 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), _) (Some((token, pos)), Some(disabled), _)
if token.is_operator() && disabled.contains(token.syntax().as_ref()) => if token.is_operator() && disabled.contains(token.syntax().as_ref()) =>
{ {
// Convert disallowed operators into lex errors
Some(( Some((
Token::LexError(Box::new(LexError::UnexpectedInput(token.syntax().into()))), Token::LexError(Box::new(LexError::UnexpectedInput(token.syntax().into()))),
pos, pos,
)) ))
} }
// Disabled standard keyword
(Some((token, pos)), Some(disabled), _) (Some((token, pos)), Some(disabled), _)
if token.is_keyword() && disabled.contains(token.syntax().as_ref()) => if token.is_keyword() && disabled.contains(token.syntax().as_ref()) =>
{ {
// Convert disallowed keywords into identifiers Some((Token::Reserved(token.syntax().into()), pos))
Some((Token::Identifier(token.syntax().into()), pos))
} }
(r, _, _) => r, (r, _, _) => r,
} }

View File

@ -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> { impl<T> Default for StaticVec<T> {
fn default() -> Self { fn default() -> Self {
Self { Self {

View File

@ -116,7 +116,7 @@ fn test_anonymous_fn() -> Result<(), Box<EvalAltResult>> {
#[test] #[test]
#[cfg(not(feature = "no_object"))] #[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(); let mut engine = Engine::new();
engine.register_raw_fn( engine.register_raw_fn(

80
tests/fn_ptr.rs Normal file
View 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(())
}

View File

@ -52,12 +52,24 @@ fn test_functions() -> Result<(), Box<EvalAltResult>> {
} }
#[test] #[test]
#[cfg(not(feature = "no_object"))]
fn test_function_pointers() -> Result<(), Box<EvalAltResult>> { fn test_function_pointers() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new(); let engine = Engine::new();
assert_eq!(engine.eval::<String>(r#"type_of(Fn("abc"))"#)?, "Fn"); 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!( assert_eq!(
engine.eval::<INT>( engine.eval::<INT>(
r#" r#"
@ -73,11 +85,13 @@ fn test_function_pointers() -> Result<(), Box<EvalAltResult>> {
42 42
); );
#[cfg(not(feature = "no_object"))]
assert!(matches!( assert!(matches!(
*engine.eval::<INT>(r#"let f = Fn("abc"); f.call(0)"#).expect_err("should error"), *engine.eval::<INT>(r#"let f = Fn("abc"); f.call(0)"#).expect_err("should error"),
EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with("abc (") EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with("abc (")
)); ));
#[cfg(not(feature = "no_object"))]
assert_eq!( assert_eq!(
engine.eval::<INT>( engine.eval::<INT>(
r#" r#"

View File

@ -1,6 +1,6 @@
#![cfg(not(feature = "no_module"))] #![cfg(not(feature = "no_module"))]
use rhai::{ use rhai::{
module_resolvers::StaticModuleResolver, Engine, EvalAltResult, Module, ParseError, module_resolvers::StaticModuleResolver, Dynamic, Engine, EvalAltResult, Module, ParseError,
ParseErrorType, Scope, INT, ParseErrorType, Scope, INT,
}; };
@ -26,6 +26,7 @@ fn test_module_sub_module() -> Result<(), Box<EvalAltResult>> {
sub_module.set_sub_module("universe", sub_module2); sub_module.set_sub_module("universe", sub_module2);
module.set_sub_module("life", sub_module); module.set_sub_module("life", sub_module);
module.set_var("MYSTIC_NUMBER", Dynamic::from(42 as INT));
assert!(module.contains_sub_module("life")); assert!(module.contains_sub_module("life"));
let m = module.get_sub_module("life").unwrap(); 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(); let mut engine = Engine::new();
engine.set_module_resolver(Some(resolver)); engine.set_module_resolver(Some(resolver));
let mut scope = Scope::new();
assert_eq!( assert_eq!(
engine.eval_with_scope::<INT>( engine.eval::<INT>(r#"import "question" as q; q::MYSTIC_NUMBER"#)?,
&mut scope,
r#"import "question" as q; q::life::universe::answer + 1"#
)?,
42 42
); );
assert_eq!( assert_eq!(
engine.eval_with_scope::<INT>( engine.eval::<INT>(r#"import "question" as q; q::life::universe::answer + 1"#)?,
&mut scope, 42
);
assert_eq!(
engine.eval::<INT>(
r#"import "question" as q; q::life::universe::inc(q::life::universe::answer)"# r#"import "question" as q; q::life::universe::inc(q::life::universe::answer)"#
)?, )?,
42 42
@ -221,36 +220,30 @@ fn test_module_from_ast() -> Result<(), Box<EvalAltResult>> {
resolver2.insert("testing", module); resolver2.insert("testing", module);
engine.set_module_resolver(Some(resolver2)); engine.set_module_resolver(Some(resolver2));
let mut scope = Scope::new();
assert_eq!( 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 123
); );
assert_eq!( 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 42
); );
assert!(engine assert!(engine.eval::<bool>(r#"import "testing" as ttt; ttt::extra::foo"#)?);
.eval_with_scope::<bool>(&mut scope, r#"import "testing" as ttt; ttt::extra::foo"#)?);
assert_eq!( 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!" "hello, 42 worlds!"
); );
assert_eq!( 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 1000
); );
assert_eq!( assert_eq!(
engine.eval_with_scope::<INT>( engine.eval::<INT>(r#"import "testing" as ttt; ttt::add_len(ttt::foo, ttt::hello)"#)?,
&mut scope,
r#"import "testing" as ttt; ttt::add_len(ttt::foo, ttt::hello)"#
)?,
59 59
); );
assert!(matches!( assert!(matches!(
*engine *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"), .expect_err("should error"),
EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "ttt::hidden" EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "ttt::hidden"
)); ));

View File

@ -1,16 +1,25 @@
#![cfg(feature = "internals")] #![cfg(feature = "internals")]
use rhai::{ 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] #[test]
fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> { fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.consume("while false {}")?;
// Disable 'while' and make sure it still works with custom syntax // Disable 'while' and make sure it still works with custom syntax
engine.disable_symbol("while"); engine.disable_symbol("while");
engine.consume("while false {}").expect_err("should error"); assert!(matches!(
engine.consume("let while = 0")?; *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 engine
.register_custom_syntax( .register_custom_syntax(

View File

@ -7,8 +7,11 @@ fn test_tokens_disabled() {
engine.disable_symbol("if"); // disable the 'if' keyword engine.disable_symbol("if"); // disable the 'if' keyword
assert!(matches!( assert!(matches!(
*engine.compile("let x = if true { 42 } else { 0 };").expect_err("should error").0, *engine
ParseErrorType::MissingToken(ref token, _) if token == ";" .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 engine.disable_symbol("+="); // disable the '+=' operator