Merge pull request #710 from schungx/master

Add typed method definition.
This commit is contained in:
Stephen Chung 2023-03-22 16:36:39 +08:00 committed by GitHub
commit b78cb6131b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 496 additions and 311 deletions

View File

@ -3,6 +3,7 @@ on:
push: push:
branches: branches:
- master - master
- perf
jobs: jobs:
benchmark: benchmark:

View File

@ -1,6 +1,17 @@
Rhai Release Notes Rhai Release Notes
================== ==================
Version 1.14.0
==============
The code hacks that attempt to optimize branch prediction performance are removed because benchmarks do not show any material speed improvements.
New features
------------
* It is now possible to require a specific _type_ to the `this` pointer for a particular script-defined function so that it is called only when the `this` pointer contains the specified type.
Version 1.13.0 Version 1.13.0
============== ==============

View File

@ -3,7 +3,7 @@ members = [".", "codegen"]
[package] [package]
name = "rhai" name = "rhai"
version = "1.13.0" version = "1.14.0"
rust-version = "1.61.0" rust-version = "1.61.0"
edition = "2018" edition = "2018"
resolver = "2" resolver = "2"

View File

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

View File

@ -620,7 +620,7 @@ impl Expr {
Self::Index(_, options, _) | Self::Dot(_, options, _) => *options, Self::Index(_, options, _) | Self::Dot(_, options, _) => *options,
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
Self::FloatConstant(..) => ASTFlags::NONE, Self::FloatConstant(..) => ASTFlags::empty(),
Self::DynamicConstant(..) Self::DynamicConstant(..)
| Self::BoolConstant(..) | Self::BoolConstant(..)
@ -638,10 +638,10 @@ impl Expr {
| Self::MethodCall(..) | Self::MethodCall(..)
| Self::InterpolatedString(..) | Self::InterpolatedString(..)
| Self::Property(..) | Self::Property(..)
| Self::Stmt(..) => ASTFlags::NONE, | Self::Stmt(..) => ASTFlags::empty(),
#[cfg(not(feature = "no_custom_syntax"))] #[cfg(not(feature = "no_custom_syntax"))]
Self::Custom(..) => ASTFlags::NONE, Self::Custom(..) => ASTFlags::empty(),
} }
} }
/// Get the [position][Position] of the expression. /// Get the [position][Position] of the expression.

View File

