Merge pull request #658 from schungx/master

Allow global functions in unaliased imports.
This commit is contained in:
Stephen Chung 2022-10-15 10:31:32 +08:00 committed by GitHub
commit 56427e1dcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 406 additions and 220 deletions

View File

@ -32,6 +32,7 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: check
# typical build with various feature combinations
build:
name: Build
@ -86,6 +87,7 @@ jobs:
with:
command: test
args: ${{matrix.flags}}
# no-std builds are a bit more extensive to test
no_std_build:
name: NoStdBuild
@ -110,32 +112,40 @@ jobs:
with:
command: build
args: --manifest-path=no_std/no_std_test/Cargo.toml ${{matrix.flags}}
wasm:
name: Check Wasm build
runs-on: ubuntu-latest
strategy:
matrix:
flags:
- "--target wasm32-wasi"
# These fail currently, future PR should fix them
# - ""
# - "--features wasm-bindgen"
- "--no-default-features"
- "--no-default-features --features wasm-bindgen"
# - "--target wasm32-unknown-unknown"
# - "--target wasm32-unknown-unknown --features wasm-bindgen"
- "--target wasm32-unknown-unknown --no-default-features"
- "--target wasm32-unknown-unknown --no-default-features --features wasm-bindgen"
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Toolchain
- name: Setup Generic Wasm Toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_MSRV }}
toolchain: stable
override: true
target: wasm32-unknown-unknown
- name: Setup Wasi Toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
target: wasm32-wasi
- name: Build
uses: actions-rs/cargo@v1
with:
command: build
args: --target wasm32-unknown-unknown ${{matrix.flags}}
args: ${{matrix.flags}}
rustfmt:
name: Check Formatting
@ -160,6 +170,7 @@ jobs:
with:
command: clippy
args: --all -- -Aclippy::all -Dclippy::perf
codegen_build:
name: Codegen Build
runs-on: ${{matrix.os}}

View File

@ -4,28 +4,51 @@ Rhai Release Notes
Version 1.11.0
==============
Bug fixes
---------
* `Engine::parse_json` now returns an error on unquoted keys to be consistent with JSON specifications.
* `import` statements inside `eval` no longer cause errors in subsequent code.
* Functions marked `global` in `import`ed modules with no alias names now work properly.
Speed Improvements
------------------
* Due to a code refactor, built-in operators for standard types now run even faster, in certain cases by 20-30%.
New features
------------
### Stable hashing
* It is now possible to specify a fixed _seed_ for use with the `ahash` hasher, via an environment variable, in order to force stable (i.e. deterministic) hashes for function signatures. This is necessary when using Rhai across shared-library boundaries.
* A build script is now used to extract the environment variable (`RHAI_AHASH_SEED`) and splice it into the source code before compilation.
* It is now possible to specify a fixed _seed_ for use with the `ahash` hasher, via an environment variable, in order to force stable (i.e. deterministic) hashes for function signatures.
* This is necessary when using Rhai across shared-library boundaries.
* A build script is used to extract the environment variable (`RHAI_AHASH_SEED`) and splice it into the source code before compilation.
Bug fixes
---------
### Serializable `Scope`
* `Engine::parse_json` now returns an error on unquoted keys to be consistent with JSON specifications.
* `Scope` is now serializable and deserializable via `serde`.
### Call native Rust functions in `NativeCallContext`
* `NativeCallContext::call_native_fn` is added to call registered native Rust functions only.
* `NativeCallContext::call_native_fn_raw` is added as the advanced version.
* This is often desirable as Rust functions typically do not want a similar-named scripted function to hijack the process -- which will cause brittleness.
### Custom syntax improvements
* The look-ahead symbol for custom syntax now renders a string literal in quotes (instead of the generic term `string`).
* This facilitates more accurate parsing by separating strings and identifiers.
### Limits API
* Methods returning maximum limits (e.g. `Engine::max_string_len`) are now available even under `unchecked`.
* This helps avoid the proliferation of unnecessary feature flags in third-party library code.
Enhancements
------------
* The look-ahead symbol for custom syntax now renders a string literal in quotes (instead of the generic term `string`). This facilitates more accurate parsing by separating strings and identifiers.
* Due to a code refactor, built-in operators for standard types now run even faster, in certain cases by 20-30%.
* `Scope` is now serializable and deserializable via `serde`.
* `Scope` now contains a const generic parameter that allows specifying how many entries to be kept inline.
* `parse_json` function is added to parse a JSON string into an object map.
* Methods returning maximum limits (e.g. `Engine::max_string_len`) are now available even under `unchecked` in order to avoid unnecessary feature flags in third-party library code.
Version 1.10.1

View File

