From afb651d0aa27198e752c3b295d9e95473d44bf7a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 8 Jan 2022 14:00:41 +0800 Subject: [PATCH 01/16] Support converting literal FnPtr. --- src/ast/expr.rs | 35 ++++++++++++++++++++++++++++++++--- src/ast/stmt.rs | 2 +- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/ast/expr.rs b/src/ast/expr.rs index f59efa87..b7cb4a52 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -1,18 +1,19 @@ //! Module defining script expressions. use super::{ASTNode, Ident, Stmt, StmtBlock}; -use crate::engine::{OP_EXCLUSIVE_RANGE, OP_INCLUSIVE_RANGE}; +use crate::engine::{KEYWORD_FN_PTR, OP_EXCLUSIVE_RANGE, OP_INCLUSIVE_RANGE}; use crate::func::hashing::ALT_ZERO_HASH; use crate::module::Namespace; use crate::tokenizer::Token; use crate::types::dynamic::Union; -use crate::{Dynamic, Identifier, ImmutableString, Position, StaticVec, INT}; +use crate::{calc_fn_hash, Dynamic, FnPtr, Identifier, ImmutableString, Position, StaticVec, INT}; #[cfg(feature = "no_std")] use std::prelude::v1::*; use std::{ collections::BTreeMap, fmt, hash::Hash, + iter::once, num::{NonZeroU8, NonZeroUsize}, }; @@ -522,8 +523,23 @@ impl Expr { })) } + // Fn + Self::FnCall(ref x, _) + if !x.is_qualified() && x.args.len() == 1 && x.name == KEYWORD_FN_PTR => + { + if let Expr::StringConstant(ref s, _) = x.args[0] { + if let Ok(fn_ptr) = FnPtr::new(s) { + fn_ptr.into() + } else { + return None; + } + } else { + return None; + } + } + // Binary operators - Self::FnCall(x, _) if x.args.len() == 2 => match x.name.as_str() { + Self::FnCall(x, _) if !x.is_qualified() && x.args.len() == 2 => match x.name.as_str() { // x..y OP_EXCLUSIVE_RANGE => { if let Expr::IntegerConstant(ref start, _) = x.args[0] { @@ -577,6 +593,19 @@ impl Expr { #[cfg(not(feature = "no_object"))] Union::Map(m, _, _) => Self::DynamicConstant(Box::new((*m).into()), pos), + Union::FnPtr(f, _, _) if !f.is_curried() => Self::FnCall( + FnCallExpr { + namespace: None, + name: KEYWORD_FN_PTR.into(), + hashes: calc_fn_hash(f.fn_name(), 1).into(), + args: once(Self::Stack(0, pos)).collect(), + constants: once(f.fn_name().into()).collect(), + capture_parent_scope: false, + } + .into(), + pos, + ), + _ => Self::DynamicConstant(value.into(), pos), } } diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index f41d566f..a4580c1b 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -483,7 +483,7 @@ impl Stmt { Self::Expr(Expr::Stmt(s)) => s.iter().all(Stmt::is_block_dependent), Self::FnCall(x, _) | Self::Expr(Expr::FnCall(x, _)) => { - x.namespace.is_none() && x.name == KEYWORD_EVAL + !x.is_qualified() && x.name == KEYWORD_EVAL } #[cfg(not(feature = "no_module"))] From f399e8a9052435ae4ccce88b2f3267de4f2e2fad Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 8 Jan 2022 18:40:19 +0800 Subject: [PATCH 02/16] Evaluate function call args more efficiently. --- src/eval/expr.rs | 9 ++- src/func/call.rs | 152 ++++++++++++++++++++++++++--------------------- 2 files changed, 92 insertions(+), 69 deletions(-) diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 5a12be07..c032823f 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -229,9 +229,14 @@ impl Engine { ) } else { // Normal function call + let (first_arg, args) = args.split_first().map_or_else( + || (None, args.as_ref()), + |(first, rest)| (Some(first), rest), + ); + self.make_function_call( - scope, global, state, lib, this_ptr, name, args, constants, *hashes, pos, *capture, - level, + scope, global, state, lib, this_ptr, name, first_arg, args, constants, *hashes, + pos, *capture, level, ) } } diff --git a/src/func/call.rs b/src/func/call.rs index bef39af7..b032049b 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -887,12 +887,16 @@ impl Engine { arg_expr: &Expr, constants: &[Dynamic], ) -> RhaiResultOf<(Dynamic, Position)> { - match arg_expr { - Expr::Stack(slot, pos) => Ok((constants[*slot].clone(), *pos)), - ref arg => self - .eval_expr(scope, global, state, lib, this_ptr, arg, level) - .map(|v| (v, arg.position())), - } + Ok(( + if let Expr::Stack(slot, _) = arg_expr { + constants[*slot].clone() + } else if let Some(value) = arg_expr.get_literal_value() { + value + } else { + self.eval_expr(scope, global, state, lib, this_ptr, arg_expr, level)? + }, + arg_expr.position(), + )) } /// Call a function in normal function-call style. @@ -904,6 +908,7 @@ impl Engine { lib: &[&Module], this_ptr: &mut Option<&mut Dynamic>, fn_name: &str, + first_arg: Option<&Expr>, args_expr: &[Expr], constants: &[Dynamic], hashes: FnCallHashes, @@ -911,8 +916,9 @@ impl Engine { capture_scope: bool, level: usize, ) -> RhaiResult { + let mut first_arg = first_arg; let mut a_expr = args_expr; - let mut total_args = a_expr.len(); + let mut total_args = if first_arg.is_some() { 1 } else { 0 } + a_expr.len(); let mut curry = FnArgsVec::new_const(); let mut name = fn_name; let mut hashes = hashes; @@ -921,26 +927,29 @@ impl Engine { match name { // Handle call() KEYWORD_FN_PTR_CALL if total_args >= 1 => { - let (arg, arg_pos) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, &a_expr[0], constants, - )?; + let arg = first_arg.unwrap(); + let (arg_value, arg_pos) = + self.get_arg_value(scope, global, state, lib, this_ptr, level, arg, constants)?; - if !arg.is::() { + if !arg_value.is::() { return Err(self.make_type_mismatch_err::( - self.map_type_name(arg.type_name()), + self.map_type_name(arg_value.type_name()), arg_pos, )); } - let fn_ptr = arg.cast::(); + let fn_ptr = arg_value.cast::(); curry.extend(fn_ptr.curry().iter().cloned()); // Redirect function name redirected = fn_ptr.take_data().0; name = &redirected; - // Skip the first argument - a_expr = &a_expr[1..]; + // Shift the arguments + first_arg = a_expr.get(0); + if !a_expr.is_empty() { + a_expr = &a_expr[1..]; + } total_args -= 1; // Recalculate hash @@ -953,12 +962,12 @@ impl Engine { } // Handle Fn() KEYWORD_FN_PTR if total_args == 1 => { - let (arg, arg_pos) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, &a_expr[0], constants, - )?; + let arg = first_arg.unwrap(); + let (arg_value, arg_pos) = + self.get_arg_value(scope, global, state, lib, this_ptr, level, arg, constants)?; // Fn - only in function call style - return arg + return arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos)) .and_then(FnPtr::try_from) @@ -968,30 +977,30 @@ impl Engine { // Handle curry() KEYWORD_FN_PTR_CURRY if total_args > 1 => { - let (arg, arg_pos) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, &a_expr[0], constants, - )?; + let first = first_arg.unwrap(); + let (arg_value, arg_pos) = self + .get_arg_value(scope, global, state, lib, this_ptr, level, first, constants)?; - if !arg.is::() { + if !arg_value.is::() { return Err(self.make_type_mismatch_err::( - self.map_type_name(arg.type_name()), + self.map_type_name(arg_value.type_name()), arg_pos, )); } - let (name, fn_curry) = arg.cast::().take_data(); + let (name, fn_curry) = arg_value.cast::().take_data(); // Append the new curried arguments to the existing list. - let fn_curry = a_expr.iter().skip(1).try_fold( - fn_curry, - |mut curried, expr| -> RhaiResultOf<_> { - let (value, _) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, expr, constants, - )?; - curried.push(value); - Ok(curried) - }, - )?; + let fn_curry = + a_expr + .iter() + .try_fold(fn_curry, |mut curried, expr| -> RhaiResultOf<_> { + let (value, _) = self.get_arg_value( + scope, global, state, lib, this_ptr, level, expr, constants, + )?; + curried.push(value); + Ok(curried) + })?; return Ok(FnPtr::new_unchecked(name, fn_curry).into()); } @@ -999,28 +1008,28 @@ impl Engine { // Handle is_shared() #[cfg(not(feature = "no_closure"))] crate::engine::KEYWORD_IS_SHARED if total_args == 1 => { - let (arg, _) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, &a_expr[0], constants, - )?; - return Ok(arg.is_shared().into()); + let arg = first_arg.unwrap(); + let (arg_value, _) = + self.get_arg_value(scope, global, state, lib, this_ptr, level, arg, constants)?; + return Ok(arg_value.is_shared().into()); } // Handle is_def_fn() #[cfg(not(feature = "no_function"))] crate::engine::KEYWORD_IS_DEF_FN if total_args == 2 => { - let (arg, arg_pos) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, &a_expr[0], constants, - )?; + let first = first_arg.unwrap(); + let (arg_value, arg_pos) = self + .get_arg_value(scope, global, state, lib, this_ptr, level, first, constants)?; - let fn_name = arg + let fn_name = arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos))?; - let (arg, arg_pos) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, &a_expr[1], constants, + let (arg_value, arg_pos) = self.get_arg_value( + scope, global, state, lib, this_ptr, level, &a_expr[0], constants, )?; - let num_params = arg + let num_params = arg_value .as_int() .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos))?; @@ -1035,10 +1044,10 @@ impl Engine { // Handle is_def_var() KEYWORD_IS_DEF_VAR if total_args == 1 => { - let (arg, arg_pos) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, &a_expr[0], constants, - )?; - let var_name = arg + let arg = first_arg.unwrap(); + let (arg_value, arg_pos) = + self.get_arg_value(scope, global, state, lib, this_ptr, level, arg, constants)?; + let var_name = arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos))?; return Ok(scope.contains(&var_name).into()); @@ -1048,10 +1057,10 @@ impl Engine { KEYWORD_EVAL if total_args == 1 => { // eval - only in function call style let orig_scope_len = scope.len(); - let (value, pos) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, &a_expr[0], constants, - )?; - let script = &value + let arg = first_arg.unwrap(); + let (arg_value, pos) = + self.get_arg_value(scope, global, state, lib, this_ptr, level, arg, constants)?; + let script = &arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, pos))?; let result = self.eval_script_expr_in_place( @@ -1085,8 +1094,8 @@ impl Engine { } // Normal function call - except for Fn, curry, call and eval (handled above) - let mut arg_values = FnArgsVec::with_capacity(a_expr.len()); - let mut args = FnArgsVec::with_capacity(a_expr.len() + curry.len()); + let mut arg_values = FnArgsVec::with_capacity(total_args); + let mut args = FnArgsVec::with_capacity(total_args + curry.len()); let mut is_ref_mut = false; // Capture parent scope? @@ -1094,10 +1103,14 @@ impl Engine { // If so, do it separately because we cannot convert the first argument (if it is a simple // variable access) to &mut because `scope` is needed. if capture_scope && !scope.is_empty() { - a_expr.iter().try_for_each(|expr| { - self.get_arg_value(scope, global, state, lib, this_ptr, level, expr, constants) - .map(|(value, _)| arg_values.push(value.flatten())) - })?; + first_arg + .iter() + .map(|&v| v) + .chain(a_expr.iter()) + .try_for_each(|expr| { + self.get_arg_value(scope, global, state, lib, this_ptr, level, expr, constants) + .map(|(value, _)| arg_values.push(value.flatten())) + })?; args.extend(curry.iter_mut()); args.extend(arg_values.iter_mut()); @@ -1113,17 +1126,17 @@ impl Engine { } // Call with blank scope - if a_expr.is_empty() && curry.is_empty() { + if total_args == 0 && curry.is_empty() { // No arguments } else { // If the first argument is a variable, and there is no curried arguments, // convert to method-call style in order to leverage potential &mut first argument and // avoid cloning the value - if curry.is_empty() && !a_expr.is_empty() && a_expr[0].is_variable_access(false) { + if curry.is_empty() && first_arg.map_or(false, |expr| expr.is_variable_access(false)) { // func(x, ...) -> x.func(...) - let (first_expr, rest_expr) = a_expr.split_first().unwrap(); + let first_expr = first_arg.unwrap(); - rest_expr.iter().try_for_each(|expr| { + a_expr.iter().try_for_each(|expr| { self.get_arg_value(scope, global, state, lib, this_ptr, level, expr, constants) .map(|(value, _)| arg_values.push(value.flatten())) })?; @@ -1155,10 +1168,15 @@ impl Engine { } } else { // func(..., ...) - a_expr.iter().try_for_each(|expr| { - self.get_arg_value(scope, global, state, lib, this_ptr, level, expr, constants) + first_arg + .into_iter() + .chain(a_expr.iter()) + .try_for_each(|expr| { + self.get_arg_value( + scope, global, state, lib, this_ptr, level, expr, constants, + ) .map(|(value, _)| arg_values.push(value.flatten())) - })?; + })?; args.extend(curry.iter_mut()); args.extend(arg_values.iter_mut()); } From d1749131c5795524753161d06244a196909068fd Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 8 Jan 2022 23:23:43 +0800 Subject: [PATCH 03/16] Add missing data size check. --- src/eval/stmt.rs | 3 ++- tests/data_size.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index 0502c996..5994236e 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -141,6 +141,7 @@ impl Engine { let args = &mut [lhs_ptr_inner, &mut new_val]; match self.call_native_fn(global, state, lib, op, hash, args, true, true, op_pos) { + Ok(_) => self.check_data_size(&mut args[0], root.1)?, Err(err) if matches!(*err, ERR::ErrorFunctionNotFound(ref f, _) if f.starts_with(op)) => { // Expand to `var = var op rhs` @@ -153,7 +154,7 @@ impl Engine { *args[0] = value.flatten(); } - err => return err.map(|_| ()), + Err(err) => return Err(err), } } else { // Normal assignment diff --git a/tests/data_size.rs b/tests/data_size.rs index a4dc1818..efe6e12c 100644 --- a/tests/data_size.rs +++ b/tests/data_size.rs @@ -1,5 +1,5 @@ #![cfg(not(feature = "unchecked"))] -use rhai::{Engine, EvalAltResult, ParseErrorType}; +use rhai::{Engine, EvalAltResult, ParseErrorType, INT}; #[cfg(not(feature = "no_index"))] use rhai::Array; @@ -101,6 +101,39 @@ fn test_max_array_size() -> Result<(), Box> { EvalAltResult::ErrorDataTooLarge(_, _) )); + #[cfg(not(feature = "no_closure"))] + assert_eq!( + engine.eval::( + " + let x = 42; + let y = []; + let f = || x; + for n in 0..10 { + y += x; + } + len(y) + " + )?, + 10 + ); + + #[cfg(not(feature = "no_closure"))] + assert!(matches!( + *engine + .run( + " + let x = 42; + let y = []; + let f = || x; + for n in 0..11 { + y += x; + } + " + ) + .expect_err("should error"), + EvalAltResult::ErrorDataTooLarge(_, _) + )); + assert!(matches!( *engine .run( @@ -113,13 +146,25 @@ fn test_max_array_size() -> Result<(), Box> { EvalAltResult::ErrorDataTooLarge(_, _) )); + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::( + " + let x = [1,2,3,4,5,6]; + x.pad(10, 42); + len(x) + " + )?, + 10 + ); + #[cfg(not(feature = "no_object"))] assert!(matches!( *engine .run( " let x = [1,2,3,4,5,6]; - x.pad(100, 42); + x.pad(11, 42); x " ) @@ -127,6 +172,16 @@ fn test_max_array_size() -> Result<(), Box> { EvalAltResult::ErrorDataTooLarge(_, _) )); + assert_eq!( + engine.eval::( + " + let x = [1,2,3]; + len([x, x, x]) + " + )?, + 3 + ); + assert!(matches!( *engine .run( From e4e2bb33569679412288b62aee97df88f4f3ecd9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 9 Jan 2022 12:44:24 +0800 Subject: [PATCH 04/16] Fix builds. --- src/eval/stmt.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index 5994236e..9dbf5433 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -141,7 +141,10 @@ impl Engine { let args = &mut [lhs_ptr_inner, &mut new_val]; match self.call_native_fn(global, state, lib, op, hash, args, true, true, op_pos) { - Ok(_) => self.check_data_size(&mut args[0], root.1)?, + Ok(_) => { + #[cfg(not(feature = "unchecked"))] + self.check_data_size(&mut args[0], root.1)?; + } Err(err) if matches!(*err, ERR::ErrorFunctionNotFound(ref f, _) if f.starts_with(op)) => { // Expand to `var = var op rhs` From 683cf31de270c0780d69a5f3d3b1bd22cba61c46 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 9 Jan 2022 17:26:46 +0800 Subject: [PATCH 05/16] Add more tests. --- benches/eval_expression.rs | 15 +++++++++++++++ tests/arrays.rs | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/benches/eval_expression.rs b/benches/eval_expression.rs index 73bae9c0..dfcdbb06 100644 --- a/benches/eval_expression.rs +++ b/benches/eval_expression.rs @@ -106,6 +106,21 @@ fn bench_eval_call(bench: &mut Bencher) { bench.iter(|| engine.eval::(script).unwrap()); } +#[bench] +fn bench_eval_deeply_nested(bench: &mut Bencher) { + let script = r#" + (1 + 2 * 3 - 9) * 4 < 5 * 6 - 70 / 8 && + (42 + 99 > 1 + 2 - 3 + 4 * 5 || 123 - 88 < 123 + 88 - 99 + 100) + && true + && !!!!!!!!false + "#; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + bench.iter(|| engine.eval::(script).unwrap()); +} + #[bench] fn bench_eval_loop_number(bench: &mut Bencher) { let script = " diff --git a/tests/arrays.rs b/tests/arrays.rs index 8da2f30d..ce1a0028 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -120,6 +120,18 @@ fn test_arrays() -> Result<(), Box> { .into_typed_array::()?, [1, 2, 3, 4, 5] ); + #[cfg(not(feature = "no_closure"))] + assert!(!engine.eval::( + " + let x = 42; + let y = []; + let f = || x; + for n in 0..10 { + y += x; + } + some(y, |x| is_shared(x)) + " + )?); Ok(()) } From 1e0d46fc13051f2f0781b165a6dd17fda288ac66 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 9 Jan 2022 18:43:12 +0800 Subject: [PATCH 06/16] Fix debug print for GlobalRuntimeStates. --- src/eval/global_state.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/eval/global_state.rs b/src/eval/global_state.rs index 603aa202..0fd0e821 100644 --- a/src/eval/global_state.rs +++ b/src/eval/global_state.rs @@ -263,9 +263,14 @@ impl, M: Into>> Extend<(K, M)> for GlobalRunt impl fmt::Debug for GlobalRuntimeState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("Imports")?; - f.debug_map() - .entries(self.keys.iter().zip(self.modules.iter())) + f.debug_struct("GlobalRuntimeState") + .field("imports", &self.keys.iter().zip(self.modules.iter())) + .field("source", &self.source) + .field("num_operations", &self.num_operations) + .field("num_modules_loaded", &self.num_modules_loaded) + .field("fn_hash_indexing", &self.fn_hash_indexing) + .field("embedded_module_resolver", &self.embedded_module_resolver) + .field("constants", &self.constants) .finish() } } From d15470fd4bd118858e5af5d3ff217a2e136252c2 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 10 Jan 2022 13:26:33 +0800 Subject: [PATCH 07/16] Change eval<()> to run. --- tests/comments.rs | 2 +- tests/maps.rs | 2 +- tests/modules.rs | 2 +- tests/operations.rs | 18 ++++++++---------- tests/stack.rs | 2 +- tests/switch.rs | 2 +- tests/throw.rs | 6 +++--- tests/unit.rs | 4 ++-- 8 files changed, 18 insertions(+), 20 deletions(-) diff --git a/tests/comments.rs b/tests/comments.rs index e0a2d2a2..38da0c1f 100644 --- a/tests/comments.rs +++ b/tests/comments.rs @@ -21,7 +21,7 @@ fn test_comments() -> Result<(), Box> { 42 ); - assert_eq!(engine.eval::<()>("/* Hello world */")?, ()); + assert_eq!(engine.run("/* Hello world */")?, ()); Ok(()) } diff --git a/tests/maps.rs b/tests/maps.rs index 10a79591..a4757423 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -36,7 +36,7 @@ fn test_map_indexing() -> Result<(), Box> { 5 ); - engine.eval::<()>("let y = #{a: 1, b: 2, c: 3}; y.z")?; + engine.run("let y = #{a: 1, b: 2, c: 3}; y.z")?; #[cfg(not(feature = "no_index"))] assert_eq!( diff --git a/tests/modules.rs b/tests/modules.rs index b1587f10..6f8e56b7 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -243,7 +243,7 @@ fn test_module_resolver() -> Result<(), Box> { engine.set_max_modules(1000); #[cfg(not(feature = "no_function"))] - engine.eval::<()>( + engine.run( r#" fn foo() { import "hello" as h; diff --git a/tests/operations.rs b/tests/operations.rs index 13c0cb48..5114ab05 100644 --- a/tests/operations.rs +++ b/tests/operations.rs @@ -15,18 +15,16 @@ fn test_max_operations() -> Result<(), Box> { None }); - engine.eval::<()>("let x = 0; while x < 20 { x += 1; }")?; + engine.run("let x = 0; while x < 20 { x += 1; }")?; assert!(matches!( - *engine - .eval::<()>("for x in 0..500 {}") - .expect_err("should error"), + *engine.run("for x in 0..500 {}").expect_err("should error"), EvalAltResult::ErrorTooManyOperations(_) )); engine.set_max_operations(0); - engine.eval::<()>("for x in 0..10000 {}")?; + engine.run("for x in 0..10000 {}")?; Ok(()) } @@ -43,7 +41,7 @@ fn test_max_operations_functions() -> Result<(), Box> { None }); - engine.eval::<()>( + engine.run( r#" print("Test1"); let x = 0; @@ -56,7 +54,7 @@ fn test_max_operations_functions() -> Result<(), Box> { )?; #[cfg(not(feature = "no_function"))] - engine.eval::<()>( + engine.run( r#" print("Test2"); fn inc(x) { x + 1 } @@ -68,7 +66,7 @@ fn test_max_operations_functions() -> Result<(), Box> { #[cfg(not(feature = "no_function"))] assert!(matches!( *engine - .eval::<()>( + .run( r#" print("Test3"); fn inc(x) { x + 1 } @@ -101,7 +99,7 @@ fn test_max_operations_eval() -> Result<(), Box> { assert!(matches!( *engine - .eval::<()>( + .run( r#" let script = "for x in 0..500 {}"; eval(script); @@ -131,7 +129,7 @@ fn test_max_operations_progress() -> Result<(), Box> { assert!(matches!( *engine - .eval::<()>("for x in 0..500 {}") + .run("for x in 0..500 {}") .expect_err("should error"), EvalAltResult::ErrorTerminated(x, _) if x.as_int()? == 42 )); diff --git a/tests/stack.rs b/tests/stack.rs index febdcbbd..c2a8d21d 100644 --- a/tests/stack.rs +++ b/tests/stack.rs @@ -21,7 +21,7 @@ fn test_stack_overflow_fn_calls() -> Result<(), Box> { #[cfg(not(feature = "unchecked"))] assert!(matches!( *engine - .eval::<()>(&format!( + .run(&format!( " fn foo(n) {{ if n == 0 {{ 0 }} else {{ n + foo(n-1) }} }} foo({}) diff --git a/tests/switch.rs b/tests/switch.rs index b3afbb6f..6d91c809 100644 --- a/tests/switch.rs +++ b/tests/switch.rs @@ -11,7 +11,7 @@ fn test_switch() -> Result<(), Box> { 'a' ); assert_eq!( - engine.eval::<()>("switch 3 { 1 => (), 2 => 'a', 42 => true }")?, + engine.run("switch 3 { 1 => (), 2 => 'a', 42 => true }")?, () ); assert_eq!( diff --git a/tests/throw.rs b/tests/throw.rs index 2ef82dd2..e0617559 100644 --- a/tests/throw.rs +++ b/tests/throw.rs @@ -5,12 +5,12 @@ fn test_throw() { let engine = Engine::new(); assert!(matches!( - *engine.eval::<()>("if true { throw 42 }").expect_err("expects error"), + *engine.run("if true { throw 42 }").expect_err("expects error"), EvalAltResult::ErrorRuntime(s, _) if s.as_int().unwrap() == 42 )); assert!(matches!( - *engine.eval::<()>(r#"throw"#).expect_err("expects error"), + *engine.run(r#"throw"#).expect_err("expects error"), EvalAltResult::ErrorRuntime(s, _) if s.is::<()>() )); } @@ -88,7 +88,7 @@ fn test_try_catch() -> Result<(), Box> { #[cfg(not(feature = "unchecked"))] assert!(matches!( *engine - .eval::<()>("try { 42/0; } catch { throw; }") + .run("try { 42/0; } catch { throw; }") .expect_err("expects error"), EvalAltResult::ErrorArithmetic(_, _) )); diff --git a/tests/unit.rs b/tests/unit.rs index 2777ae54..edfcf61c 100644 --- a/tests/unit.rs +++ b/tests/unit.rs @@ -3,7 +3,7 @@ use rhai::{Engine, EvalAltResult}; #[test] fn test_unit() -> Result<(), Box> { let engine = Engine::new(); - engine.eval::<()>("let x = (); x")?; + engine.run("let x = (); x")?; Ok(()) } @@ -17,6 +17,6 @@ fn test_unit_eq() -> Result<(), Box> { #[test] fn test_unit_with_spaces() -> Result<(), Box> { let engine = Engine::new(); - engine.eval::<()>("let x = ( ); x")?; + engine.run("let x = ( ); x")?; Ok(()) } From efd57c600b105e61e40ada0a2e1d7f1679446ab5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 10 Jan 2022 13:29:22 +0800 Subject: [PATCH 08/16] Add test for literal operations. --- tests/operations.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/operations.rs b/tests/operations.rs index 5114ab05..8f31530f 100644 --- a/tests/operations.rs +++ b/tests/operations.rs @@ -29,6 +29,38 @@ fn test_max_operations() -> Result<(), Box> { Ok(()) } +#[test] +fn test_max_operations_literal() -> Result<(), Box> { + let mut engine = Engine::new(); + #[cfg(not(feature = "no_optimize"))] + engine.set_optimization_level(rhai::OptimizationLevel::None); + engine.set_max_operations(10); + + #[cfg(not(feature = "no_index"))] + engine.run("[1, 2, 3, 4, 5, 6, 7]")?; + + #[cfg(not(feature = "no_index"))] + assert!(matches!( + *engine + .run("[1, 2, 3, 4, 5, 6, 7, 8, 9]") + .expect_err("should error"), + EvalAltResult::ErrorTooManyOperations(_) + )); + + #[cfg(not(feature = "no_object"))] + engine.run("#{a:1, b:2, c:3, d:4, e:5, f:6, g:7}")?; + + #[cfg(not(feature = "no_object"))] + assert!(matches!( + *engine + .run("#{a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9}") + .expect_err("should error"), + EvalAltResult::ErrorTooManyOperations(_) + )); + + Ok(()) +} + #[test] fn test_max_operations_functions() -> Result<(), Box> { let mut engine = Engine::new(); From ea6c264f5f15eaccd6b387fdd22a1d64bb8d9fd2 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 10 Jan 2022 13:43:30 +0800 Subject: [PATCH 09/16] Fix builds. --- src/eval/global_state.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/eval/global_state.rs b/src/eval/global_state.rs index 0fd0e821..5378eba9 100644 --- a/src/eval/global_state.rs +++ b/src/eval/global_state.rs @@ -263,14 +263,23 @@ impl, M: Into>> Extend<(K, M)> for GlobalRunt impl fmt::Debug for GlobalRuntimeState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("GlobalRuntimeState") - .field("imports", &self.keys.iter().zip(self.modules.iter())) + let mut f = f.debug_struct("GlobalRuntimeState"); + + f.field("imports", &self.keys.iter().zip(self.modules.iter())) .field("source", &self.source) .field("num_operations", &self.num_operations) - .field("num_modules_loaded", &self.num_modules_loaded) - .field("fn_hash_indexing", &self.fn_hash_indexing) - .field("embedded_module_resolver", &self.embedded_module_resolver) - .field("constants", &self.constants) - .finish() + .field("num_modules_loaded", &self.num_modules_loaded); + + #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] + f.field("fn_hash_indexing", &self.fn_hash_indexing); + + #[cfg(not(feature = "no_module"))] + f.field("embedded_module_resolver", &self.embedded_module_resolver); + + #[cfg(not(feature = "no_module"))] + #[cfg(not(feature = "no_function"))] + f.field("constants", &self.constants); + + f.finish() } } From 5d90b3274c7e2dd56cb3d1ebc4716cefe9fde57c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 10 Jan 2022 20:08:03 +0800 Subject: [PATCH 10/16] Catch unsupported custom syntax. --- src/eval/expr.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/eval/expr.rs b/src/eval/expr.rs index c032823f..2a0e580f 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -419,7 +419,13 @@ impl Engine { Expr::Custom(custom, _) => { let expressions: StaticVec<_> = custom.inputs.iter().map(Into::into).collect(); let key_token = custom.tokens.first().unwrap(); - let custom_def = self.custom_syntax.get(key_token).unwrap(); + let custom_def = self.custom_syntax.get(key_token).ok_or_else(|| { + let code: Vec<_> = custom.tokens.iter().map(|s| s.as_str()).collect(); + Box::new(ERR::ErrorSystem( + format!("Invalid custom syntax prefix: {}", key_token), + code.join(" ").into(), + )) + })?; let mut context = EvalContext { engine: self, scope, From 56217e386ed9913862131f5345156c1f23e83b3b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 10 Jan 2022 22:51:04 +0800 Subject: [PATCH 11/16] Fix debug for Engine. --- src/engine.rs | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 698068d4..0487f652 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -140,9 +140,37 @@ pub struct Engine { } impl fmt::Debug for Engine { - #[inline(always)] + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("Engine") + let mut f = f.debug_struct("Engine"); + + f.field("global_modules", &self.global_modules) + .field("global_sub_modules", &self.global_sub_modules); + + #[cfg(not(feature = "no_module"))] + f.field("module_resolver", &self.module_resolver.is_some()); + + f.field("type_names", &self.type_names) + .field("disabled_symbols", &self.disabled_symbols) + .field("custom_keywords", &self.custom_keywords) + .field("custom_syntax", &(!self.custom_syntax.is_empty())) + .field("resolve_var", &self.resolve_var.is_some()) + .field("token_mapper", &self.token_mapper.is_some()) + .field("print", &self.print.is_some()) + .field("debug", &self.debug.is_some()); + + #[cfg(not(feature = "unchecked"))] + f.field("progress", &self.progress.is_some()); + + #[cfg(not(feature = "no_optimize"))] + f.field("optimization_level", &self.optimization_level); + + f.field("options", &self.options); + + #[cfg(not(feature = "unchecked"))] + f.field("limits", &self.limits); + + f.finish() } } From f205091d0a9f4081f5eae336d74a2ec5ec5018ff Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 10 Jan 2022 22:51:24 +0800 Subject: [PATCH 12/16] Inline traits impl. --- src/eval/global_state.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/eval/global_state.rs b/src/eval/global_state.rs index 5378eba9..aaa8e50a 100644 --- a/src/eval/global_state.rs +++ b/src/eval/global_state.rs @@ -245,6 +245,7 @@ impl IntoIterator for GlobalRuntimeState { } impl, M: Into>> FromIterator<(K, M)> for GlobalRuntimeState { + #[inline] fn from_iter>(iter: T) -> Self { let mut lib = Self::new(); lib.extend(iter); @@ -253,6 +254,7 @@ impl, M: Into>> FromIterator<(K, M)> for Glob } impl, M: Into>> Extend<(K, M)> for GlobalRuntimeState { + #[inline] fn extend>(&mut self, iter: T) { iter.into_iter().for_each(|(k, m)| { self.keys.push(k.into()); @@ -262,6 +264,7 @@ impl, M: Into>> Extend<(K, M)> for GlobalRunt } impl fmt::Debug for GlobalRuntimeState { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut f = f.debug_struct("GlobalRuntimeState"); From 00255a9b78f5a4e3e799b3fa280c34fcc6d59ac8 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 11 Jan 2022 09:05:18 +0800 Subject: [PATCH 13/16] Merge 1.3.1 into 1.4.0. --- CHANGELOG.md | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f801b08d..d1574404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ Breaking changes Bug fixes --------- +* Custom syntax now works properly inside binary expressions and with method calls. +* Hex numbers with the high-bit set now parse correctly into negative integer numbers. * Constructing a literal array or object map now checks for size limits for each item instead of at the very end when it is already too late. * Non-`INT` integer types are now treated exactly as custom types under `only_i64` and `only_i32`. * Calling `pad` on an array now checks for total size over limit after each item added. @@ -33,6 +35,7 @@ New features Enhancements ------------ +* `BLOB`'s are refined to display in a more compact hex format. * A new syntax is introduced for `def_package!` that will replace the old syntax in future versions. * Added `NativeCallContext::call_fn` to easily call a function. * Doc-comments on plugin module functions are extracted into the functions' metadata. @@ -44,21 +47,6 @@ Deprecated API's * The old syntax of `def_package!` is deprecated in favor of the new syntax. -Version 1.3.1 -============= - -Bug fixes ---------- - -* Custom syntax now works properly inside binary expressions and with method calls. -* Hex numbers with the high-bit set now parse correctly into negative integer numbers. - -Enhancements ------------- - -* `BLOB`'s are refined to display in a more compact hex format. - - Version 1.3.0 ============= From 6dedb1ed9f69337196c560b3d2bcd67525e91e0e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 11 Jan 2022 11:33:54 +0800 Subject: [PATCH 14/16] Fix no_std builds. --- CHANGELOG.md | 1 + src/eval/expr.rs | 10 ++++++---- src/types/error.rs | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1574404..74369277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ New features ------------ * Added support for integer _ranges_ via the `..` and `..=` operators. +* Added `EvalAltResult::ErrorCustomSyntax` to catch errors in custom syntax, which should not happen unless an `AST` is compiled on one `Engine` but evaluated on another unrelated `Engine`. Enhancements ------------ diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 2a0e580f..775bb8ae 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -416,14 +416,16 @@ impl Engine { .into()) } - Expr::Custom(custom, _) => { + Expr::Custom(custom, pos) => { let expressions: StaticVec<_> = custom.inputs.iter().map(Into::into).collect(); + // The first token acts as the custom syntax's key let key_token = custom.tokens.first().unwrap(); + // The key should exist, unless the AST is compiled in a different Engine let custom_def = self.custom_syntax.get(key_token).ok_or_else(|| { - let code: Vec<_> = custom.tokens.iter().map(|s| s.as_str()).collect(); - Box::new(ERR::ErrorSystem( + Box::new(ERR::ErrorCustomSyntax( format!("Invalid custom syntax prefix: {}", key_token), - code.join(" ").into(), + custom.tokens.iter().map(|s| s.to_string()).collect(), + *pos, )) })?; let mut context = EvalContext { diff --git a/src/types/error.rs b/src/types/error.rs index 93e7f3d1..4be7e573 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -71,6 +71,7 @@ pub enum EvalAltResult { ErrorDotExpr(String, Position), /// Arithmetic error encountered. Wrapped value is the error message. ErrorArithmetic(String, Position), + /// Number of operations over maximum limit. ErrorTooManyOperations(Position), /// [Modules][crate::Module] over maximum limit. @@ -81,6 +82,14 @@ pub enum EvalAltResult { ErrorDataTooLarge(String, Position), /// The script is prematurely terminated. Wrapped value is the termination token. ErrorTerminated(Dynamic, Position), + + /// Error encountered for a custom syntax. Wrapped values are the error message and + /// custom syntax symbols stream. + /// + /// Normally this should never happen, unless an [`AST`][crate::AST] is compiled on one + /// [`Engine`][crate::Engine] but evaluated on another unrelated [`Engine`][crate::Engine]. + ErrorCustomSyntax(String, Vec, Position), + /// Run-time error encountered. Wrapped value is the error token. ErrorRuntime(Dynamic, Position), @@ -204,6 +213,8 @@ impl fmt::Display for EvalAltResult { index, max )?, Self::ErrorDataTooLarge(typ, _) => write!(f, "{} exceeds maximum limit", typ)?, + + Self::ErrorCustomSyntax(s, tokens, _) => write!(f, "{}: {}", s, tokens.join(" "))?, } // Do not write any position if None @@ -266,7 +277,8 @@ impl EvalAltResult { | Self::ErrorArithmetic(_, _) | Self::ErrorRuntime(_, _) => true, - Self::ErrorTooManyOperations(_) + Self::ErrorCustomSyntax(_, _, _) + | Self::ErrorTooManyOperations(_) | Self::ErrorTooManyModules(_) | Self::ErrorStackOverflow(_) | Self::ErrorDataTooLarge(_, _) @@ -282,7 +294,8 @@ impl EvalAltResult { Self::ErrorSystem(_, _) => true, Self::ErrorParsing(_, _) => true, - Self::ErrorTooManyOperations(_) + Self::ErrorCustomSyntax(_, _, _) + | Self::ErrorTooManyOperations(_) | Self::ErrorTooManyModules(_) | Self::ErrorStackOverflow(_) | Self::ErrorDataTooLarge(_, _) => true, @@ -295,6 +308,8 @@ impl EvalAltResult { /// Get the [position][Position] of this error. #[cfg(not(feature = "no_object"))] pub(crate) fn dump_fields(&self, map: &mut crate::Map) { + use std::str::FromStr; + map.insert( "error".into(), format!("{:?}", self) @@ -358,6 +373,17 @@ impl EvalAltResult { Self::ErrorTerminated(t, _) => { map.insert("token".into(), t.clone()); } + Self::ErrorCustomSyntax(_, tokens, _) => { + map.insert( + "tokens".into(), + Dynamic::from_array( + tokens + .iter() + .map(|s| Dynamic::from_str(s).unwrap()) + .collect(), + ), + ); + } }; } /// Get the [position][Position] of this error. @@ -389,6 +415,7 @@ impl EvalAltResult { | Self::ErrorStackOverflow(pos) | Self::ErrorDataTooLarge(_, pos) | Self::ErrorTerminated(_, pos) + | Self::ErrorCustomSyntax(_, _, pos) | Self::ErrorRuntime(_, pos) | Self::LoopBreak(_, pos) | Self::Return(_, pos) => *pos, @@ -436,6 +463,7 @@ impl EvalAltResult { | Self::ErrorStackOverflow(pos) | Self::ErrorDataTooLarge(_, pos) | Self::ErrorTerminated(_, pos) + | Self::ErrorCustomSyntax(_, _, pos) | Self::ErrorRuntime(_, pos) | Self::LoopBreak(_, pos) | Self::Return(_, pos) => *pos = new_position, From 6416e406d3869ce9ba6d52db27303bdae75357c4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 11 Jan 2022 11:34:15 +0800 Subject: [PATCH 15/16] Remove unnamed profile feature. --- no_std/no_std_test/Cargo.toml | 2 -- no_std/no_std_test/README.md | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/no_std/no_std_test/Cargo.toml b/no_std/no_std_test/Cargo.toml index 6a85b79d..e5005276 100644 --- a/no_std/no_std_test/Cargo.toml +++ b/no_std/no_std_test/Cargo.toml @@ -1,5 +1,3 @@ -cargo-features = ["named-profiles"] - [workspace] [package] diff --git a/no_std/no_std_test/README.md b/no_std/no_std_test/README.md index 042e15c0..3559a4a5 100644 --- a/no_std/no_std_test/README.md +++ b/no_std/no_std_test/README.md @@ -18,7 +18,7 @@ cargo +nightly build --release A specific profile can also be used: ```bash -cargo +nightly build --profile unix -Z unstable-options +cargo +nightly build --profile unix ``` Three profiles are defined: `unix`, `windows` and `macos`. From 77eb96bd7e1f8c5b6fa9e32d0cf8dfd7e44d02a7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 11 Jan 2022 11:40:08 +0800 Subject: [PATCH 16/16] Fix errors. --- src/types/error.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/types/error.rs b/src/types/error.rs index 4be7e573..78cf4e24 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -308,8 +308,6 @@ impl EvalAltResult { /// Get the [position][Position] of this error. #[cfg(not(feature = "no_object"))] pub(crate) fn dump_fields(&self, map: &mut crate::Map) { - use std::str::FromStr; - map.insert( "error".into(), format!("{:?}", self) @@ -376,12 +374,15 @@ impl EvalAltResult { Self::ErrorCustomSyntax(_, tokens, _) => { map.insert( "tokens".into(), - Dynamic::from_array( - tokens - .iter() - .map(|s| Dynamic::from_str(s).unwrap()) - .collect(), - ), + #[cfg(not(feature = "no_index"))] + Dynamic::from_array(tokens.iter().map(Into::into).collect()), + #[cfg(feature = "no_index")] + tokens + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(" ") + .into(), ); } };