@ -41,8 +41,6 @@ bitflags! {
/// _(internals)_ Bit-flags containing [`AST`][crate::AST] node configuration options. /// _(internals)_ Bit-flags containing [`AST`][crate::AST] node configuration options.
/// Exported under the `internals` feature only. /// Exported under the `internals` feature only.
pub struct ASTFlags: u8 { pub struct ASTFlags: u8 {
/// No options for the [`AST`][crate::AST] node.
const NONE = 0b_0000_0000;
/// The [`AST`][crate::AST] node is read-only. /// The [`AST`][crate::AST] node is read-only.
const CONSTANT = 0b_0000_0001; const CONSTANT = 0b_0000_0001;
/// The [`AST`][crate::AST] node is exposed to the outside (i.e. public). /// The [`AST`][crate::AST] node is exposed to the outside (i.e. public).

View File

@ -17,6 +17,10 @@ pub struct ScriptFnDef {
pub name: ImmutableString, pub name: ImmutableString,
/// Function access mode. /// Function access mode.
pub access: FnAccess, pub access: FnAccess,
#[cfg(not(feature = "no_object"))]
/// Type of `this` pointer, if any.
/// Not available under `no_object`.
pub this_type: Option<ImmutableString>,
/// Names of function parameters. /// Names of function parameters.
pub params: FnArgsVec<ImmutableString>, pub params: FnArgsVec<ImmutableString>,
/// _(metadata)_ Function doc-comments (if any). /// _(metadata)_ Function doc-comments (if any).
@ -39,13 +43,23 @@ pub struct ScriptFnDef {
impl fmt::Display for ScriptFnDef { impl fmt::Display for ScriptFnDef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(not(feature = "no_object"))]
let this_type = self
.this_type
.as_ref()
.map_or(String::new(), |s| format!("{:?}.", s));
#[cfg(feature = "no_object")]
let this_type = "";
write!( write!(
f, f,
"{}{}({})", "{}{}{}({})",
match self.access { match self.access {
FnAccess::Public => "", FnAccess::Public => "",
FnAccess::Private => "private ", FnAccess::Private => "private ",
}, },
this_type,
self.name, self.name,
self.params self.params
.iter() .iter()
@ -70,6 +84,10 @@ pub struct ScriptFnMetadata<'a> {
pub params: Vec<&'a str>, pub params: Vec<&'a str>,
/// Function access mode. /// Function access mode.
pub access: FnAccess, pub access: FnAccess,
#[cfg(not(feature = "no_object"))]
/// Type of `this` pointer, if any.
/// Not available under `no_object`.
pub this_type: Option<&'a str>,
/// _(metadata)_ Function doc-comments (if any). /// _(metadata)_ Function doc-comments (if any).
/// Exported under the `metadata` feature only. /// Exported under the `metadata` feature only.
/// ///
@ -90,13 +108,23 @@ pub struct ScriptFnMetadata<'a> {
impl fmt::Display for ScriptFnMetadata<'_> { impl fmt::Display for ScriptFnMetadata<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(not(feature = "no_object"))]
let this_type = self
.this_type
.as_ref()
.map_or(String::new(), |s| format!("{:?}.", s));
#[cfg(feature = "no_object")]
let this_type = "";
write!( write!(
f, f,
"{}{}({})", "{}{}{}({})",
match self.access { match self.access {
FnAccess::Public => "", FnAccess::Public => "",
FnAccess::Private => "private ", FnAccess::Private => "private ",
}, },
this_type,
self.name, self.name,
self.params self.params
.iter() .iter()
@ -114,6 +142,8 @@ impl<'a> From<&'a ScriptFnDef> for ScriptFnMetadata<'a> {
name: &value.name, name: &value.name,
params: value.params.iter().map(|s| s.as_str()).collect(), params: value.params.iter().map(|s| s.as_str()).collect(),
access: value.access, access: value.access,
#[cfg(not(feature = "no_object"))]
this_type: value.this_type.as_ref().map(|s| s.as_str()),
#[cfg(feature = "metadata")] #[cfg(feature = "metadata")]
comments: value.comments.iter().map(<_>::as_ref).collect(), comments: value.comments.iter().map(<_>::as_ref).collect(),
} }

View File

@ -779,13 +779,13 @@ impl Stmt {
| Self::While(..) | Self::While(..)
| Self::For(..) | Self::For(..)
| Self::TryCatch(..) | Self::TryCatch(..)
| Self::Assignment(..) => ASTFlags::NONE, | Self::Assignment(..) => ASTFlags::empty(),
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
Self::Import(..) | Self::Export(..) => ASTFlags::NONE, Self::Import(..) | Self::Export(..) => ASTFlags::empty(),
#[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_closure"))]
Self::Share(..) => ASTFlags::NONE, Self::Share(..) => ASTFlags::empty(),
} }
} }
/// Get the [position][Position] of this statement. /// Get the [position][Position] of this statement.

View File

@ -367,17 +367,4 @@ impl Engine {
pub(crate) const fn is_debugger_registered(&self) -> bool { pub(crate) const fn is_debugger_registered(&self) -> bool {
self.debugger_interface.is_some() self.debugger_interface.is_some()
} }
/// Imitation of std::hints::black_box which requires nightly.
#[cfg(not(target_family = "wasm"))]
#[inline(never)]
pub(crate) fn black_box() -> usize {
unsafe { core::ptr::read_volatile(&0_usize as *const usize) }
}
/// Imitation of std::hints::black_box which requires nightly.
#[cfg(target_family = "wasm")]
#[inline(always)]
pub(crate) fn black_box() -> usize {
0
}
} }

View File

@ -247,37 +247,9 @@ impl Engine {
self.track_operation(global, expr.position())?; self.track_operation(global, expr.position())?;
// Function calls should account for a relatively larger portion of expressions because
// binary operators are also function calls.
if let Expr::FnCall(x, pos) = expr {
return self.eval_fn_call_expr(global, caches, scope, this_ptr, x, *pos);
}
// Then variable access.
if let Expr::Variable(x, index, var_pos) = expr {
return if index.is_none() && x.0.is_none() && x.3 == KEYWORD_THIS {
this_ptr
.ok_or_else(|| ERR::ErrorUnboundThis(*var_pos).into())
.cloned()
} else {
self.search_namespace(global, caches, scope, this_ptr, expr)
.map(Target::take_or_clone)
};
}
// Then integer constants.
if let Expr::IntegerConstant(x, ..) = expr {
return Ok((*x).into());
}
// Stop merging branches here!
// We shouldn't lift out too many variants because, soon or later, the added comparisons
// will cost more than the mis-predicted `match` branch.
Self::black_box();
match expr { match expr {
// Constants // Constants
Expr::IntegerConstant(..) => unreachable!(), Expr::IntegerConstant(x, ..) => Ok((*x).into()),
Expr::StringConstant(x, ..) => Ok(x.clone().into()), Expr::StringConstant(x, ..) => Ok(x.clone().into()),
Expr::BoolConstant(x, ..) => Ok((*x).into()), Expr::BoolConstant(x, ..) => Ok((*x).into()),
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
@ -286,7 +258,21 @@ impl Engine {
Expr::Unit(..) => Ok(Dynamic::UNIT), Expr::Unit(..) => Ok(Dynamic::UNIT),
Expr::DynamicConstant(x, ..) => Ok(x.as_ref().clone()), Expr::DynamicConstant(x, ..) => Ok(x.as_ref().clone()),
// `... ${...} ...` Expr::FnCall(x, pos) => {
self.eval_fn_call_expr(global, caches, scope, this_ptr, x, *pos)
}
Expr::Variable(x, index, var_pos) => {
if index.is_none() && x.0.is_none() && x.3 == KEYWORD_THIS {
this_ptr
.ok_or_else(|| ERR::ErrorUnboundThis(*var_pos).into())
.cloned()
} else {
self.search_namespace(global, caches, scope, this_ptr, expr)
.map(Target::take_or_clone)
}
}
Expr::InterpolatedString(x, _) => { Expr::InterpolatedString(x, _) => {
let mut concat = SmartString::new_const(); let mut concat = SmartString::new_const();
@ -445,6 +431,7 @@ impl Engine {
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
Expr::Dot(..) => self.eval_dot_index_chain(global, caches, scope, this_ptr, expr, None), Expr::Dot(..) => self.eval_dot_index_chain(global, caches, scope, this_ptr, expr, None),
#[allow(unreachable_patterns)]
_ => unreachable!("expression cannot be evaluated: {:?}", expr), _ => unreachable!("expression cannot be evaluated: {:?}", expr),
} }
} }

View File

@ -251,13 +251,23 @@ impl GlobalRuntimeState {
pub fn get_qualified_fn( pub fn get_qualified_fn(
&self, &self,
hash: u64, hash: u64,
global_namespace_only: bool,
) -> Option<(&crate::func::CallableFunction, Option<&ImmutableString>)> { ) -> Option<(&crate::func::CallableFunction, Option<&ImmutableString>)> {
if global_namespace_only {
self.modules.as_ref().and_then(|m| {
m.iter()
.rev()
.filter(|m| m.contains_indexed_global_functions())
.find_map(|m| m.get_qualified_fn(hash).map(|f| (f, m.id_raw())))
})
} else {
self.modules.as_ref().and_then(|m| { self.modules.as_ref().and_then(|m| {
m.iter() m.iter()
.rev() .rev()
.find_map(|m| m.get_qualified_fn(hash).map(|f| (f, m.id_raw()))) .find_map(|m| m.get_qualified_fn(hash).map(|f| (f, m.id_raw())))
}) })
} }
}
/// Does the specified [`TypeId`][std::any::TypeId] iterator exist in the stack of /// Does the specified [`TypeId`][std::any::TypeId] iterator exist in the stack of
/// globally-imported [modules][crate::Module]? /// globally-imported [modules][crate::Module]?
/// ///

View File

@ -275,16 +275,31 @@ impl Engine {
self.track_operation(global, stmt.position())?; self.track_operation(global, stmt.position())?;
// Coded this way for better branch prediction. match stmt {
// Popular branches are lifted out of the `match` statement into their own branches. // No-op
Stmt::Noop(..) => Ok(Dynamic::UNIT),
// Function calls should account for a relatively larger portion of statements. // Expression as statement
if let Stmt::FnCall(x, pos) = stmt { Stmt::Expr(expr) => self
return self.eval_fn_call_expr(global, caches, scope, this_ptr, x, *pos); .eval_expr(global, caches, scope, this_ptr, expr)
.map(Dynamic::flatten),
// Block scope
Stmt::Block(statements, ..) => {
if statements.is_empty() {
Ok(Dynamic::UNIT)
} else {
self.eval_stmt_block(global, caches, scope, this_ptr, statements, true)
}
} }
// Then assignments. // Function call
if let Stmt::Assignment(x, ..) = stmt { Stmt::FnCall(x, pos) => {
self.eval_fn_call_expr(global, caches, scope, this_ptr, x, *pos)
}
// Assignment
Stmt::Assignment(x, ..) => {
let (op_info, BinaryExpr { lhs, rhs }) = &**x; let (op_info, BinaryExpr { lhs, rhs }) = &**x;
if let Expr::Variable(x, ..) = lhs { if let Expr::Variable(x, ..) = lhs {
@ -314,10 +329,7 @@ impl Engine {
self.track_operation(global, lhs.position())?; self.track_operation(global, lhs.position())?;
self.eval_op_assignment(global, caches, op_info, lhs, &mut target, rhs_val)?; self.eval_op_assignment(global, caches, op_info, lhs, &mut target, rhs_val)?;
} else {
return Ok(Dynamic::UNIT);
}
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
{ {
let rhs_val = self let rhs_val = self
@ -329,29 +341,31 @@ impl Engine {
// Must be either `var[index] op= val` or `var.prop op= val`. // Must be either `var[index] op= val` or `var.prop op= val`.
// The return value of any op-assignment (should be `()`) is thrown away and not used. // The return value of any op-assignment (should be `()`) is thrown away and not used.
let _ = let _ = match lhs {
match lhs {
// name op= rhs (handled above) // name op= rhs (handled above)
Expr::Variable(..) => { Expr::Variable(..) => {
unreachable!("Expr::Variable case is already handled") unreachable!("Expr::Variable case is already handled")
} }
// idx_lhs[idx_expr] op= rhs // idx_lhs[idx_expr] op= rhs
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Expr::Index(..) => self Expr::Index(..) => self.eval_dot_index_chain(
.eval_dot_index_chain(global, caches, scope, this_ptr, lhs, _new_val), global, caches, scope, this_ptr, lhs, _new_val,
),
// dot_lhs.dot_rhs op= rhs // dot_lhs.dot_rhs op= rhs
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
Expr::Dot(..) => self Expr::Dot(..) => self.eval_dot_index_chain(
.eval_dot_index_chain(global, caches, scope, this_ptr, lhs, _new_val), global, caches, scope, this_ptr, lhs, _new_val,
),
_ => unreachable!("cannot assign to expression: {:?}", lhs), _ => unreachable!("cannot assign to expression: {:?}", lhs),
}?; }?;
return Ok(Dynamic::UNIT);
} }
} }
// Then variable definitions. Ok(Dynamic::UNIT)
if let Stmt::Var(x, options, pos) = stmt { }
// Variable definition
Stmt::Var(x, options, pos) => {
if !self.allow_shadowing() && scope.contains(&x.0) { if !self.allow_shadowing() && scope.contains(&x.0) {
return Err(ERR::ErrorVariableExists(x.0.to_string(), *pos).into()); return Err(ERR::ErrorVariableExists(x.0.to_string(), *pos).into());
} }
@ -406,7 +420,9 @@ impl Engine {
&& global.lib.iter().any(|m| !m.is_empty()) && global.lib.iter().any(|m| !m.is_empty())
{ {
crate::func::locked_write(global.constants.get_or_insert_with(|| { crate::func::locked_write(global.constants.get_or_insert_with(|| {
crate::Shared::new(crate::Locked::new(std::collections::BTreeMap::new())) crate::Shared::new(
crate::Locked::new(std::collections::BTreeMap::new()),
)
})) }))
.insert(var_name.name.clone(), value.clone()); .insert(var_name.name.clone(), value.clone());
} }
@ -434,30 +450,7 @@ impl Engine {
scope.add_alias_by_index(scope.len() - 1, alias.as_str().into()); scope.add_alias_by_index(scope.len() - 1, alias.as_str().into());
} }
return Ok(Dynamic::UNIT);
}
// Stop merging branches here!
// We shouldn't lift out too many variants because, soon or later, the added comparisons
// will cost more than the mis-predicted `match` branch.
Self::black_box();
match stmt {
// No-op
Stmt::Noop(..) => Ok(Dynamic::UNIT),
// Expression as statement
Stmt::Expr(expr) => self
.eval_expr(global, caches, scope, this_ptr, expr)
.map(Dynamic::flatten),
// Block scope
Stmt::Block(statements, ..) => {
if statements.is_empty() {
Ok(Dynamic::UNIT) Ok(Dynamic::UNIT)
} else {
self.eval_stmt_block(global, caches, scope, this_ptr, statements, true)
}
} }
// If statement // If statement
@ -953,6 +946,7 @@ impl Engine {
Ok(Dynamic::UNIT) Ok(Dynamic::UNIT)
} }
#[allow(unreachable_patterns)]
_ => unreachable!("statement cannot be evaluated: {:?}", stmt), _ => unreachable!("statement cannot be evaluated: {:?}", stmt),
} }
} }

View File

@ -202,20 +202,18 @@ impl Engine {
}); });
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
let func = if args.is_none() { let func = func
// Scripted functions are not exposed globally .or_else(|| _global.get_qualified_fn(hash, true))
func .or_else(|| {
} else {
func.or_else(|| _global.get_qualified_fn(hash)).or_else(|| {
self.global_sub_modules self.global_sub_modules
.as_deref() .as_deref()
.into_iter() .into_iter()
.flatten() .flatten()
.filter(|(_, m)| m.contains_indexed_global_functions())
.find_map(|(_, m)| { .find_map(|(_, m)| {
m.get_qualified_fn(hash).map(|f| (f, m.id_raw())) m.get_qualified_fn(hash).map(|f| (f, m.id_raw()))
}) })
}) });
};
if let Some((f, s)) = func { if let Some((f, s)) = func {
// Specific version found // Specific version found
@ -335,8 +333,7 @@ impl Engine {
/// Function call arguments be _consumed_ when the function requires them to be passed by value. /// Function call arguments be _consumed_ when the function requires them to be passed by value.
/// All function arguments not in the first position are always passed by value and thus consumed. /// All function arguments not in the first position are always passed by value and thus consumed.
/// ///
/// **DO NOT** reuse the argument values unless for the first `&mut` argument - /// **DO NOT** reuse the argument values except for the first `&mut` argument - all others are silently replaced by `()`!
/// all others are silently replaced by `()`!
pub(crate) fn exec_native_fn_call( pub(crate) fn exec_native_fn_call(
&self, &self,
global: &mut GlobalRuntimeState, global: &mut GlobalRuntimeState,
@ -562,8 +559,7 @@ impl Engine {
/// Function call arguments may be _consumed_ when the function requires them to be passed by /// Function call arguments may be _consumed_ when the function requires them to be passed by
/// value. All function arguments not in the first position are always passed by value and thus consumed. /// value. All function arguments not in the first position are always passed by value and thus consumed.
/// ///
/// **DO NOT** reuse the argument values unless for the first `&mut` argument - /// **DO NOT** reuse the argument values except for the first `&mut` argument - all others are silently replaced by `()`!
/// all others are silently replaced by `()`!
pub(crate) fn exec_fn_call( pub(crate) fn exec_fn_call(
&self, &self,
global: &mut GlobalRuntimeState, global: &mut GlobalRuntimeState,
@ -572,14 +568,14 @@ impl Engine {
fn_name: &str, fn_name: &str,
op_token: Option<&Token>, op_token: Option<&Token>,
hashes: FnCallHashes, hashes: FnCallHashes,
mut _args: &mut FnCallArgs, args: &mut FnCallArgs,
is_ref_mut: bool, is_ref_mut: bool,
_is_method_call: bool, _is_method_call: bool,
pos: Position, pos: Position,
) -> RhaiResultOf<(Dynamic, bool)> { ) -> RhaiResultOf<(Dynamic, bool)> {
// Check for data race. // Check for data race.
#[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_closure"))]
ensure_no_data_race(fn_name, _args, is_ref_mut)?; ensure_no_data_race(fn_name, args, is_ref_mut)?;
auto_restore! { let orig_level = global.level; global.level += 1 } auto_restore! { let orig_level = global.level; global.level += 1 }
@ -587,18 +583,18 @@ impl Engine {
if hashes.is_native_only() { if hashes.is_native_only() {
match fn_name { match fn_name {
// Handle type_of() // Handle type_of()
KEYWORD_TYPE_OF if _args.len() == 1 => { KEYWORD_TYPE_OF if args.len() == 1 => {
let typ = self.map_type_name(_args[0].type_name()).to_string().into(); let typ = self.get_interned_string(self.map_type_name(args[0].type_name()));
return Ok((typ, false)); return Ok((typ.into(), false));
} }
// Handle is_def_fn() // Handle is_def_fn()
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
crate::engine::KEYWORD_IS_DEF_FN crate::engine::KEYWORD_IS_DEF_FN
if _args.len() == 2 && _args[0].is_fnptr() && _args[1].is_int() => if args.len() == 2 && args[0].is_fnptr() && args[1].is_int() =>
{ {
let fn_name = _args[0].read_lock::<ImmutableString>().expect("`FnPtr`"); let fn_name = args[0].read_lock::<ImmutableString>().expect("`FnPtr`");
let num_params = _args[1].as_int().expect("`INT`"); let num_params = args[1].as_int().expect("`INT`");
return Ok(( return Ok((
if (0..=crate::MAX_USIZE_INT).contains(&num_params) { if (0..=crate::MAX_USIZE_INT).contains(&num_params) {
@ -629,16 +625,30 @@ impl Engine {
} }
} }
// Script-defined function call?
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
if !hashes.is_native_only() { if !hashes.is_native_only() {
// Script-defined function call?
let hash = hashes.script(); let hash = hashes.script();
let local_entry = &mut None; let local_entry = &mut None;
if let Some(FnResolutionCacheEntry { func, ref source }) = self #[cfg(not(feature = "no_object"))]
.resolve_fn(global, caches, local_entry, None, hash, None, false) let resolved = if _is_method_call && !args.is_empty() {
.cloned() let typed_hash =
{ crate::calc_typed_method_hash(hash, self.map_type_name(args[0].type_name()));
self.resolve_fn(global, caches, local_entry, None, typed_hash, None, false)
} else {
None
};
#[cfg(feature = "no_object")]
let resolved = None;
let resolved = if resolved.is_none() {
self.resolve_fn(global, caches, local_entry, None, hash, None, false)
} else {
resolved
};
if let Some(FnResolutionCacheEntry { func, ref source }) = resolved.cloned() {
// Script function call // Script function call
debug_assert!(func.is_script()); debug_assert!(func.is_script());
@ -662,7 +672,7 @@ impl Engine {
return if _is_method_call { return if _is_method_call {
// Method call of script function - map first argument to `this` // Method call of script function - map first argument to `this`
let (first_arg, rest_args) = _args.split_first_mut().unwrap(); let (first_arg, rest_args) = args.split_first_mut().unwrap();
self.call_script_fn( self.call_script_fn(
global, global,
@ -680,13 +690,13 @@ impl Engine {
let backup = &mut ArgBackup::new(); let backup = &mut ArgBackup::new();
// The first argument is a reference? // The first argument is a reference?
let swap = is_ref_mut && !_args.is_empty(); let swap = is_ref_mut && !args.is_empty();
if swap { if swap {
backup.change_first_arg_to_copy(_args); backup.change_first_arg_to_copy(args);
} }
auto_restore! { args = (_args) if swap => move |a| backup.restore_first_arg(a) } auto_restore! { args = (args) if swap => move |a| backup.restore_first_arg(a) }
self.call_script_fn(global, caches, scope, None, environ, f, args, true, pos) self.call_script_fn(global, caches, scope, None, environ, f, args, true, pos)
} }
@ -698,7 +708,7 @@ impl Engine {
let hash = hashes.native(); let hash = hashes.native();
self.exec_native_fn_call( self.exec_native_fn_call(
global, caches, fn_name, op_token, hash, _args, is_ref_mut, pos, global, caches, fn_name, op_token, hash, args, is_ref_mut, pos,
) )
} }
@ -1392,15 +1402,11 @@ impl Engine {
.ok_or_else(|| ERR::ErrorModuleNotFound(namespace.to_string(), namespace.position()))?; .ok_or_else(|| ERR::ErrorModuleNotFound(namespace.to_string(), namespace.position()))?;
// First search script-defined functions in namespace (can override built-in) // First search script-defined functions in namespace (can override built-in)
let mut func = match module.get_qualified_fn(hash) { let mut func = module.get_qualified_fn(hash).or_else(|| {
// Then search native Rust functions // Then search native Rust functions
None => {
self.track_operation(global, pos)?;
let hash_qualified_fn = calc_fn_hash_full(hash, args.iter().map(|a| a.type_id())); let hash_qualified_fn = calc_fn_hash_full(hash, args.iter().map(|a| a.type_id()));
module.get_qualified_fn(hash_qualified_fn) module.get_qualified_fn(hash_qualified_fn)
} });
r => r,
};
// Check for `Dynamic` parameters. // Check for `Dynamic` parameters.
// //

View File

@ -76,6 +76,9 @@ pub fn get_hasher() -> ahash::AHasher {
#[must_use] #[must_use]
pub fn calc_var_hash<'a>(namespace: impl IntoIterator<Item = &'a str>, var_name: &str) -> u64 { pub fn calc_var_hash<'a>(namespace: impl IntoIterator<Item = &'a str>, var_name: &str) -> u64 {
let s = &mut get_hasher(); let s = &mut get_hasher();
s.write_u8(b'V'); // hash a discriminant
let mut count = 0; let mut count = 0;
// We always skip the first module // We always skip the first module
@ -111,6 +114,9 @@ pub fn calc_fn_hash<'a>(
num: usize, num: usize,
) -> u64 { ) -> u64 {
let s = &mut get_hasher(); let s = &mut get_hasher();
s.write_u8(b'F'); // hash a discriminant
let mut count = 0; let mut count = 0;
namespace.into_iter().for_each(|m| { namespace.into_iter().for_each(|m| {
@ -134,6 +140,9 @@ pub fn calc_fn_hash<'a>(
#[must_use] #[must_use]
pub fn calc_fn_hash_full(base: u64, params: impl IntoIterator<Item = TypeId>) -> u64 { pub fn calc_fn_hash_full(base: u64, params: impl IntoIterator<Item = TypeId>) -> u64 {
let s = &mut get_hasher(); let s = &mut get_hasher();
s.write_u8(b'A'); // hash a discriminant
let mut count = 0; let mut count = 0;
params.into_iter().for_each(|t| { params.into_iter().for_each(|t| {
t.hash(s); t.hash(s);
@ -143,3 +152,17 @@ pub fn calc_fn_hash_full(base: u64, params: impl IntoIterator<Item = TypeId>) ->
s.finish() ^ base s.finish() ^ base
} }
/// Calculate a [`u64`] hash key from a base [`u64`] hash key and the type of the `this` pointer.
#[cfg(not(feature = "no_object"))]
#[cfg(not(feature = "no_function"))]
#[inline]
#[must_use]
pub fn calc_typed_method_hash(base: u64, this_type: &str) -> u64 {
let s = &mut get_hasher();
s.write_u8(b'T'); // hash a discriminant
this_type.hash(s);
s.finish() ^ base
}

View File

@ -21,6 +21,9 @@ pub use call::FnCallArgs;
pub use callable_function::{CallableFunction, EncapsulatedEnviron}; pub use callable_function::{CallableFunction, EncapsulatedEnviron};
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
pub use func::Func; pub use func::Func;
#[cfg(not(feature = "no_object"))]
#[cfg(not(feature = "no_function"))]
pub use hashing::calc_typed_method_hash;
pub use hashing::{calc_fn_hash, calc_fn_hash_full, calc_var_hash, get_hasher, StraightHashMap}; pub use hashing::{calc_fn_hash, calc_fn_hash_full, calc_var_hash, get_hasher, StraightHashMap};
#[cfg(feature = "internals")] #[cfg(feature = "internals")]
#[allow(deprecated)] #[allow(deprecated)]

View File

@ -22,7 +22,7 @@ impl Engine {
/// Function call arguments may be _consumed_ when the function requires them to be passed by value. /// Function call arguments may be _consumed_ when the function requires them to be passed by value.
/// All function arguments not in the first position are always passed by value and thus consumed. /// All function arguments not in the first position are always passed by value and thus consumed.
/// ///
/// **DO NOT** reuse the argument values unless for the first `&mut` argument - all others are silently replaced by `()`! /// **DO NOT** reuse the argument values except for the first `&mut` argument - all others are silently replaced by `()`!
pub(crate) fn call_script_fn( pub(crate) fn call_script_fn(
&self, &self,
global: &mut GlobalRuntimeState, global: &mut GlobalRuntimeState,

View File

@ -227,6 +227,9 @@ pub use api::{eval::eval, events::VarDefInfo, run::run};
pub use ast::{FnAccess, AST}; pub use ast::{FnAccess, AST};
pub use engine::{Engine, OP_CONTAINS, OP_EQUALS}; pub use engine::{Engine, OP_CONTAINS, OP_EQUALS};
pub use eval::EvalContext; pub use eval::EvalContext;
#[cfg(not(feature = "no_function"))]
#[cfg(not(feature = "no_object"))]
use func::calc_typed_method_hash;
use func::{calc_fn_hash, calc_fn_hash_full, calc_var_hash}; use func::{calc_fn_hash, calc_fn_hash_full, calc_var_hash};
pub use func::{plugin, FuncArgs, NativeCallContext, RegisterNativeFunction}; pub use func::{plugin, FuncArgs, NativeCallContext, RegisterNativeFunction};
pub use module::{FnNamespace, Module}; pub use module::{FnNamespace, Module};

View File

@ -73,6 +73,9 @@ pub struct FuncInfoMetadata {
pub access: FnAccess, pub access: FnAccess,
/// Function name. /// Function name.
pub name: Identifier, pub name: Identifier,
#[cfg(not(feature = "no_object"))]
/// Type of `this` pointer, if any.
pub this_type: Option<ImmutableString>,
/// Number of parameters. /// Number of parameters.
pub num_params: usize, pub num_params: usize,
/// Parameter types (if applicable). /// Parameter types (if applicable).
@ -728,8 +731,16 @@ impl Module {
let fn_def = fn_def.into(); let fn_def = fn_def.into();
// None + function name + number of arguments. // None + function name + number of arguments.
let namespace = FnNamespace::Internal;
let num_params = fn_def.params.len(); let num_params = fn_def.params.len();
let hash_script = crate::calc_fn_hash(None, &fn_def.name, num_params); let hash_script = crate::calc_fn_hash(None, &fn_def.name, num_params);
#[cfg(not(feature = "no_object"))]
let (hash_script, namespace) = if let Some(ref this_type) = fn_def.this_type {
let hash = crate::calc_typed_method_hash(hash_script, this_type);
(hash, FnNamespace::Global)
} else {
(hash_script, namespace)
};
// Catch hash collisions in testing environment only. // Catch hash collisions in testing environment only.
#[cfg(feature = "testing-environ")] #[cfg(feature = "testing-environ")]
@ -750,8 +761,10 @@ impl Module {
FuncInfo { FuncInfo {
metadata: FuncInfoMetadata { metadata: FuncInfoMetadata {
name: fn_def.name.as_str().into(), name: fn_def.name.as_str().into(),
namespace: FnNamespace::Internal, namespace,
access: fn_def.access, access: fn_def.access,
#[cfg(not(feature = "no_object"))]
this_type: fn_def.this_type.clone(),
num_params, num_params,
param_types: FnArgsVec::new_const(), param_types: FnArgsVec::new_const(),
#[cfg(feature = "metadata")] #[cfg(feature = "metadata")]
@ -765,8 +778,10 @@ impl Module {
func: fn_def.into(), func: fn_def.into(),
}, },
); );
self.flags self.flags
.remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS); .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS);
hash_script hash_script
} }
@ -1009,7 +1024,7 @@ impl Module {
type_id type_id
} }
/// Set a Rust function into the [`Module`], returning a [`u64`] hash key. /// Set a native Rust function into the [`Module`], returning a [`u64`] hash key.
/// ///
/// If there is an existing Rust function of the same hash, it is replaced. /// If there is an existing Rust function of the same hash, it is replaced.
/// ///
@ -1067,22 +1082,22 @@ impl Module {
}; };
let name = name.as_ref(); let name = name.as_ref();
let hash_script = calc_fn_hash(None, name, param_types.len()); let hash_base = calc_fn_hash(None, name, param_types.len());
let hash_fn = calc_fn_hash_full(hash_script, param_types.iter().copied()); let hash_fn = calc_fn_hash_full(hash_base, param_types.iter().copied());
// Catch hash collisions in testing environment only. // Catch hash collisions in testing environment only.
#[cfg(feature = "testing-environ")] #[cfg(feature = "testing-environ")]
if let Some(f) = self.functions.as_ref().and_then(|f| f.get(&hash_script)) { if let Some(f) = self.functions.as_ref().and_then(|f| f.get(&hash_base)) {
panic!( panic!(
"Hash {} already exists when registering function {}:\n{:#?}", "Hash {} already exists when registering function {}:\n{:#?}",
hash_script, name, f hash_base, name, f
); );
} }
if is_dynamic { if is_dynamic {
self.dynamic_functions_filter self.dynamic_functions_filter
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.mark(hash_script); .mark(hash_base);
} }
self.functions self.functions
@ -1095,6 +1110,8 @@ impl Module {
name: name.into(), name: name.into(),
namespace, namespace,
access, access,
#[cfg(not(feature = "no_object"))]
this_type: None,
num_params: param_types.len(), num_params: param_types.len(),
param_types, param_types,
#[cfg(feature = "metadata")] #[cfg(feature = "metadata")]
@ -1114,7 +1131,7 @@ impl Module {
hash_fn hash_fn
} }
/// _(metadata)_ Set a Rust function into the [`Module`], returning a [`u64`] hash key. /// _(metadata)_ Set a native Rust function into the [`Module`], returning a [`u64`] hash key.
/// Exported under the `metadata` feature only. /// Exported under the `metadata` feature only.
/// ///
/// If there is an existing Rust function of the same hash, it is replaced. /// If there is an existing Rust function of the same hash, it is replaced.
@ -1167,13 +1184,7 @@ impl Module {
hash hash
} }
/// Set a Rust function taking a reference to the scripting [`Engine`][crate::Engine], /// Set a native Rust function into the [`Module`], returning a [`u64`] hash key.
/// the current set of functions, plus a list of mutable [`Dynamic`] references
/// into the [`Module`], returning a [`u64`] hash key.
///
/// Use this to register a built-in function which must reference settings on the scripting
/// [`Engine`][crate::Engine] (e.g. to prevent growing an array beyond the allowed maximum size),
/// or to call a script-defined function in the current evaluation context.
/// ///
/// If there is a similar existing Rust function, it is replaced. /// If there is a similar existing Rust function, it is replaced.
/// ///
@ -1259,7 +1270,7 @@ impl Module {
) )
} }
/// Set a Rust function into the [`Module`], returning a [`u64`] hash key. /// Set a native Rust function into the [`Module`], returning a [`u64`] hash key.
/// ///
/// If there is a similar existing Rust function, it is replaced. /// If there is a similar existing Rust function, it is replaced.
/// ///
@ -1618,7 +1629,7 @@ impl Module {
) )
} }
/// Look up a Rust function by hash. /// Look up a native Rust function by hash.
/// ///
/// The [`u64`] hash is returned by the [`set_native_fn`][Module::set_native_fn] call. /// The [`u64`] hash is returned by the [`set_native_fn`][Module::set_native_fn] call.
#[inline] #[inline]
@ -2298,14 +2309,14 @@ impl Module {
} }
} }
// Index type iterators // Index all type iterators
if let Some(ref t) = module.type_iterators { if let Some(ref t) = module.type_iterators {
for (&type_id, func) in t.iter() { for (&type_id, func) in t.iter() {
type_iterators.insert(type_id, func.clone()); type_iterators.insert(type_id, func.clone());
} }
} }
// Index all Rust functions // Index all functions
for (&hash, f) in module.functions.iter().flatten() { for (&hash, f) in module.functions.iter().flatten() {
match f.metadata.namespace { match f.metadata.namespace {
FnNamespace::Global => { FnNamespace::Global => {
@ -2346,12 +2357,21 @@ impl Module {
} }
functions.insert(hash_qualified_fn, f.func.clone()); functions.insert(hash_qualified_fn, f.func.clone());
} else if cfg!(not(feature = "no_function")) { } else {
#[cfg(not(feature = "no_function"))]
{
let hash_qualified_script = crate::calc_fn_hash( let hash_qualified_script = crate::calc_fn_hash(
path.iter().copied(), path.iter().copied(),
&f.metadata.name, &f.metadata.name,
f.metadata.num_params, f.metadata.num_params,
); );
#[cfg(not(feature = "no_object"))]
let hash_qualified_script =
if let Some(ref this_type) = f.metadata.this_type {
crate::calc_typed_method_hash(hash_qualified_script, this_type)
} else {
hash_qualified_script
};
// Catch hash collisions in testing environment only. // Catch hash collisions in testing environment only.
#[cfg(feature = "testing-environ")] #[cfg(feature = "testing-environ")]
@ -2365,6 +2385,7 @@ impl Module {
functions.insert(hash_qualified_script, f.func.clone()); functions.insert(hash_qualified_script, f.func.clone());
} }
} }
}
contains_indexed_global_functions contains_indexed_global_functions
} }

View File

@ -1348,8 +1348,9 @@ impl Engine {
name: fn_def.name.clone(), name: fn_def.name.clone(),
access: fn_def.access, access: fn_def.access,
body: crate::ast::StmtBlock::NONE, body: crate::ast::StmtBlock::NONE,
#[cfg(not(feature = "no_object"))]
this_type: fn_def.this_type.clone(),
params: fn_def.params.clone(), params: fn_def.params.clone(),
#[cfg(not(feature = "no_function"))]
#[cfg(feature = "metadata")] #[cfg(feature = "metadata")]
comments: Box::default(), comments: Box::default(),
}) })

View File

@ -208,6 +208,10 @@ fn collect_fn_metadata(
"is_anonymous".into(), "is_anonymous".into(),
func.name.starts_with(FN_ANONYMOUS).into(), func.name.starts_with(FN_ANONYMOUS).into(),
); );
#[cfg(not(feature = "no_object"))]
if let Some(ref this_type) = func.this_type {
map.insert("this_type".into(), this_type.into());
}
map.insert( map.insert(
"params".into(), "params".into(),
func.params func.params

View File

@ -864,7 +864,7 @@ impl Engine {
let settings = settings.level_up()?; let settings = settings.level_up()?;
// Recursively parse the indexing chain, right-binding each // Recursively parse the indexing chain, right-binding each
let options = match token { let options = match token {
Token::LeftBracket => ASTFlags::NONE, Token::LeftBracket => ASTFlags::empty(),
Token::QuestionBracket => ASTFlags::NEGATED, Token::QuestionBracket => ASTFlags::NEGATED,
_ => unreachable!("`[` or `?[`"), _ => unreachable!("`[` or `?[`"),
}; };
@ -1810,7 +1810,7 @@ impl Engine {
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
(expr, token @ (Token::LeftBracket | Token::QuestionBracket)) => { (expr, token @ (Token::LeftBracket | Token::QuestionBracket)) => {
let opt = match token { let opt = match token {
Token::LeftBracket => ASTFlags::NONE, Token::LeftBracket => ASTFlags::empty(),
Token::QuestionBracket => ASTFlags::NEGATED, Token::QuestionBracket => ASTFlags::NEGATED,
_ => unreachable!("`[` or `?[`"), _ => unreachable!("`[` or `?[`"),
}; };
@ -1834,7 +1834,7 @@ impl Engine {
} }
let op_flags = match op { let op_flags = match op {
Token::Period => ASTFlags::NONE, Token::Period => ASTFlags::empty(),
Token::Elvis => ASTFlags::NEGATED, Token::Elvis => ASTFlags::NEGATED,
_ => unreachable!("`.` or `?.`"), _ => unreachable!("`.` or `?.`"),
}; };
@ -1842,7 +1842,7 @@ impl Engine {
let rhs = let rhs =
self.parse_primary(input, state, lib, settings.level_up()?, options)?; self.parse_primary(input, state, lib, settings.level_up()?, options)?;
Self::make_dot_expr(state, expr, rhs, ASTFlags::NONE, op_flags, tail_pos)? Self::make_dot_expr(state, expr, rhs, ASTFlags::empty(), op_flags, tail_pos)?
} }
// Unknown postfix operator // Unknown postfix operator
(expr, token) => { (expr, token) => {
@ -2141,7 +2141,7 @@ impl Engine {
{ {
let options = options | parent_options; let options = options | parent_options;
x.rhs = Self::make_dot_expr(state, x.rhs, rhs, options, op_flags, op_pos)?; x.rhs = Self::make_dot_expr(state, x.rhs, rhs, options, op_flags, op_pos)?;
Ok(Expr::Index(x, ASTFlags::NONE, pos)) Ok(Expr::Index(x, ASTFlags::empty(), pos))
} }
// lhs.module::id - syntax error // lhs.module::id - syntax error
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
@ -2770,7 +2770,7 @@ impl Engine {
let body = self.parse_block(input, state, lib, settings)?.into(); let body = self.parse_block(input, state, lib, settings)?.into();
let negated = match input.next().expect(NEVER_ENDS) { let negated = match input.next().expect(NEVER_ENDS) {
(Token::While, ..) => ASTFlags::NONE, (Token::While, ..) => ASTFlags::empty(),
(Token::Until, ..) => ASTFlags::NEGATED, (Token::Until, ..) => ASTFlags::NEGATED,
(.., pos) => { (.., pos) => {
return Err( return Err(
@ -2966,7 +2966,7 @@ impl Engine {
let export = if is_export { let export = if is_export {
ASTFlags::EXPORTED ASTFlags::EXPORTED
} else { } else {
ASTFlags::NONE ASTFlags::empty()
}; };
let (existing, hit_barrier) = state.find_var(&name); let (existing, hit_barrier) = state.find_var(&name);
@ -3392,10 +3392,15 @@ impl Engine {
comments, comments,
)?; )?;
// Restore parse state
let hash = calc_fn_hash(None, &f.name, f.params.len()); let hash = calc_fn_hash(None, &f.name, f.params.len());
#[cfg(not(feature = "no_object"))]
let hash = if let Some(ref this_type) = f.this_type {
crate::calc_typed_method_hash(hash, this_type)
} else {
hash
};
if !lib.is_empty() && lib.contains_key(&hash) { if !lib.is_empty() && lib.contains_key(&hash) {
return Err(PERR::FnDuplicatedDefinition( return Err(PERR::FnDuplicatedDefinition(
f.name.to_string(), f.name.to_string(),
@ -3433,7 +3438,7 @@ impl Engine {
if self.allow_looping() && settings.has_flag(ParseSettingFlags::BREAKABLE) => if self.allow_looping() && settings.has_flag(ParseSettingFlags::BREAKABLE) =>
{ {
let pos = eat_token(input, Token::Continue); let pos = eat_token(input, Token::Continue);
Ok(Stmt::BreakLoop(None, ASTFlags::NONE, pos)) Ok(Stmt::BreakLoop(None, ASTFlags::empty(), pos))
} }
Token::Break Token::Break
if self.allow_looping() && settings.has_flag(ParseSettingFlags::BREAKABLE) => if self.allow_looping() && settings.has_flag(ParseSettingFlags::BREAKABLE) =>
@ -3465,7 +3470,7 @@ impl Engine {
.next() .next()
.map(|(token, pos)| { .map(|(token, pos)| {
let flags = match token { let flags = match token {
Token::Return => ASTFlags::NONE, Token::Return => ASTFlags::empty(),
Token::Throw => ASTFlags::BREAK, Token::Throw => ASTFlags::BREAK,
token => unreachable!( token => unreachable!(
"Token::Return or Token::Throw expected but gets {:?}", "Token::Return or Token::Throw expected but gets {:?}",
@ -3605,6 +3610,35 @@ impl Engine {
let (token, pos) = input.next().expect(NEVER_ENDS); let (token, pos) = input.next().expect(NEVER_ENDS);
// Parse type for `this` pointer
#[cfg(not(feature = "no_object"))]
let ((token, pos), this_type) = match token {
Token::StringConstant(s) if input.peek().expect(NEVER_ENDS).0 == Token::Period => {
eat_token(input, Token::Period);
let s = match s.as_str() {
"int" => state.get_interned_string(std::any::type_name::<crate::INT>()),
#[cfg(not(feature = "no_float"))]
"float" => state.get_interned_string(std::any::type_name::<crate::FLOAT>()),
_ => state.get_interned_string(*s),
};
(input.next().expect(NEVER_ENDS), Some(s))
}
Token::StringConstant(..) => {
return Err(PERR::MissingSymbol(".".to_string()).into_err(pos))
}
Token::Identifier(s) if input.peek().expect(NEVER_ENDS).0 == Token::Period => {
eat_token(input, Token::Period);
let s = match s.as_str() {
"int" => state.get_interned_string(std::any::type_name::<crate::INT>()),
#[cfg(not(feature = "no_float"))]
"float" => state.get_interned_string(std::any::type_name::<crate::FLOAT>()),
_ => state.get_interned_string(*s),
};
(input.next().expect(NEVER_ENDS), Some(s))
}
_ => ((token, pos), None),
};
let name = match token.into_function_name_for_override() { let name = match token.into_function_name_for_override() {
Ok(r) => r, Ok(r) => r,
Err(Token::Reserved(s)) => return Err(PERR::Reserved(s.to_string()).into_err(pos)), Err(Token::Reserved(s)) => return Err(PERR::Reserved(s.to_string()).into_err(pos)),
@ -3679,6 +3713,8 @@ impl Engine {
Ok(ScriptFnDef { Ok(ScriptFnDef {
name: state.get_interned_string(name), name: state.get_interned_string(name),
access, access,
#[cfg(not(feature = "no_object"))]
this_type,
params, params,
body, body,
#[cfg(feature = "metadata")] #[cfg(feature = "metadata")]
@ -3839,6 +3875,8 @@ impl Engine {
let script = Shared::new(ScriptFnDef { let script = Shared::new(ScriptFnDef {
name: fn_name.clone(), name: fn_name.clone(),
access: crate::FnAccess::Public, access: crate::FnAccess::Public,
#[cfg(not(feature = "no_object"))]
this_type: None,
params, params,
body: body.into(), body: body.into(),
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]

View File

@ -38,6 +38,9 @@ struct FnMetadata<'a> {
pub is_anonymous: bool, pub is_anonymous: bool,
#[serde(rename = "type")] #[serde(rename = "type")]
pub typ: FnType, pub typ: FnType,
#[cfg(not(feature = "no_object"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub this_type: Option<&'a str>,
pub num_params: usize, pub num_params: usize,
#[serde(default, skip_serializing_if = "StaticVec::is_empty")] #[serde(default, skip_serializing_if = "StaticVec::is_empty")]
pub params: StaticVec<FnParam<'a>>, pub params: StaticVec<FnParam<'a>>,
@ -88,6 +91,8 @@ impl<'a> From<&'a FuncInfo> for FnMetadata<'a> {
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
is_anonymous: crate::parser::is_anonymous_fn(&info.metadata.name), is_anonymous: crate::parser::is_anonymous_fn(&info.metadata.name),
typ, typ,
#[cfg(not(feature = "no_object"))]
this_type: info.metadata.this_type.as_ref().map(|s| s.as_str()),
num_params: info.metadata.num_params, num_params: info.metadata.num_params,
params: info params: info
.metadata .metadata

View File

@ -75,3 +75,67 @@ fn test_method_call_with_full_optimization() -> Result<(), Box<EvalAltResult>> {
Ok(()) Ok(())
} }
#[cfg(not(feature = "no_function"))]
#[test]
fn test_method_call_typed() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine
.register_type_with_name::<TestStruct>("Test-Struct#ABC")
.register_fn("update", TestStruct::update)
.register_fn("new_ts", TestStruct::new);
assert_eq!(
engine.eval::<TestStruct>(
r#"
fn "Test-Struct#ABC".foo(x) {
this.update(x);
}
fn foo(x) {
this += x;
}
let z = 1000;
z.foo(1);
let x = new_ts();
x.foo(z);
x
"#
)?,
TestStruct { x: 1002 }
);
assert!(matches!(
*engine
.run(
r#"
fn "Test-Struct#ABC".foo(x) {
this.update(x);
}
foo(1000);
"#
)
.expect_err("should error"),
EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("foo")
));
assert!(matches!(
*engine
.run(
r#"
fn "Test-Struct#ABC".foo(x) {
this.update(x);
}
let x = 42;
x.foo(1000);
"#
)
.expect_err("should error"),
EvalAltResult::ErrorFunctionNotFound(f, ..) if f.starts_with("foo")
));
Ok(())
}