Merge pull request #708 from schungx/master

Speed improvement attempts.
This commit is contained in:
Stephen Chung 2023-03-16 19:20:15 +08:00 committed by GitHub
commit 07d5886100
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 2109 additions and 1160 deletions

View File

@ -43,28 +43,28 @@ jobs:
os: [ubuntu-latest]
flags:
- ""
- "--features debugging"
- "--features metadata,serde,internals"
- "--features unchecked,serde,metadata,internals,debugging"
- "--features sync,serde,metadata,internals,debugging"
- "--features no_position,serde,metadata,internals,debugging"
- "--features no_optimize,serde,metadata,internals,debugging"
- "--features no_float,serde,metadata,internals,debugging"
- "--features f32_float,serde,metadata,internals,debugging"
- "--features decimal,serde,metadata,internals,debugging"
- "--features no_custom_syntax,serde,metadata,internals,debugging"
- "--features no_float,decimal"
- "--tests --features only_i32,serde,metadata,internals,debugging"
- "--features only_i64,serde,metadata,internals,debugging"
- "--features no_index,serde,metadata,internals,debugging"
- "--features no_object,serde,metadata,internals,debugging"
- "--features no_function,serde,metadata,internals,debugging"
- "--features no_module,serde,metadata,internals,debugging"
- "--features no_time,serde,metadata,internals,debugging"
- "--features no_closure,serde,metadata,internals,debugging"
- "--features unicode-xid-ident,serde,metadata,internals,debugging"
- "--features sync,no_time,no_function,no_float,no_position,no_optimize,no_module,no_closure,no_custom_syntax,metadata,serde,unchecked,debugging"
- "--features no_time,no_function,no_float,no_position,no_index,no_object,no_optimize,no_module,no_closure,no_custom_syntax,unchecked"
- "--features testing-environ,debugging"
- "--features testing-environ,metadata,serde,internals"
- "--features testing-environ,unchecked,serde,metadata,internals,debugging"
- "--features testing-environ,sync,serde,metadata,internals,debugging"
- "--features testing-environ,no_position,serde,metadata,internals,debugging"
- "--features testing-environ,no_optimize,serde,metadata,internals,debugging"
- "--features testing-environ,no_float,serde,metadata,internals,debugging"
- "--features testing-environ,f32_float,serde,metadata,internals,debugging"
- "--features testing-environ,decimal,serde,metadata,internals,debugging"
- "--features testing-environ,no_custom_syntax,serde,metadata,internals,debugging"
- "--features testing-environ,no_float,decimal"
- "--tests --features testing-environ,only_i32,serde,metadata,internals,debugging"
- "--features testing-environ,only_i64,serde,metadata,internals,debugging"
- "--features testing-environ,no_index,serde,metadata,internals,debugging"
- "--features testing-environ,no_object,serde,metadata,internals,debugging"
- "--features testing-environ,no_function,serde,metadata,internals,debugging"
- "--features testing-environ,no_module,serde,metadata,internals,debugging"
- "--features testing-environ,no_time,serde,metadata,internals,debugging"
- "--features testing-environ,no_closure,serde,metadata,internals,debugging"
- "--features testing-environ,unicode-xid-ident,serde,metadata,internals,debugging"
- "--features testing-environ,sync,no_time,no_function,no_float,no_position,no_optimize,no_module,no_closure,no_custom_syntax,metadata,serde,unchecked,debugging"
- "--features testing-environ,no_time,no_function,no_float,no_position,no_index,no_object,no_optimize,no_module,no_closure,no_custom_syntax,unchecked"
toolchain: [stable]
experimental: [false]
include:

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ benches/results
clippy.toml
Rhai.toml
**/*.bat
**/*.exe
doc/rhai-sync.json
doc/rhai.json
.idea/

View File

@ -12,11 +12,26 @@ Bug fixes
* Syntax such as `foo.bar::baz` no longer panics, but returns a proper parse error.
* `x += y` where `x` and `y` are `char` now works correctly.
* Expressions such as `!inside` now parses correctly instead of as `!in` followed by `side`.
* Custom syntax starting with symbols now works correctly and no longer raises a parse error.
* Comparing different custom types now works correctly when the appropriate comparison operators are registered.
* Op-assignments to bit flags or bit ranges now work correctly.
Potentially breaking changes
----------------------------
* The trait method `ModuleResolver::resolve_raw` (which is a low-level API) now takes a `&mut Scope` parameter. This is a breaking change because the signature is modified, but this trait method has a default and is rarely called/implemented in practice.
* `Module::eval_ast_as_new_raw` (a low-level API) now takes a `&mut Scope` instead of the `Scope` parameter. This is a breaking change because the `&mut` is now required.
* `Engine::allow_loop_expressions` now correctly defaults to `true` (was erroneously `false` by default).
Enhancements
------------
* `Engine::new_raw` is now `const` and runs very fast, delaying all other initialization until first use.
* The functions `min` and `max` are added for numbers.
* Range cases in `switch` statements now also match floating-point and decimal values. In order to support this, however, small numeric ranges cases are no longer unrolled.
* Loading a module via `import` now gives the module access to the current scope, including variables and constants defined inside.
* Some very simple operator calls (e.g. integer add) are short-circuited to avoid the overhead of a function call, resulting in a small speed improvement.
* The tokenizer now uses table-driven keyword recognizers generated by GNU gperf. At least _theoretically_ it should be faster...
Version 1.12.0

View File

