Make Engine faster to create.

This commit is contained in:
Stephen Chung 2023-02-25 19:57:19 +08:00
parent c2a8c342bb
commit 51581cdef5
13 changed files with 184 additions and 115 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,24 +38,26 @@ impl LangOptions {
/// Create a new [`LangOptions`] with default values. /// Create a new [`LangOptions`] with default values.
#[inline(always)] #[inline(always)]
#[must_use] #[must_use]
pub fn new() -> Self { pub const fn new() -> Self {
Self::IF_EXPR Self::from_bits_truncate(
| Self::SWITCH_EXPR Self::IF_EXPR.bits()
| Self::LOOP_EXPR | Self::SWITCH_EXPR.bits()
| Self::STMT_EXPR | Self::LOOP_EXPR.bits()
| Self::LOOPING | Self::STMT_EXPR.bits()
| Self::SHADOWING | Self::LOOPING.bits()
| Self::FAST_OPS | Self::SHADOWING.bits()
| { | Self::FAST_OPS.bits()
#[cfg(not(feature = "no_function"))] | {
{ #[cfg(not(feature = "no_function"))]
Self::ANON_FN {
} Self::ANON_FN.bits()
#[cfg(feature = "no_function")] }
{ #[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`]. //! Module that defines the public function/module registration API of [`Engine`].
use crate::func::{FnCallArgs, RegisterNativeFunction, SendSync}; use crate::func::{FnCallArgs, RegisterNativeFunction, SendSync};
use crate::module::ModuleFlags;
use crate::types::dynamic::Variant; use crate::types::dynamic::Variant;
use crate::{ use crate::{
Engine, FnAccess, FnNamespace, Identifier, Module, NativeCallContext, RhaiResultOf, Shared, Engine, FnAccess, FnNamespace, Identifier, Module, NativeCallContext, RhaiResultOf, Shared,
@ -14,20 +15,18 @@ use std::prelude::v1::*;
use crate::func::register::Mut; use crate::func::register::Mut;
impl Engine { 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 /// Get a mutable reference to the global namespace module
/// (which is the first module in `global_modules`). /// (which is the first module in `global_modules`).
#[inline(always)] #[inline(always)]
#[must_use] #[must_use]
pub(crate) fn global_namespace_mut(&mut self) -> &mut Module { fn global_namespace_mut(&mut self) -> &mut Module {
let module = self.global_modules.first_mut().unwrap(); if self.global_modules.is_empty() {
Shared::get_mut(module).expect("not shared") 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`]. /// Register a custom function with the [`Engine`].
/// ///
@ -677,6 +676,9 @@ impl Engine {
/// modules are searched in reverse order. /// modules are searched in reverse order.
#[inline(always)] #[inline(always)]
pub fn register_global_module(&mut self, module: SharedModule) -> &mut Self { 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. // Insert the module into the front.
// The first module is always the global namespace. // The first module is always the global namespace.
self.global_modules.insert(1, module); self.global_modules.insert(1, module);
@ -779,7 +781,9 @@ impl Engine {
pub fn gen_fn_signatures(&self, include_packages: bool) -> Vec<String> { pub fn gen_fn_signatures(&self, include_packages: bool) -> Vec<String> {
let mut signatures = Vec::with_capacity(64); 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"))] #[cfg(not(feature = "no_module"))]
for (name, m) in self.global_sub_modules.as_deref().into_iter().flatten() { 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::eval::{Caches, GlobalRuntimeState};
use crate::func::native::locked_write; use crate::func::native::locked_write;
use crate::parser::ParseState; use crate::parser::ParseState;
use crate::types::StringsInterner;
use crate::{Engine, RhaiResultOf, Scope, AST}; use crate::{Engine, RhaiResultOf, Scope, AST};
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
@ -59,7 +60,17 @@ impl Engine {
let scripts = [script]; let scripts = [script];
let ast = { let ast = {
let (stream, tc) = self.lex_raw(&scripts, self.token_mapper.as_deref()); 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); let state = &mut ParseState::new(Some(scope), interned_strings, tc);
self.parse(stream.peekable(), state, self.optimization_level)? self.parse(stream.peekable(), state, self.optimization_level)?
}; };

View File

@ -5,13 +5,11 @@ use crate::func::native::{
locked_write, OnDebugCallback, OnDefVarCallback, OnParseTokenCallback, OnPrintCallback, locked_write, OnDebugCallback, OnDefVarCallback, OnParseTokenCallback, OnPrintCallback,
OnVarCallback, OnVarCallback,
}; };
use crate::module::ModuleFlags;
use crate::packages::{Package, StandardPackage}; use crate::packages::{Package, StandardPackage};
use crate::tokenizer::Token; use crate::tokenizer::Token;
use crate::types::StringsInterner; use crate::types::StringsInterner;
use crate::{ use crate::{
Dynamic, Identifier, ImmutableString, Locked, Module, OptimizationLevel, SharedModule, Dynamic, Identifier, ImmutableString, Locked, OptimizationLevel, SharedModule, StaticVec,
StaticVec,
}; };
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
@ -103,10 +101,10 @@ pub struct Engine {
/// A module resolution service. /// A module resolution service.
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
pub(crate) module_resolver: Box<dyn crate::ModuleResolver>, pub(crate) module_resolver: Option<Box<dyn crate::ModuleResolver>>,
/// Strings interner. /// Strings interner.
pub(crate) interned_strings: Locked<StringsInterner>, pub(crate) interned_strings: Option<Box<Locked<StringsInterner>>>,
/// A set of symbols to disable. /// A set of symbols to disable.
pub(crate) disabled_symbols: Option<Box<BTreeSet<Identifier>>>, pub(crate) disabled_symbols: Option<Box<BTreeSet<Identifier>>>,
@ -127,9 +125,9 @@ pub struct Engine {
pub(crate) token_mapper: Option<Box<OnParseTokenCallback>>, pub(crate) token_mapper: Option<Box<OnParseTokenCallback>>,
/// Callback closure for implementing the `print` command. /// 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. /// Callback closure for implementing the `debug` command.
pub(crate) debug: Box<OnDebugCallback>, pub(crate) debug: Option<Box<OnDebugCallback>>,
/// Callback closure for progress reporting. /// Callback closure for progress reporting.
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
pub(crate) progress: Option<Box<crate::func::native::OnProgressCallback>>, pub(crate) progress: Option<Box<crate::func::native::OnProgressCallback>>,
@ -231,6 +229,49 @@ pub fn make_setter(id: &str) -> Identifier {
} }
impl Engine { impl Engine {
/// An empty [`Engine`].
pub const EMPTY: Self = Self {
global_modules: StaticVec::new_const(),
#[cfg(not(feature = "no_module"))]
global_sub_modules: None,
#[cfg(not(feature = "no_module"))]
module_resolver: None,
interned_strings: None,
disabled_symbols: None,
#[cfg(not(feature = "no_custom_syntax"))]
custom_keywords: None,
#[cfg(not(feature = "no_custom_syntax"))]
custom_syntax: None,
def_var_filter: None,
resolve_var: None,
token_mapper: None,
print: None,
debug: None,
#[cfg(not(feature = "unchecked"))]
progress: None,
options: LangOptions::new(),
def_tag: Dynamic::UNIT,
#[cfg(not(feature = "no_optimize"))]
optimization_level: OptimizationLevel::Simple,
#[cfg(feature = "no_optimize")]
optimization_level: (),
#[cfg(not(feature = "unchecked"))]
limits: crate::api::limits::Limits::new(),
#[cfg(feature = "debugging")]
debugger_interface: None,
};
/// Create a new [`Engine`]. /// Create a new [`Engine`].
#[inline] #[inline]
#[must_use] #[must_use]
@ -242,22 +283,25 @@ impl Engine {
#[cfg(not(feature = "no_std"))] #[cfg(not(feature = "no_std"))]
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
{ {
engine.module_resolver = Box::new(crate::module::resolvers::FileModuleResolver::new()); engine.module_resolver =
Some(Box::new(crate::module::resolvers::FileModuleResolver::new()));
} }
engine.interned_strings = Some(Locked::new(StringsInterner::new()).into());
// default print/debug implementations // default print/debug implementations
#[cfg(not(feature = "no_std"))] #[cfg(not(feature = "no_std"))]
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
{ {
engine.print = Box::new(|s| println!("{s}")); engine.print = Some(Box::new(|s| println!("{s}")));
engine.debug = Box::new(|s, source, pos| match (source, pos) { engine.debug = Some(Box::new(|s, source, pos| match (source, pos) {
(Some(source), crate::Position::NONE) => println!("{source} | {s}"), (Some(source), crate::Position::NONE) => println!("{source} | {s}"),
#[cfg(not(feature = "no_position"))] #[cfg(not(feature = "no_position"))]
(Some(source), pos) => println!("{source} @ {pos:?} | {s}"), (Some(source), pos) => println!("{source} @ {pos:?} | {s}"),
(None, crate::Position::NONE) => println!("{s}"), (None, crate::Position::NONE) => println!("{s}"),
#[cfg(not(feature = "no_position"))] #[cfg(not(feature = "no_position"))]
(None, pos) => println!("{pos:?} | {s}"), (None, pos) => println!("{pos:?} | {s}"),
}); }));
} }
engine.register_global_module(StandardPackage::new().as_shared_module()); engine.register_global_module(StandardPackage::new().as_shared_module());
@ -270,55 +314,8 @@ impl Engine {
/// Use [`register_global_module`][Engine::register_global_module] to add packages of functions. /// Use [`register_global_module`][Engine::register_global_module] to add packages of functions.
#[inline] #[inline]
#[must_use] #[must_use]
pub fn new_raw() -> Self { pub const fn new_raw() -> Self {
let mut engine = Self { Self::EMPTY
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()),
interned_strings: StringsInterner::new().into(),
disabled_symbols: None,
#[cfg(not(feature = "no_custom_syntax"))]
custom_keywords: None,
#[cfg(not(feature = "no_custom_syntax"))]
custom_syntax: None,
def_var_filter: None,
resolve_var: None,
token_mapper: None,
print: Box::new(|_| {}),
debug: Box::new(|_, _, _| {}),
#[cfg(not(feature = "unchecked"))]
progress: None,
options: LangOptions::new(),
def_tag: Dynamic::UNIT,
#[cfg(not(feature = "no_optimize"))]
optimization_level: OptimizationLevel::Simple,
#[cfg(feature = "no_optimize")]
optimization_level: (),
#[cfg(not(feature = "unchecked"))]
limits: crate::api::limits::Limits::new(),
#[cfg(feature = "debugging")]
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());
engine
} }
/// Get an interned [string][ImmutableString]. /// Get an interned [string][ImmutableString].
@ -338,13 +335,17 @@ impl Engine {
/// [`Engine`] keeps a cache of [`ImmutableString`] instances and tries to avoid new allocations /// [`Engine`] keeps a cache of [`ImmutableString`] instances and tries to avoid new allocations
/// when an existing instance is found. /// when an existing instance is found.
#[cfg(feature = "internals")] #[cfg(feature = "internals")]
#[inline(always)] #[inline]
#[must_use] #[must_use]
pub fn get_interned_string( pub fn get_interned_string(
&self, &self,
string: impl AsRef<str> + Into<ImmutableString>, string: impl AsRef<str> + Into<ImmutableString>,
) -> 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. /// Get an empty [`ImmutableString`] which refers to a shared instance.

View File

@ -806,7 +806,7 @@ impl Engine {
) )
.or_else(|| { .or_else(|| {
Some( Some(
self.module_resolver self.module_resolver()
.resolve_raw(self, global, scope, &path, path_pos), .resolve_raw(self, global, scope, &path, path_pos),
) )
}) })

View File

@ -459,18 +459,26 @@ impl Engine {
// See if the function match print/debug (which requires special processing) // See if the function match print/debug (which requires special processing)
return Ok(match name { return Ok(match name {
KEYWORD_PRINT => { KEYWORD_PRINT => {
let text = result.into_immutable_string().map_err(|typ| { if let Some(ref print) = self.print {
let t = self.map_type_name(type_name::<ImmutableString>()).into(); let text = result.into_immutable_string().map_err(|typ| {
ERR::ErrorMismatchOutputType(t, typ.into(), pos) 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 => { KEYWORD_DEBUG => {
let text = result.into_immutable_string().map_err(|typ| { if let Some(ref debug) = self.debug {
let t = self.map_type_name(type_name::<ImmutableString>()).into(); let text = result.into_immutable_string().map_err(|typ| {
ERR::ErrorMismatchOutputType(t, typ.into(), pos) 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), _ => (result, is_method),
}); });

View File

@ -7,6 +7,7 @@ use crate::{Dynamic, RhaiResultOf, ERR, INT};
use std::prelude::v1::*; use std::prelude::v1::*;
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
#[cfg(not(feature = "no_std"))]
use crate::FLOAT; use crate::FLOAT;
def_package! { def_package! {

View File

@ -2354,7 +2354,6 @@ impl Engine {
let op = op_token.to_string(); let op = op_token.to_string();
let hash = calc_fn_hash(None, &op, 2); let hash = calc_fn_hash(None, &op, 2);
let native_only = !is_valid_function_name(&op); let native_only = !is_valid_function_name(&op);
let operator_token = native_only.then(|| op_token.clone());
let mut args = FnArgsVec::new_const(); let mut args = FnArgsVec::new_const();
args.push(root); args.push(root);
@ -2366,7 +2365,7 @@ impl Engine {
name: state.get_interned_string(&op), name: state.get_interned_string(&op),
hashes: FnCallHashes::from_native_only(hash), hashes: FnCallHashes::from_native_only(hash),
args, args,
op_token: operator_token, op_token: native_only.then(|| op_token.clone()),
capture_parent_scope: false, capture_parent_scope: false,
}; };