@ -233,6 +233,7 @@ impl Engine {
arg_values,
)
}
/// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments.
fn _call_fn(
&self,

View File

@ -181,7 +181,7 @@ impl FnCallHashes {
/// _(internals)_ A function call.
/// Exported under the `internals` feature only.
#[derive(Clone, Default, Hash)]
#[derive(Clone, Hash)]
pub struct FnCallExpr {
/// Namespace of the function, if any.
#[cfg(not(feature = "no_module"))]
@ -196,6 +196,9 @@ pub struct FnCallExpr {
pub capture_parent_scope: bool,
/// Is this function call a native operator?
pub operator_token: Option<Token>,
/// Can this function call be a scripted function?
#[cfg(not(feature = "no_function"))]
pub can_be_script: bool,
/// [Position] of the function name.
pub pos: Position,
}
@ -215,6 +218,10 @@ impl fmt::Debug for FnCallExpr {
if let Some(ref token) = self.operator_token {
ff.field("operator_token", token);
}
#[cfg(not(feature = "no_function"))]
if self.can_be_script {
ff.field("can_be_script", &self.can_be_script);
}
ff.field("hash", &self.hashes)
.field("name", &self.name)
.field("args", &self.args);
@ -493,6 +500,10 @@ impl fmt::Debug for Expr {
}
}
f.write_str(&x.3)?;
#[cfg(not(feature = "no_module"))]
if let Some(n) = x.1.index() {
write!(f, " #{}", n)?;
}
if let Some(n) = i.map_or_else(|| x.0, |n| NonZeroUsize::new(n.get() as usize)) {
write!(f, " #{}", n)?;
}
@ -680,6 +691,8 @@ impl Expr {
args: once(Self::StringConstant(f.fn_name().into(), pos)).collect(),
capture_parent_scope: false,
operator_token: None,
#[cfg(not(feature = "no_function"))]
can_be_script: true,
pos,
}
.into(),

View File

@ -302,6 +302,10 @@ pub struct TryCatchBlock {
pub catch_block: StmtBlock,
}
/// Number of items to keep inline for [`StmtBlockContainer`].
#[cfg(not(feature = "no_std"))]
const STMT_BLOCK_INLINE_SIZE: usize = 8;
/// _(internals)_ The underlying container type for [`StmtBlock`].
/// Exported under the `internals` feature only.
///
@ -309,7 +313,7 @@ pub struct TryCatchBlock {
/// hold a statements block, with the assumption that most program blocks would container fewer than
/// 8 statements, and those that do have a lot more statements.
#[cfg(not(feature = "no_std"))]
pub type StmtBlockContainer = smallvec::SmallVec<[Stmt; 8]>;
pub type StmtBlockContainer = smallvec::SmallVec<[Stmt; STMT_BLOCK_INLINE_SIZE]>;
/// _(internals)_ The underlying container type for [`StmtBlock`].
/// Exported under the `internals` feature only.
@ -491,9 +495,9 @@ impl From<Stmt> for StmtBlock {
impl IntoIterator for StmtBlock {
type Item = Stmt;
#[cfg(not(feature = "no_std"))]
type IntoIter = smallvec::IntoIter<[Stmt; 8]>;
type IntoIter = smallvec::IntoIter<[Stmt; STMT_BLOCK_INLINE_SIZE]>;
#[cfg(feature = "no_std")]
type IntoIter = smallvec::IntoIter<[Stmt; 3]>;
type IntoIter = smallvec::IntoIter<[Stmt; crate::STATIC_VEC_INLINE_SIZE]>;
#[inline(always)]
fn into_iter(self) -> Self::IntoIter {

View File

@ -24,13 +24,13 @@ impl Engine {
let root = namespace.root();
// Qualified - check if the root module is directly indexed
let index = if global.always_search_scope {
None
} else {
namespace.index()
};
// Qualified - check if the root module is directly indexed
if let Some(index) = index {
let offset = global.num_imports() - index.get();
@ -223,9 +223,16 @@ impl Engine {
hashes,
args,
operator_token,
#[cfg(not(feature = "no_function"))]
can_be_script,
..
} = expr;
#[cfg(not(feature = "no_function"))]
let native = !can_be_script;
#[cfg(feature = "no_function")]
let native = true;
// Short-circuit native binary operator call if under Fast Operators mode
if operator_token.is_some() && self.fast_operators() && args.len() == 2 {
let mut lhs = self
@ -251,7 +258,8 @@ impl Engine {
return self
.exec_fn_call(
None, global, caches, lib, name, *hashes, operands, false, false, pos, level,
None, global, caches, lib, name, native, *hashes, operands, false, false, pos,
level,
)
.map(|(v, ..)| v);
}
@ -280,6 +288,7 @@ impl Engine {
lib,
this_ptr,
name,
native,
first_arg,
args,
*hashes,

View File

@ -23,12 +23,9 @@ pub type GlobalConstants =
// corresponds to that key.
#[derive(Clone)]
pub struct GlobalRuntimeState<'a> {
/// Stack of module names.
#[cfg(not(feature = "no_module"))]
keys: crate::StaticVec<crate::ImmutableString>,
/// Stack of imported [modules][crate::Module].
#[cfg(not(feature = "no_module"))]
modules: crate::StaticVec<crate::Shared<crate::Module>>,
modules: crate::StaticVec<(crate::ImmutableString, crate::Shared<crate::Module>)>,
/// Source of the current context.
///
/// No source if the string is empty.
@ -80,8 +77,6 @@ impl GlobalRuntimeState<'_> {
#[must_use]
pub fn new(engine: &Engine) -> Self {
Self {
#[cfg(not(feature = "no_module"))]
keys: crate::StaticVec::new_const(),
#[cfg(not(feature = "no_module"))]
modules: crate::StaticVec::new_const(),
source: Identifier::new_const(),
@ -123,7 +118,7 @@ impl GlobalRuntimeState<'_> {
#[inline(always)]
#[must_use]
pub fn num_imports(&self) -> usize {
self.keys.len()
self.modules.len()
}
/// Get the globally-imported [module][crate::Module] at a particular index.
///
@ -132,7 +127,7 @@ impl GlobalRuntimeState<'_> {
#[inline(always)]
#[must_use]
pub fn get_shared_import(&self, index: usize) -> Option<crate::Shared<crate::Module>> {
self.modules.get(index).cloned()
self.modules.get(index).map(|(_, m)| m).cloned()
}
/// Get a mutable reference to the globally-imported [module][crate::Module] at a
/// particular index.
@ -146,7 +141,7 @@ impl GlobalRuntimeState<'_> {
&mut self,
index: usize,
) -> Option<&mut crate::Shared<crate::Module>> {
self.modules.get_mut(index)
self.modules.get_mut(index).map(|(_, m)| m)
}
/// Get the index of a globally-imported [module][crate::Module] by name.
///
@ -155,12 +150,12 @@ impl GlobalRuntimeState<'_> {
#[inline]
#[must_use]
pub fn find_import(&self, name: &str) -> Option<usize> {
let len = self.keys.len();
let len = self.modules.len();
self.keys
self.modules
.iter()
.rev()
.position(|key| key.as_str() == name)
.position(|(key, _)| key.as_str() == name)
.map(|i| len - 1 - i)
}
/// Push an imported [module][crate::Module] onto the stack.
@ -173,8 +168,7 @@ impl GlobalRuntimeState<'_> {
name: impl Into<crate::ImmutableString>,
module: impl Into<crate::Shared<crate::Module>>,
) {
self.keys.push(name.into());
self.modules.push(module.into());
self.modules.push((name.into(), module.into()));
}
/// Truncate the stack of globally-imported [modules][crate::Module] to a particular length.
///
@ -182,7 +176,6 @@ impl GlobalRuntimeState<'_> {
#[cfg(not(feature = "no_module"))]
#[inline(always)]
pub fn truncate_imports(&mut self, size: usize) {
self.keys.truncate(size);
self.modules.truncate(size);
}
/// Get an iterator to the stack of globally-imported [modules][crate::Module] in reverse order.
@ -192,10 +185,9 @@ impl GlobalRuntimeState<'_> {
#[allow(dead_code)]
#[inline]
pub fn iter_imports(&self) -> impl Iterator<Item = (&str, &crate::Module)> {
self.keys
self.modules
.iter()
.rev()
.zip(self.modules.iter().rev())
.map(|(name, module)| (name.as_str(), &**module))
}
/// Get an iterator to the stack of globally-imported [modules][crate::Module] in reverse order.
@ -206,8 +198,8 @@ impl GlobalRuntimeState<'_> {
#[inline]
pub(crate) fn iter_imports_raw(
&self,
) -> impl Iterator<Item = (&crate::ImmutableString, &crate::Shared<crate::Module>)> {
self.keys.iter().rev().zip(self.modules.iter().rev())
) -> impl Iterator<Item = &(crate::ImmutableString, crate::Shared<crate::Module>)> {
self.modules.iter().rev()
}
/// Get an iterator to the stack of globally-imported [modules][crate::Module] in forward order.
///
@ -217,8 +209,8 @@ impl GlobalRuntimeState<'_> {
#[inline]
pub fn scan_imports_raw(
&self,
) -> impl Iterator<Item = (&crate::ImmutableString, &crate::Shared<crate::Module>)> {
self.keys.iter().zip(self.modules.iter())
) -> impl Iterator<Item = &(crate::ImmutableString, crate::Shared<crate::Module>)> {
self.modules.iter()
}
/// Does the specified function hash key exist in the stack of globally-imported
/// [modules][crate::Module]?
@ -229,7 +221,9 @@ impl GlobalRuntimeState<'_> {
#[inline]
#[must_use]
pub fn contains_qualified_fn(&self, hash: u64) -> bool {
self.modules.iter().any(|m| m.contains_qualified_fn(hash))
self.modules
.iter()
.any(|(_, m)| m.contains_qualified_fn(hash))
}
/// Get the specified function via its hash key from the stack of globally-imported
/// [modules][crate::Module].
@ -245,7 +239,7 @@ impl GlobalRuntimeState<'_> {
self.modules
.iter()
.rev()
.find_map(|m| m.get_qualified_fn(hash).map(|f| (f, m.id())))
.find_map(|(_, m)| m.get_qualified_fn(hash).map(|f| (f, m.id())))
}
/// Does the specified [`TypeId`][std::any::TypeId] iterator exist in the stack of
/// globally-imported [modules][crate::Module]?
@ -256,7 +250,9 @@ impl GlobalRuntimeState<'_> {
#[inline]
#[must_use]
pub fn contains_iter(&self, id: std::any::TypeId) -> bool {
self.modules.iter().any(|m| m.contains_qualified_iter(id))
self.modules
.iter()
.any(|(_, m)| m.contains_qualified_iter(id))
}
/// Get the specified [`TypeId`][std::any::TypeId] iterator from the stack of globally-imported
/// [modules][crate::Module].
@ -269,7 +265,7 @@ impl GlobalRuntimeState<'_> {
self.modules
.iter()
.rev()
.find_map(|m| m.get_qualified_iter(id))
.find_map(|(_, m)| m.get_qualified_iter(id))
}
/// Get the current source.
#[inline]
@ -312,29 +308,26 @@ impl GlobalRuntimeState<'_> {
#[cfg(not(feature = "no_module"))]
impl IntoIterator for GlobalRuntimeState<'_> {
type Item = (crate::ImmutableString, crate::Shared<crate::Module>);
type IntoIter = std::iter::Zip<
std::iter::Rev<smallvec::IntoIter<[crate::ImmutableString; 3]>>,
std::iter::Rev<smallvec::IntoIter<[crate::Shared<crate::Module>; 3]>>,
type IntoIter = std::iter::Rev<
smallvec::IntoIter<
[(crate::ImmutableString, crate::Shared<crate::Module>); crate::STATIC_VEC_INLINE_SIZE],
>,
>;
fn into_iter(self) -> Self::IntoIter {
self.keys
.into_iter()
.rev()
.zip(self.modules.into_iter().rev())
self.modules.into_iter().rev()
}
}
#[cfg(not(feature = "no_module"))]
impl<'a> IntoIterator for &'a GlobalRuntimeState<'_> {
type Item = (&'a crate::ImmutableString, &'a crate::Shared<crate::Module>);
type IntoIter = std::iter::Zip<
std::iter::Rev<std::slice::Iter<'a, crate::ImmutableString>>,
std::iter::Rev<std::slice::Iter<'a, crate::Shared<crate::Module>>>,
type Item = &'a (crate::ImmutableString, crate::Shared<crate::Module>);
type IntoIter = std::iter::Rev<
std::slice::Iter<'a, (crate::ImmutableString, crate::Shared<crate::Module>)>,
>;
fn into_iter(self) -> Self::IntoIter {
self.keys.iter().rev().zip(self.modules.iter().rev())
self.modules.iter().rev()
}
}
@ -345,8 +338,7 @@ impl<K: Into<crate::ImmutableString>, M: Into<crate::Shared<crate::Module>>> Ext
#[inline]
fn extend<T: IntoIterator<Item = (K, M)>>(&mut self, iter: T) {
for (k, m) in iter {
self.keys.push(k.into());
self.modules.push(m.into());
self.modules.push((k.into(), m.into()));
}
}
}
@ -358,7 +350,7 @@ impl fmt::Debug for GlobalRuntimeState<'_> {
let mut f = f.debug_struct("GlobalRuntimeState");
#[cfg(not(feature = "no_module"))]
f.field("imports", &self.keys.iter().zip(self.modules.iter()));
f.field("imports", &self.modules);
f.field("source", &self.source)
.field("num_operations", &self.num_operations);

View File

@ -952,15 +952,19 @@ impl Engine {
});
if let Ok(module) = module_result {
if !export.is_empty() {
if module.is_indexed() {
global.push_import(export.name.clone(), module);
} else {
// Index the module (making a clone copy if necessary) if it is not indexed
let mut m = crate::func::shared_take_or_clone(module);
m.build_index();
global.push_import(export.name.clone(), m);
}
let (export, must_be_indexed) = if !export.is_empty() {
(export.name.clone(), true)
} else {
(self.get_interned_string(""), false)
};
if !must_be_indexed || module.is_indexed() {
global.push_import(export, module);
} else {
// Index the module (making a clone copy if necessary) if it is not indexed
let mut m = crate::func::shared_take_or_clone(module);
m.build_index();
global.push_import(export, m);
}
global.num_modules_loaded += 1;

View File

@ -578,6 +578,7 @@ impl Engine {
caches: &mut Caches,
lib: &[&Module],
fn_name: &str,
_native_only: bool,
hashes: FnCallHashes,
args: &mut FnCallArgs,
is_ref_mut: bool,
@ -644,89 +645,90 @@ impl Engine {
let level = level + 1;
// Script-defined function call?
#[cfg(not(feature = "no_function"))]
let local_entry = &mut None;
if !_native_only {
// Script-defined function call?
let local_entry = &mut None;
#[cfg(not(feature = "no_function"))]
if let Some(FnResolutionCacheEntry { func, ref source }) = self
.resolve_fn(
global,
caches,
local_entry,
lib,
fn_name,
hashes.script,
None,
false,
None,
)
.cloned()
{
// Script function call
assert!(func.is_script());
let func = func.get_script_fn_def().expect("script-defined function");
if func.body.is_empty() {
return Ok((Dynamic::UNIT, false));
}
let mut empty_scope;
let scope = match _scope {
Some(scope) => scope,
None => {
empty_scope = Scope::new();
&mut empty_scope
}
};
let orig_source = mem::replace(
&mut global.source,
source
.as_ref()
.map_or(crate::Identifier::new_const(), |s| (**s).clone()),
);
let result = if _is_method_call {
// Method call of script function - map first argument to `this`
let (first_arg, rest_args) = args.split_first_mut().unwrap();
self.call_script_fn(
scope,
if let Some(FnResolutionCacheEntry { func, ref source }) = self
.resolve_fn(
global,
caches,
local_entry,
lib,
&mut Some(*first_arg),
func,
rest_args,
true,
pos,
level,
fn_name,
hashes.script,
None,
false,
None,
)
} else {
// Normal call of script function
let mut backup = ArgBackup::new();
.cloned()
{
// Script function call
assert!(func.is_script());
// The first argument is a reference?
if is_ref_mut && !args.is_empty() {
backup.change_first_arg_to_copy(args);
let func = func.get_script_fn_def().expect("script-defined function");
if func.body.is_empty() {
return Ok((Dynamic::UNIT, false));
}
let result = self.call_script_fn(
scope, global, caches, lib, &mut None, func, args, true, pos, level,
let mut empty_scope;
let scope = match _scope {
Some(scope) => scope,
None => {
empty_scope = Scope::new();
&mut empty_scope
}
};
let orig_source = mem::replace(
&mut global.source,
source
.as_ref()
.map_or(crate::Identifier::new_const(), |s| (**s).clone()),
);
// Restore the original reference
backup.restore_first_arg(args);
let result = if _is_method_call {
// Method call of script function - map first argument to `this`
let (first_arg, rest_args) = args.split_first_mut().unwrap();
result
};
self.call_script_fn(
scope,
global,
caches,
lib,
&mut Some(*first_arg),
func,
rest_args,
true,
pos,
level,
)
} else {
// Normal call of script function
let mut backup = ArgBackup::new();
// Restore the original source
global.source = orig_source;
// The first argument is a reference?
if is_ref_mut && !args.is_empty() {
backup.change_first_arg_to_copy(args);
}
return Ok((result?, false));
let result = self.call_script_fn(
scope, global, caches, lib, &mut None, func, args, true, pos, level,
);
// Restore the original reference
backup.restore_first_arg(args);
result
};
// Restore the original source
global.source = orig_source;
return Ok((result?, false));
}
}
// Native function call
@ -836,6 +838,7 @@ impl Engine {
caches,
lib,
fn_name,
false,
new_hash,
&mut args,
false,
@ -881,6 +884,7 @@ impl Engine {
caches,
lib,
fn_name,
false,
new_hash,
&mut args,
is_ref_mut,
@ -968,6 +972,7 @@ impl Engine {
caches,
lib,
fn_name,
false,
hash,
&mut args,
is_ref_mut,
@ -995,6 +1000,7 @@ impl Engine {
lib: &[&Module],
this_ptr: &mut Option<&mut Dynamic>,
fn_name: &str,
native_only: bool,
first_arg: Option<&Expr>,
args_expr: &[Expr],
hashes: FnCallHashes,
@ -1003,6 +1009,7 @@ impl Engine {
pos: Position,
level: usize,
) -> RhaiResult {
let native = native_only;
let mut first_arg = first_arg;
let mut a_expr = args_expr;
let mut total_args = if first_arg.is_some() { 1 } else { 0 } + a_expr.len();
@ -1137,25 +1144,25 @@ impl Engine {
KEYWORD_EVAL if total_args == 1 => {
// eval - only in function call style
let orig_scope_len = scope.len();
#[cfg(not(feature = "no_module"))]
let orig_imports_len = global.num_imports();
let arg = first_arg.unwrap();
let (arg_value, pos) =
self.get_arg_value(scope, global, caches, lib, this_ptr, arg, level)?;
let script = &arg_value
let s = &arg_value
.into_immutable_string()
.map_err(|typ| self.make_type_mismatch_err::<ImmutableString>(typ, pos))?;
let result = self.eval_script_expr_in_place(
scope,
global,
caches,
lib,
script,
pos,
level + 1,
);
let result =
self.eval_script_expr_in_place(scope, global, caches, lib, s, pos, level + 1);
// IMPORTANT! If the eval defines new variables in the current scope,
// all variable offsets from this point on will be mis-aligned.
if scope.len() != orig_scope_len {
// The same is true for imports.
let scope_changed = scope.len() != orig_scope_len;
#[cfg(not(feature = "no_module"))]
let scope_changed = scope_changed || global.num_imports() != orig_imports_len;
if scope_changed {
global.always_search_scope = true;
}
@ -1199,8 +1206,8 @@ impl Engine {
return self
.exec_fn_call(
scope, global, caches, lib, name, hashes, &mut args, is_ref_mut, false, pos,
level,
scope, global, caches, lib, name, native, hashes, &mut args, is_ref_mut, false,
pos, level,
)
.map(|(v, ..)| v);
}
@ -1262,7 +1269,8 @@ impl Engine {
}
self.exec_fn_call(
None, global, caches, lib, name, hashes, &mut args, is_ref_mut, false, pos, level,
None, global, caches, lib, name, native, hashes, &mut args, is_ref_mut, false, pos,
level,
)
.map(|(v, ..)| v)
}

View File

@ -199,7 +199,7 @@ impl CallableFunction {
Self::Script(..) => None,
}
}
/// Create a new [`CallableFunction::Method`] from a [built-in function][`FnBuiltin`].
/// Create a new [`CallableFunction::Method`] from a built-in function.
#[inline(always)]
#[must_use]
pub fn from_fn_builtin(func: FnBuiltin) -> Self {

View File

@ -135,7 +135,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef<str> + 'a + ?Sized>
impl<'a> NativeCallContext<'a> {
/// _(internals)_ Create a new [`NativeCallContext`].
/// Exported under the `metadata` feature only.
/// Exported under the `internals` feature only.
#[deprecated(
since = "1.3.0",
note = "`NativeCallContext::new` will be moved under `internals`. Use `FnPtr::call` to call a function pointer directly."
@ -235,7 +235,7 @@ impl<'a> NativeCallContext<'a> {
#[inline]
pub(crate) fn iter_imports_raw(
&self,
) -> impl Iterator<Item = (&crate::ImmutableString, &Shared<Module>)> {
) -> impl Iterator<Item = &(crate::ImmutableString, Shared<Module>)> {
self.global.iter().flat_map(|&g| g.iter_imports_raw())
}
/// _(internals)_ The current [`GlobalRuntimeState`], if any.
@ -274,7 +274,7 @@ impl<'a> NativeCallContext<'a> {
let mut args: StaticVec<_> = arg_values.iter_mut().collect();
let result = self.call_fn_raw(fn_name, false, false, &mut args)?;
let result = self._call_fn_raw(fn_name, false, false, false, &mut args)?;
let typ = self.engine().map_type_name(result.type_name());
@ -283,7 +283,32 @@ impl<'a> NativeCallContext<'a> {
ERR::ErrorMismatchOutputType(t, typ.into(), Position::NONE).into()
})
}
/// Call a function inside the call context.
/// Call a registered native Rust function inside the call context with the provided arguments.
///
/// This is often useful because Rust functions typically only want to cross-call other
/// registered Rust functions and not have to worry about scripted functions hijacking the
/// process unknowingly (or deliberately).
#[inline]
pub fn call_native_fn<T: Variant + Clone>(
&self,
fn_name: impl AsRef<str>,
args: impl FuncArgs,
) -> RhaiResultOf<T> {
let mut arg_values = StaticVec::new_const();
args.parse(&mut arg_values);
let mut args: StaticVec<_> = arg_values.iter_mut().collect();
let result = self._call_fn_raw(fn_name, true, false, false, &mut args)?;
let typ = self.engine().map_type_name(result.type_name());
result.try_cast().ok_or_else(|| {
let t = self.engine().map_type_name(type_name::<T>()).into();
ERR::ErrorMismatchOutputType(t, typ.into(), Position::NONE).into()
})
}
/// Call a function (native Rust or scripted) inside the call context.
///
/// If `is_method_call` is [`true`], the first argument is assumed to be the `this` pointer for
/// a script-defined function (or the object of a method call).
@ -302,6 +327,7 @@ impl<'a> NativeCallContext<'a> {
///
/// If `is_ref_mut` is [`true`], the first argument is assumed to be passed by reference and is
/// not consumed.
#[inline(always)]
pub fn call_fn_raw(
&self,
fn_name: impl AsRef<str>,
@ -309,15 +335,76 @@ impl<'a> NativeCallContext<'a> {
is_method_call: bool,
args: &mut [&mut Dynamic],
) -> RhaiResult {
let mut global = self
self._call_fn_raw(fn_name, false, is_ref_mut, is_method_call, args)
}
/// Call a registered native Rust function inside the call context.
///
/// This is often useful because Rust functions typically only want to cross-call other
/// registered Rust functions and not have to worry about scripted functions hijacking the
/// process unknowingly (or deliberately).
///
/// # WARNING - Low Level API
///
/// This function is very low level.
///
/// # Arguments
///
/// All arguments may be _consumed_, meaning that they may be replaced by `()`. This is to avoid
/// unnecessarily cloning the arguments.
///
/// **DO NOT** reuse the arguments after this call. If they are needed afterwards, clone them
/// _before_ calling this function.
///
/// If `is_ref_mut` is [`true`], the first argument is assumed to be passed by reference and is
/// not consumed.
#[inline(always)]
pub fn call_native_fn_raw(
&self,
fn_name: impl AsRef<str>,
is_ref_mut: bool,
args: &mut [&mut Dynamic],
) -> RhaiResult {
self._call_fn_raw(fn_name, true, is_ref_mut, false, args)
}
/// Call a function (native Rust or scripted) inside the call context.
fn _call_fn_raw(
&self,
fn_name: impl AsRef<str>,
native_only: bool,
is_ref_mut: bool,
is_method_call: bool,
args: &mut [&mut Dynamic],
) -> RhaiResult {
let global = &mut self
.global
.cloned()
.unwrap_or_else(|| GlobalRuntimeState::new(self.engine()));
let mut caches = Caches::new();
let caches = &mut Caches::new();
let fn_name = fn_name.as_ref();
let args_len = args.len();
if native_only {
return self
.engine()
.call_native_fn(
global,
caches,
self.lib,
fn_name,
calc_fn_hash(None, fn_name, args_len),
args,
is_ref_mut,
false,
Position::NONE,
self.level + 1,
)
.map(|(r, ..)| r);
}
// Native or script
let hash = if is_method_call {
FnCallHashes::from_all(
#[cfg(not(feature = "no_function"))]
@ -331,10 +418,11 @@ impl<'a> NativeCallContext<'a> {
self.engine()
.exec_fn_call(
None,
&mut global,
&mut caches,
global,
caches,
self.lib,
fn_name,
false,
hash,
args,
is_ref_mut,

View File

@ -341,6 +341,9 @@ pub use eval::{Caches, FnResolutionCache, FnResolutionCacheEntry, GlobalRuntimeS
#[cfg(feature = "metadata")]
pub use api::definitions::Definitions;
/// Number of items to keep inline for [`StaticVec`].
const STATIC_VEC_INLINE_SIZE: usize = 3;
/// Alias to [`smallvec::SmallVec<[T; 3]>`](https://crates.io/crates/smallvec), which is a
/// specialized [`Vec`] backed by a small, inline, fixed-size array when there are ≤ 3 items stored.
///
@ -373,7 +376,7 @@ pub use api::definitions::Definitions;
/// most scripts load fewer than 4 external modules; most module paths contain fewer than 4 levels
/// (e.g. `std::collections::map::HashMap` is 4 levels and it is just about as long as they get).
#[cfg(not(feature = "internals"))]
type StaticVec<T> = smallvec::SmallVec<[T; 3]>;
type StaticVec<T> = smallvec::SmallVec<[T; STATIC_VEC_INLINE_SIZE]>;
/// _(internals)_ Alias to [`smallvec::SmallVec<[T; 3]>`](https://crates.io/crates/smallvec),
/// which is a [`Vec`] backed by a small, inline, fixed-size array when there are ≤ 3 items stored.
@ -408,7 +411,11 @@ type StaticVec<T> = smallvec::SmallVec<[T; 3]>;
/// most scripts load fewer than 4 external modules; most module paths contain fewer than 4 levels
/// (e.g. `std::collections::map::HashMap` is 4 levels and it is just about as long as they get).
#[cfg(feature = "internals")]
pub type StaticVec<T> = smallvec::SmallVec<[T; 3]>;
pub type StaticVec<T> = smallvec::SmallVec<[T; STATIC_VEC_INLINE_SIZE]>;
/// Number of items to keep inline for [`FnArgsVec`].
#[cfg(not(feature = "no_closure"))]
const FN_ARGS_VEC_INLINE_SIZE: usize = 5;
/// Inline arguments storage for function calls.
///
@ -423,7 +430,7 @@ pub type StaticVec<T> = smallvec::SmallVec<[T; 3]>;
///
/// Under `no_closure`, this type aliases to [`StaticVec`][crate::StaticVec] instead.
#[cfg(not(feature = "no_closure"))]
type FnArgsVec<T> = smallvec::SmallVec<[T; 5]>;
type FnArgsVec<T> = smallvec::SmallVec<[T; FN_ARGS_VEC_INLINE_SIZE]>;
/// Inline arguments storage for function calls.
/// This type aliases to [`StaticVec`][crate::StaticVec].

View File

@ -1,7 +1,10 @@
use crate::{Engine, Module, ModuleResolver, Position, RhaiResultOf, Shared, ERR};
use crate::{
Engine, Module, ModuleResolver, Position, RhaiResultOf, Shared, StaticVec, ERR,
STATIC_VEC_INLINE_SIZE,
};
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
use std::{ops::AddAssign, slice::Iter, vec::IntoIter};
use std::{ops::AddAssign, slice::Iter};
/// [Module] resolution service that holds a collection of module resolvers,
/// to be searched in sequential order.
@ -21,7 +24,7 @@ use std::{ops::AddAssign, slice::Iter, vec::IntoIter};
/// engine.set_module_resolver(collection);
/// ```
#[derive(Default)]
pub struct ModuleResolversCollection(Vec<Box<dyn ModuleResolver>>);
pub struct ModuleResolversCollection(StaticVec<Box<dyn ModuleResolver>>);
impl ModuleResolversCollection {
/// Create a new [`ModuleResolversCollection`].
@ -43,7 +46,7 @@ impl ModuleResolversCollection {
#[inline(always)]
#[must_use]
pub const fn new() -> Self {
Self(Vec::new())
Self(StaticVec::new_const())
}
/// Append a [module resolver][ModuleResolver] to the end.
#[inline(always)]
@ -109,7 +112,7 @@ impl ModuleResolversCollection {
impl IntoIterator for ModuleResolversCollection {
type Item = Box<dyn ModuleResolver>;
type IntoIter = IntoIter<Box<dyn ModuleResolver>>;
type IntoIter = smallvec::IntoIter<[Box<dyn ModuleResolver>; STATIC_VEC_INLINE_SIZE]>;
#[inline(always)]
#[must_use]

View File

@ -897,24 +897,17 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
Stmt::Expr(expr) => {
optimize_expr(expr, state, false);
match &mut **expr {
// func(...)
Expr::FnCall(x, pos) => {
state.set_dirty();
*stmt = Stmt::FnCall(mem::take(x), *pos);
}
// {...};
Expr::Stmt(x) => {
if x.is_empty() {
state.set_dirty();
*stmt = Stmt::Noop(x.position());
} else {
state.set_dirty();
*stmt = mem::take(&mut **x).into();
}
}
// expr;
_ => (),
if matches!(**expr, Expr::FnCall(..) | Expr::Stmt(..)) {
state.set_dirty();
*stmt = match *mem::take(expr) {
// func(...);
Expr::FnCall(x, pos) => Stmt::FnCall(x, pos),
// {};
Expr::Stmt(x) if x.is_empty() => Stmt::Noop(x.position()),
// {...};
Expr::Stmt(x) => (*x).into(),
_ => unreachable!(),
};
}
}

View File

@ -835,7 +835,7 @@ pub mod array_functions {
for item in array {
if ctx
.call_fn_raw(OP_EQUALS, true, false, &mut [item, &mut value.clone()])
.call_native_fn_raw(OP_EQUALS, true, &mut [item, &mut value.clone()])
.or_else(|err| match *err {
ERR::ErrorFunctionNotFound(ref fn_sig, ..) if fn_sig.starts_with(OP_EQUALS) => {
if item.type_id() == value.type_id() {
@ -927,7 +927,7 @@ pub mod array_functions {
for (i, item) in array.iter_mut().enumerate().skip(start) {
if ctx
.call_fn_raw(OP_EQUALS, true, false, &mut [item, &mut value.clone()])
.call_native_fn_raw(OP_EQUALS, true, &mut [item, &mut value.clone()])
.or_else(|err| match *err {
ERR::ErrorFunctionNotFound(ref fn_sig, ..) if fn_sig.starts_with(OP_EQUALS) => {
if item.type_id() == value.type_id() {
@ -2313,7 +2313,7 @@ pub mod array_functions {
for (a1, a2) in array1.iter_mut().zip(array2.iter_mut()) {
if !ctx
.call_fn_raw(OP_EQUALS, true, false, &mut [a1, a2])
.call_native_fn_raw(OP_EQUALS, true, &mut [a1, a2])
.or_else(|err| match *err {
ERR::ErrorFunctionNotFound(ref fn_sig, ..) if fn_sig.starts_with(OP_EQUALS) => {
if a1.type_id() == a2.type_id() {

View File

@ -213,7 +213,7 @@ mod map_functions {
for (m1, v1) in map1 {
if let Some(v2) = map2.get_mut(m1) {
let equals = ctx
.call_fn_raw(OP_EQUALS, true, false, &mut [v1, v2])?
.call_native_fn_raw(OP_EQUALS, true, &mut [v1, v2])?
.as_bool()
.unwrap_or(false);

View File

@ -38,7 +38,7 @@ pub fn print_with_func(
ctx: &NativeCallContext,
value: &mut Dynamic,
) -> crate::ImmutableString {
match ctx.call_fn_raw(fn_name, true, false, &mut [value]) {
match ctx.call_native_fn_raw(fn_name, true, &mut [value]) {
Ok(result) if result.is::<crate::ImmutableString>() => {
result.into_immutable_string().expect("`ImmutableString`")
}

View File

@ -193,7 +193,6 @@ impl<'e> ParseState<'e> {
#[cfg(not(feature = "no_function"))]
let is_func_name = _lib.values().any(|f| f.name == name);
#[cfg(feature = "no_function")]
let is_func_name = false;
@ -600,7 +599,9 @@ impl Engine {
#[cfg(feature = "no_module")]
let hash = calc_fn_hash(None, &id, 0);
let hashes = if is_valid_function_name(&id) {
let is_valid_function_name = is_valid_function_name(&id);
let hashes = if is_valid_function_name {
hash.into()
} else {
FnCallHashes::from_native(hash)
@ -612,6 +613,8 @@ impl Engine {
name: state.get_interned_string(id),
capture_parent_scope,
operator_token: None,
#[cfg(not(feature = "no_function"))]
can_be_script: is_valid_function_name,
#[cfg(not(feature = "no_module"))]
namespace,
hashes,
@ -668,7 +671,9 @@ impl Engine {
#[cfg(feature = "no_module")]
let hash = calc_fn_hash(None, &id, args.len());
let hashes = if is_valid_function_name(&id) {
let is_valid_function_name = is_valid_function_name(&id);
let hashes = if is_valid_function_name {
hash.into()
} else {
FnCallHashes::from_native(hash)
@ -680,6 +685,8 @@ impl Engine {
name: state.get_interned_string(id),
capture_parent_scope,
operator_token: None,
#[cfg(not(feature = "no_function"))]
can_be_script: is_valid_function_name,
#[cfg(not(feature = "no_module"))]
namespace,
hashes,
@ -1912,12 +1919,16 @@ impl Engine {
args.shrink_to_fit();
Ok(FnCallExpr {
#[cfg(not(feature = "no_module"))]
namespace: Default::default(),
name: state.get_interned_string("-"),
hashes: FnCallHashes::from_native(calc_fn_hash(None, "-", 1)),
args,
pos,
operator_token: Some(token),
..Default::default()
capture_parent_scope: false,
#[cfg(not(feature = "no_function"))]
can_be_script: false,
}
.into_fn_call_expr(pos))
}
@ -1940,12 +1951,16 @@ impl Engine {
args.shrink_to_fit();
Ok(FnCallExpr {
#[cfg(not(feature = "no_module"))]
namespace: Default::default(),
name: state.get_interned_string("+"),
hashes: FnCallHashes::from_native(calc_fn_hash(None, "+", 1)),
args,
pos,
operator_token: Some(token),
..Default::default()
capture_parent_scope: false,
#[cfg(not(feature = "no_function"))]
can_be_script: false,
}
.into_fn_call_expr(pos))
}
@ -1961,12 +1976,16 @@ impl Engine {
args.shrink_to_fit();
Ok(FnCallExpr {
#[cfg(not(feature = "no_module"))]
namespace: Default::default(),
name: state.get_interned_string("!"),
hashes: FnCallHashes::from_native(calc_fn_hash(None, "!", 1)),
args,
pos,
operator_token: Some(token),
..Default::default()
capture_parent_scope: false,
#[cfg(not(feature = "no_function"))]
can_be_script: false,
}
.into_fn_call_expr(pos))
}
@ -2335,18 +2354,24 @@ impl Engine {
let op = op_token.syntax();
let hash = calc_fn_hash(None, &op, 2);
let operator_token = if is_valid_function_name(&op) {
let is_function = is_valid_function_name(&op);
let operator_token = if is_function {
None
} else {
Some(op_token.clone())
};
let op_base = FnCallExpr {
#[cfg(not(feature = "no_module"))]
namespace: Default::default(),
name: state.get_interned_string(op.as_ref()),
hashes: FnCallHashes::from_native(hash),
args: StaticVec::new_const(),
pos,
operator_token,
..Default::default()
capture_parent_scope: false,
#[cfg(not(feature = "no_function"))]
can_be_script: is_function,
};
let mut args = StaticVec::new_const();
@ -2432,7 +2457,7 @@ impl Engine {
let pos = args[0].start_position();
FnCallExpr {
hashes: if is_valid_function_name(&s) {
hashes: if is_function {
hash.into()
} else {
FnCallHashes::from_native(hash)
@ -2993,24 +3018,24 @@ impl Engine {
// import expr ...
let expr = self.parse_expr(input, state, lib, settings.level_up())?;
// import expr;
if !match_token(input, Token::As).0 {
let empty = Ident {
let export = if !match_token(input, Token::As).0 {
// import expr;
Ident {
name: state.get_interned_string(""),
pos: Position::NONE,
};
return Ok(Stmt::Import((expr, empty).into(), settings.pos));
}
}
} else {
// import expr as name ...
let (name, pos) = parse_var_name(input)?;
Ident {
name: state.get_interned_string(name),
pos,
}
};
// import expr as name ...
let (name, pos) = parse_var_name(input)?;
let name = state.get_interned_string(name);
state.imports.push(name.clone());
state.imports.push(export.name.clone());
Ok(Stmt::Import(
(expr, Ident { name, pos }).into(),
settings.pos,
))
Ok(Stmt::Import((expr, export).into(), settings.pos))
}
/// Parse an export statement.
@ -3659,6 +3684,8 @@ impl Engine {
);
let expr = FnCallExpr {
#[cfg(not(feature = "no_module"))]
namespace: Default::default(),
name: state.get_interned_string(crate::engine::KEYWORD_FN_PTR_CURRY),
hashes: FnCallHashes::from_native(calc_fn_hash(
None,
@ -3667,7 +3694,10 @@ impl Engine {
)),
args,
pos,
..Default::default()
operator_token: None,
capture_parent_scope: false,
#[cfg(not(feature = "no_function"))]
can_be_script: false,
}
.into_fn_call_expr(pos);

View File

@ -110,7 +110,7 @@ impl StringsInterner<'_> {
// If the interner is over capacity, remove the longest entry that has the lowest count
if self.cache.len() > self.capacity {
// Leave some buffer to grow when shrinking the cache.
// Throttle: leave some buffer to grow when shrinking the cache.
// We leave at least two entries, one for the empty string, and one for the string
// that has just been inserted.
let max = if self.capacity < 5 {