diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da5f1126..7a72d1d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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}} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2848dd..ffcc3516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/api/call_fn.rs b/src/api/call_fn.rs index ac1fb7f3..3b0a7615 100644 --- a/src/api/call_fn.rs +++ b/src/api/call_fn.rs @@ -233,6 +233,7 @@ impl Engine { arg_values, ) } + /// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments. fn _call_fn( &self, diff --git a/src/ast/expr.rs b/src/ast/expr.rs index 76c9587d..4adaaecf 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -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, + /// 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(), diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index a763860c..a72e2f01 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -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 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 { diff --git a/src/eval/expr.rs b/src/eval/expr.rs index ce34a8ad..c09b48a3 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -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, diff --git a/src/eval/global_state.rs b/src/eval/global_state.rs index 1144b851..98e39870 100644 --- a/src/eval/global_state.rs +++ b/src/eval/global_state.rs @@ -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, /// Stack of imported [modules][crate::Module]. #[cfg(not(feature = "no_module"))] - modules: crate::StaticVec>, + modules: crate::StaticVec<(crate::ImmutableString, crate::Shared)>, /// 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> { - 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> { - 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 { - 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, module: impl Into>, ) { - 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 { - 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)> { - self.keys.iter().rev().zip(self.modules.iter().rev()) + ) -> impl Iterator)> { + 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)> { - self.keys.iter().zip(self.modules.iter()) + ) -> impl Iterator)> { + 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); - type IntoIter = std::iter::Zip< - std::iter::Rev>, - std::iter::Rev; 3]>>, + type IntoIter = std::iter::Rev< + smallvec::IntoIter< + [(crate::ImmutableString, crate::Shared); 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); - type IntoIter = std::iter::Zip< - std::iter::Rev>, - std::iter::Rev>>, + type Item = &'a (crate::ImmutableString, crate::Shared); + type IntoIter = std::iter::Rev< + std::slice::Iter<'a, (crate::ImmutableString, crate::Shared)>, >; fn into_iter(self) -> Self::IntoIter { - self.keys.iter().rev().zip(self.modules.iter().rev()) + self.modules.iter().rev() } } @@ -345,8 +338,7 @@ impl, M: Into>> Ext #[inline] fn extend>(&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); diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index 726b661f..77e39dea 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -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; diff --git a/src/func/call.rs b/src/func/call.rs index c093c237..c3ca7511 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -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::(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) } diff --git a/src/func/callable_function.rs b/src/func/callable_function.rs index 3973b1b1..e6b85a29 100644 --- a/src/func/callable_function.rs +++ b/src/func/callable_function.rs @@ -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 { diff --git a/src/func/native.rs b/src/func/native.rs index f421c262..b3681e74 100644 --- a/src/func/native.rs +++ b/src/func/native.rs @@ -135,7 +135,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef + '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)> { + ) -> impl Iterator)> { 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( + &self, + fn_name: impl AsRef, + args: impl FuncArgs, + ) -> RhaiResultOf { + 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::()).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, @@ -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, + 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, + 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, diff --git a/src/lib.rs b/src/lib.rs index 4976d4b7..7b36c9cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 = smallvec::SmallVec<[T; 3]>; +type StaticVec = 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 = 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 = smallvec::SmallVec<[T; 3]>; +pub type StaticVec = 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 = smallvec::SmallVec<[T; 3]>; /// /// Under `no_closure`, this type aliases to [`StaticVec`][crate::StaticVec] instead. #[cfg(not(feature = "no_closure"))] -type FnArgsVec = smallvec::SmallVec<[T; 5]>; +type FnArgsVec = smallvec::SmallVec<[T; FN_ARGS_VEC_INLINE_SIZE]>; /// Inline arguments storage for function calls. /// This type aliases to [`StaticVec`][crate::StaticVec]. diff --git a/src/module/resolvers/collection.rs b/src/module/resolvers/collection.rs index fd6780d4..8a61d715 100644 --- a/src/module/resolvers/collection.rs +++ b/src/module/resolvers/collection.rs @@ -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>); +pub struct ModuleResolversCollection(StaticVec>); 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; - type IntoIter = IntoIter>; + type IntoIter = smallvec::IntoIter<[Box; STATIC_VEC_INLINE_SIZE]>; #[inline(always)] #[must_use] diff --git a/src/optimizer.rs b/src/optimizer.rs index a0f187c9..dea68ea3 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -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!(), + }; } } diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 7e639275..2c5bd716 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -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() { diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index 3360be62..e5174c1b 100644 --- a/src/packages/map_basic.rs +++ b/src/packages/map_basic.rs @@ -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); diff --git a/src/packages/string_basic.rs b/src/packages/string_basic.rs index d8c3845a..a0763ada 100644 --- a/src/packages/string_basic.rs +++ b/src/packages/string_basic.rs @@ -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::() => { result.into_immutable_string().expect("`ImmutableString`") } diff --git a/src/parser.rs b/src/parser.rs index 798067c9..6c100008 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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); diff --git a/src/types/interner.rs b/src/types/interner.rs index 5832dbe0..4b7d387e 100644 --- a/src/types/interner.rs +++ b/src/types/interner.rs @@ -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 {