@ -34,7 +34,7 @@ serde_json = { version = "1.0", default-features = false, features = ["alloc"],
unicode-xid = { version = "0.2", default-features = false, optional = true }
rust_decimal = { version = "1.16", default-features = false, features = ["maths"], optional = true }
getrandom = { version = "0.2", optional = true }
rustyline = { version = "10", optional = true }
rustyline = { version = "11", optional = true }
document-features = { version = "0.2", optional = true }
[dev-dependencies]
@ -117,6 +117,11 @@ std = ["ahash/std", "num-traits/std", "smartstring/std"]
## Use [`stdweb`](https://crates.io/crates/stdweb) as JavaScript interface.
stdweb = ["getrandom/js", "instant/stdweb"]
#! ### Features used in testing environments only
## Running under a testing environment.
testing-environ = []
[[bin]]
name = "rhai-repl"
required-features = ["rustyline"]
@ -151,4 +156,4 @@ features = ["document-features", "metadata", "serde", "internals", "decimal", "d
[patch.crates-io]
# Notice that a custom modified version of `rustyline` is used which supports bracketed paste on Windows.
# This can be moved to the official version when bracketed paste is added.
rustyline = { git = "https://github.com/schungx/rustyline", branch = "v10" }
rustyline = { git = "https://github.com/schungx/rustyline", branch = "v11" }

View File

@ -8,7 +8,7 @@ note: required by a bound in `rhai::Dynamic::cast`
--> $WORKSPACE/src/types/dynamic.rs
|
| pub fn cast<T: Any + Clone>(self) -> T {
| ^^^^^ required by this bound in `rhai::Dynamic::cast`
| ^^^^^ required by this bound in `Dynamic::cast`
help: consider annotating `NonClonable` with `#[derive(Clone)]`
|
3 | #[derive(Clone)]

View File

@ -8,7 +8,7 @@ note: required by a bound in `rhai::Dynamic::cast`
--> $WORKSPACE/src/types/dynamic.rs
|
| pub fn cast<T: Any + Clone>(self) -> T {
| ^^^^^ required by this bound in `rhai::Dynamic::cast`
| ^^^^^ required by this bound in `Dynamic::cast`
help: consider annotating `NonClonable` with `#[derive(Clone)]`
|
3 | #[derive(Clone)]

View File

@ -10,7 +10,7 @@ note: required by a bound in `rhai::Dynamic::from`
--> $WORKSPACE/src/types/dynamic.rs
|
| pub fn from<T: Variant + Clone>(value: T) -> Self {
| ^^^^^ required by this bound in `rhai::Dynamic::from`
| ^^^^^ required by this bound in `Dynamic::from`
= note: this error originates in the attribute macro `export_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `NonClonable` with `#[derive(Clone)]`
|

View File

@ -11,7 +11,7 @@ note: required by a bound in `rhai::Dynamic::from`
--> $WORKSPACE/src/types/dynamic.rs
|
| pub fn from<T: Variant + Clone>(value: T) -> Self {
| ^^^^^ required by this bound in `rhai::Dynamic::from`
| ^^^^^ required by this bound in `Dynamic::from`
= note: this error originates in the attribute macro `export_module` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `NonClonable` with `#[derive(Clone)]`
|

View File

@ -28,4 +28,5 @@ Sub-Directories
| `func` | Support for function calls |
| `eval` | Evaluation engine |
| `serde` | Support for [`serde`](https://crates.io/crates/serde) |
| `tools` | External tools needed for building |
| `bin` | Pre-built CLI binaries (e.g. `rhai-run`, `rhai-repl`) |

View File

@ -2,6 +2,7 @@
use crate::func::native::locked_write;
use crate::parser::{ParseResult, ParseState};
use crate::types::StringsInterner;
use crate::{Engine, OptimizationLevel, Scope, AST};
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
@ -126,7 +127,7 @@ impl Engine {
let path = path.clone();
match self
.module_resolver
.module_resolver()
.resolve_ast(self, None, &path, crate::Position::NONE)
{
Some(Ok(module_ast)) => collect_imports(&module_ast, &resolver, &mut imports),
@ -135,7 +136,7 @@ impl Engine {
}
let module =
self.module_resolver
self.module_resolver()
.resolve(self, None, &path, crate::Position::NONE)?;
let module = shared_take_or_clone(module);
@ -223,7 +224,17 @@ impl Engine {
optimization_level: OptimizationLevel,
) -> ParseResult<AST> {
let (stream, tc) = self.lex_raw(scripts.as_ref(), self.token_mapper.as_deref());
let interned_strings = &mut *locked_write(&self.interned_strings);
let mut interner;
let mut guard;
let interned_strings = if let Some(ref interner) = self.interned_strings {
guard = locked_write(interner);
&mut *guard
} else {
interner = StringsInterner::new();
&mut interner
};
let state = &mut ParseState::new(scope, interned_strings, tc);
let mut _ast = self.parse(stream.peekable(), state, optimization_level)?;
#[cfg(feature = "metadata")]
@ -294,7 +305,17 @@ impl Engine {
) -> ParseResult<AST> {
let scripts = [script];
let (stream, t) = self.lex_raw(&scripts, self.token_mapper.as_deref());
let interned_strings = &mut *locked_write(&self.interned_strings);
let mut interner;
let mut guard;
let interned_strings = if let Some(ref interner) = self.interned_strings {
guard = locked_write(interner);
&mut *guard
} else {
interner = StringsInterner::new();
&mut interner
};
let state = &mut ParseState::new(Some(scope), interned_strings, t);
self.parse_global_expr(stream.peekable(), state, |_| {}, self.optimization_level)
}

View File

@ -232,7 +232,7 @@ impl Engine {
}
let token = Token::lookup_symbol_from_syntax(s).or_else(|| {
if is_reserved_keyword_or_symbol(s) {
if is_reserved_keyword_or_symbol(s).0 {
Some(Token::Reserved(Box::new(s.into())))
} else {
None
@ -255,7 +255,8 @@ impl Engine {
// Markers not in first position
#[cfg(not(feature = "no_float"))]
CUSTOM_SYNTAX_MARKER_FLOAT if !segments.is_empty() => s.into(),
// Standard or reserved keyword/symbol not in first position
// Keyword/symbol not in first position
_ if !segments.is_empty() && token.is_some() => {
// Make it a custom keyword/symbol if it is disabled or reserved
if (self
@ -274,6 +275,7 @@ impl Engine {
}
s.into()
}
// Standard keyword in first position but not disabled
_ if segments.is_empty()
&& token.as_ref().map_or(false, Token::is_standard_keyword)
@ -291,8 +293,11 @@ impl Engine {
)
.into_err(Position::NONE));
}
// Identifier in first position
_ if segments.is_empty() && is_valid_identifier(s) => {
// Identifier or symbol in first position
_ if segments.is_empty()
&& (is_valid_identifier(s) || is_reserved_keyword_or_symbol(s).0) =>
{
// Make it a custom keyword/symbol if it is disabled or reserved
if self
.disabled_symbols
@ -310,6 +315,7 @@ impl Engine {
}
s.into()
}
// Anything else is an error
_ => {
return Err(LexError::ImproperSymbol(
@ -326,23 +332,20 @@ impl Engine {
segments.push(seg);
}
// If the syntax has no symbols, just ignore the registration
// If the syntax has nothing, just ignore the registration
if segments.is_empty() {
return Ok(self);
}
// The first keyword is the discriminator
// The first keyword/symbol is the discriminator
let key = segments[0].clone();
self.register_custom_syntax_with_state_raw(
key,
// Construct the parsing function
move |stream, _, _| {
if stream.len() >= segments.len() {
Ok(None)
} else {
Ok(Some(segments[stream.len()].clone()))
}
move |stream, _, _| match stream.len() {
len if len >= segments.len() => Ok(None),
len => Ok(Some(segments[len].clone())),
},
scope_may_be_changed,
move |context, expressions, _| func(context, expressions),
@ -399,7 +402,8 @@ impl Engine {
parse: Box::new(parse),
func: Box::new(func),
scope_may_be_changed,
},
}
.into(),
);
self
}

View File

@ -4,6 +4,7 @@ use crate::eval::{Caches, GlobalRuntimeState};
use crate::func::native::locked_write;
use crate::parser::ParseState;
use crate::types::dynamic::Variant;
use crate::types::StringsInterner;
use crate::{
Dynamic, Engine, OptimizationLevel, Position, RhaiResult, RhaiResultOf, Scope, AST, ERR,
};
@ -119,7 +120,15 @@ impl Engine {
) -> RhaiResultOf<T> {
let scripts = [script];
let ast = {
let interned_strings = &mut *locked_write(&self.interned_strings);
let mut interner;
let mut guard;
let interned_strings = if let Some(ref interner) = self.interned_strings {
guard = locked_write(interner);
&mut *guard
} else {
interner = StringsInterner::new();
&mut interner
};
let (stream, tc) = self.lex_raw(&scripts, self.token_mapper.as_deref());

View File

@ -286,7 +286,7 @@ impl Engine {
/// ```
#[inline(always)]
pub fn on_print(&mut self, callback: impl Fn(&str) + SendSync + 'static) -> &mut Self {
self.print = Box::new(callback);
self.print = Some(Box::new(callback));
self
}
/// Override default action of `debug` (print to stdout using [`println!`])
@ -336,7 +336,7 @@ impl Engine {
&mut self,
callback: impl Fn(&str, Option<&str>, Position) + SendSync + 'static,
) -> &mut Self {
self.debug = Box::new(callback);
self.debug = Some(Box::new(callback));
self
}
/// _(debugging)_ Register a callback for debugging.
@ -363,7 +363,7 @@ impl Engine {
+ SendSync
+ 'static,
) -> &mut Self {
self.debugger_interface = Some(Box::new((Box::new(init), Box::new(callback))));
self.debugger_interface = Some((Box::new(init), Box::new(callback)));
self
}
}

View File

@ -4,6 +4,7 @@
use crate::func::native::locked_write;
use crate::parser::{ParseSettingFlags, ParseState};
use crate::tokenizer::Token;
use crate::types::StringsInterner;
use crate::{Engine, LexError, Map, OptimizationLevel, RhaiResultOf};
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
@ -115,7 +116,16 @@ impl Engine {
);
let ast = {
let interned_strings = &mut *locked_write(&self.interned_strings);
let mut interner;
let mut guard;
let interned_strings = if let Some(ref interner) = self.interned_strings {
guard = locked_write(interner);
&mut *guard
} else {
interner = StringsInterner::new();
&mut interner
};
let state = &mut ParseState::new(None, interned_strings, tokenizer_control);
self.parse_global_expr(

View File

@ -54,7 +54,10 @@ impl Engine {
#[inline(always)]
#[must_use]
pub fn module_resolver(&self) -> &dyn crate::ModuleResolver {
&*self.module_resolver
static DUMMY_RESOLVER: crate::module::resolvers::DummyModuleResolver =
crate::module::resolvers::DummyModuleResolver;
self.module_resolver.as_deref().unwrap_or(&DUMMY_RESOLVER)
}
/// Set the module resolution service used by the [`Engine`].
@ -66,7 +69,7 @@ impl Engine {
&mut self,
resolver: impl crate::ModuleResolver + 'static,
) -> &mut Self {
self.module_resolver = Box::new(resolver);
self.module_resolver = Some(Box::new(resolver));
self
}

View File

@ -63,7 +63,11 @@ impl Engine {
);
#[cfg(feature = "metadata")]
_new_ast.set_doc(std::mem::take(ast.doc_mut()));
if let Some(doc) = ast.doc_mut() {
_new_ast.set_doc(std::mem::take(doc));
} else {
_new_ast.clear_doc();
}
_new_ast
}

View File

@ -38,23 +38,26 @@ impl LangOptions {
/// Create a new [`LangOptions`] with default values.
#[inline(always)]
#[must_use]
pub fn new() -> Self {
Self::IF_EXPR
| Self::SWITCH_EXPR
| Self::STMT_EXPR
| Self::LOOPING
| Self::SHADOWING
| Self::FAST_OPS
pub const fn new() -> Self {
Self::from_bits_truncate(
Self::IF_EXPR.bits()
| Self::SWITCH_EXPR.bits()
| Self::LOOP_EXPR.bits()
| Self::STMT_EXPR.bits()
| Self::LOOPING.bits()
| Self::SHADOWING.bits()
| Self::FAST_OPS.bits()
| {
#[cfg(not(feature = "no_function"))]
{
Self::ANON_FN
Self::ANON_FN.bits()
}
#[cfg(feature = "no_function")]
{
Self::empty()
}
Self::empty().bits()
}
},
)
}
}

View File

@ -1,6 +1,7 @@
//! Module that defines the public function/module registration API of [`Engine`].
use crate::func::{FnCallArgs, RegisterNativeFunction, SendSync};
use crate::module::ModuleFlags;
use crate::types::dynamic::Variant;
use crate::{
Engine, FnAccess, FnNamespace, Identifier, Module, NativeCallContext, RhaiResultOf, Shared,
@ -14,20 +15,18 @@ use std::prelude::v1::*;
use crate::func::register::Mut;
impl Engine {
/// Get the global namespace module (which is the fist module in `global_modules`).
#[inline(always)]
#[allow(dead_code)]
#[must_use]
pub(crate) fn global_namespace(&self) -> &Module {
self.global_modules.first().unwrap()
}
/// Get a mutable reference to the global namespace module
/// (which is the first module in `global_modules`).
#[inline(always)]
#[must_use]
pub(crate) fn global_namespace_mut(&mut self) -> &mut Module {
let module = self.global_modules.first_mut().unwrap();
Shared::get_mut(module).expect("not shared")
fn global_namespace_mut(&mut self) -> &mut Module {
if self.global_modules.is_empty() {
let mut global_namespace = Module::new();
global_namespace.flags |= ModuleFlags::INTERNAL;
self.global_modules.push(global_namespace.into());
}
Shared::get_mut(self.global_modules.first_mut().unwrap()).expect("not shared")
}
/// Register a custom function with the [`Engine`].
///
@ -677,6 +676,9 @@ impl Engine {
/// modules are searched in reverse order.
#[inline(always)]
pub fn register_global_module(&mut self, module: SharedModule) -> &mut Self {
// Make sure the global namespace is created.
let _ = self.global_namespace_mut();
// Insert the module into the front.
// The first module is always the global namespace.
self.global_modules.insert(1, module);
@ -729,7 +731,7 @@ impl Engine {
name: &str,
module: SharedModule,
) {
let separator = crate::tokenizer::Token::DoubleColon.literal_syntax();
let separator = crate::engine::NAMESPACE_SEPARATOR;
if name.contains(separator) {
let mut iter = name.splitn(2, separator);
@ -779,7 +781,9 @@ impl Engine {
pub fn gen_fn_signatures(&self, include_packages: bool) -> Vec<String> {
let mut signatures = Vec::with_capacity(64);
signatures.extend(self.global_namespace().gen_fn_signatures());
if let Some(global_namespace) = self.global_modules.first() {
signatures.extend(global_namespace.gen_fn_signatures());
}
#[cfg(not(feature = "no_module"))]
for (name, m) in self.global_sub_modules.as_deref().into_iter().flatten() {

View File

@ -3,6 +3,7 @@
use crate::eval::{Caches, GlobalRuntimeState};
use crate::func::native::locked_write;
use crate::parser::ParseState;
use crate::types::StringsInterner;
use crate::{Engine, RhaiResultOf, Scope, AST};
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
@ -59,7 +60,17 @@ impl Engine {
let scripts = [script];
let ast = {
let (stream, tc) = self.lex_raw(&scripts, self.token_mapper.as_deref());
let interned_strings = &mut *locked_write(&self.interned_strings);
let mut interner;
let mut guard;
let interned_strings = if let Some(ref interner) = self.interned_strings {
guard = locked_write(interner);
&mut *guard
} else {
interner = StringsInterner::new();
&mut interner
};
let state = &mut ParseState::new(Some(scope), interned_strings, tc);
self.parse(stream.peekable(), state, self.optimization_level)?
};

View File

@ -23,9 +23,9 @@ pub struct AST {
source: Option<ImmutableString>,
/// [`AST`] documentation.
#[cfg(feature = "metadata")]
doc: crate::SmartString,
doc: Option<Box<crate::SmartString>>,
/// Global statements.
body: StmtBlock,
body: Option<Box<StmtBlock>>,
/// Script-defined functions.
#[cfg(not(feature = "no_function"))]
lib: crate::SharedModule,
@ -54,7 +54,14 @@ impl fmt::Debug for AST {
#[cfg(not(feature = "no_module"))]
fp.field("resolver", &self.resolver);
fp.field("body", &self.body.as_slice());
fp.field(
"body",
&self
.body
.as_deref()
.map(|b| b.as_slice())
.unwrap_or_default(),
);
#[cfg(not(feature = "no_function"))]
for (.., fn_def) in self.lib.iter_script_fn() {
@ -75,11 +82,13 @@ impl AST {
statements: impl IntoIterator<Item = Stmt>,
#[cfg(not(feature = "no_function"))] functions: impl Into<crate::SharedModule>,
) -> Self {
let stmt = StmtBlock::new(statements, Position::NONE, Position::NONE);
Self {
source: None,
#[cfg(feature = "metadata")]
doc: crate::SmartString::new_const(),
body: StmtBlock::new(statements, Position::NONE, Position::NONE),
doc: None,
body: (!stmt.is_empty()).then(|| stmt.into()),
#[cfg(not(feature = "no_function"))]
lib: functions.into(),
#[cfg(not(feature = "no_module"))]
@ -95,11 +104,13 @@ impl AST {
statements: impl IntoIterator<Item = Stmt>,
#[cfg(not(feature = "no_function"))] functions: impl Into<crate::SharedModule>,
) -> Self {
let stmt = StmtBlock::new(statements, Position::NONE, Position::NONE);
Self {
source: None,
#[cfg(feature = "metadata")]
doc: crate::SmartString::new_const(),
body: StmtBlock::new(statements, Position::NONE, Position::NONE),
doc: None,
body: (!stmt.is_empty()).then(|| stmt.into()),
#[cfg(not(feature = "no_function"))]
lib: functions.into(),
#[cfg(not(feature = "no_module"))]
@ -148,8 +159,8 @@ impl AST {
Self {
source: None,
#[cfg(feature = "metadata")]
doc: crate::SmartString::new_const(),
body: StmtBlock::NONE,
doc: None,
body: None,
#[cfg(not(feature = "no_function"))]
lib: crate::Module::new().into(),
#[cfg(not(feature = "no_module"))]
@ -178,11 +189,7 @@ impl AST {
.as_mut()
.map(|m| m.set_id(source.clone()));
if source.is_empty() {
self.source = None;
} else {
self.source = Some(source);
}
self.source = (!source.is_empty()).then(|| source);
self
}
@ -202,14 +209,14 @@ impl AST {
#[inline(always)]
#[must_use]
pub fn doc(&self) -> &str {
&self.doc
self.doc.as_ref().map(|s| s.as_str()).unwrap_or_default()
}
/// Clear the documentation.
/// Exported under the `metadata` feature only.
#[cfg(feature = "metadata")]
#[inline(always)]
pub fn clear_doc(&mut self) -> &mut Self {
self.doc.clear();
self.doc = None;
self
}
/// Get a mutable reference to the documentation.
@ -219,8 +226,8 @@ impl AST {
#[inline(always)]
#[must_use]
#[allow(dead_code)]
pub(crate) fn doc_mut(&mut self) -> &mut crate::SmartString {
&mut self.doc
pub(crate) fn doc_mut(&mut self) -> Option<&mut crate::SmartString> {
self.doc.as_deref_mut()
}
/// Set the documentation.
///
@ -228,14 +235,18 @@ impl AST {
#[cfg(feature = "metadata")]
#[inline(always)]
pub(crate) fn set_doc(&mut self, doc: impl Into<crate::SmartString>) {
self.doc = doc.into();
let doc = doc.into();
self.doc = (!doc.is_empty()).then(|| doc.into());
}
/// Get the statements.
#[cfg(not(feature = "internals"))]
#[inline(always)]
#[must_use]
pub(crate) fn statements(&self) -> &[Stmt] {
self.body.statements()
self.body
.as_deref()
.map(StmtBlock::statements)
.unwrap_or_default()
}
/// _(internals)_ Get the statements.
/// Exported under the `internals` feature only.
@ -243,14 +254,20 @@ impl AST {
#[inline(always)]
#[must_use]
pub fn statements(&self) -> &[Stmt] {
self.body.statements()
self.body
.as_deref()
.map(StmtBlock::statements)
.unwrap_or_default()
}
/// Extract the statements.
#[allow(dead_code)]
#[inline(always)]
#[must_use]
pub(crate) fn take_statements(&mut self) -> StmtBlockContainer {
self.body.take_statements()
self.body
.as_deref_mut()
.map(StmtBlock::take_statements)
.unwrap_or_default()
}
/// Does this [`AST`] contain script-defined functions?
///
@ -344,7 +361,7 @@ impl AST {
source: self.source.clone(),
#[cfg(feature = "metadata")]
doc: self.doc.clone(),
body: StmtBlock::NONE,
body: None,
lib: lib.into(),
#[cfg(not(feature = "no_module"))]
resolver: self.resolver.clone(),
@ -542,15 +559,15 @@ impl AST {
other: &Self,
_filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool,
) -> Self {
let merged = match (self.body.is_empty(), other.body.is_empty()) {
(false, false) => {
let mut body = self.body.clone();
body.extend(other.body.iter().cloned());
let merged = match (&self.body, &other.body) {
(Some(body), Some(other)) => {
let mut body = body.as_ref().clone();
body.extend(other.iter().cloned());
body
}
(false, true) => self.body.clone(),
(true, false) => other.body.clone(),
(true, true) => StmtBlock::NONE,
(Some(body), None) => body.as_ref().clone(),
(None, Some(body)) => body.as_ref().clone(),
(None, None) => StmtBlock::NONE,
};
#[cfg(not(feature = "no_function"))]
@ -598,11 +615,13 @@ impl AST {
}
#[cfg(feature = "metadata")]
if !other.doc.is_empty() {
if !_ast.doc.is_empty() {
_ast.doc.push('\n');
if let Some(ref other_doc) = other.doc {
if let Some(ref mut ast_doc) = _ast.doc {
ast_doc.push('\n');
ast_doc.push_str(other_doc);
} else {
_ast.doc = Some(other_doc.clone());
}
_ast.doc.push_str(other.doc());
}
_ast
@ -690,7 +709,12 @@ impl AST {
}
}
self.body.extend(other.body.into_iter());
match (&mut self.body, other.body) {
(Some(body), Some(other)) => body.extend(other.into_iter()),
(Some(_), None) => (),
(None, body @ Some(_)) => self.body = body,
(None, None) => (),
}
#[cfg(not(feature = "no_function"))]
if !other.lib.is_empty() {
@ -698,11 +722,13 @@ impl AST {
}
#[cfg(feature = "metadata")]
if !other.doc.is_empty() {
if !self.doc.is_empty() {
self.doc.push('\n');
if let Some(other_doc) = other.doc {
if let Some(ref mut self_doc) = self.doc {
self_doc.push('\n');
self_doc.push_str(&other_doc);
} else {
self.doc = Some(other_doc);
}
self.doc.push_str(&other.doc);
}
self
@ -785,7 +811,7 @@ impl AST {
/// Clear all statements in the [`AST`], leaving only function definitions.
#[inline(always)]
pub fn clear_statements(&mut self) -> &mut Self {
self.body = StmtBlock::NONE;
self.body = None;
self
}
/// Extract all top-level literal constant and/or variable definitions.

View File

@ -185,7 +185,7 @@ impl FnCallHashes {
#[inline(always)]
#[must_use]
pub fn script(&self) -> u64 {
assert!(self.script.is_some());
debug_assert!(self.script.is_some());
self.script.unwrap()
}
}
@ -248,11 +248,7 @@ impl FnCallExpr {
#[inline]
#[must_use]
pub fn constant_args(&self) -> bool {
if self.args.is_empty() {
true
} else {
self.args.iter().all(Expr::is_constant)
}
self.args.is_empty() || self.args.iter().all(Expr::is_constant)
}
}
@ -383,7 +379,7 @@ impl fmt::Debug for Expr {
#[cfg(not(feature = "no_module"))]
if !x.1.is_empty() {
write!(f, "{}{}", x.1, Token::DoubleColon.literal_syntax())?;
write!(f, "{}{}", x.1, crate::engine::NAMESPACE_SEPARATOR)?;
let pos = x.1.position();
if !pos.is_none() {
display_pos = pos;

View File

@ -2,7 +2,6 @@
#![cfg(not(feature = "no_module"))]
use crate::ast::Ident;
use crate::tokenizer::Token;
use crate::{Position, StaticVec};
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
@ -46,7 +45,7 @@ impl fmt::Debug for Namespace {
.iter()
.map(Ident::as_str)
.collect::<StaticVec<_>>()
.join(Token::DoubleColon.literal_syntax()),
.join(crate::engine::NAMESPACE_SEPARATOR),
)
}
}
@ -63,7 +62,7 @@ impl fmt::Display for Namespace {
.iter()
.map(Ident::as_str)
.collect::<StaticVec<_>>()
.join(Token::DoubleColon.literal_syntax()),
.join(crate::engine::NAMESPACE_SEPARATOR),
)
}
}

View File

@ -34,7 +34,7 @@ pub struct ScriptFnDef {
///
/// Each line in non-block doc-comments starts with `///`.
#[cfg(feature = "metadata")]
pub comments: Box<[crate::Identifier]>,
pub comments: Box<[crate::SmartString]>,
}
impl fmt::Display for ScriptFnDef {

View File

@ -1,17 +1,18 @@
//! Module defining script statements.
use super::{ASTFlags, ASTNode, BinaryExpr, Expr, FnCallExpr, Ident};
use crate::engine::KEYWORD_EVAL;
use crate::engine::{KEYWORD_EVAL, OP_EQUALS};
use crate::func::StraightHashMap;
use crate::tokenizer::Token;
use crate::types::dynamic::Union;
use crate::types::Span;
use crate::{calc_fn_hash, Position, StaticVec, INT};
use crate::{calc_fn_hash, Dynamic, Position, StaticVec, INT};
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
use std::{
borrow::Borrow,
collections::BTreeMap,
fmt,
hash::Hash,
hash::{Hash, Hasher},
mem,
num::NonZeroUsize,
ops::{Deref, DerefMut, Range, RangeInclusive},
@ -29,8 +30,12 @@ pub struct OpAssignment {
hash_op: u64,
/// Op-assignment operator.
op_assign: Token,
/// Syntax of op-assignment operator.
op_assign_syntax: &'static str,
/// Underlying operator.
op: Token,
/// Syntax of underlying operator.
op_syntax: &'static str,
/// [Position] of the op-assignment operator.
pos: Position,
}
@ -44,7 +49,9 @@ impl OpAssignment {
hash_op_assign: 0,
hash_op: 0,
op_assign: Token::Equals,
op_assign_syntax: OP_EQUALS,
op: Token::Equals,
op_syntax: OP_EQUALS,
pos,
}
}
@ -56,17 +63,28 @@ impl OpAssignment {
}
/// Get information if this [`OpAssignment`] is an op-assignment.
///
/// Returns `( hash_op_assign, hash_op, op_assign, op )`:
/// Returns `( hash_op_assign, hash_op, op_assign, op_assign_syntax, op, op_syntax )`:
///
/// * `hash_op_assign`: Hash of the op-assignment call.
/// * `hash_op`: Hash of the underlying operator call (for fallback).
/// * `op_assign`: Op-assignment operator.
/// * `op_assign_syntax`: Syntax of op-assignment operator.
/// * `op`: Underlying operator.
/// * `op_syntax`: Syntax of underlying operator.
#[must_use]
#[inline]
pub fn get_op_assignment_info(&self) -> Option<(u64, u64, &Token, &Token)> {
pub fn get_op_assignment_info(
&self,
) -> Option<(u64, u64, &Token, &'static str, &Token, &'static str)> {
if self.is_op_assignment() {
Some((self.hash_op_assign, self.hash_op, &self.op_assign, &self.op))
Some((
self.hash_op_assign,
self.hash_op,
&self.op_assign,
self.op_assign_syntax,
&self.op,
self.op_syntax,
))
} else {
None
}
@ -99,11 +117,16 @@ impl OpAssignment {
.get_base_op_from_assignment()
.expect("op-assignment operator");
let op_assign_syntax = op_assign.literal_syntax();
let op_syntax = op.literal_syntax();
Self {
hash_op_assign: calc_fn_hash(None, op_assign.literal_syntax(), 2),
hash_op: calc_fn_hash(None, op.literal_syntax(), 2),
hash_op_assign: calc_fn_hash(None, op_assign_syntax, 2),
hash_op: calc_fn_hash(None, op_syntax, 2),
op_assign,
op_assign_syntax,
op,
op_syntax,
pos,
}
}
@ -139,7 +162,9 @@ impl fmt::Debug for OpAssignment {
.field("hash_op_assign", &self.hash_op_assign)
.field("hash_op", &self.hash_op)
.field("op_assign", &self.op_assign)
.field("op_assign_syntax", &self.op_assign_syntax)
.field("op", &self.op)
.field("op_syntax", &self.op_syntax)
.field("pos", &self.pos)
.finish()
} else {
@ -233,7 +258,7 @@ impl IntoIterator for RangeCase {
type Item = INT;
type IntoIter = Box<dyn Iterator<Item = Self::Item>>;
#[inline(always)]
#[inline]
#[must_use]
fn into_iter(self) -> Self::IntoIter {
match self {
@ -245,7 +270,7 @@ impl IntoIterator for RangeCase {
impl RangeCase {
/// Returns `true` if the range contains no items.
#[inline(always)]
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
match self {
@ -254,7 +279,7 @@ impl RangeCase {
}
}
/// Size of the range.
#[inline(always)]
#[inline]
#[must_use]
pub fn len(&self) -> INT {
match self {
@ -264,15 +289,56 @@ impl RangeCase {
Self::InclusiveInt(r, ..) => *r.end() - *r.start() + 1,
}
}
/// Is the specified number within this range?
#[inline(always)]
/// Is the specified value within this range?
#[inline]
#[must_use]
pub fn contains(&self, n: INT) -> bool {
pub fn contains(&self, value: &Dynamic) -> bool {
match value {
Dynamic(Union::Int(v, ..)) => self.contains_int(*v),
#[cfg(not(feature = "no_float"))]
Dynamic(Union::Float(v, ..)) => self.contains_float(**v),
#[cfg(feature = "decimal")]
Dynamic(Union::Decimal(v, ..)) => self.contains_decimal(**v),
_ => false,
}
}
/// Is the specified number within this range?
#[inline]
#[must_use]
pub fn contains_int(&self, n: INT) -> bool {
match self {
Self::ExclusiveInt(r, ..) => r.contains(&n),
Self::InclusiveInt(r, ..) => r.contains(&n),
}
}
/// Is the specified floating-point number within this range?
#[cfg(not(feature = "no_float"))]
#[inline]
#[must_use]
pub fn contains_float(&self, n: crate::FLOAT) -> bool {
use crate::FLOAT;
match self {
Self::ExclusiveInt(r, ..) => ((r.start as FLOAT)..(r.end as FLOAT)).contains(&n),
Self::InclusiveInt(r, ..) => ((*r.start() as FLOAT)..=(*r.end() as FLOAT)).contains(&n),
}
}
/// Is the specified decimal number within this range?
#[cfg(feature = "decimal")]
#[inline]
#[must_use]
pub fn contains_decimal(&self, n: rust_decimal::Decimal) -> bool {
use rust_decimal::Decimal;
match self {
Self::ExclusiveInt(r, ..) => {
(Into::<Decimal>::into(r.start)..Into::<Decimal>::into(r.end)).contains(&n)
}
Self::InclusiveInt(r, ..) => {
(Into::<Decimal>::into(*r.start())..=Into::<Decimal>::into(*r.end())).contains(&n)
}
}
}
/// Is the specified range inclusive?
#[inline(always)]
#[must_use]
@ -303,18 +369,31 @@ pub type CaseBlocksList = smallvec::SmallVec<[usize; 1]>;
/// _(internals)_ A type containing all cases for a `switch` statement.
/// Exported under the `internals` feature only.
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone)]
pub struct SwitchCasesCollection {
/// List of [`ConditionalExpr`]'s.
pub expressions: StaticVec<ConditionalExpr>,
/// Dictionary mapping value hashes to [`ConditionalExpr`]'s.
pub cases: BTreeMap<u64, CaseBlocksList>,
pub cases: StraightHashMap<CaseBlocksList>,
/// List of range cases.
pub ranges: StaticVec<RangeCase>,
/// Statements block for the default case (there can be no condition for the default case).
pub def_case: Option<usize>,
}
impl Hash for SwitchCasesCollection {
#[inline(always)]
fn hash<H: Hasher>(&self, state: &mut H) {
self.expressions.hash(state);
self.cases.len().hash(state);
self.cases.iter().for_each(|kv| kv.hash(state));
self.ranges.hash(state);
self.def_case.hash(state);
}
}
/// Number of items to keep inline for [`StmtBlockContainer`].
#[cfg(not(feature = "no_std"))]
const STMT_BLOCK_INLINE_SIZE: usize = 8;

View File

@ -2,7 +2,8 @@ use rhai::plugin::*;
use rhai::{Dynamic, Engine, EvalAltResult, Module, Scope, AST, INT};
use rustyline::config::Builder;
use rustyline::error::ReadlineError;
use rustyline::{Cmd, Editor, Event, EventHandler, KeyCode, KeyEvent, Modifiers, Movement};
use rustyline::history::{History, SearchDirection};
use rustyline::{Cmd, DefaultEditor, Event, EventHandler, KeyCode, KeyEvent, Modifiers, Movement};
use std::{env, fs::File, io::Read, path::Path, process::exit};
@ -188,14 +189,14 @@ fn load_script_files(engine: &mut Engine) {
}
// Setup the Rustyline editor.
fn setup_editor() -> Editor<()> {
fn setup_editor() -> DefaultEditor {
//env_logger::init();
let config = Builder::new()
.tab_stop(4)
.indent_size(4)
.bracketed_paste(true)
.build();
let mut rl = Editor::<()>::with_config(config).unwrap();
let mut rl = DefaultEditor::with_config(config).unwrap();
// Bind more keys
@ -336,7 +337,10 @@ fn main() {
'main_loop: loop {
if let Some(replace) = replacement.take() {
input = replace;
if rl.add_history_entry(input.clone()) {
if rl
.add_history_entry(input.clone())
.expect("Failed to add history entry")
{
history_offset += 1;
}
if input.contains('\n') {
@ -366,7 +370,9 @@ fn main() {
if !cmd.is_empty()
&& !cmd.starts_with('!')
&& cmd.trim() != "history"
&& rl.add_history_entry(input.clone())
&& rl
.add_history_entry(input.clone())
.expect("Failed to add history entry")
{
history_offset += 1;
}
@ -476,7 +482,7 @@ fn main() {
let json = engine
.gen_fn_metadata_with_ast_to_json(&main_ast, false)
.unwrap();
.expect("Unable to generate JSON");
let mut f = std::fs::File::create("metadata.json")
.expect("Unable to create `metadata.json`");
f.write_all(json.as_bytes()).expect("Unable to write data");
@ -484,7 +490,7 @@ fn main() {
continue;
}
"!!" => {
match rl.history().last() {
match rl.history().iter().last() {
Some(line) => {
replacement = Some(line.clone());
replacement_index = history_offset + rl.history().len() - 1;
@ -514,8 +520,12 @@ fn main() {
_ if cmd.starts_with('!') => {
if let Ok(num) = cmd[1..].parse::<usize>() {
if num >= history_offset {
if let Some(line) = rl.history().get(num - history_offset) {
replacement = Some(line.clone());
if let Some(line) = rl
.history()
.get(num - history_offset, SearchDirection::Forward)
.expect("Failed to get history entry")
{
replacement = Some(line.entry.into());
replacement_index = num;
continue;
}
@ -578,7 +588,8 @@ fn main() {
main_ast.clear_statements();
}
rl.save_history(HISTORY_FILE).unwrap();
rl.save_history(HISTORY_FILE)
.expect("Failed to save history");
println!("Bye!");
}

View File

@ -5,13 +5,11 @@ use crate::func::native::{
locked_write, OnDebugCallback, OnDefVarCallback, OnParseTokenCallback, OnPrintCallback,
OnVarCallback,
};
use crate::module::ModuleFlags;
use crate::packages::{Package, StandardPackage};
use crate::tokenizer::Token;
use crate::types::StringsInterner;
use crate::{
Dynamic, Identifier, ImmutableString, Locked, Module, OptimizationLevel, SharedModule,
StaticVec,
Dynamic, Identifier, ImmutableString, Locked, OptimizationLevel, SharedModule, StaticVec,
};
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
@ -57,12 +55,19 @@ pub const OP_EQUALS: &str = Token::EqualsTo.literal_syntax();
/// The `in` operator is implemented as a call to this function.
pub const OP_CONTAINS: &str = "contains";
/// Standard not operator.
pub const OP_NOT: &str = Token::Bang.literal_syntax();
/// Standard exclusive range operator.
pub const OP_EXCLUSIVE_RANGE: &str = Token::ExclusiveRange.literal_syntax();
/// Standard inclusive range operator.
pub const OP_INCLUSIVE_RANGE: &str = Token::InclusiveRange.literal_syntax();
/// Separator for namespaces.
#[cfg(not(feature = "no_module"))]
pub const NAMESPACE_SEPARATOR: &str = Token::DoubleColon.literal_syntax();
/// Rhai main scripting engine.
///
/// # Thread Safety
@ -96,10 +101,10 @@ pub struct Engine {
/// A module resolution service.
#[cfg(not(feature = "no_module"))]
pub(crate) module_resolver: Box<dyn crate::ModuleResolver>,
pub(crate) module_resolver: Option<Box<dyn crate::ModuleResolver>>,
/// Strings interner.
pub(crate) interned_strings: Locked<StringsInterner>,
pub(crate) interned_strings: Option<Box<Locked<StringsInterner>>>,
/// A set of symbols to disable.
pub(crate) disabled_symbols: Option<Box<BTreeSet<Identifier>>>,
@ -110,7 +115,7 @@ pub struct Engine {
/// Custom syntax.
#[cfg(not(feature = "no_custom_syntax"))]
pub(crate) custom_syntax: Option<
Box<std::collections::BTreeMap<Identifier, crate::api::custom_syntax::CustomSyntax>>,
Box<std::collections::BTreeMap<Identifier, Box<crate::api::custom_syntax::CustomSyntax>>>,
>,
/// Callback closure for filtering variable definition.
pub(crate) def_var_filter: Option<Box<OnDefVarCallback>>,
@ -120,9 +125,9 @@ pub struct Engine {
pub(crate) token_mapper: Option<Box<OnParseTokenCallback>>,
/// Callback closure for implementing the `print` command.
pub(crate) print: Box<OnPrintCallback>,
pub(crate) print: Option<Box<OnPrintCallback>>,
/// Callback closure for implementing the `debug` command.
pub(crate) debug: Box<OnDebugCallback>,
pub(crate) debug: Option<Box<OnDebugCallback>>,
/// Callback closure for progress reporting.
#[cfg(not(feature = "unchecked"))]
pub(crate) progress: Option<Box<crate::func::native::OnProgressCallback>>,
@ -142,12 +147,10 @@ pub struct Engine {
/// Callback closure for debugging.
#[cfg(feature = "debugging")]
pub(crate) debugger_interface: Option<
Box<(
pub(crate) debugger_interface: Option<(
Box<crate::eval::OnDebuggingInit>,
Box<crate::eval::OnDebuggerCallback>,
)>,
>,
}
impl fmt::Debug for Engine {
@ -224,56 +227,17 @@ pub fn make_setter(id: &str) -> Identifier {
}
impl Engine {
/// Create a new [`Engine`].
#[inline]
#[must_use]
pub fn new() -> Self {
// Create the new scripting Engine
let mut engine = Self::new_raw();
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_family = "wasm"))]
{
engine.module_resolver = Box::new(crate::module::resolvers::FileModuleResolver::new());
}
// default print/debug implementations
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_family = "wasm"))]
{
engine.print = Box::new(|s| println!("{s}"));
engine.debug = Box::new(|s, source, pos| match (source, pos) {
(Some(source), crate::Position::NONE) => println!("{source} | {s}"),
#[cfg(not(feature = "no_position"))]
(Some(source), pos) => println!("{source} @ {pos:?} | {s}"),
(None, crate::Position::NONE) => println!("{s}"),
#[cfg(not(feature = "no_position"))]
(None, pos) => println!("{pos:?} | {s}"),
});
}
engine.register_global_module(StandardPackage::new().as_shared_module());
engine
}
/// Create a new [`Engine`] with minimal built-in functions.
///
/// Use [`register_global_module`][Engine::register_global_module] to add packages of functions.
#[inline]
#[must_use]
pub fn new_raw() -> Self {
let mut engine = Self {
/// An empty raw [`Engine`].
pub const RAW: Self = Self {
global_modules: StaticVec::new_const(),
#[cfg(not(feature = "no_module"))]
global_sub_modules: None,
#[cfg(not(feature = "no_module"))]
module_resolver: Box::new(crate::module::resolvers::DummyModuleResolver::new()),
module_resolver: None,
interned_strings: StringsInterner::new().into(),
interned_strings: None,
disabled_symbols: None,
#[cfg(not(feature = "no_custom_syntax"))]
custom_keywords: None,
@ -284,8 +248,8 @@ impl Engine {
resolve_var: None,
token_mapper: None,
print: Box::new(|_| {}),
debug: Box::new(|_, _, _| {}),
print: None,
debug: None,
#[cfg(not(feature = "unchecked"))]
progress: None,
@ -306,14 +270,55 @@ impl Engine {
debugger_interface: None,
};
// Add the global namespace module
let mut global_namespace = Module::new();
global_namespace.flags |= ModuleFlags::INTERNAL;
engine.global_modules.push(global_namespace.into());
/// Create a new [`Engine`].
#[inline]
#[must_use]
pub fn new() -> Self {
// Create the new scripting Engine
let mut engine = Self::new_raw();
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_family = "wasm"))]
{
engine.module_resolver =
Some(Box::new(crate::module::resolvers::FileModuleResolver::new()));
}
engine.interned_strings = Some(Locked::new(StringsInterner::new()).into());
// default print/debug implementations
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_family = "wasm"))]
{
engine.print = Some(Box::new(|s| println!("{s}")));
engine.debug = Some(Box::new(|s, source, pos| match (source, pos) {
(Some(source), crate::Position::NONE) => println!("{source} | {s}"),
#[cfg(not(feature = "no_position"))]
(Some(source), pos) => println!("{source} @ {pos:?} | {s}"),
(None, crate::Position::NONE) => println!("{s}"),
#[cfg(not(feature = "no_position"))]
(None, pos) => println!("{pos:?} | {s}"),
}));
}
engine.register_global_module(StandardPackage::new().as_shared_module());
engine
}
/// Create a new [`Engine`] with minimal built-in functions.
/// It returns a copy of [`Engine::RAW`].
///
/// This is useful for creating a custom scripting engine with only the functions you need.
///
/// Use [`register_global_module`][Engine::register_global_module] to add packages of functions.
#[inline]
#[must_use]
pub const fn new_raw() -> Self {
Self::RAW
}
/// Get an interned [string][ImmutableString].
#[cfg(not(feature = "internals"))]
#[inline(always)]
@ -322,7 +327,11 @@ impl Engine {
&self,
string: impl AsRef<str> + Into<ImmutableString>,
) -> ImmutableString {
locked_write(&self.interned_strings).get(string)
if let Some(ref interner) = self.interned_strings {
locked_write(interner).get(string)
} else {
string.into()
}
}
/// _(internals)_ Get an interned [string][ImmutableString].
@ -331,13 +340,17 @@ impl Engine {
/// [`Engine`] keeps a cache of [`ImmutableString`] instances and tries to avoid new allocations
/// when an existing instance is found.
#[cfg(feature = "internals")]
#[inline(always)]
#[inline]
#[must_use]
pub fn get_interned_string(
&self,
string: impl AsRef<str> + Into<ImmutableString>,
) -> ImmutableString {
locked_write(&self.interned_strings).get(string)
if let Some(ref interner) = self.interned_strings {
locked_write(interner).get(string)
} else {
string.into()
}
}
/// Get an empty [`ImmutableString`] which refers to a shared instance.
@ -354,4 +367,17 @@ impl Engine {
pub(crate) const fn is_debugger_registered(&self) -> bool {
self.debugger_interface.is_some()
}
/// Imitation of std::hints::black_box which requires nightly.
#[cfg(not(target_family = "wasm"))]
#[inline(never)]
pub(crate) fn black_box() -> usize {
unsafe { core::ptr::read_volatile(&0_usize as *const usize) }
}
/// Imitation of std::hints::black_box which requires nightly.
#[cfg(target_family = "wasm")]
#[inline(always)]
pub(crate) fn black_box() -> usize {
0
}
}

View File

@ -63,8 +63,8 @@ impl Caches {
#[inline]
#[must_use]
pub fn fn_resolution_cache_mut(&mut self) -> &mut FnResolutionCache {
if self.0.is_empty() {
// Push a new function resolution cache if the stack is empty
if self.0.is_empty() {
self.push_fn_resolution_cache();
}
self.0.last_mut().unwrap()

View File

@ -504,10 +504,8 @@ impl Engine {
global, caches, scope, this_ptr, expr, rhs, idx_values,
)?;
if !_arg_values.is_empty() {
idx_values.extend(_arg_values);
}
}
#[cfg(not(feature = "no_object"))]
_ if chain_type == ChainType::Dotting => {

View File

@ -8,17 +8,16 @@ use std::borrow::Borrow;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
impl Dynamic {
/// Recursively calculate the sizes of an array.
///
/// Sizes returned are `(` [`Array`][crate::Array], [`Map`][crate::Map] and [`String`] `)`.
///
/// # Panics
///
/// Panics if any interior data is shared (should never happen).
#[cfg(not(feature = "no_index"))]
#[inline]
pub(crate) fn calc_array_sizes(array: &crate::Array) -> (usize, usize, usize) {
/// Recursively calculate the sizes of an array.
///
/// Sizes returned are `(` [`Array`][crate::Array], [`Map`][crate::Map] and [`String`] `)`.
///
/// # Panics
///
/// Panics if any interior data is shared (should never happen).
#[cfg(not(feature = "no_index"))]
#[inline]
pub fn calc_array_sizes(array: &crate::Array) -> (usize, usize, usize) {
let (mut ax, mut mx, mut sx) = (0, 0, 0);
for value in array {
@ -26,7 +25,7 @@ impl Dynamic {
match value.0 {
Union::Array(ref a, ..) => {
let (a, m, s) = Self::calc_array_sizes(a);
let (a, m, s) = calc_array_sizes(a);
ax += a;
mx += m;
sx += s;
@ -34,7 +33,7 @@ impl Dynamic {
Union::Blob(ref a, ..) => ax += 1 + a.len(),
#[cfg(not(feature = "no_object"))]
Union::Map(ref m, ..) => {
let (a, m, s) = Self::calc_map_sizes(m);
let (a, m, s) = calc_map_sizes(m);
ax += a;
mx += m;
sx += s;
@ -49,17 +48,17 @@ impl Dynamic {
}
(ax, mx, sx)
}
/// Recursively calculate the sizes of a map.
///
/// Sizes returned are `(` [`Array`][crate::Array], [`Map`][crate::Map] and [`String`] `)`.
///
/// # Panics
///
/// Panics if any interior data is shared (should never happen).
#[cfg(not(feature = "no_object"))]
#[inline]
pub(crate) fn calc_map_sizes(map: &crate::Map) -> (usize, usize, usize) {
}
/// Recursively calculate the sizes of a map.
///
/// Sizes returned are `(` [`Array`][crate::Array], [`Map`][crate::Map] and [`String`] `)`.
///
/// # Panics
///
/// Panics if any interior data is shared (should never happen).
#[cfg(not(feature = "no_object"))]
#[inline]
pub fn calc_map_sizes(map: &crate::Map) -> (usize, usize, usize) {
let (mut ax, mut mx, mut sx) = (0, 0, 0);
for value in map.values() {
@ -68,7 +67,7 @@ impl Dynamic {
match value.0 {
#[cfg(not(feature = "no_index"))]
Union::Array(ref a, ..) => {
let (a, m, s) = Self::calc_array_sizes(a);
let (a, m, s) = calc_array_sizes(a);
ax += a;
mx += m;
sx += s;
@ -76,7 +75,7 @@ impl Dynamic {
#[cfg(not(feature = "no_index"))]
Union::Blob(ref a, ..) => ax += 1 + a.len(),
Union::Map(ref m, ..) => {
let (a, m, s) = Self::calc_map_sizes(m);
let (a, m, s) = calc_map_sizes(m);
ax += a;
mx += m;
sx += s;
@ -91,7 +90,9 @@ impl Dynamic {
}
(ax, mx, sx)
}
}
impl Dynamic {
/// Recursively calculate the sizes of a value.
///
/// Sizes returned are `(` [`Array`][crate::Array], [`Map`][crate::Map] and [`String`] `)`.
@ -103,11 +104,11 @@ impl Dynamic {
pub(crate) fn calc_data_sizes(&self, _top: bool) -> (usize, usize, usize) {
match self.0 {
#[cfg(not(feature = "no_index"))]
Union::Array(ref arr, ..) => Self::calc_array_sizes(arr),
Union::Array(ref arr, ..) => calc_array_sizes(arr),
#[cfg(not(feature = "no_index"))]
Union::Blob(ref blob, ..) => (blob.len(), 0, 0),
#[cfg(not(feature = "no_object"))]
Union::Map(ref map, ..) => Self::calc_map_sizes(map),
Union::Map(ref map, ..) => calc_map_sizes(map),
Union::Str(ref s, ..) => (0, 0, s.len()),
#[cfg(not(feature = "no_closure"))]
Union::Shared(..) if _top => self.read_lock::<Self>().unwrap().calc_data_sizes(true),
@ -190,6 +191,7 @@ impl Engine {
}
/// Check if the number of operations stay within limit.
#[inline(always)]
pub(crate) fn track_operation(
&self,
global: &mut GlobalRuntimeState,
@ -198,16 +200,16 @@ impl Engine {
global.num_operations += 1;
// Guard against too many operations
let max = self.max_operations();
if max > 0 && global.num_operations > max {
return Err(ERR::ErrorTooManyOperations(pos).into());
}
// Report progress
if self.max_operations() > 0 && global.num_operations > self.max_operations() {
Err(ERR::ErrorTooManyOperations(pos).into())
} else {
self.progress
.as_ref()
.and_then(|p| p(global.num_operations))
.map_or(Ok(()), |token| Err(ERR::ErrorTerminated(token, pos).into()))
.and_then(|progress| {
progress(global.num_operations)
.map(|token| Err(ERR::ErrorTerminated(token, pos).into()))
})
.unwrap_or(Ok(()))
}
}
}

View File

@ -510,7 +510,7 @@ impl Engine {
let src = global.source_raw().cloned();
let src = src.as_ref().map(|s| s.as_str());
let context = EvalContext::new(self, global, caches, scope, this_ptr);
let (.., ref on_debugger) = **x;
let (.., ref on_debugger) = *x;
let command = on_debugger(context, event, node, src, node.position());

View File

@ -20,7 +20,7 @@ impl Engine {
global: &GlobalRuntimeState,
namespace: &crate::ast::Namespace,
) -> Option<crate::SharedModule> {
assert!(!namespace.is_empty());
debug_assert!(!namespace.is_empty());
let root = namespace.root();
@ -74,7 +74,7 @@ impl Engine {
if let Some(module) = self.search_imports(global, ns) {
return module.get_qualified_var(*hash_var).map_or_else(
|| {
let sep = crate::tokenizer::Token::DoubleColon.literal_syntax();
let sep = crate::engine::NAMESPACE_SEPARATOR;
Err(ERR::ErrorVariableNotFound(
format!("{ns}{sep}{var_name}"),
@ -104,7 +104,7 @@ impl Engine {
}
}
let sep = crate::tokenizer::Token::DoubleColon.literal_syntax();
let sep = crate::engine::NAMESPACE_SEPARATOR;
return Err(ERR::ErrorVariableNotFound(
format!("{ns}{sep}{var_name}"),
@ -239,9 +239,6 @@ impl Engine {
// Coded this way for better branch prediction.
// Popular branches are lifted out of the `match` statement into their own branches.
// Function calls should account for a relatively larger portion of expressions because
// binary operators are also function calls.
if let Expr::FnCall(x, pos) = expr {
#[cfg(feature = "debugging")]
let reset =
self.run_debugger_with_reset(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
@ -250,18 +247,14 @@ impl Engine {
self.track_operation(global, expr.position())?;
// Function calls should account for a relatively larger portion of expressions because
// binary operators are also function calls.
if let Expr::FnCall(x, pos) = expr {
return self.eval_fn_call_expr(global, caches, scope, this_ptr, x, *pos);
}
// Then variable access.
// We shouldn't do this for too many variants because, soon or later, the added comparisons
// will cost more than the mis-predicted `match` branch.
if let Expr::Variable(x, index, var_pos) = expr {
#[cfg(feature = "debugging")]
self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
self.track_operation(global, expr.position())?;
return if index.is_none() && x.0.is_none() && x.3 == KEYWORD_THIS {
this_ptr
.ok_or_else(|| ERR::ErrorUnboundThis(*var_pos).into())
@ -272,24 +265,26 @@ impl Engine {
};
}
#[cfg(feature = "debugging")]
let reset =
self.run_debugger_with_reset(global, caches, scope, this_ptr.as_deref_mut(), expr)?;
#[cfg(feature = "debugging")]
auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset));
// Then integer constants.
if let Expr::IntegerConstant(x, ..) = expr {
return Ok((*x).into());
}
self.track_operation(global, expr.position())?;
// Stop merging branches here!
// We shouldn't lift out too many variants because, soon or later, the added comparisons
// will cost more than the mis-predicted `match` branch.
Self::black_box();
match expr {
// Constants
Expr::DynamicConstant(x, ..) => Ok(x.as_ref().clone()),
Expr::IntegerConstant(x, ..) => Ok((*x).into()),
Expr::IntegerConstant(..) => unreachable!(),
Expr::StringConstant(x, ..) => Ok(x.clone().into()),
Expr::BoolConstant(x, ..) => Ok((*x).into()),
#[cfg(not(feature = "no_float"))]
Expr::FloatConstant(x, ..) => Ok((*x).into()),
Expr::StringConstant(x, ..) => Ok(x.clone().into()),
Expr::CharConstant(x, ..) => Ok((*x).into()),
Expr::BoolConstant(x, ..) => Ok((*x).into()),
Expr::Unit(..) => Ok(Dynamic::UNIT),
Expr::DynamicConstant(x, ..) => Ok(x.as_ref().clone()),
// `... ${...} ...`
Expr::InterpolatedString(x, _) => {
@ -434,8 +429,13 @@ impl Engine {
.and_then(|r| self.check_data_size(r, expr.start_position()))
}
Expr::Stmt(x) if x.is_empty() => Ok(Dynamic::UNIT),
Expr::Stmt(x) => self.eval_stmt_block(global, caches, scope, this_ptr, x, true),
Expr::Stmt(x) => {
if x.is_empty() {
Ok(Dynamic::UNIT)
} else {
self.eval_stmt_block(global, caches, scope, this_ptr, x, true)
}
}
#[cfg(not(feature = "no_index"))]
Expr::Index(..) => {

View File

@ -127,20 +127,6 @@ impl GlobalRuntimeState {
pub fn get_shared_import(&self, index: usize) -> Option<crate::SharedModule> {
self.modules.as_ref().and_then(|m| m.get(index).cloned())
}
/// Get a mutable reference to the globally-imported [module][crate::Module] at a
/// particular index.
///
/// Not available under `no_module`.
#[cfg(not(feature = "no_module"))]
#[allow(dead_code)]
#[inline]
#[must_use]
pub(crate) fn get_shared_import_mut(
&mut self,
index: usize,
) -> Option<&mut crate::SharedModule> {
self.modules.as_deref_mut().and_then(|m| m.get_mut(index))
}
/// Get the index of a globally-imported [module][crate::Module] by name.
///
/// Not available under `no_module`.

View File

@ -11,6 +11,12 @@ mod target;
pub use cache::{Caches, FnResolutionCache, FnResolutionCacheEntry};
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
pub use chaining::ChainType;
#[cfg(not(feature = "unchecked"))]
#[cfg(not(feature = "no_index"))]
pub use data_check::calc_array_sizes;
#[cfg(not(feature = "unchecked"))]
#[cfg(not(feature = "no_object"))]
pub use data_check::calc_map_sizes;
#[cfg(feature = "debugging")]
pub use debugger::{
BreakPoint, CallStackFrame, Debugger, DebuggerCommand, DebuggerEvent, DebuggerStatus,

View File

@ -3,9 +3,11 @@
use super::{Caches, EvalContext, GlobalRuntimeState, Target};
use crate::api::events::VarDefInfo;
use crate::ast::{
ASTFlags, BinaryExpr, Expr, FlowControl, OpAssignment, Stmt, SwitchCasesCollection,
ASTFlags, BinaryExpr, ConditionalExpr, Expr, FlowControl, OpAssignment, Stmt,
SwitchCasesCollection,
};
use crate::func::{get_builtin_op_assignment_fn, get_hasher};
use crate::tokenizer::Token;
use crate::types::dynamic::{AccessMode, Union};
use crate::{Dynamic, Engine, RhaiResult, RhaiResultOf, Scope, ERR, INT};
use std::hash::{Hash, Hasher};
@ -127,48 +129,119 @@ impl Engine {
let pos = op_info.position();
if let Some((hash1, hash2, op_assign, op)) = op_info.get_op_assignment_info() {
if let Some((hash_x, hash, op_x, op_x_str, op, op_str)) = op_info.get_op_assignment_info() {
let mut lock_guard = target.write_lock::<Dynamic>().unwrap();
let mut done = false;
// Short-circuit built-in op-assignments if under Fast Operators mode
if self.fast_operators() {
#[allow(clippy::wildcard_imports)]
use Token::*;
done = true;
// For extremely simple primary data operations, do it directly
// to avoid the overhead of calling a function.
match (&mut lock_guard.0, &mut new_val.0) {
(Union::Bool(b1, ..), Union::Bool(b2, ..)) => match op_x {
AndAssign => *b1 = *b1 && *b2,
OrAssign => *b1 = *b1 || *b2,
XOrAssign => *b1 = *b1 ^ *b2,
_ => done = false,
},
(Union::Int(n1, ..), Union::Int(n2, ..)) => {
#[cfg(not(feature = "unchecked"))]
#[allow(clippy::wildcard_imports)]
use crate::packages::arithmetic::arith_basic::INT::functions::*;
#[cfg(not(feature = "unchecked"))]
match op_x {
PlusAssign => {
*n1 = add(*n1, *n2).map_err(|err| err.fill_position(pos))?
}
MinusAssign => {
*n1 = subtract(*n1, *n2).map_err(|err| err.fill_position(pos))?
}
MultiplyAssign => {
*n1 = multiply(*n1, *n2).map_err(|err| err.fill_position(pos))?
}
DivideAssign => {
*n1 = divide(*n1, *n2).map_err(|err| err.fill_position(pos))?
}
ModuloAssign => {
*n1 = modulo(*n1, *n2).map_err(|err| err.fill_position(pos))?
}
_ => done = false,
}
#[cfg(feature = "unchecked")]
match op_x {
PlusAssign => *n1 += *n2,
MinusAssign => *n1 -= *n2,
MultiplyAssign => *n1 *= *n2,
DivideAssign => *n1 /= *n2,
ModuloAssign => *n1 %= *n2,
_ => done = false,
}
}
#[cfg(not(feature = "no_float"))]
(Union::Float(f1, ..), Union::Float(f2, ..)) => match op_x {
PlusAssign => **f1 += **f2,
MinusAssign => **f1 -= **f2,
MultiplyAssign => **f1 *= **f2,
DivideAssign => **f1 /= **f2,
ModuloAssign => **f1 %= **f2,
_ => done = false,
},
#[cfg(not(feature = "no_float"))]
(Union::Float(f1, ..), Union::Int(n2, ..)) => match op_x {
PlusAssign => **f1 += *n2 as crate::FLOAT,
MinusAssign => **f1 -= *n2 as crate::FLOAT,
MultiplyAssign => **f1 *= *n2 as crate::FLOAT,
DivideAssign => **f1 /= *n2 as crate::FLOAT,
ModuloAssign => **f1 %= *n2 as crate::FLOAT,
_ => done = false,
},
_ => done = false,
}
if !done {
if let Some((func, need_context)) =
get_builtin_op_assignment_fn(op_x, &*lock_guard, &new_val)
{
// We may not need to bump the level because built-in's do not need it.
//auto_restore! { let orig_level = global.level; global.level += 1 }
let args = &mut [&mut *lock_guard, &mut new_val];
let context = need_context
.then(|| (self, op_x_str, global.source(), &*global, pos).into());
let _ = func(context, args).map_err(|err| err.fill_position(pos))?;
done = true;
}
}
}
if !done {
let opx = Some(op_x);
let args = &mut [&mut *lock_guard, &mut new_val];
if self.fast_operators() {
if let Some((func, need_context)) =
get_builtin_op_assignment_fn(op_assign, args[0], args[1])
{
// Built-in found
auto_restore! { let orig_level = global.level; global.level += 1 }
let context = if need_context {
let op = op_assign.literal_syntax();
let source = global.source();
Some((self, op, source, &*global, pos).into())
} else {
None
};
return func(context, args).map(|_| ());
}
}
let token = Some(op_assign);
let op_assign = op_assign.literal_syntax();
match self.exec_native_fn_call(global, caches, op_assign, token, hash1, args, true, pos)
match self
.exec_native_fn_call(global, caches, op_x_str, opx, hash_x, args, true, pos)
{
Ok(_) => (),
Err(err) if matches!(*err, ERR::ErrorFunctionNotFound(ref f, ..) if f.starts_with(op_assign)) =>
Err(err) if matches!(*err, ERR::ErrorFunctionNotFound(ref f, ..) if f.starts_with(op_x_str)) =>
{
// Expand to `var = var op rhs`
let token = Some(op);
let op = op.literal_syntax();
let op = Some(op);
*args[0] = self
.exec_native_fn_call(global, caches, op, token, hash2, args, true, pos)?
.exec_native_fn_call(global, caches, op_str, op, hash, args, true, pos)?
.0;
}
Err(err) => return Err(err),
}
self.check_data_size(&*args[0], root.position())?;
}
} else {
// Normal assignment
match target {
@ -200,24 +273,20 @@ impl Engine {
#[cfg(feature = "debugging")]
auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset));
self.track_operation(global, stmt.position())?;
// Coded this way for better branch prediction.
// Popular branches are lifted out of the `match` statement into their own branches.
// Function calls should account for a relatively larger portion of statements.
if let Stmt::FnCall(x, pos) = stmt {
self.track_operation(global, stmt.position())?;
return self.eval_fn_call_expr(global, caches, scope, this_ptr, x, *pos);
}
// Then assignments.
// We shouldn't do this for too many variants because, soon or later, the added comparisons
// will cost more than the mis-predicted `match` branch.
if let Stmt::Assignment(x, ..) = stmt {
let (op_info, BinaryExpr { lhs, rhs }) = &**x;
self.track_operation(global, stmt.position())?;
if let Expr::Variable(x, ..) = lhs {
let rhs_val = self
.eval_expr(global, caches, scope, this_ptr.as_deref_mut(), rhs)?
@ -281,7 +350,97 @@ impl Engine {
}
}
self.track_operation(global, stmt.position())?;
// Then variable definitions.
if let Stmt::Var(x, options, pos) = stmt {
if !self.allow_shadowing() && scope.contains(&x.0) {
return Err(ERR::ErrorVariableExists(x.0.to_string(), *pos).into());
}
// Let/const statement
let (var_name, expr, index) = &**x;
let access = if options.contains(ASTFlags::CONSTANT) {
AccessMode::ReadOnly
} else {
AccessMode::ReadWrite
};
let export = options.contains(ASTFlags::EXPORTED);
// Check variable definition filter
if let Some(ref filter) = self.def_var_filter {
let will_shadow = scope.contains(var_name);
let is_const = access == AccessMode::ReadOnly;
let info = VarDefInfo {
name: var_name,
is_const,
nesting_level: global.scope_level,
will_shadow,
};
let orig_scope_len = scope.len();
let context =
EvalContext::new(self, global, caches, scope, this_ptr.as_deref_mut());
let filter_result = filter(true, info, context);
if orig_scope_len != scope.len() {
// The scope is changed, always search from now on
global.always_search_scope = true;
}
if !filter_result? {
return Err(ERR::ErrorForbiddenVariable(var_name.to_string(), *pos).into());
}
}
// Evaluate initial value
let mut value = self
.eval_expr(global, caches, scope, this_ptr, expr)?
.flatten()
.intern_string(self);
let _alias = if !rewind_scope {
// Put global constants into global module
#[cfg(not(feature = "no_function"))]
#[cfg(not(feature = "no_module"))]
if global.scope_level == 0
&& access == AccessMode::ReadOnly
&& global.lib.iter().any(|m| !m.is_empty())
{
crate::func::locked_write(global.constants.get_or_insert_with(|| {
crate::Shared::new(crate::Locked::new(std::collections::BTreeMap::new()))
}))
.insert(var_name.name.clone(), value.clone());
}
if export {
Some(var_name)
} else {
None
}
} else if export {
unreachable!("exported variable not on global level");
} else {
None
};
if let Some(index) = index {
value.set_access_mode(access);
*scope.get_mut_by_index(scope.len() - index.get()) = value;
} else {
scope.push_entry(var_name.name.clone(), access, value);
}
#[cfg(not(feature = "no_module"))]
if let Some(alias) = _alias {
scope.add_alias_by_index(scope.len() - 1, alias.as_str().into());
}
return Ok(Dynamic::UNIT);
}
// Stop merging branches here!
// We shouldn't lift out too many variants because, soon or later, the added comparisons
// will cost more than the mis-predicted `match` branch.
Self::black_box();
match stmt {
// No-op
@ -293,10 +452,13 @@ impl Engine {
.map(Dynamic::flatten),
// Block scope
Stmt::Block(statements, ..) if statements.is_empty() => Ok(Dynamic::UNIT),
Stmt::Block(statements, ..) => {
if statements.is_empty() {
Ok(Dynamic::UNIT)
} else {
self.eval_stmt_block(global, caches, scope, this_ptr, statements, true)
}
}
// If statement
Stmt::If(x, ..) => {
@ -311,12 +473,14 @@ impl Engine {
.as_bool()
.map_err(|typ| self.make_type_mismatch_err::<bool>(typ, expr.position()))?;
if guard_val && !if_block.is_empty() {
match guard_val {
true if !if_block.is_empty() => {
self.eval_stmt_block(global, caches, scope, this_ptr, if_block, true)
} else if !guard_val && !else_block.is_empty() {
}
false if !else_block.is_empty() => {
self.eval_stmt_block(global, caches, scope, this_ptr, else_block, true)
} else {
Ok(Dynamic::UNIT)
}
_ => Ok(Dynamic::UNIT),
}
}
@ -343,7 +507,7 @@ impl Engine {
// First check hashes
if let Some(case_blocks_list) = cases.get(&hash) {
assert!(!case_blocks_list.is_empty());
debug_assert!(!case_blocks_list.is_empty());
for &index in case_blocks_list {
let block = &expressions[index];
@ -363,15 +527,13 @@ impl Engine {
break;
}
}
} else if value.is_int() && !ranges.is_empty() {
} else if !ranges.is_empty() {
// Then check integer ranges
let value = value.as_int().expect("`INT`");
for r in ranges.iter().filter(|r| r.contains(&value)) {
let ConditionalExpr { condition, expr } = &expressions[r.index()];
for r in ranges.iter().filter(|r| r.contains(value)) {
let block = &expressions[r.index()];
let cond_result = match block.condition {
Expr::BoolConstant(b, ..) => b,
let cond_result = match condition {
Expr::BoolConstant(b, ..) => *b,
ref c => self
.eval_expr(global, caches, scope, this_ptr.as_deref_mut(), c)?
.as_bool()
@ -381,7 +543,7 @@ impl Engine {
};
if cond_result {
result = Some(&block.expr);
result = Some(expr);
break;
}
}
@ -521,12 +683,10 @@ impl Engine {
auto_restore! { scope => rewind; let orig_scope_len = scope.len(); }
// Add the loop variables
let counter_index = if counter.is_empty() {
usize::MAX
} else {
let counter_index = (!counter.is_empty()).then(|| {
scope.push(counter.name.clone(), 0 as INT);
scope.len() - 1
};
});
scope.push(var_name.name.clone(), ());
let index = scope.len() - 1;
@ -535,7 +695,7 @@ impl Engine {
for (x, iter_value) in iter_func(iter_obj).enumerate() {
// Increment counter
if counter_index < usize::MAX {
if let Some(counter_index) = counter_index {
// As the variable increments from 0, this should always work
// since any overflow will first be caught below.
let index_value = x as INT;
@ -695,94 +855,6 @@ impl Engine {
// Empty return
Stmt::Return(None, .., pos) => Err(ERR::Return(Dynamic::UNIT, *pos).into()),
// Let/const statement - shadowing disallowed
Stmt::Var(x, .., pos) if !self.allow_shadowing() && scope.contains(&x.0) => {
Err(ERR::ErrorVariableExists(x.0.to_string(), *pos).into())
}
// Let/const statement
Stmt::Var(x, options, pos) => {
let (var_name, expr, index) = &**x;
let access = if options.contains(ASTFlags::CONSTANT) {
AccessMode::ReadOnly
} else {
AccessMode::ReadWrite
};
let export = options.contains(ASTFlags::EXPORTED);
// Check variable definition filter
if let Some(ref filter) = self.def_var_filter {
let will_shadow = scope.contains(var_name);
let is_const = access == AccessMode::ReadOnly;
let info = VarDefInfo {
name: var_name,
is_const,
nesting_level: global.scope_level,
will_shadow,
};
let orig_scope_len = scope.len();
let context =
EvalContext::new(self, global, caches, scope, this_ptr.as_deref_mut());
let filter_result = filter(true, info, context);
if orig_scope_len != scope.len() {
// The scope is changed, always search from now on
global.always_search_scope = true;
}
if !filter_result? {
return Err(ERR::ErrorForbiddenVariable(var_name.to_string(), *pos).into());
}
}
// Evaluate initial value
let mut value = self
.eval_expr(global, caches, scope, this_ptr, expr)?
.flatten()
.intern_string(self);
let _alias = if !rewind_scope {
// Put global constants into global module
#[cfg(not(feature = "no_function"))]
#[cfg(not(feature = "no_module"))]
if global.scope_level == 0
&& access == AccessMode::ReadOnly
&& global.lib.iter().any(|m| !m.is_empty())
{
crate::func::locked_write(global.constants.get_or_insert_with(|| {
crate::Shared::new(
crate::Locked::new(std::collections::BTreeMap::new()),
)
}))
.insert(var_name.name.clone(), value.clone());
}
if export {
Some(var_name)
} else {
None
}
} else if export {
unreachable!("exported variable not on global level");
} else {
None
};
if let Some(index) = index {
value.set_access_mode(access);
*scope.get_mut_by_index(scope.len() - index.get()) = value;
} else {
scope.push_entry(var_name.name.clone(), access, value);
}
#[cfg(not(feature = "no_module"))]
if let Some(alias) = _alias {
scope.add_alias_by_index(scope.len() - 1, alias.as_str().into());
}
Ok(Dynamic::UNIT)
}
// Import statement
#[cfg(not(feature = "no_module"))]
Stmt::Import(x, _pos) => {
@ -807,14 +879,16 @@ impl Engine {
let module = resolver
.as_ref()
.and_then(|r| match r.resolve_raw(self, global, &path, path_pos) {
.and_then(
|r| match r.resolve_raw(self, global, scope, &path, path_pos) {
Err(err) if matches!(*err, ERR::ErrorModuleNotFound(..)) => None,
result => Some(result),
})
},
)
.or_else(|| {
Some(
self.module_resolver
.resolve_raw(self, global, &path, path_pos),
self.module_resolver()
.resolve_raw(self, global, scope, &path, path_pos),
)
})
.unwrap_or_else(|| {

View File

@ -27,48 +27,6 @@ use rust_decimal::Decimal;
/// The `unchecked` feature is not active.
const CHECKED_BUILD: bool = cfg!(not(feature = "unchecked"));
/// Is the type a numeric type?
#[inline]
#[must_use]
fn is_numeric(type_id: TypeId) -> bool {
if type_id == TypeId::of::<INT>() {
return true;
}
#[cfg(not(feature = "only_i64"))]
#[cfg(not(feature = "only_i32"))]
if type_id == TypeId::of::<u8>()
|| type_id == TypeId::of::<u16>()
|| type_id == TypeId::of::<u32>()
|| type_id == TypeId::of::<u64>()
|| type_id == TypeId::of::<i8>()
|| type_id == TypeId::of::<i16>()
|| type_id == TypeId::of::<i32>()
|| type_id == TypeId::of::<i64>()
{
return true;
}
#[cfg(not(feature = "only_i64"))]
#[cfg(not(feature = "only_i32"))]
#[cfg(not(target_family = "wasm"))]
if type_id == TypeId::of::<u128>() || type_id == TypeId::of::<i128>() {
return true;
}
#[cfg(not(feature = "no_float"))]
if type_id == TypeId::of::<f32>() || type_id == TypeId::of::<f64>() {
return true;
}
#[cfg(feature = "decimal")]
if type_id == TypeId::of::<rust_decimal::Decimal>() {
return true;
}
false
}
/// A function that returns `true`.
#[inline(always)]
#[allow(clippy::unnecessary_wraps)]
@ -584,22 +542,7 @@ pub fn get_builtin_binary_op_fn(op: &Token, x: &Dynamic, y: &Dynamic) -> Option<
// One of the operands is a custom type, so it is never built-in
if x.is_variant() || y.is_variant() {
return if is_numeric(type1) && is_numeric(type2) {
// Disallow comparisons between different numeric types
None
} else if type1 != type2 {
// If the types are not the same, default to not compare
match op {
NotEqualsTo => Some((const_true_fn, false)),
EqualsTo | GreaterThan | GreaterThanEqualsTo | LessThan | LessThanEqualsTo => {
Some((const_false_fn, false))
}
_ => None,
}
} else {
// Disallow comparisons between the same type
None
};
return None;
}
// Default comparison operators for different types
@ -741,6 +684,7 @@ pub fn get_builtin_op_assignment_fn(op: &Token, x: &Dynamic, y: &Dynamic) -> Opt
return match op {
AndAssign => impl_op!(bool = x && as_bool),
OrAssign => impl_op!(bool = x || as_bool),
XOrAssign => impl_op!(bool = x ^ as_bool),
_ => None,
};
}

View File

@ -9,6 +9,7 @@ use crate::engine::{
};
use crate::eval::{Caches, FnResolutionCacheEntry, GlobalRuntimeState};
use crate::tokenizer::{is_valid_function_name, Token};
use crate::types::dynamic::Union;
use crate::{
calc_fn_hash, calc_fn_hash_full, Dynamic, Engine, FnArgsVec, FnPtr, ImmutableString,
OptimizationLevel, Position, RhaiResult, RhaiResultOf, Scope, Shared, ERR,
@ -363,7 +364,7 @@ impl Engine {
);
if let Some(FnResolutionCacheEntry { func, source }) = func {
assert!(func.is_native());
debug_assert!(func.is_native());
// Push a new call stack frame
#[cfg(feature = "debugging")]
@ -398,11 +399,9 @@ impl Engine {
let is_method = func.is_method();
let src = source.as_ref().map(|s| s.as_str());
let context = if func.has_context() {
Some((self, name, src, &*global, pos).into())
} else {
None
};
let context = func
.has_context()
.then(|| (self, name, src, &*global, pos).into());
let mut _result = if let Some(f) = func.get_plugin_fn() {
if !f.is_pure() && !args.is_empty() && args[0].is_read_only() {
@ -461,18 +460,26 @@ impl Engine {
// See if the function match print/debug (which requires special processing)
return Ok(match name {
KEYWORD_PRINT => {
if let Some(ref print) = self.print {
let text = result.into_immutable_string().map_err(|typ| {
let t = self.map_type_name(type_name::<ImmutableString>()).into();
ERR::ErrorMismatchOutputType(t, typ.into(), pos)
})?;
((*self.print)(&text).into(), false)
(print(&text).into(), false)
} else {
(Dynamic::UNIT, false)
}
}
KEYWORD_DEBUG => {
if let Some(ref debug) = self.debug {
let text = result.into_immutable_string().map_err(|typ| {
let t = self.map_type_name(type_name::<ImmutableString>()).into();
ERR::ErrorMismatchOutputType(t, typ.into(), pos)
})?;
((*self.debug)(&text, global.source(), pos).into(), false)
(debug(&text, global.source(), pos).into(), false)
} else {
(Dynamic::UNIT, false)
}
}
_ => (result, is_method),
});
@ -633,7 +640,7 @@ impl Engine {
.cloned()
{
// Script function call
assert!(func.is_script());
debug_assert!(func.is_script());
let f = func.get_script_fn_def().expect("script-defined function");
let environ = func.get_encapsulated_environ();
@ -810,7 +817,8 @@ impl Engine {
if call_args.is_empty() {
let typ = self.map_type_name(target.type_name());
return Err(self.make_type_mismatch_err::<FnPtr>(typ, fn_call_pos));
} else if !call_args[0].is_fnptr() {
}
if !call_args[0].is_fnptr() {
let typ = self.map_type_name(call_args[0].type_name());
return Err(self.make_type_mismatch_err::<FnPtr>(typ, first_arg_pos));
}
@ -1251,9 +1259,7 @@ impl Engine {
}
// Call with blank scope
if total_args == 0 && curry.is_empty() {
// No arguments
} else {
if total_args > 0 || !curry.is_empty() {
// If the first argument is a variable, and there is no curried arguments,
// convert to method-call style in order to leverage potential &mut first argument and
// avoid cloning the value
@ -1324,9 +1330,7 @@ impl Engine {
let args = &mut FnArgsVec::with_capacity(args_expr.len());
let mut first_arg_value = None;
if args_expr.is_empty() {
// No arguments
} else {
if !args_expr.is_empty() {
// See if the first argument is a variable (not namespace-qualified).
// If so, convert to method-call style in order to leverage potential &mut first argument
// and avoid cloning the value
@ -1460,11 +1464,9 @@ impl Engine {
Some(f) if f.is_plugin_fn() => {
let f = f.get_plugin_fn().expect("plugin function");
let context = if f.has_context() {
Some((self, fn_name, module.id(), &*global, pos).into())
} else {
None
};
let context = f
.has_context()
.then(|| (self, fn_name, module.id(), &*global, pos).into());
if !f.is_pure() && !args.is_empty() && args[0].is_read_only() {
Err(ERR::ErrorNonPureMethodCallOnConstant(fn_name.to_string(), pos).into())
} else {
@ -1475,29 +1477,27 @@ impl Engine {
Some(f) if f.is_native() => {
let func = f.get_native_fn().expect("native function");
let context = if f.has_context() {
Some((self, fn_name, module.id(), &*global, pos).into())
} else {
None
};
let context = f
.has_context()
.then(|| (self, fn_name, module.id(), &*global, pos).into());
func(context, args).and_then(|r| self.check_data_size(r, pos))
}
Some(f) => unreachable!("unknown function type: {:?}", f),
None => {
let sig = if namespace.is_empty() {
None => Err(ERR::ErrorFunctionNotFound(
if namespace.is_empty() {
self.gen_fn_call_signature(fn_name, args)
} else {
format!(
"{namespace}{}{}",
crate::tokenizer::Token::DoubleColon.literal_syntax(),
crate::engine::NAMESPACE_SEPARATOR,
self.gen_fn_call_signature(fn_name, args)
)
};
Err(ERR::ErrorFunctionNotFound(sig, pos).into())
}
},
pos,
)
.into()),
}
}
@ -1547,6 +1547,9 @@ impl Engine {
/// # Main Entry-Point (`FnCallExpr`)
///
/// Evaluate a function call expression.
///
/// This method tries to short-circuit function resolution under Fast Operators mode if the
/// function call is an operator.
pub(crate) fn eval_fn_call_expr(
&self,
global: &mut GlobalRuntimeState,
@ -1570,23 +1573,29 @@ impl Engine {
let op_token = op_token.as_ref();
// Short-circuit native unary operator call if under Fast Operators mode
if op_token == Some(&Token::Bang) && self.fast_operators() && args.len() == 1 {
if self.fast_operators() && args.len() == 1 && op_token == Some(&Token::Bang) {
let mut value = self
.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), &args[0])?
.0
.flatten();
return value.as_bool().map(|r| (!r).into()).or_else(|_| {
return match value.0 {
Union::Bool(b, ..) => Ok((!b).into()),
_ => {
let operand = &mut [&mut value];
self.exec_fn_call(
global, caches, None, name, op_token, *hashes, operand, false, false, pos,
)
.map(|(v, ..)| v)
});
}
};
}
// Short-circuit native binary operator call if under Fast Operators mode
if op_token.is_some() && self.fast_operators() && args.len() == 2 {
#[allow(clippy::wildcard_imports)]
use Token::*;
let mut lhs = self
.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), &args[0])?
.0
@ -1597,19 +1606,128 @@ impl Engine {
.0
.flatten();
// For extremely simple primary data operations, do it directly
// to avoid the overhead of calling a function.
match (&lhs.0, &rhs.0) {
(Union::Unit(..), Union::Unit(..)) => match op_token.unwrap() {
EqualsTo => return Ok(Dynamic::TRUE),
NotEqualsTo | GreaterThan | GreaterThanEqualsTo | LessThan
| LessThanEqualsTo => return Ok(Dynamic::FALSE),
_ => (),
},
(Union::Bool(b1, ..), Union::Bool(b2, ..)) => match op_token.unwrap() {
EqualsTo => return Ok((*b1 == *b2).into()),
NotEqualsTo => return Ok((*b1 != *b2).into()),
GreaterThan | GreaterThanEqualsTo | LessThan | LessThanEqualsTo => {
return Ok(Dynamic::FALSE)
}
Pipe => return Ok((*b1 || *b2).into()),
Ampersand => return Ok((*b1 && *b2).into()),
_ => (),
},
(Union::Int(n1, ..), Union::Int(n2, ..)) => {
#[cfg(not(feature = "unchecked"))]
#[allow(clippy::wildcard_imports)]
use crate::packages::arithmetic::arith_basic::INT::functions::*;
#[cfg(not(feature = "unchecked"))]
match op_token.unwrap() {
EqualsTo => return Ok((*n1 == *n2).into()),
NotEqualsTo => return Ok((*n1 != *n2).into()),
GreaterThan => return Ok((*n1 > *n2).into()),
GreaterThanEqualsTo => return Ok((*n1 >= *n2).into()),
LessThan => return Ok((*n1 < *n2).into()),
LessThanEqualsTo => return Ok((*n1 <= *n2).into()),
Plus => return add(*n1, *n2).map(Into::into),
Minus => return subtract(*n1, *n2).map(Into::into),
Multiply => return multiply(*n1, *n2).map(Into::into),
Divide => return divide(*n1, *n2).map(Into::into),
Modulo => return modulo(*n1, *n2).map(Into::into),
_ => (),
}
#[cfg(feature = "unchecked")]
match op_token.unwrap() {
EqualsTo => return Ok((*n1 == *n2).into()),
NotEqualsTo => return Ok((*n1 != *n2).into()),
GreaterThan => return Ok((*n1 > *n2).into()),
GreaterThanEqualsTo => return Ok((*n1 >= *n2).into()),
LessThan => return Ok((*n1 < *n2).into()),
LessThanEqualsTo => return Ok((*n1 <= *n2).into()),
Plus => return Ok((*n1 + *n2).into()),
Minus => return Ok((*n1 - *n2).into()),
Multiply => return Ok((*n1 * *n2).into()),
Divide => return Ok((*n1 / *n2).into()),
Modulo => return Ok((*n1 % *n2).into()),
_ => (),
}
}
#[cfg(not(feature = "no_float"))]
(Union::Float(f1, ..), Union::Float(f2, ..)) => match op_token.unwrap() {
EqualsTo => return Ok((**f1 == **f2).into()),
NotEqualsTo => return Ok((**f1 != **f2).into()),
GreaterThan => return Ok((**f1 > **f2).into()),
GreaterThanEqualsTo => return Ok((**f1 >= **f2).into()),
LessThan => return Ok((**f1 < **f2).into()),
LessThanEqualsTo => return Ok((**f1 <= **f2).into()),
Plus => return Ok((**f1 + **f2).into()),
Minus => return Ok((**f1 - **f2).into()),
Multiply => return Ok((**f1 * **f2).into()),
Divide => return Ok((**f1 / **f2).into()),
Modulo => return Ok((**f1 % **f2).into()),
_ => (),
},
#[cfg(not(feature = "no_float"))]
(Union::Float(f1, ..), Union::Int(n2, ..)) => match op_token.unwrap() {
EqualsTo => return Ok((**f1 == (*n2 as crate::FLOAT)).into()),
NotEqualsTo => return Ok((**f1 != (*n2 as crate::FLOAT)).into()),
GreaterThan => return Ok((**f1 > (*n2 as crate::FLOAT)).into()),
GreaterThanEqualsTo => return Ok((**f1 >= (*n2 as crate::FLOAT)).into()),
LessThan => return Ok((**f1 < (*n2 as crate::FLOAT)).into()),
LessThanEqualsTo => return Ok((**f1 <= (*n2 as crate::FLOAT)).into()),
Plus => return Ok((**f1 + (*n2 as crate::FLOAT)).into()),
Minus => return Ok((**f1 - (*n2 as crate::FLOAT)).into()),
Multiply => return Ok((**f1 * (*n2 as crate::FLOAT)).into()),
Divide => return Ok((**f1 / (*n2 as crate::FLOAT)).into()),
Modulo => return Ok((**f1 % (*n2 as crate::FLOAT)).into()),
_ => (),
},
#[cfg(not(feature = "no_float"))]
(Union::Int(n1, ..), Union::Float(f2, ..)) => match op_token.unwrap() {
EqualsTo => return Ok(((*n1 as crate::FLOAT) == **f2).into()),
NotEqualsTo => return Ok(((*n1 as crate::FLOAT) != **f2).into()),
GreaterThan => return Ok(((*n1 as crate::FLOAT) > **f2).into()),
GreaterThanEqualsTo => return Ok(((*n1 as crate::FLOAT) >= **f2).into()),
LessThan => return Ok(((*n1 as crate::FLOAT) < **f2).into()),
LessThanEqualsTo => return Ok(((*n1 as crate::FLOAT) <= **f2).into()),
Plus => return Ok(((*n1 as crate::FLOAT) + **f2).into()),
Minus => return Ok(((*n1 as crate::FLOAT) - **f2).into()),
Multiply => return Ok(((*n1 as crate::FLOAT) * **f2).into()),
Divide => return Ok(((*n1 as crate::FLOAT) / **f2).into()),
Modulo => return Ok(((*n1 as crate::FLOAT) % **f2).into()),
_ => (),
},
(Union::Str(s1, ..), Union::Str(s2, ..)) => match op_token.unwrap() {
EqualsTo => return Ok((s1 == s2).into()),
NotEqualsTo => return Ok((s1 != s2).into()),
GreaterThan => return Ok((s1 > s2).into()),
GreaterThanEqualsTo => return Ok((s1 >= s2).into()),
LessThan => return Ok((s1 < s2).into()),
LessThanEqualsTo => return Ok((s1 <= s2).into()),
_ => (),
},
_ => (),
}
let operands = &mut [&mut lhs, &mut rhs];
if let Some((func, need_context)) =
get_builtin_binary_op_fn(op_token.as_ref().unwrap(), operands[0], operands[1])
{
// Built-in found
auto_restore! { let orig_level = global.level; global.level += 1 }
// We may not need to bump the level because built-in's do not need it.
//auto_restore! { let orig_level = global.level; global.level += 1 }
let context = if need_context {
Some((self, name.as_str(), None, &*global, pos).into())
} else {
None
};
let context =
need_context.then(|| (self, name.as_str(), None, &*global, pos).into());
return func(context, operands);
}

View File

@ -27,7 +27,8 @@ impl Hasher for StraightHasher {
fn finish(&self) -> u64 {
self.0
}
#[inline(always)]
#[cold]
#[inline(never)]
fn write(&mut self, _bytes: &[u8]) {
panic!("StraightHasher can only hash u64 values");
}
@ -85,7 +86,7 @@ pub fn calc_var_hash<'a>(namespace: impl IntoIterator<Item = &'a str>, var_name:
}
count += 1;
});
count.hash(s);
s.write_usize(count);
var_name.hash(s);
s.finish()
@ -119,9 +120,9 @@ pub fn calc_fn_hash<'a>(
}
count += 1;
});
count.hash(s);
s.write_usize(count);
fn_name.hash(s);
num.hash(s);
s.write_usize(num);
s.finish()
}
@ -133,13 +134,12 @@ pub fn calc_fn_hash<'a>(
#[must_use]
pub fn calc_fn_hash_full(base: u64, params: impl IntoIterator<Item = TypeId>) -> u64 {
let s = &mut get_hasher();
base.hash(s);
let mut count = 0;
params.into_iter().for_each(|t| {
t.hash(s);
count += 1;
});
count.hash(s);
s.write_usize(count);
s.finish()
s.finish() ^ base
}

View File

@ -86,7 +86,7 @@ impl Engine {
let orig_fn_resolution_caches_len = caches.fn_resolution_caches_len();
#[cfg(not(feature = "no_module"))]
let orig_constants = if let Some(environ) = _environ {
let orig_constants = _environ.map(|environ| {
let EncapsulatedEnviron {
lib,
imports,
@ -100,10 +100,8 @@ impl Engine {
global.lib.push(lib.clone());
Some(mem::replace(&mut global.constants, constants.clone()))
} else {
None
};
mem::replace(&mut global.constants, constants.clone())
});
#[cfg(feature = "debugging")]
if self.is_debugger_registered() {

View File

@ -85,7 +85,7 @@ pub struct FuncInfoMetadata {
pub return_type: Identifier,
/// Comments.
#[cfg(feature = "metadata")]
pub comments: Box<[Identifier]>,
pub comments: Box<[SmartString]>,
}
/// A type containing a single registered function.
@ -292,6 +292,11 @@ impl<M: Into<Module>> AddAssign<M> for Module {
}
}
#[inline(always)]
fn new_hash_map<T>(size: usize) -> StraightHashMap<T> {
StraightHashMap::with_capacity_and_hasher(size, Default::default())
}
impl Module {
/// Create a new [`Module`].
///
@ -361,13 +366,7 @@ impl Module {
#[inline(always)]
pub fn set_id(&mut self, id: impl Into<ImmutableString>) -> &mut Self {
let id = id.into();
if id.is_empty() {
self.id = None;
} else {
self.id = Some(id);
}
self.id = (!id.is_empty()).then(|| id);
self
}
@ -691,6 +690,16 @@ impl Module {
if self.is_indexed() {
let hash_var = crate::calc_var_hash(Some(""), &ident);
// Catch hash collisions in testing environment only.
#[cfg(feature = "testing-environ")]
if let Some(_) = self.all_variables.as_ref().and_then(|f| f.get(&hash_var)) {
panic!(
"Hash {} already exists when registering variable {}",
hash_var, ident
);
}
self.all_variables
.get_or_insert_with(Default::default)
.insert(hash_var, value.clone());
@ -721,12 +730,21 @@ impl Module {
// None + function name + number of arguments.
let num_params = fn_def.params.len();
let hash_script = crate::calc_fn_hash(None, &fn_def.name, num_params);
// Catch hash collisions in testing environment only.
#[cfg(feature = "testing-environ")]
if let Some(f) = self.functions.as_ref().and_then(|f| f.get(&hash_script)) {
panic!(
"Hash {} already exists when registering function {:#?}:\n{:#?}",
hash_script, fn_def, f
);
}
#[cfg(feature = "metadata")]
let params_info = fn_def.params.iter().map(Into::into).collect();
self.functions
.get_or_insert_with(|| {
StraightHashMap::with_capacity_and_hasher(FN_MAP_SIZE, Default::default())
})
.get_or_insert_with(|| new_hash_map(FN_MAP_SIZE))
.insert(
hash_script,
FuncInfo {
@ -1052,6 +1070,15 @@ impl Module {
let hash_script = calc_fn_hash(None, name, param_types.len());
let hash_fn = calc_fn_hash_full(hash_script, param_types.iter().copied());
// Catch hash collisions in testing environment only.
#[cfg(feature = "testing-environ")]
if let Some(f) = self.functions.as_ref().and_then(|f| f.get(&hash_script)) {
panic!(
"Hash {} already exists when registering function {}:\n{:#?}",
hash_script, name, f
);
}
if is_dynamic {
self.dynamic_functions_filter
.get_or_insert_with(Default::default)
@ -1059,9 +1086,7 @@ impl Module {
}
self.functions
.get_or_insert_with(|| {
StraightHashMap::with_capacity_and_hasher(FN_MAP_SIZE, Default::default())
})
.get_or_insert_with(|| new_hash_map(FN_MAP_SIZE))
.insert(
hash_fn,
FuncInfo {
@ -1781,9 +1806,9 @@ impl Module {
let others_len = functions.len();
for (&k, f) in functions.iter() {
let map = self.functions.get_or_insert_with(|| {
StraightHashMap::with_capacity_and_hasher(others_len, Default::default())
});
let map = self
.functions
.get_or_insert_with(|| new_hash_map(FN_MAP_SIZE));
map.reserve(others_len);
map.entry(k).or_insert_with(|| f.clone());
}
@ -2083,9 +2108,10 @@ impl Module {
ast: &crate::AST,
engine: &crate::Engine,
) -> RhaiResultOf<Self> {
let mut scope = scope;
let global = &mut crate::eval::GlobalRuntimeState::new(engine);
Self::eval_ast_as_new_raw(engine, scope, global, ast)
Self::eval_ast_as_new_raw(engine, &mut scope, global, ast)
}
/// Create a new [`Module`] by evaluating an [`AST`][crate::AST].
///
@ -2101,13 +2127,12 @@ impl Module {
#[cfg(not(feature = "no_module"))]
pub fn eval_ast_as_new_raw(
engine: &crate::Engine,
scope: crate::Scope,
scope: &mut crate::Scope,
global: &mut crate::eval::GlobalRuntimeState,
ast: &crate::AST,
) -> RhaiResultOf<Self> {
let mut scope = scope;
// Save global state
let orig_scope_len = scope.len();
let orig_imports_len = global.num_imports();
let orig_source = global.source.clone();
@ -2120,7 +2145,7 @@ impl Module {
// Run the script
let caches = &mut crate::eval::Caches::new();
let result = engine.eval_ast_with_scope_raw(global, caches, &mut scope, ast);
let result = engine.eval_ast_with_scope_raw(global, caches, scope, ast);
// Create new module
let mut module = Module::new();
@ -2162,7 +2187,9 @@ impl Module {
});
// Variables with an alias left in the scope become module variables
for (_name, mut value, mut aliases) in scope {
while scope.len() > orig_scope_len {
let (_name, mut value, mut aliases) = scope.pop_entry().expect("not empty");
value.deep_scan(|v| {
if let Some(fn_ptr) = v.downcast_mut::<crate::FnPtr>() {
fn_ptr.set_encapsulated_environ(Some(environ.clone()));
@ -2257,6 +2284,16 @@ impl Module {
if let Some(ref v) = module.variables {
for (var_name, value) in v.iter() {
let hash_var = crate::calc_var_hash(path.iter().copied(), var_name);
// Catch hash collisions in testing environment only.
#[cfg(feature = "testing-environ")]
if let Some(_) = variables.get(&hash_var) {
panic!(
"Hash {} already exists when indexing variable {}",
hash_var, var_name
);
}
variables.insert(hash_var, value.clone());
}
}
@ -2272,6 +2309,15 @@ impl Module {
for (&hash, f) in module.functions.iter().flatten() {
match f.metadata.namespace {
FnNamespace::Global => {
// Catch hash collisions in testing environment only.
#[cfg(feature = "testing-environ")]
if let Some(fx) = functions.get(&hash) {
panic!(
"Hash {} already exists when indexing function {:#?}:\n{:#?}",
hash, f.func, fx
);
}
// Flatten all functions with global namespace
functions.insert(hash, f.func.clone());
contains_indexed_global_functions = true;
@ -2289,6 +2335,16 @@ impl Module {
f.metadata.name.as_str(),
&f.metadata.param_types,
);
// Catch hash collisions in testing environment only.
#[cfg(feature = "testing-environ")]
if let Some(fx) = functions.get(&hash_qualified_fn) {
panic!(
"Hash {} already exists when indexing function {:#?}:\n{:#?}",
hash_qualified_fn, f.func, fx
);
}
functions.insert(hash_qualified_fn, f.func.clone());
} else if cfg!(not(feature = "no_function")) {
let hash_qualified_script = crate::calc_fn_hash(
@ -2296,6 +2352,16 @@ impl Module {
&f.metadata.name,
f.metadata.num_params,
);
// Catch hash collisions in testing environment only.
#[cfg(feature = "testing-environ")]
if let Some(fx) = functions.get(&hash_qualified_script) {
panic!(
"Hash {} already exists when indexing function {:#?}:\n{:#?}",
hash_qualified_script, f.func, fx
);
}
functions.insert(hash_qualified_script, f.func.clone());
}
}
@ -2305,14 +2371,9 @@ impl Module {
if !self.is_indexed() {
let mut path = Vec::with_capacity(4);
let mut variables = StraightHashMap::with_capacity_and_hasher(
self.variables.as_deref().map_or(0, BTreeMap::len),
Default::default(),
);
let mut functions = StraightHashMap::with_capacity_and_hasher(
self.functions.as_ref().map_or(0, StraightHashMap::len),
Default::default(),
);
let mut variables = new_hash_map(self.variables.as_deref().map_or(0, BTreeMap::len));
let mut functions =
new_hash_map(self.functions.as_ref().map_or(0, StraightHashMap::len));
let mut type_iterators = BTreeMap::new();
path.push("");
@ -2328,21 +2389,9 @@ impl Module {
self.flags
.set(ModuleFlags::INDEXED_GLOBAL_FUNCTIONS, has_global_functions);
self.all_variables = if variables.is_empty() {
None
} else {
Some(variables.into())
};
self.all_functions = if functions.is_empty() {
None
} else {
Some(functions.into())
};
self.all_type_iterators = if type_iterators.is_empty() {
None
} else {
Some(type_iterators.into())
};
self.all_variables = (!variables.is_empty()).then(|| variables.into());
self.all_functions = (!functions.is_empty()).then(|| functions.into());
self.all_type_iterators = (!type_iterators.is_empty()).then(|| type_iterators.into());
self.flags |= ModuleFlags::INDEXED;
}

View File

@ -239,14 +239,7 @@ impl FileModuleResolver {
if !self.cache_enabled {
return false;
}
let cache = locked_read(&self.cache);
if cache.is_empty() {
false
} else {
cache.contains_key(path.as_ref())
}
locked_read(&self.cache).contains_key(path.as_ref())
}
/// Empty the internal cache.
#[inline]
@ -290,15 +283,15 @@ impl FileModuleResolver {
fn impl_resolve(
&self,
engine: &Engine,
global: Option<&mut GlobalRuntimeState>,
global: &mut GlobalRuntimeState,
scope: &mut Scope,
source: Option<&str>,
path: &str,
pos: Position,
) -> Result<SharedModule, Box<crate::EvalAltResult>> {
// Load relative paths from source if there is no base path specified
let source_path = global
.as_ref()
.and_then(|g| g.source())
.source()
.or(source)
.and_then(|p| Path::new(p).parent());
@ -321,12 +314,7 @@ impl FileModuleResolver {
ast.set_source(path);
let scope = Scope::new();
let m: Shared<_> = match global {
Some(global) => Module::eval_ast_as_new_raw(engine, scope, global, &ast),
None => Module::eval_ast_as_new(scope, &ast, engine),
}
let m: Shared<_> = Module::eval_ast_as_new_raw(engine, scope, global, &ast)
.map_err(|err| Box::new(ERR::ErrorInModule(path.to_string(), err, pos)))?
.into();
@ -343,10 +331,11 @@ impl ModuleResolver for FileModuleResolver {
&self,
engine: &Engine,
global: &mut GlobalRuntimeState,
scope: &mut Scope,
path: &str,
pos: Position,
) -> RhaiResultOf<SharedModule> {
self.impl_resolve(engine, Some(global), None, path, pos)
self.impl_resolve(engine, global, scope, None, path, pos)
}
#[inline(always)]
@ -357,7 +346,9 @@ impl ModuleResolver for FileModuleResolver {
path: &str,
pos: Position,
) -> RhaiResultOf<SharedModule> {
self.impl_resolve(engine, None, source, path, pos)
let global = &mut GlobalRuntimeState::new(engine);
let scope = &mut Scope::new();
self.impl_resolve(engine, global, scope, source, path, pos)
}
/// Resolve an `AST` based on a path string.

View File

@ -1,6 +1,6 @@
use crate::eval::GlobalRuntimeState;
use crate::func::SendSync;
use crate::{Engine, Position, RhaiResultOf, SharedModule, AST};
use crate::{Engine, Position, RhaiResultOf, Scope, SharedModule, AST};
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
@ -27,15 +27,17 @@ pub trait ModuleResolver: SendSync {
pos: Position,
) -> RhaiResultOf<SharedModule>;
/// Resolve a module based on a path string, given a [`GlobalRuntimeState`].
/// Resolve a module based on a path string, given a [`GlobalRuntimeState`] and the current [`Scope`].
///
/// # WARNING - Low Level API
///
/// This function is very low level.
#[allow(unused_variables)]
fn resolve_raw(
&self,
engine: &Engine,
global: &mut GlobalRuntimeState,
scope: &mut Scope,
path: &str,
pos: Position,
) -> RhaiResultOf<SharedModule> {

View File

@ -73,12 +73,8 @@ impl StaticModuleResolver {
#[inline(always)]
#[must_use]
pub fn contains_path(&self, path: &str) -> bool {
if self.0.is_empty() {
false
} else {
self.0.contains_key(path)
}
}
/// Get an iterator of all the [modules][Module].
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (&str, &SharedModule)> {
@ -123,9 +119,7 @@ impl StaticModuleResolver {
/// Existing modules of the same path name are overwritten.
#[inline]
pub fn merge(&mut self, other: Self) -> &mut Self {
if !other.is_empty() {
self.0.extend(other.0.into_iter());
}
self
}
}

View File

@ -5,7 +5,9 @@ use crate::ast::{
ASTFlags, Expr, FlowControl, OpAssignment, Stmt, StmtBlock, StmtBlockContainer,
SwitchCasesCollection,
};
use crate::engine::{KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_PRINT, KEYWORD_TYPE_OF};
use crate::engine::{
KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_PRINT, KEYWORD_TYPE_OF, OP_NOT,
};
use crate::eval::{Caches, GlobalRuntimeState};
use crate::func::builtin::get_builtin_binary_op_fn;
use crate::func::hashing::get_hasher;
@ -25,9 +27,6 @@ use std::{
mem,
};
/// Standard not operator.
const OP_NOT: &str = Token::Bang.literal_syntax();
/// Level of optimization performed.
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
#[non_exhaustive]
@ -564,16 +563,14 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
}
// Then check ranges
if value.is_int() && !ranges.is_empty() {
let value = value.as_int().unwrap();
if !ranges.is_empty() {
// Only one range or all ranges without conditions
if ranges.len() == 1
|| ranges
.iter()
.all(|r| expressions[r.index()].is_always_true())
{
if let Some(r) = ranges.iter().find(|r| r.contains(value)) {
if let Some(r) = ranges.iter().find(|r| r.contains(&value)) {
let range_block = &mut expressions[r.index()];
if range_block.is_always_true() {
@ -620,7 +617,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
let old_ranges_len = ranges.len();
ranges.retain(|r| r.contains(value));
ranges.retain(|r| r.contains(&value));
if ranges.len() != old_ranges_len {
state.set_dirty();
@ -1140,13 +1137,9 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
_ if x.args.len() == 2 && x.op_token.is_some() && (state.engine.fast_operators() || !state.engine.has_native_fn_override(x.hashes.native(), &arg_types)) => {
if let Some(result) = get_builtin_binary_op_fn(x.op_token.as_ref().unwrap(), &arg_values[0], &arg_values[1])
.and_then(|(f, ctx)| {
let context = if ctx {
Some((state.engine, x.name.as_str(), None, &state.global, *pos).into())
} else {
None
};
let context = ctx.then(|| (state.engine, x.name.as_str(), None, &state.global, *pos).into());
let (first, second) = arg_values.split_first_mut().unwrap();
(f)(context, &mut [ first, &mut second[0] ]).ok()
f(context, &mut [ first, &mut second[0] ]).ok()
}) {
state.set_dirty();
*expr = Expr::from_dynamic(result, *pos);

View File

@ -5,6 +5,7 @@ use crate::engine::OP_EQUALS;
use crate::eval::{calc_index, calc_offset_len};
use crate::module::ModuleFlags;
use crate::plugin::*;
use crate::{
def_package, Array, Dynamic, ExclusiveRange, FnPtr, InclusiveRange, NativeCallContext,
Position, RhaiResultOf, StaticVec, ERR, INT, MAX_USIZE_INT,
@ -237,7 +238,7 @@ pub mod array_functions {
#[cfg(not(feature = "unchecked"))]
if _ctx.engine().max_array_size() > 0 {
let pad = len - array.len();
let (a, m, s) = Dynamic::calc_array_sizes(array);
let (a, m, s) = crate::eval::calc_array_sizes(array);
let (ax, mx, sx) = item.calc_data_sizes(true);
_ctx.engine()

View File

@ -7,6 +7,7 @@ use crate::{Dynamic, RhaiResultOf, ERR, INT};
use std::prelude::v1::*;
#[cfg(not(feature = "no_float"))]
#[cfg(not(feature = "no_std"))]
use crate::FLOAT;
def_package! {
@ -287,7 +288,8 @@ fn collect_fn_metadata(
#[cfg(not(feature = "no_module"))]
{
use crate::{tokenizer::Token::DoubleColon, Shared, SmartString};
use crate::engine::NAMESPACE_SEPARATOR;
use crate::{Shared, SmartString};
// Recursively scan modules for script-defined functions.
fn scan_module(
@ -305,7 +307,7 @@ fn collect_fn_metadata(
use std::fmt::Write;
let mut ns = SmartString::new_const();
write!(&mut ns, "{namespace}{}{name}", DoubleColon.literal_syntax()).unwrap();
write!(&mut ns, "{namespace}{}{name}", NAMESPACE_SEPARATOR).unwrap();
scan_module(engine, list, &ns, m, filter);
}
}

View File

@ -7,25 +7,24 @@ use crate::ast::{
FnCallHashes, Ident, Namespace, OpAssignment, RangeCase, ScriptFnDef, Stmt, StmtBlock,
StmtBlockContainer, SwitchCasesCollection,
};
use crate::engine::{Precedence, KEYWORD_THIS, OP_CONTAINS};
use crate::engine::{Precedence, KEYWORD_THIS, OP_CONTAINS, OP_NOT};
use crate::eval::{Caches, GlobalRuntimeState};
use crate::func::{hashing::get_hasher, StraightHashMap};
use crate::tokenizer::{
is_keyword_function, is_valid_function_name, is_valid_identifier, Token, TokenStream,
is_reserved_keyword_or_symbol, is_valid_function_name, is_valid_identifier, Token, TokenStream,
TokenizerControl,
};
use crate::types::dynamic::AccessMode;
use crate::types::dynamic::{AccessMode, Union};
use crate::types::StringsInterner;
use crate::{
calc_fn_hash, Dynamic, Engine, EvalAltResult, EvalContext, ExclusiveRange, FnArgsVec,
Identifier, ImmutableString, InclusiveRange, LexError, OptimizationLevel, ParseError, Position,
Scope, Shared, SmartString, StaticVec, AST, INT, PERR,
Scope, Shared, SmartString, StaticVec, AST, PERR,
};
use bitflags::bitflags;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
use std::{
collections::BTreeMap,
convert::TryFrom,
fmt,
hash::{Hash, Hasher},
@ -42,9 +41,6 @@ const SCOPE_SEARCH_BARRIER_MARKER: &str = "$ BARRIER $";
/// The message: `TokenStream` never ends
const NEVER_ENDS: &str = "`Token`";
/// Unroll `switch` ranges no larger than this.
const SMALL_SWITCH_RANGE: INT = 16;
/// _(internals)_ A type that encapsulates the current state of the parser.
/// Exported under the `internals` feature only.
pub struct ParseState<'e, 's> {
@ -218,11 +214,7 @@ impl<'e, 's> ParseState<'e, 's> {
self.allow_capture = true;
}
let index = if hit_barrier {
None
} else {
NonZeroUsize::new(index)
};
let index = (!hit_barrier).then(|| NonZeroUsize::new(index)).flatten();
(index, is_func_name)
}
@ -986,7 +978,7 @@ impl Engine {
settings.pos = eat_token(input, Token::MapStart);
let mut map = StaticVec::<(Ident, Expr)>::new();
let mut template = BTreeMap::<Identifier, crate::Dynamic>::new();
let mut template = std::collections::BTreeMap::<Identifier, crate::Dynamic>::new();
loop {
const MISSING_RBRACE: &str = "to end this object map literal";
@ -1121,7 +1113,7 @@ impl Engine {
}
let mut expressions = StaticVec::<ConditionalExpr>::new();
let mut cases = BTreeMap::<u64, CaseBlocksList>::new();
let mut cases = StraightHashMap::<CaseBlocksList>::default();
let mut ranges = StaticVec::<RangeCase>::new();
let mut def_case = None;
let mut def_case_pos = Position::NONE;
@ -1216,7 +1208,6 @@ impl Engine {
let stmt_block: StmtBlock = stmt.into();
(Expr::Stmt(stmt_block.into()), need_comma)
};
let has_condition = !matches!(condition, Expr::BoolConstant(true, ..));
expressions.push((condition, action_expr).into());
let index = expressions.len() - 1;
@ -1240,29 +1231,28 @@ impl Engine {
if let Some(mut r) = range_value {
if !r.is_empty() {
// Do not unroll ranges if there are previous non-unrolled ranges
if !has_condition && ranges.is_empty() && r.len() <= SMALL_SWITCH_RANGE
{
// Unroll small range
r.into_iter().for_each(|n| {
let hasher = &mut get_hasher();
Dynamic::from_int(n).hash(hasher);
cases
.entry(hasher.finish())
.and_modify(|cases| cases.push(index))
.or_insert_with(|| [index].into());
});
} else {
// Other range
r.set_index(index);
ranges.push(r);
}
}
continue;
}
if value.is_int() && !ranges.is_empty() {
return Err(PERR::WrongSwitchIntegerCase.into_err(expr.start_position()));
if !ranges.is_empty() {
let forbidden = match value {
Dynamic(Union::Int(..)) => true,
#[cfg(not(feature = "no_float"))]
Dynamic(Union::Float(..)) => true,
#[cfg(feature = "decimal")]
Dynamic(Union::Decimal(..)) => true,
_ => false,
};
if forbidden {
return Err(
PERR::WrongSwitchIntegerCase.into_err(expr.start_position())
);
}
}
let hasher = &mut get_hasher();
@ -1299,6 +1289,10 @@ impl Engine {
}
}
expressions.shrink_to_fit();
cases.shrink_to_fit();
ranges.shrink_to_fit();
let cases = SwitchCasesCollection {
expressions,
cases,
@ -1398,20 +1392,26 @@ impl Engine {
.into(),
)),
// Loops are allowed to act as expressions
Token::While | Token::Loop if settings.has_option(LangOptions::LOOP_EXPR) => {
Token::While | Token::Loop
if self.allow_looping() && settings.has_option(LangOptions::LOOP_EXPR) =>
{
Expr::Stmt(Box::new(
self.parse_while_loop(input, state, lib, settings.level_up()?)?
.into(),
))
}
Token::Do if settings.has_option(LangOptions::LOOP_EXPR) => Expr::Stmt(Box::new(
Token::Do if self.allow_looping() && settings.has_option(LangOptions::LOOP_EXPR) => {
Expr::Stmt(Box::new(
self.parse_do(input, state, lib, settings.level_up()?)?
.into(),
)),
Token::For if settings.has_option(LangOptions::LOOP_EXPR) => Expr::Stmt(Box::new(
))
}
Token::For if self.allow_looping() && settings.has_option(LangOptions::LOOP_EXPR) => {
Expr::Stmt(Box::new(
self.parse_for(input, state, lib, settings.level_up()?)?
.into(),
)),
))
}
// Switch statement is allowed to act as expressions
Token::Switch if settings.has_option(LangOptions::SWITCH_EXPR) => Expr::Stmt(Box::new(
self.parse_switch(input, state, lib, settings.level_up()?)?
@ -1665,7 +1665,9 @@ impl Engine {
match input.peek().expect(NEVER_ENDS).0 {
// Function call is allowed to have reserved keyword
Token::LeftParen | Token::Bang | Token::Unit if is_keyword_function(&s).0 => {
Token::LeftParen | Token::Bang | Token::Unit
if is_reserved_keyword_or_symbol(&s).1 =>
{
Expr::Variable(
(None, ns, 0, state.get_interned_string(*s)).into(),
None,
@ -1722,7 +1724,7 @@ impl Engine {
lib: &mut FnLib,
settings: ParseSettings,
mut lhs: Expr,
options: ChainingFlags,
_options: ChainingFlags,
) -> ParseResult<Expr> {
let mut settings = settings;
@ -1783,7 +1785,7 @@ impl Engine {
// Disallowed module separator
#[cfg(not(feature = "no_module"))]
(_, token @ Token::DoubleColon)
if options.contains(ChainingFlags::DISALLOW_NAMESPACES) =>
if _options.contains(ChainingFlags::DISALLOW_NAMESPACES) =>
{
return Err(LexError::ImproperSymbol(
token.literal_syntax().into(),
@ -1824,7 +1826,7 @@ impl Engine {
// Prevents capturing of the object properties as vars: xxx.<var>
state.allow_capture = false;
}
(Token::Reserved(s), ..) if is_keyword_function(s).1 => (),
(Token::Reserved(s), ..) if is_reserved_keyword_or_symbol(s).2 => (),
(Token::Reserved(s), pos) => {
return Err(PERR::Reserved(s.to_string()).into_err(*pos))
}
@ -2352,12 +2354,7 @@ impl Engine {
let op = op_token.to_string();
let hash = calc_fn_hash(None, &op, 2);
let is_valid_script_function = is_valid_function_name(&op);
let operator_token = if is_valid_script_function {
None
} else {
Some(op_token.clone())
};
let native_only = !is_valid_function_name(&op);
let mut args = FnArgsVec::new_const();
args.push(root);
@ -2369,7 +2366,7 @@ impl Engine {
name: state.get_interned_string(&op),
hashes: FnCallHashes::from_native_only(hash),
args,
op_token: operator_token,
op_token: native_only.then(|| op_token.clone()),
capture_parent_scope: false,
};
@ -2418,14 +2415,13 @@ impl Engine {
fn_call
} else {
// Put a `!` call in front
let op = Token::Bang.literal_syntax();
let mut args = FnArgsVec::new_const();
args.push(fn_call);
let not_base = FnCallExpr {
namespace: Namespace::NONE,
name: state.get_interned_string(op),
hashes: FnCallHashes::from_native_only(calc_fn_hash(None, op, 1)),
name: state.get_interned_string(OP_NOT),
hashes: FnCallHashes::from_native_only(calc_fn_hash(None, OP_NOT, 1)),
args,
op_token: Some(Token::Bang),
capture_parent_scope: false,
@ -2442,10 +2438,10 @@ impl Engine {
.and_then(|m| m.get(s.as_str()))
.map_or(false, Option::is_some) =>
{
op_base.hashes = if is_valid_script_function {
FnCallHashes::from_hash(calc_fn_hash(None, &s, 2))
} else {
op_base.hashes = if native_only {
FnCallHashes::from_native_only(calc_fn_hash(None, &s, 2))
} else {
FnCallHashes::from_hash(calc_fn_hash(None, &s, 2))
};
op_base.into_fn_call_expr(pos)
}
@ -3081,8 +3077,7 @@ impl Engine {
let (id, id_pos) = parse_var_name(input)?;
let (alias, alias_pos) = if match_token(input, Token::As).0 {
let (name, pos) = parse_var_name(input)?;
(Some(name), pos)
parse_var_name(input).map(|(name, pos)| (Some(name), pos))?
} else {
(None, Position::NONE)
};
@ -3788,7 +3783,7 @@ impl Engine {
(.., pos) => {
return Err(PERR::MissingToken(
Token::Pipe.into(),
"to close the parameters list of anonymous function".into(),
"to close the parameters list of anonymous function or closure".into(),
)
.into_err(pos))
}

View File

@ -13,6 +13,7 @@ fn check_struct_sizes() {
feature = "only_i32",
any(feature = "no_float", feature = "f32_float")
));
const WORD_SIZE: usize = size_of::<usize>();
assert_eq!(size_of::<Dynamic>(), if PACKED { 8 } else { 16 });
assert_eq!(size_of::<Option<Dynamic>>(), if PACKED { 8 } else { 16 });
@ -20,10 +21,7 @@ fn check_struct_sizes() {
size_of::<Position>(),
if cfg!(feature = "no_position") { 0 } else { 4 }
);
assert_eq!(
size_of::<tokenizer::Token>(),
if IS_32_BIT { 8 } else { 16 }
);
assert_eq!(size_of::<tokenizer::Token>(), 2 * WORD_SIZE);
assert_eq!(size_of::<ast::Expr>(), if PACKED { 12 } else { 16 });
assert_eq!(size_of::<Option<ast::Expr>>(), if PACKED { 12 } else { 16 });
assert_eq!(size_of::<ast::Stmt>(), if IS_32_BIT { 12 } else { 16 });
@ -34,40 +32,41 @@ fn check_struct_sizes() {
#[cfg(feature = "internals")]
{
assert_eq!(
size_of::<CallableFunction>(),
if IS_32_BIT { 12 } else { 24 }
);
assert_eq!(
size_of::<module::FuncInfo>(),
if IS_32_BIT { 16 } else { 32 }
);
assert_eq!(size_of::<CallableFunction>(), 3 * WORD_SIZE);
assert_eq!(size_of::<module::FuncInfo>(), 4 * WORD_SIZE);
}
// The following only on 64-bit platforms
if !cfg!(target_pointer_width = "64") {
return;
}
#[cfg(target_pointer_width = "64")]
{
assert_eq!(size_of::<Scope>(), 536);
assert_eq!(
size_of::<FnPtr>(),
if cfg!(feature = "no_function") {
72
80 - if cfg!(feature = "no_function") {
WORD_SIZE
} else {
80
0
}
);
assert_eq!(size_of::<LexError>(), 56);
assert_eq!(
size_of::<ParseError>(),
if cfg!(feature = "no_position") { 8 } else { 16 }
16 - if cfg!(feature = "no_position") {
WORD_SIZE
} else {
0
}
);
assert_eq!(size_of::<EvalAltResult>(), 64);
assert_eq!(
size_of::<NativeCallContext>(),
if cfg!(feature = "no_position") {
48
56 - if cfg!(feature = "no_position") {
WORD_SIZE
} else {
56
0
}
);
}
}

View File

@ -1,9 +1,6 @@
//! Main module defining the lexer and parser.
use crate::engine::{
Precedence, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL,
KEYWORD_FN_PTR_CURRY, KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_THIS, KEYWORD_TYPE_OF,
};
use crate::engine::Precedence;
use crate::func::native::OnParseTokenCallback;
use crate::{Engine, Identifier, LexError, Position, SmartString, StaticVec, INT, UNSIGNED_INT};
use smallvec::SmallVec;
@ -308,6 +305,348 @@ impl fmt::Display for Token {
}
}
// Table-driven keyword recognizer generated by GNU gperf on the file `tools/keywords.txt`.
//
// When adding new keywords, make sure to update `tools/keywords.txt` and re-generate this.
const MIN_KEYWORD_LEN: usize = 1;
const MAX_KEYWORD_LEN: usize = 8;
const MIN_KEYWORD_HASH_VALUE: usize = 1;
const MAX_KEYWORD_HASH_VALUE: usize = 152;
static KEYWORD_ASSOC_VALUES: [u8; 257] = [
153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 115, 153, 100, 153, 110,
105, 40, 80, 2, 20, 25, 125, 95, 15, 40, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 55,
35, 10, 5, 0, 30, 110, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 120, 105, 100, 85, 90, 153, 125, 5,
0, 125, 35, 10, 100, 153, 20, 0, 153, 10, 0, 45, 55, 0, 153, 50, 55, 5, 0, 153, 0, 0, 35, 153,
45, 50, 30, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153,
153,
];
static KEYWORDS_LIST: [(&str, Token); 153] = [
("", Token::EOF),
(">", Token::GreaterThan),
(">=", Token::GreaterThanEqualsTo),
(")", Token::RightParen),
("", Token::EOF),
("const", Token::Const),
("=", Token::Equals),
("==", Token::EqualsTo),
("continue", Token::Continue),
("", Token::EOF),
("catch", Token::Catch),
("<", Token::LessThan),
("<=", Token::LessThanEqualsTo),
("for", Token::For),
("loop", Token::Loop),
("", Token::EOF),
(".", Token::Period),
("<<", Token::LeftShift),
("<<=", Token::LeftShiftAssign),
("", Token::EOF),
("false", Token::False),
("*", Token::Multiply),
("*=", Token::MultiplyAssign),
("let", Token::Let),
("", Token::EOF),
("while", Token::While),
("+", Token::Plus),
("+=", Token::PlusAssign),
("", Token::EOF),
("", Token::EOF),
("throw", Token::Throw),
("}", Token::RightBrace),
(">>", Token::RightShift),
(">>=", Token::RightShiftAssign),
("", Token::EOF),
("", Token::EOF),
(";", Token::SemiColon),
("=>", Token::DoubleArrow),
("", Token::EOF),
("else", Token::Else),
("", Token::EOF),
("/", Token::Divide),
("/=", Token::DivideAssign),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("{", Token::LeftBrace),
("**", Token::PowerOf),
("**=", Token::PowerOfAssign),
("", Token::EOF),
("", Token::EOF),
("|", Token::Pipe),
("|=", Token::OrAssign),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
(":", Token::Colon),
("..", Token::ExclusiveRange),
("..=", Token::InclusiveRange),
("", Token::EOF),
("until", Token::Until),
("switch", Token::Switch),
#[cfg(not(feature = "no_function"))]
("private", Token::Private),
#[cfg(feature = "no_function")]
("", Token::EOF),
("try", Token::Try),
("true", Token::True),
("break", Token::Break),
("return", Token::Return),
#[cfg(not(feature = "no_function"))]
("fn", Token::Fn),
#[cfg(feature = "no_function")]
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
#[cfg(not(feature = "no_module"))]
("import", Token::Import),
#[cfg(feature = "no_module")]
("", Token::EOF),
#[cfg(not(feature = "no_object"))]
("?.", Token::Elvis),
#[cfg(feature = "no_object")]
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
#[cfg(not(feature = "no_module"))]
("export", Token::Export),
#[cfg(feature = "no_module")]
("", Token::EOF),
("in", Token::In),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("(", Token::LeftParen),
("||", Token::Or),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("^", Token::XOr),
("^=", Token::XOrAssign),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("_", Token::Underscore),
("::", Token::DoubleColon),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("-", Token::Minus),
("-=", Token::MinusAssign),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("]", Token::RightBracket),
("()", Token::Unit),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("&", Token::Ampersand),
("&=", Token::AndAssign),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("%", Token::Modulo),
("%=", Token::ModuloAssign),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("!", Token::Bang),
("!=", Token::NotEqualsTo),
("!in", Token::NotIn),
("", Token::EOF),
("", Token::EOF),
("[", Token::LeftBracket),
("if", Token::If),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
(",Token::", Token::Comma),
("do", Token::Do),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
#[cfg(not(feature = "no_module"))]
("as", Token::As),
#[cfg(feature = "no_module")]
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
#[cfg(not(feature = "no_index"))]
("?[", Token::QuestionBracket),
#[cfg(feature = "no_index")]
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("??", Token::DoubleQuestion),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("&&", Token::And),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
("#{", Token::MapStart),
];
// Table-driven reserved symbol recognizer generated by GNU gperf on the file `tools/reserved.txt`.
//
// When adding new reserved symbols, make sure to update `tools/reserved.txt` and re-generate this.
const MIN_RESERVED_LEN: usize = 1;
const MAX_RESERVED_LEN: usize = 10;
const MIN_RESERVED_HASH_VALUE: usize = 1;
const MAX_RESERVED_HASH_VALUE: usize = 112;
static RESERVED_ASSOC_VALUES: [u8; 256] = [
113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 35, 113, 45, 25, 113,
113, 113, 60, 55, 50, 50, 113, 15, 0, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
10, 85, 45, 5, 55, 50, 5, 113, 113, 113, 113, 113, 85, 113, 113, 113, 113, 113, 113, 113, 113,
113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 35, 113, 113, 113, 55, 113, 10, 40,
5, 0, 5, 35, 10, 5, 0, 113, 113, 20, 25, 5, 45, 0, 113, 0, 0, 0, 15, 30, 20, 25, 20, 113, 113,
20, 113, 0, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
];
static RESERVED_LIST: [(&str, bool, bool, bool); 113] = [
("", false, false, false),
("~", true, false, false),
("is", true, false, false),
("...", true, false, false),
("", false, false, false),
("print", true, true, false),
("@", true, false, false),
("private", cfg!(feature = "no_function"), false, false),
("", false, false, false),
("this", true, false, false),
("", false, false, false),
("thread", true, false, false),
("as", cfg!(feature = "no_module"), false, false),
("", false, false, false),
("", false, false, false),
("spawn", true, false, false),
("static", true, false, false),
(":=", true, false, false),
("===", true, false, false),
("case", true, false, false),
("super", true, false, false),
("shared", true, false, false),
("package", true, false, false),
("use", true, false, false),
("with", true, false, false),
("curry", true, true, true),
("$", true, false, false),
("type_of", true, true, true),
("nil", true, false, false),
("sync", true, false, false),
("yield", true, false, false),
("import", cfg!(feature = "no_module"), false, false),
("--", true, false, false),
("new", true, false, false),
("exit", true, false, false),
("async", true, false, false),
("export", cfg!(feature = "no_module"), false, false),
("!.", true, false, false),
("", false, false, false),
("call", true, true, true),
("match", true, false, false),
("", false, false, false),
("fn", cfg!(feature = "no_function"), false, false),
("var", true, false, false),
("null", true, false, false),
("await", true, false, false),
("#", true, false, false),
("default", true, false, false),
("!==", true, false, false),
("eval", true, true, false),
("debug", true, true, false),
("?", true, false, false),
("?.", cfg!(feature = "no_object"), false, false),
("", false, false, false),
("protected", true, false, false),
("", false, false, false),
("", false, false, false),
("go", true, false, false),
("", false, false, false),
("goto", true, false, false),
("", false, false, false),
("public", true, false, false),
("<-", true, false, false),
("", false, false, false),
("is_def_fn", cfg!(not(feature = "no_function")), true, false),
("is_def_var", true, true, false),
("", false, false, false),
("<|", true, false, false),
("::<", true, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("->", true, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("module", true, false, false),
("|>", true, false, false),
("", false, false, false),
("void", true, false, false),
("", false, false, false),
("", false, false, false),
("#!", true, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("?[", cfg!(feature = "no_index"), false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("Fn", true, true, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
(":;", true, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("++", true, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("*)", true, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("", false, false, false),
("(*", true, false, false),
];
impl Token {
/// Is the token a literal symbol?
#[must_use]
@ -529,101 +868,38 @@ impl Token {
}
/// Reverse lookup a symbol token from a piece of syntax.
#[inline]
#[must_use]
pub fn lookup_symbol_from_syntax(syntax: &str) -> Option<Self> {
#[allow(clippy::enum_glob_use)]
use Token::*;
// This implementation is based upon a pre-calculated table generated
// by GNU gperf on the list of keywords.
let utf8 = syntax.as_bytes();
let len = utf8.len();
let mut hash_val = len;
Some(match syntax {
"{" => LeftBrace,
"}" => RightBrace,
"(" => LeftParen,
")" => RightParen,
"[" => LeftBracket,
"]" => RightBracket,
"()" => Unit,
"+" => Plus,
"-" => Minus,
"*" => Multiply,
"/" => Divide,
";" => SemiColon,
":" => Colon,
"::" => DoubleColon,
"=>" => DoubleArrow,
"_" => Underscore,
"," => Comma,
"." => Period,
#[cfg(not(feature = "no_object"))]
"?." => Elvis,
"??" => DoubleQuestion,
#[cfg(not(feature = "no_index"))]
"?[" => QuestionBracket,
".." => ExclusiveRange,
"..=" => InclusiveRange,
"#{" => MapStart,
"=" => Equals,
"true" => True,
"false" => False,
"let" => Let,
"const" => Const,
"if" => If,
"else" => Else,
"switch" => Switch,
"do" => Do,
"while" => While,
"until" => Until,
"loop" => Loop,
"for" => For,
"in" => In,
"!in" => NotIn,
"<" => LessThan,
">" => GreaterThan,
"!" => Bang,
"<=" => LessThanEqualsTo,
">=" => GreaterThanEqualsTo,
"==" => EqualsTo,
"!=" => NotEqualsTo,
"|" => Pipe,
"||" => Or,
"&" => Ampersand,
"&&" => And,
"continue" => Continue,
"break" => Break,
"return" => Return,
"throw" => Throw,
"try" => Try,
"catch" => Catch,
"+=" => PlusAssign,
"-=" => MinusAssign,
"*=" => MultiplyAssign,
"/=" => DivideAssign,
"<<=" => LeftShiftAssign,
">>=" => RightShiftAssign,
"&=" => AndAssign,
"|=" => OrAssign,
"^=" => XOrAssign,
"<<" => LeftShift,
">>" => RightShift,
"^" => XOr,
"%" => Modulo,
"%=" => ModuloAssign,
"**" => PowerOf,
"**=" => PowerOfAssign,
if !(MIN_KEYWORD_LEN..=MAX_KEYWORD_LEN).contains(&len) {
return None;
}
#[cfg(not(feature = "no_function"))]
"fn" => Fn,
#[cfg(not(feature = "no_function"))]
"private" => Private,
match len {
1 => (),
_ => hash_val += KEYWORD_ASSOC_VALUES[(utf8[1] as usize) + 1] as usize,
}
hash_val += KEYWORD_ASSOC_VALUES[utf8[0] as usize] as usize;
#[cfg(not(feature = "no_module"))]
"import" => Import,
#[cfg(not(feature = "no_module"))]
"export" => Export,
#[cfg(not(feature = "no_module"))]
"as" => As,
if !(MIN_KEYWORD_HASH_VALUE..=MAX_KEYWORD_HASH_VALUE).contains(&hash_val) {
return None;
}
_ => return None,
})
match KEYWORDS_LIST[hash_val] {
(_, Token::EOF) => None,
// Fail early to avoid calling memcmp()
// Since we are already working with bytes, mind as well check the first one
(s, ref t) if s.len() == len && s.as_bytes()[0] == utf8[0] && s == syntax => {
Some(t.clone())
}
_ => None,
}
}
/// If another operator is after these, it's probably a unary operator
@ -932,7 +1208,7 @@ pub fn parse_string_literal(
}
loop {
assert!(
debug_assert!(
!verbatim || escape.is_empty(),
"verbatim strings should not have any escapes"
);
@ -1229,11 +1505,7 @@ fn get_next_token_inner(
// Still inside a comment?
if state.comment_level > 0 {
let start_pos = *pos;
let mut comment = if state.include_comments {
Some(String::new())
} else {
None
};
let mut comment = state.include_comments.then(|| String::new());
state.comment_level =
scan_block_comment(stream, state.comment_level, pos, comment.as_mut());
@ -1273,8 +1545,10 @@ fn get_next_token_inner(
pos.advance();
let start_pos = *pos;
let cc = stream.peek_next().unwrap_or('\0');
match (c, stream.peek_next().unwrap_or('\0')) {
// Identifiers and strings that can have non-ASCII characters
match (c, cc) {
// \n
('\n', ..) => pos.new_line(),
@ -1438,16 +1712,6 @@ fn get_next_token_inner(
return Some((token, num_pos));
}
// letter or underscore ...
#[cfg(not(feature = "unicode-xid-ident"))]
('a'..='z' | '_' | 'A'..='Z', ..) => {
return Some(parse_identifier_token(stream, state, pos, start_pos, c));
}
#[cfg(feature = "unicode-xid-ident")]
(ch, ..) if unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_' => {
return Some(parse_identifier_token(stream, state, pos, start_pos, c));
}
// " - string literal
('"', ..) => {
return parse_string_literal(stream, state, pos, c, false, true, false)
@ -1919,11 +2183,16 @@ fn get_next_token_inner(
}
('?', ..) => return Some((Token::Reserved(Box::new("?".into())), start_pos)),
(ch, ..) if ch.is_whitespace() => (),
// letter or underscore ...
_ if is_id_first_alphabetic(c) || c == '_' => {
return Some(parse_identifier_token(stream, state, pos, start_pos, c));
}
(ch, ..) => {
_ if c.is_whitespace() => (),
_ => {
return Some((
Token::LexError(LERR::UnexpectedInput(ch.to_string()).into()),
Token::LexError(LERR::UnexpectedInput(c.to_string()).into()),
start_pos,
))
}
@ -1967,7 +2236,7 @@ fn parse_identifier_token(
return (token, start_pos);
}
if is_reserved_keyword_or_symbol(&identifier) {
if is_reserved_keyword_or_symbol(&identifier).0 {
return (Token::Reserved(Box::new(identifier)), start_pos);
}
@ -1981,30 +2250,6 @@ fn parse_identifier_token(
(Token::Identifier(identifier.into()), start_pos)
}
/// Can a keyword be called like a function?
///
/// # Return values
///
/// The first `bool` indicates whether the keyword can be called normally as a function.
///
/// The second `bool` indicates whether the keyword can be called in method-call style.
#[inline]
#[must_use]
pub fn is_keyword_function(name: &str) -> (bool, bool) {
match name {
KEYWORD_TYPE_OF | KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY => (true, true),
KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_EVAL | KEYWORD_FN_PTR | KEYWORD_IS_DEF_VAR => {
(true, false)
}
#[cfg(not(feature = "no_function"))]
crate::engine::KEYWORD_IS_DEF_FN => (true, false),
_ => (false, false),
}
}
/// _(internals)_ Is a text string a valid identifier?
/// Exported under the `internals` feature only.
#[must_use]
@ -2030,72 +2275,71 @@ pub fn is_valid_identifier(name: &str) -> bool {
#[must_use]
pub fn is_valid_function_name(name: &str) -> bool {
is_valid_identifier(name)
&& !is_reserved_keyword_or_symbol(name)
&& !is_reserved_keyword_or_symbol(name).0
&& Token::lookup_symbol_from_syntax(name).is_none()
}
/// Is a character valid to start an identifier?
#[cfg(feature = "unicode-xid-ident")]
#[inline(always)]
#[must_use]
pub fn is_id_first_alphabetic(x: char) -> bool {
unicode_xid::UnicodeXID::is_xid_start(x)
#[cfg(feature = "unicode-xid-ident")]
return unicode_xid::UnicodeXID::is_xid_start(x);
#[cfg(not(feature = "unicode-xid-ident"))]
return x.is_ascii_alphabetic();
}
/// Is a character valid for an identifier?
#[cfg(feature = "unicode-xid-ident")]
#[inline(always)]
#[must_use]
pub fn is_id_continue(x: char) -> bool {
unicode_xid::UnicodeXID::is_xid_continue(x)
#[cfg(feature = "unicode-xid-ident")]
return unicode_xid::UnicodeXID::is_xid_continue(x);
#[cfg(not(feature = "unicode-xid-ident"))]
return x.is_ascii_alphanumeric() || x == '_';
}
/// Is a character valid to start an identifier?
#[cfg(not(feature = "unicode-xid-ident"))]
#[inline(always)]
/// Is a piece of syntax a reserved keyword or reserved symbol?
///
/// # Return values
///
/// The first `bool` indicates whether it is a reserved keyword or symbol.
///
/// The second `bool` indicates whether the keyword can be called normally as a function.
///
/// The third `bool` indicates whether the keyword can be called in method-call style.
#[inline]
#[must_use]
pub const fn is_id_first_alphabetic(x: char) -> bool {
x.is_ascii_alphabetic()
}
pub fn is_reserved_keyword_or_symbol(syntax: &str) -> (bool, bool, bool) {
// This implementation is based upon a pre-calculated table generated
// by GNU gperf on the list of keywords.
let utf8 = syntax.as_bytes();
let len = utf8.len();
let rounds = len.min(3);
let mut hash_val = len;
/// Is a character valid for an identifier?
#[cfg(not(feature = "unicode-xid-ident"))]
#[inline(always)]
#[must_use]
pub const fn is_id_continue(x: char) -> bool {
x.is_ascii_alphanumeric() || x == '_'
}
if !(MIN_RESERVED_LEN..=MAX_RESERVED_LEN).contains(&len) {
return (false, false, false);
}
/// Is a piece of syntax a reserved keyword or symbol?
#[must_use]
pub fn is_reserved_keyword_or_symbol(syntax: &str) -> bool {
match syntax {
#[cfg(feature = "no_object")]
"?." => true,
#[cfg(feature = "no_index")]
"?[" => true,
#[cfg(feature = "no_function")]
"fn" | "private" => true,
#[cfg(feature = "no_module")]
"import" | "export" | "as" => true,
for x in 0..rounds {
hash_val += RESERVED_ASSOC_VALUES[utf8[rounds - 1 - x] as usize] as usize;
}
// List of reserved operators
"===" | "!==" | "->" | "<-" | "?" | ":=" | ":;" | "~" | "!." | "::<" | "(*" | "*)"
| "#" | "#!" | "@" | "$" | "++" | "--" | "..." | "<|" | "|>" => true,
if !(MIN_RESERVED_HASH_VALUE..=MAX_RESERVED_HASH_VALUE).contains(&hash_val) {
return (false, false, false);
}
// List of reserved keywords
"public" | "protected" | "super" | "new" | "use" | "module" | "package" | "var"
| "static" | "shared" | "with" | "is" | "goto" | "exit" | "match" | "case" | "default"
| "void" | "null" | "nil" | "spawn" | "thread" | "go" | "sync" | "async" | "await"
| "yield" => true,
KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_TYPE_OF | KEYWORD_EVAL | KEYWORD_FN_PTR
| KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY | KEYWORD_THIS | KEYWORD_IS_DEF_VAR => true,
#[cfg(not(feature = "no_function"))]
crate::engine::KEYWORD_IS_DEF_FN => true,
_ => false,
match RESERVED_LIST[hash_val] {
("", ..) => (false, false, false),
(s, true, a, b) => (
// Fail early to avoid calling memcmp()
// Since we are already working with bytes, mind as well check the first one
s.len() == len && s.as_bytes()[0] == utf8[0] && s == syntax,
a,
b,
),
_ => (false, false, false),
}
}

7
src/tools/README.md Normal file
View File

@ -0,0 +1,7 @@
Build Tools
===========
| File | Description |
| -------------- | ------------------------------------------- |
| `keywords.txt` | Input file for GNU gperf for the tokenizer. |
| `reserved.txt` | Input file for GNU gperf for the tokenizer. |

102
src/tools/keywords.txt Normal file
View File

@ -0,0 +1,102 @@
// This file holds a list of keywords/symbols for the Rhai language, with mapping to
// an appropriate `Token` variant.
//
// Generate the output table via:
// ```bash
// gperf -t keywords.txt
// ```
//
// Since GNU gperf does not produce Rust output, the ANSI-C output must be hand-edited and
// manually spliced into `tokenizer.rs`.
//
// This includes:
// * Rewrite the C hashing program (especially since it uses a `switch` statement with fall-through)
// into equivalent Rust as the function `lookup_symbol_from_syntax`.
// * Update the values for the `???_KEYWORD_???` constants.
// * Copy the `asso_values` array into `KEYWORD_ASSOC_VALUES`.
// * Copy the `wordlist` array into `KEYWORDS_LIST` with the following modifications:
// - Remove the `#line` comments
// - Change the entry wrapping `{ .. }` into tuples `( .. )`
// - Replace all entries `("")` by `("", Token::EOF)`
// - Put feature flags on the appropriate lines, and duplicating lines that maps to `Token::EOF`
// for the opposite feature flags
//
struct keyword;
%%
"{", Token::LeftBrace
"}", Token::RightBrace
"(", Token::LeftParen
")", Token::RightParen
"[", Token::LeftBracket
"]", Token::RightBracket
"()", Token::Unit
"+", Token::Plus
"-", Token::Minus
"*", Token::Multiply
"/", Token::Divide
";", Token::SemiColon
":", Token::Colon
"::", Token::DoubleColon
"=>", Token::DoubleArrow
"_", Token::Underscore
",", Token::Comma
".", Token::Period
"?.", Token::Elvis
"??", Token::DoubleQuestion
"?[", Token::QuestionBracket
"..", Token::ExclusiveRange
"..=", Token::InclusiveRange
"#{", Token::MapStart
"=", Token::Equals
"true", Token::True
"false", Token::False
"let", Token::Let
"const", Token::Const
"if", Token::If
"else", Token::Else
"switch", Token::Switch
"do", Token::Do
"while", Token::While
"until", Token::Until
"loop", Token::Loop
"for", Token::For
"in", Token::In
"!in", Token::NotIn
"<", Token::LessThan
">", Token::GreaterThan
"<=", Token::LessThanEqualsTo
">=", Token::GreaterThanEqualsTo
"==", Token::EqualsTo
"!=", Token::NotEqualsTo
"!", Token::Bang
"|", Token::Pipe
"||", Token::Or
"&", Token::Ampersand
"&&", Token::And
"continue", Token::Continue
"break", Token::Break
"return", Token::Return
"throw", Token::Throw
"try", Token::Try
"catch", Token::Catch
"+=", Token::PlusAssign
"-=", Token::MinusAssign
"*=", Token::MultiplyAssign
"/=", Token::DivideAssign
"<<=", Token::LeftShiftAssign
">>=", Token::RightShiftAssign
"&=", Token::AndAssign
"|=", Token::OrAssign
"^=", Token::XOrAssign
"<<", Token::LeftShift
">>", Token::RightShift
"^", Token::XOr
"%", Token::Modulo
"%=", Token::ModuloAssign
"**", Token::PowerOf
"**=", Token::PowerOfAssign
"fn", Token::Fn
"private", Token::Private
"import", Token::Import
"export", Token::Export
"as", Token::As

93
src/tools/reserved.txt Normal file
View File

@ -0,0 +1,93 @@
// This file holds a list of reserved symbols for the Rhai language.
//
// The mapped attributes are:
// - is this a reserved symbol? (bool)
// - can this keyword be called normally as a function? (bool)
// - can this keyword be called in method-call style? (bool)
//
// Generate the output table via:
// ```bash
// gperf -t reserved.txt
// ```
//
// Since GNU gperf does not produce Rust output, the ANSI-C output must be hand-edited and
// manually spliced into `tokenizer.rs`.
//
// This includes:
// * Rewrite the C hashing program (especially since it uses a `switch` statement with fall-through)
// into equivalent Rust as the function `is_reserved_keyword_or_symbol`.
// * Update the values for the `???_RESERVED_???` constants.
// * Copy the `asso_values` array into `RESERVED_ASSOC_VALUES`.
// * Copy the `wordlist` array into `RESERVED_LIST` with the following modifications:
// - Remove the `#line` comments
// - Change the entry wrapping `{ .. }` into tuples `( .. )`
// - Replace all entries `("")` by `("", false, false, false)`
// - Feature flags can be incorporated directly into the output via the `cfg!` macro
//
struct reserved;
%%
"?.", cfg!(feature = "no_object"), false, false
"?[", cfg!(feature = "no_index"), false, false
"fn", cfg!(feature = "no_function"), false, false
"private", cfg!(feature = "no_function"), false, false
"import", cfg!(feature = "no_module"), false, false
"export", cfg!(feature = "no_module"), false, false
"as", cfg!(feature = "no_module"), false, false
"===", true, false, false
"!==", true, false, false
"->", true, false, false
"<-", true, false, false
"?", true, false, false
":=", true, false, false
":;", true, false, false
"~", true, false, false
"!.", true, false, false
"::<", true, false, false
"(*", true, false, false
"*)", true, false, false
"#", true, false, false
"#!", true, false, false
"@", true, false, false
"$", true, false, false
"++", true, false, false
"--", true, false, false
"...", true, false, false
"<|", true, false, false
"|>", true, false, false
"public", true, false, false
"protected", true, false, false
"super", true, false, false
"new", true, false, false
"use", true, false, false
"module", true, false, false
"package", true, false, false
"var", true, false, false
"static", true, false, false
"shared", true, false, false
"with", true, false, false
"is", true, false, false
"goto", true, false, false
"exit", true, false, false
"match", true, false, false
"case", true, false, false
"default", true, false, false
"void", true, false, false
"null", true, false, false
"nil", true, false, false
"spawn", true, false, false
"thread", true, false, false
"go", true, false, false
"sync", true, false, false
"async", true, false, false
"await", true, false, false
"yield", true, false, false
"print", true, true, false
"debug", true, true, false
"type_of", true, true, true
"eval", true, true, false
"Fn", true, true, false
"call", true, true, true
"curry", true, true, true
"this", true, false, false
"is_def_var", true, true, false
"is_def_fn", cfg!(not(feature = "no_function")), true, false

View File

@ -538,7 +538,7 @@ impl TryFrom<ImmutableString> for FnPtr {
#[cfg(not(feature = "no_function"))]
fn_def: None,
})
} else if is_reserved_keyword_or_symbol(&value)
} else if is_reserved_keyword_or_symbol(&value).0
|| Token::lookup_symbol_from_syntax(&value).is_some()
{
Err(

View File

@ -105,7 +105,7 @@ pub enum ParseErrorType {
DuplicatedSwitchCase,
/// A variable name is duplicated. Wrapped value is the variable name.
DuplicatedVariable(String),
/// An integer case of a `switch` statement is in an appropriate place.
/// A numeric case of a `switch` statement is in an appropriate place.
WrongSwitchIntegerCase,
/// The default case of a `switch` statement is in an appropriate place.
WrongSwitchDefaultCase,
@ -236,7 +236,7 @@ impl fmt::Display for ParseErrorType {
Self::Reserved(s) if is_valid_identifier(s.as_str()) => write!(f, "'{s}' is a reserved keyword"),
Self::Reserved(s) => write!(f, "'{s}' is a reserved symbol"),
Self::UnexpectedEOF => f.write_str("Script is incomplete"),
Self::WrongSwitchIntegerCase => f.write_str("Integer switch case cannot follow a range case"),
Self::WrongSwitchIntegerCase => f.write_str("Numeric switch case cannot follow a range case"),
Self::WrongSwitchDefaultCase => f.write_str("Default switch case must be the last"),
Self::WrongSwitchCaseCondition => f.write_str("This switch case cannot have a condition"),
Self::PropertyExpected => f.write_str("Expecting name of a property"),

View File

@ -385,11 +385,23 @@ impl Scope<'_> {
/// ```
#[inline(always)]
pub fn pop(&mut self) -> &mut Self {
self.names.pop().expect("`Scope` must not be empty");
let _ = self.values.pop().expect("`Scope` must not be empty");
self.aliases.pop().expect("`Scope` must not be empty");
self.names.pop().expect("not empty");
let _ = self.values.pop().expect("not empty");
self.aliases.pop().expect("not empty");
self
}
/// Remove the last entry from the [`Scope`] and return it.
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn pop_entry(&mut self) -> Option<(Identifier, Dynamic, Vec<ImmutableString>)> {
self.values.pop().map(|value| {
(
self.names.pop().expect("not empty"),
value,
self.aliases.pop().expect("not empty"),
)
})
}
/// Truncate (rewind) the [`Scope`] to a previous size.
///
/// # Example

View File

@ -515,14 +515,12 @@ fn test_arrays_map_reduce() -> Result<(), Box<EvalAltResult>> {
3
);
engine
.eval::<()>(
engine.eval::<()>(
"
let x = [1, 2, 3, 2, 1];
x.find(|v| v > 4)
",
)
.unwrap();
)?;
assert_eq!(
engine.eval::<INT>(
@ -534,14 +532,12 @@ fn test_arrays_map_reduce() -> Result<(), Box<EvalAltResult>> {
2
);
engine
.eval::<()>(
engine.eval::<()>(
"
let x = [#{alice: 1}, #{bob: 2}, #{clara: 3}];
x.find_map(|v| v.dave)
",
)
.unwrap();
)?;
Ok(())
}
@ -550,7 +546,7 @@ fn test_arrays_map_reduce() -> Result<(), Box<EvalAltResult>> {
fn test_arrays_elvis() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
engine.eval::<()>("let x = (); x?[2]").unwrap();
engine.eval::<()>("let x = (); x?[2]")?;
engine.run("let x = (); x?[2] = 42")?;

View File

@ -4,8 +4,8 @@ use rhai::{Engine, EvalAltResult};
fn test_bool_op1() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert!(engine.eval::<bool>("true && (false || true)").unwrap());
assert!(engine.eval::<bool>("true & (false | true)").unwrap());
assert!(engine.eval::<bool>("true && (false || true)")?);
assert!(engine.eval::<bool>("true & (false | true)")?);
Ok(())
}
@ -14,8 +14,8 @@ fn test_bool_op1() -> Result<(), Box<EvalAltResult>> {
fn test_bool_op2() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert!(!engine.eval::<bool>("false && (false || true)").unwrap());
assert!(!engine.eval::<bool>("false & (false | true)").unwrap());
assert!(!engine.eval::<bool>("false && (false || true)")?);
assert!(!engine.eval::<bool>("false & (false | true)")?);
Ok(())
}
@ -25,9 +25,9 @@ fn test_bool_op3() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert!(engine.eval::<bool>("true && (false || 123)").is_err());
assert!(engine.eval::<bool>("true && (true || { throw })").unwrap());
assert!(engine.eval::<bool>("true && (true || { throw })")?);
assert!(engine.eval::<bool>("123 && (false || true)").is_err());
assert!(!engine.eval::<bool>("false && (true || { throw })").unwrap());
assert!(!engine.eval::<bool>("false && (true || { throw })")?);
Ok(())
}
@ -36,23 +36,19 @@ fn test_bool_op3() -> Result<(), Box<EvalAltResult>> {
fn test_bool_op_short_circuit() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert!(engine
.eval::<bool>(
assert!(engine.eval::<bool>(
"
let x = true;
x || { throw; };
"
)
.unwrap());
)?);
assert!(!engine
.eval::<bool>(
assert!(!engine.eval::<bool>(
"
let x = false;
x && { throw; };
"
)
.unwrap());
)?);
Ok(())
}

View File

@ -369,9 +369,9 @@ fn test_closures_external() -> Result<(), Box<EvalAltResult>> {
let fn_ptr = engine.eval_ast::<FnPtr>(&ast)?;
let f = move |x: INT| -> String { fn_ptr.call(&engine, &ast, (x,)).unwrap() };
let f = move |x: INT| fn_ptr.call::<String>(&engine, &ast, (x,));
assert_eq!(f(42), "hello42");
assert_eq!(f(42)?, "hello42");
Ok(())
}
@ -383,20 +383,20 @@ fn test_closures_callback() -> Result<(), Box<EvalAltResult>> {
type SingleNode = Rc<dyn Node>;
trait Node {
fn run(&self, x: INT) -> INT;
fn run(&self, x: INT) -> Result<INT, Box<EvalAltResult>>;
}
struct PhaserNode {
func: Box<dyn Fn(INT) -> INT>,
func: Box<dyn Fn(INT) -> Result<INT, Box<EvalAltResult>>>,
}
impl Node for PhaserNode {
fn run(&self, x: INT) -> INT {
fn run(&self, x: INT) -> Result<INT, Box<EvalAltResult>> {
(self.func)(x)
}
}
fn phaser(callback: impl Fn(INT) -> INT + 'static) -> impl Node {
fn phaser(callback: impl Fn(INT) -> Result<INT, Box<EvalAltResult>> + 'static) -> impl Node {
PhaserNode {
func: Box::new(callback),
}
@ -419,7 +419,7 @@ fn test_closures_callback() -> Result<(), Box<EvalAltResult>> {
let engine = engine2.clone();
let ast = ast2.clone();
let callback = Box::new(move |x: INT| fp.call(&engine.borrow(), &ast, (x,)).unwrap());
let callback = Box::new(move |x: INT| fp.call(&engine.borrow(), &ast, (x,)));
Rc::new(phaser(callback)) as SingleNode
});
@ -428,7 +428,7 @@ fn test_closures_callback() -> Result<(), Box<EvalAltResult>> {
let cb = shared_engine.borrow().eval_ast::<SingleNode>(&ast)?;
assert_eq!(cb.run(21), 42);
assert_eq!(cb.run(21)?, 42);
Ok(())
}

View File

@ -21,7 +21,7 @@ fn test_comments() -> Result<(), Box<EvalAltResult>> {
42
);
engine.run("/* Hello world */").unwrap();
engine.run("/* Hello world */")?;
Ok(())
}

View File

@ -5,8 +5,8 @@ fn test_or_equals() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert_eq!(engine.eval::<INT>("let x = 16; x |= 74; x")?, 90);
assert!(engine.eval::<bool>("let x = true; x |= false; x").unwrap());
assert!(engine.eval::<bool>("let x = false; x |= true; x").unwrap());
assert!(engine.eval::<bool>("let x = true; x |= false; x")?);
assert!(engine.eval::<bool>("let x = false; x |= true; x")?);
Ok(())
}
@ -16,9 +16,9 @@ fn test_and_equals() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert_eq!(engine.eval::<INT>("let x = 16; x &= 31; x")?, 16);
assert!(!engine.eval::<bool>("let x = true; x &= false; x").unwrap());
assert!(!engine.eval::<bool>("let x = false; x &= true; x").unwrap());
assert!(engine.eval::<bool>("let x = true; x &= true; x").unwrap());
assert!(!engine.eval::<bool>("let x = true; x &= false; x")?);
assert!(!engine.eval::<bool>("let x = false; x &= true; x")?);
assert!(engine.eval::<bool>("let x = true; x &= true; x")?);
Ok(())
}

View File

@ -251,6 +251,62 @@ fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[test]
fn test_custom_syntax_matrix() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.disable_symbol("|");
engine.register_custom_syntax(
[
"@", //
"|", "$expr$", "$expr$", "$expr$", "|", //
"|", "$expr$", "$expr$", "$expr$", "|", //
"|", "$expr$", "$expr$", "$expr$", "|",
],
false,
|context, inputs| {
let mut values = [[0; 3]; 3];
for y in 0..3 {
for x in 0..3 {
let offset = y * 3 + x;
match context.eval_expression_tree(&inputs[offset])?.as_int() {
Ok(v) => values[y][x] = v,
Err(typ) => {
return Err(Box::new(EvalAltResult::ErrorMismatchDataType(
"integer".to_string(),
typ.to_string(),
inputs[offset].position(),
)))
}
}
}
}
Ok(Dynamic::from(values))
},
)?;
let r = engine.eval::<[[INT; 3]; 3]>(
"
let a = 42;
let b = 123;
let c = 1;
let d = 99;
@| a b 0 |
| -b a 0 |
| 0 0 c*d |
",
)?;
assert_eq!(r, [[42, 123, 0], [-123, 42, 0], [0, 0, 99]]);
Ok(())
}
#[test]
fn test_custom_syntax_raw() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();

View File

@ -50,8 +50,8 @@ fn test_expressions() -> Result<(), Box<EvalAltResult>> {
"
switch x {
0 => 1,
1..10 => 123,
10 => 42,
1..10 => 123,
}
"
)?,
@ -63,11 +63,11 @@ fn test_expressions() -> Result<(), Box<EvalAltResult>> {
"
switch x {
0 => 1,
10 => 42,
1..10 => {
let y = 123;
y
}
10 => 42,
}
"
)

View File

@ -9,9 +9,7 @@ fn test_float() -> Result<(), Box<EvalAltResult>> {
assert!(engine.eval::<bool>("let x = 0.0; let y = 1.0; x < y")?);
assert!(!engine.eval::<bool>("let x = 0.0; let y = 1.0; x > y")?);
assert!(!engine
.eval::<bool>("let x = 0.; let y = 1.; x > y")
.unwrap());
assert!(!engine.eval::<bool>("let x = 0.; let y = 1.; x > y")?);
assert!((engine.eval::<FLOAT>("let x = 9.9999; x")? - 9.9999 as FLOAT).abs() < EPSILON);
Ok(())

View File

@ -399,11 +399,9 @@ fn test_get_set_indexer() -> Result<(), Box<EvalAltResult>> {
fn test_get_set_elvis() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
engine.eval::<()>("let x = (); x?.foo.bar.baz").unwrap();
engine.eval::<()>("let x = (); x?.foo(1,2,3)").unwrap();
engine
.eval::<()>("let x = #{a:()}; x.a?.foo.bar.baz")
.unwrap();
engine.eval::<()>("let x = (); x?.foo.bar.baz")?;
engine.eval::<()>("let x = (); x?.foo(1,2,3)")?;
engine.eval::<()>("let x = #{a:()}; x.a?.foo.bar.baz")?;
assert_eq!(engine.eval::<String>("let x = 'x'; x?.type_of()")?, "char");
Ok(())

View File

@ -20,7 +20,7 @@ fn test_loop() -> Result<(), Box<EvalAltResult>> {
}
}
return x;
x
"
)?,
21
@ -53,3 +53,43 @@ fn test_loop() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[test]
fn test_loop_expression() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
assert_eq!(
engine.eval::<INT>(
"
let x = 0;
let value = while x < 10 {
if x % 5 == 0 { break 42; }
x += 1;
};
value
"
)?,
42
);
engine.set_allow_loop_expressions(false);
assert!(engine
.eval::<INT>(
"
let x = 0;
let value = while x < 10 {
if x % 5 == 0 { break 42; }
x += 1;
};
value
"
)
.is_err());
Ok(())
}

View File

@ -39,7 +39,10 @@ fn test_mismatched_op_custom_type() -> Result<(), Box<EvalAltResult>> {
").expect_err("should error"),
EvalAltResult::ErrorFunctionNotFound(f, ..) if f == "== (TestStruct, TestStruct)"));
assert!(!engine.eval::<bool>("new_ts() == 42")?);
assert!(
matches!(*engine.eval::<bool>("new_ts() == 42").expect_err("should error"),
EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("== (TestStruct, "))
);
assert!(matches!(
*engine.eval::<INT>("60 + new_ts()").expect_err("should error"),

View File

@ -4,14 +4,12 @@ use rhai::{Engine, EvalAltResult};
fn test_not() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert!(!engine
.eval::<bool>("let not_true = !true; not_true")
.unwrap());
assert!(!engine.eval::<bool>("let not_true = !true; not_true")?);
#[cfg(not(feature = "no_function"))]
assert!(engine.eval::<bool>("fn not(x) { !x } not(false)").unwrap());
assert!(engine.eval::<bool>("fn not(x) { !x } not(false)")?);
assert!(engine.eval::<bool>("!!!!true").unwrap());
assert!(engine.eval::<bool>("!!!!true")?);
Ok(())
}

View File

@ -35,7 +35,11 @@ fn test_ops_other_number_types() -> Result<(), Box<EvalAltResult>> {
EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("== (u16,")
));
assert!(!engine.eval_with_scope::<bool>(&mut scope, r#"x == "hello""#)?);
assert!(
matches!(*engine.eval_with_scope::<bool>(&mut scope, r#"x == "hello""#).expect_err("should error"),
EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("== (u16,")
)
);
Ok(())
}
@ -63,3 +67,24 @@ fn test_ops_precedence() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[test]
fn test_ops_custom_types() -> Result<(), Box<EvalAltResult>> {
#[derive(Debug, Clone, PartialEq, Eq)]
struct Test1;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Test2;
let mut engine = Engine::new();
engine
.register_type_with_name::<Test1>("Test1")
.register_type_with_name::<Test2>("Test2")
.register_fn("new_ts1", || Test1)
.register_fn("new_ts2", || Test2)
.register_fn("==", |x: Test1, y: Test2| true);
assert!(engine.eval::<bool>("let x = new_ts1(); let y = new_ts2(); x == y")?);
Ok(())
}

View File

@ -89,21 +89,21 @@ fn test_optimizer_parse() -> Result<(), Box<EvalAltResult>> {
assert_eq!(
format!("{ast:?}"),
r#"AST { source: None, doc: "", resolver: None, body: [Expr(123 @ 1:53)] }"#
r#"AST { source: None, doc: None, resolver: None, body: [Expr(123 @ 1:53)] }"#
);
let ast = engine.compile("const DECISION = false; if DECISION { 42 } else { 123 }")?;
assert_eq!(
format!("{ast:?}"),
r#"AST { source: None, doc: "", resolver: None, body: [Var(("DECISION" @ 1:7, false @ 1:18, None), CONSTANT, 1:1), Expr(123 @ 1:51)] }"#
r#"AST { source: None, doc: None, resolver: None, body: [Var(("DECISION" @ 1:7, false @ 1:18, None), CONSTANT, 1:1), Expr(123 @ 1:51)] }"#
);
let ast = engine.compile("if 1 == 2 { 42 }")?;
assert_eq!(
format!("{ast:?}"),
r#"AST { source: None, doc: "", resolver: None, body: [] }"#
r#"AST { source: None, doc: None, resolver: None, body: [] }"#
);
engine.set_optimization_level(OptimizationLevel::Full);
@ -112,14 +112,14 @@ fn test_optimizer_parse() -> Result<(), Box<EvalAltResult>> {
assert_eq!(
format!("{ast:?}"),
r#"AST { source: None, doc: "", resolver: None, body: [Expr(42 @ 1:1)] }"#
r#"AST { source: None, doc: None, resolver: None, body: [Expr(42 @ 1:1)] }"#
);
let ast = engine.compile("NUMBER")?;
assert_eq!(
format!("{ast:?}"),
r#"AST { source: None, doc: "", resolver: None, body: [Expr(Variable(NUMBER) @ 1:1)] }"#
r#"AST { source: None, doc: None, resolver: None, body: [Expr(Variable(NUMBER) @ 1:1)] }"#
);
let mut module = Module::new();
@ -131,7 +131,7 @@ fn test_optimizer_parse() -> Result<(), Box<EvalAltResult>> {
assert_eq!(
format!("{ast:?}"),
r#"AST { source: None, doc: "", resolver: None, body: [Expr(42 @ 1:1)] }"#
r#"AST { source: None, doc: None, resolver: None, body: [Expr(42 @ 1:1)] }"#
);
Ok(())

View File

@ -10,9 +10,7 @@ fn test_switch() -> Result<(), Box<EvalAltResult>> {
engine.eval::<char>("switch 2 { 1 => (), 2 => 'a', 42 => true }")?,
'a'
);
engine
.run("switch 3 { 1 => (), 2 => 'a', 42 => true }")
.unwrap();
engine.run("switch 3 { 1 => (), 2 => 'a', 42 => true }")?;
assert_eq!(
engine.eval::<INT>("switch 3 { 1 => (), 2 => 'a', 42 => true, _ => 123 }")?,
123
@ -31,15 +29,13 @@ fn test_switch() -> Result<(), Box<EvalAltResult>> {
)?,
'a'
);
assert!(engine
.eval_with_scope::<bool>(&mut scope, "switch x { 1 => (), 2 => 'a', 42 => true }")
.unwrap());
assert!(engine
.eval_with_scope::<bool>(&mut scope, "switch x { 1 => (), 2 => 'a', _ => true }")
.unwrap());
let _: () = engine
.eval_with_scope::<()>(&mut scope, "switch x { 1 => 123, 2 => 'a' }")
.unwrap();
assert!(
engine.eval_with_scope::<bool>(&mut scope, "switch x { 1 => (), 2 => 'a', 42 => true }")?
);
assert!(
engine.eval_with_scope::<bool>(&mut scope, "switch x { 1 => (), 2 => 'a', _ => true }")?
);
let _: () = engine.eval_with_scope::<()>(&mut scope, "switch x { 1 => 123, 2 => 'a' }")?;
assert_eq!(
engine.eval_with_scope::<INT>(
@ -276,6 +272,13 @@ fn test_switch_ranges() -> Result<(), Box<EvalAltResult>> {
).expect_err("should error").err_type(),
ParseErrorType::WrongSwitchIntegerCase
));
#[cfg(not(feature = "no_float"))]
assert!(matches!(
engine.compile(
"switch x { 10..20 => (), 20..=42 => 'a', 25..45 => 'z', 42.0 => 'x', 30..100 => true }"
).expect_err("should error").err_type(),
ParseErrorType::WrongSwitchIntegerCase
));
assert_eq!(
engine.eval_with_scope::<char>(
&mut scope,

View File

@ -10,9 +10,7 @@ fn test_unit() -> Result<(), Box<EvalAltResult>> {
#[test]
fn test_unit_eq() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert!(engine
.eval::<bool>("let x = (); let y = (); x == y")
.unwrap());
assert!(engine.eval::<bool>("let x = (); let y = (); x == y")?);
Ok(())
}