diff --git a/RELEASES.md b/RELEASES.md index f91a0b16..9530b5a7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -15,6 +15,7 @@ Bug fixes * Fix bug when accessing properties in closures. * Fix bug when accessing a deep index with a function call. +* Fix bug that sometimes allow assigning to an invalid l-value. Breaking changes ---------------- diff --git a/src/ast.rs b/src/ast.rs index 0707c87b..e6694324 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -88,6 +88,7 @@ pub struct ScriptFnDef { } impl fmt::Display for ScriptFnDef { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -130,6 +131,7 @@ pub struct ScriptFnMetadata<'a> { } impl fmt::Display for ScriptFnMetadata<'_> { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -146,6 +148,7 @@ impl fmt::Display for ScriptFnMetadata<'_> { } impl<'a> Into> for &'a ScriptFnDef { + #[inline(always)] fn into(self) -> ScriptFnMetadata<'a> { ScriptFnMetadata { comments: self.comments.iter().map(|s| s.as_str()).collect(), @@ -172,6 +175,7 @@ pub struct AST { } impl Default for AST { + #[inline(always)] fn default() -> Self { Self { source: None, @@ -208,14 +212,17 @@ impl AST { } } /// Get the source. + #[inline(always)] pub fn source(&self) -> Option<&str> { self.source.as_ref().map(|s| s.as_str()) } /// Clone the source. + #[inline(always)] pub(crate) fn clone_source(&self) -> Option { self.source.clone() } /// Set the source. + #[inline(always)] pub fn set_source>(&mut self, source: Option) { self.source = source.map(|s| s.into()) } @@ -655,6 +662,7 @@ pub struct Ident { } impl fmt::Debug for Ident { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Ident({:?} @ {:?})", self.name, self.pos) } @@ -736,6 +744,7 @@ impl Default for Stmt { impl Stmt { /// Is this statement [`Noop`][Stmt::Noop]? + #[inline(always)] pub fn is_noop(&self) -> bool { match self { Self::Noop(_) => true, @@ -1048,6 +1057,7 @@ impl Expr { }) } /// Is the expression a simple variable access? + #[inline(always)] pub(crate) fn get_variable_access(&self, non_qualified: bool) -> Option<&str> { match self { Self::Variable(x) if !non_qualified || x.1.is_none() => Some((x.2).name.as_str()), diff --git a/src/dynamic.rs b/src/dynamic.rs index 75b3c7a4..08e473ae 100644 --- a/src/dynamic.rs +++ b/src/dynamic.rs @@ -88,21 +88,27 @@ pub trait Variant: Any + Send + Sync + private::Sealed { } impl Variant for T { + #[inline(always)] fn as_any(&self) -> &dyn Any { self } + #[inline(always)] fn as_mut_any(&mut self) -> &mut dyn Any { self } + #[inline(always)] fn as_box_any(self: Box) -> Box { self } + #[inline(always)] fn type_name(&self) -> &'static str { type_name::() } + #[inline(always)] fn into_dynamic(self) -> Dynamic { Dynamic::from(self) } + #[inline(always)] fn clone_into_dynamic(&self) -> Dynamic { Dynamic::from(self.clone()) } @@ -127,6 +133,7 @@ pub enum AccessMode { impl AccessMode { /// Is the access type [`ReadOnly`]? + #[inline(always)] pub fn is_read_only(self) -> bool { match self { Self::ReadWrite => false, diff --git a/src/engine.rs b/src/engine.rs index 5d05c85a..dae39f5f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -59,18 +59,22 @@ pub struct Imports(StaticVec<(ImmutableString, Shared)>); impl Imports { /// Get the length of this stack of imported [modules][Module]. + #[inline(always)] pub fn len(&self) -> usize { self.0.len() } /// Is this stack of imported [modules][Module] empty? + #[inline(always)] pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Get the imported [modules][Module] at a particular index. + #[inline(always)] pub fn get(&self, index: usize) -> Option> { self.0.get(index).map(|(_, m)| m).cloned() } /// Get the index of an imported [modules][Module] by name. + #[inline(always)] pub fn find(&self, name: &str) -> Option { self.0 .iter() @@ -80,15 +84,18 @@ impl Imports { .map(|(index, _)| index) } /// Push an imported [modules][Module] onto the stack. + #[inline(always)] pub fn push(&mut self, name: impl Into, module: impl Into>) { self.0.push((name.into(), module.into())); } /// Truncate the stack of imported [modules][Module] to a particular length. + #[inline(always)] pub fn truncate(&mut self, size: usize) { self.0.truncate(size); } /// Get an iterator to this stack of imported [modules][Module] in reverse order. #[allow(dead_code)] + #[inline(always)] pub fn iter<'a>(&'a self) -> impl Iterator + 'a { self.0 .iter() @@ -97,25 +104,30 @@ impl Imports { } /// Get an iterator to this stack of imported [modules][Module] in reverse order. #[allow(dead_code)] + #[inline(always)] pub(crate) fn iter_raw<'a>( &'a self, ) -> impl Iterator)> + 'a { self.0.iter().rev().map(|(n, m)| (n, m)) } /// Get a consuming iterator to this stack of imported [modules][Module] in reverse order. + #[inline(always)] pub fn into_iter(self) -> impl Iterator)> { self.0.into_iter().rev() } /// Add a stream of imported [modules][Module]. + #[inline(always)] pub fn extend(&mut self, stream: impl Iterator)>) { self.0.extend(stream) } /// Does the specified function hash key exist in this stack of imported [modules][Module]? #[allow(dead_code)] + #[inline(always)] pub fn contains_fn(&self, hash: NonZeroU64) -> bool { self.0.iter().any(|(_, m)| m.contains_qualified_fn(hash)) } /// Get specified function via its hash key. + #[inline(always)] pub fn get_fn(&self, hash: NonZeroU64) -> Option<&CallableFunction> { self.0 .iter() @@ -124,10 +136,12 @@ impl Imports { } /// Does the specified [`TypeId`][std::any::TypeId] iterator exist in this stack of imported [modules][Module]? #[allow(dead_code)] + #[inline(always)] pub fn contains_iter(&self, id: TypeId) -> bool { self.0.iter().any(|(_, m)| m.contains_qualified_iter(id)) } /// Get the specified [`TypeId`][std::any::TypeId] iterator. + #[inline(always)] pub fn get_iter(&self, id: TypeId) -> Option { self.0 .iter() @@ -137,6 +151,7 @@ impl Imports { } impl<'a, T: IntoIterator)>> From for Imports { + #[inline(always)] fn from(value: T) -> Self { Self( value @@ -147,6 +162,7 @@ impl<'a, T: IntoIterator)>> From } } impl FromIterator<(ImmutableString, Shared)> for Imports { + #[inline(always)] fn from_iter)>>(iter: T) -> Self { Self(iter.into_iter().collect()) } @@ -222,6 +238,7 @@ impl IndexChainValue { /// # Panics /// /// Panics if not `IndexChainValue::Value`. + #[inline(always)] #[cfg(not(feature = "no_index"))] pub fn as_value(self) -> Dynamic { match self { @@ -234,6 +251,7 @@ impl IndexChainValue { /// # Panics /// /// Panics if not `IndexChainValue::FnCallArgs`. + #[inline(always)] #[cfg(not(feature = "no_object"))] pub fn as_fn_call_args(self) -> StaticVec { match self { @@ -245,6 +263,7 @@ impl IndexChainValue { #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] impl From> for IndexChainValue { + #[inline(always)] fn from(value: StaticVec) -> Self { Self::FnCallArgs(value) } @@ -252,6 +271,7 @@ impl From> for IndexChainValue { #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] impl From for IndexChainValue { + #[inline(always)] fn from(value: Dynamic) -> Self { Self::Value(value) } @@ -1088,7 +1108,7 @@ impl Engine { ChainType::Dot => { match rhs { // xxx.fn_name(arg_expr_list) - Expr::FnCall(x, pos) if x.namespace.is_none() => { + Expr::FnCall(x, pos) if x.namespace.is_none() && new_val.is_none() => { let FnCallExpr { name, hash_script: hash, @@ -1102,6 +1122,10 @@ impl Engine { level, ) } + // xxx.fn_name(...) = ??? + Expr::FnCall(_, _) if new_val.is_some() => { + unreachable!("method call cannot be assigned to") + } // xxx.module::fn_name(...) - syntax error Expr::FnCall(_, _) => { unreachable!("function call in dot chain should not be namespace-qualified") @@ -1578,7 +1602,7 @@ impl Engine { let args = &mut [&mut lhs_value.clone(), value]; // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. - let hash = + let hash_fn = calc_native_fn_hash(empty(), OP_EQUALS, args.iter().map(|a| a.type_id())) .unwrap(); @@ -1586,7 +1610,8 @@ impl Engine { if self .call_native_fn( - mods, state, lib, OP_EQUALS, hash, args, false, false, pos, def_value, + mods, state, lib, OP_EQUALS, hash_fn, args, false, false, pos, + def_value, )? .0 .as_bool() @@ -1748,17 +1773,14 @@ impl Engine { Expr::StringConstant(x, _) => Ok(x.clone().into()), Expr::CharConstant(x, _) => Ok((*x).into()), Expr::FnPointer(x, _) => Ok(FnPtr::new_unchecked(x.clone(), Default::default()).into()), - Expr::Variable(x) if (x.2).name == KEYWORD_THIS => { - if let Some(val) = this_ptr { - Ok(val.clone()) - } else { - EvalAltResult::ErrorUnboundThis((x.2).pos).into() - } - } - Expr::Variable(_) => { - let (val, _) = self.search_namespace(scope, mods, state, lib, this_ptr, expr)?; - Ok(val.take_or_clone()) - } + + Expr::Variable(x) if (x.2).name == KEYWORD_THIS => this_ptr + .as_deref() + .cloned() + .ok_or_else(|| EvalAltResult::ErrorUnboundThis((x.2).pos).into()), + Expr::Variable(_) => self + .search_namespace(scope, mods, state, lib, this_ptr, expr) + .map(|(val, _)| val.take_or_clone()), // Statement block Expr::Stmt(x, _) => { @@ -1822,13 +1844,13 @@ impl Engine { let FnCallExpr { name, namespace, - hash_script: hash, + hash_script, args, def_value, .. } = x.as_ref(); let namespace = namespace.as_ref(); - let hash = hash.unwrap(); + let hash = hash_script.unwrap(); let def_value = def_value.as_ref(); self.make_qualified_function_call( scope, mods, state, lib, this_ptr, namespace, name, args, def_value, hash, @@ -2111,7 +2133,6 @@ impl Engine { )?; Ok(Dynamic::UNIT) } - // Non-lvalue expression (should be caught during parsing) _ => unreachable!("cannot assign to expression: {:?}", lhs_expr), } } @@ -2236,8 +2257,8 @@ impl Engine { for iter_value in func(iter_obj) { let loop_var = scope.get_mut_by_index(index); - let value = iter_value.flatten(); + if cfg!(not(feature = "no_closure")) && loop_var.is_shared() { *loop_var.write_lock().unwrap() = value; } else { @@ -2272,7 +2293,7 @@ impl Engine { // Try/Catch statement Stmt::TryCatch(x, _, _) => { - let (try_body, var_def, catch_body) = x.as_ref(); + let (try_body, err_var, catch_body) = x.as_ref(); let result = self .eval_stmt(scope, mods, state, lib, this_ptr, try_body, level) @@ -2280,51 +2301,41 @@ impl Engine { match result { Ok(_) => result, - Err(err) => match *err { - mut err @ EvalAltResult::ErrorRuntime(_, _) | mut err - if err.is_catchable() => - { - let value = if let EvalAltResult::ErrorRuntime(ref x, _) = err { - x.clone() - } else { + Err(err) if !err.is_catchable() => Err(err), + Err(mut err) => { + let value = match *err { + EvalAltResult::ErrorRuntime(ref x, _) => x.clone(), + _ => { err.set_position(Position::NONE); err.to_string().into() - }; - - let orig_scope_len = scope.len(); - state.scope_level += 1; - - if let Some(Ident { name, .. }) = var_def { - let var_name: Cow<'_, str> = if state.is_global() { - name.to_string().into() - } else { - unsafe_cast_var_name_to_lifetime(&name).into() - }; - scope.push(var_name, value); } + }; - let mut result = self - .eval_stmt(scope, mods, state, lib, this_ptr, catch_body, level) - .map(|_| ().into()); + let orig_scope_len = scope.len(); + state.scope_level += 1; - if let Some(result_err) = result.as_ref().err() { - if let EvalAltResult::ErrorRuntime( - Dynamic(Union::Unit(_, _)), - pos, - ) = result_err.as_ref() - { - err.set_position(*pos); - result = Err(Box::new(err)); - } - } - - state.scope_level -= 1; - scope.rewind(orig_scope_len); - - result + if let Some(Ident { name, .. }) = err_var { + scope.push(unsafe_cast_var_name_to_lifetime(&name), value); } - _ => Err(err), - }, + + let result = + self.eval_stmt(scope, mods, state, lib, this_ptr, catch_body, level); + + state.scope_level -= 1; + scope.rewind(orig_scope_len); + + match result { + Ok(_) => Ok(Dynamic::UNIT), + Err(result_err) => match *result_err { + // Re-throw exception + EvalAltResult::ErrorRuntime(Dynamic(Union::Unit(_, _)), pos) => { + err.set_position(pos); + Err(err) + } + _ => Err(result_err), + }, + } + } } } diff --git a/src/engine_api.rs b/src/engine_api.rs index 026e0df8..fff309b4 100644 --- a/src/engine_api.rs +++ b/src/engine_api.rs @@ -971,7 +971,7 @@ impl Engine { optimization_level: OptimizationLevel, ) -> Result { let hash = calc_hash_for_scripts(scripts); - let stream = self.lex(scripts, None); + let stream = self.lex(scripts); self.parse(hash, &mut stream.peekable(), scope, optimization_level) } /// Read the contents of a file into a string. @@ -1129,18 +1129,20 @@ impl Engine { }; let hash = calc_hash_for_scripts(&scripts); - let stream = self.lex( + + let stream = self.lex_with_map( &scripts, if has_null { - Some(Box::new(|token| match token { + |token| match token { // If `null` is present, make sure `null` is treated as a variable Token::Reserved(s) if s == "null" => Token::Identifier(s), _ => token, - })) + } } else { - None + |t| t }, ); + let ast = self.parse_global_expr( hash, &mut stream.peekable(), @@ -1226,7 +1228,7 @@ impl Engine { ) -> Result { let scripts = [script]; let hash = calc_hash_for_scripts(&scripts); - let stream = self.lex(&scripts, None); + let stream = self.lex(&scripts); let mut peekable = stream.peekable(); self.parse_global_expr(hash, &mut peekable, scope, self.optimization_level) @@ -1384,7 +1386,7 @@ impl Engine { ) -> Result> { let scripts = [script]; let hash = calc_hash_for_scripts(&scripts); - let stream = self.lex(&scripts, None); + let stream = self.lex(&scripts); // No need to optimize a lone expression let ast = @@ -1517,7 +1519,7 @@ impl Engine { ) -> Result<(), Box> { let scripts = [script]; let hash = calc_hash_for_scripts(&scripts); - let stream = self.lex(&scripts, None); + let stream = self.lex(&scripts); let ast = self.parse(hash, &mut stream.peekable(), scope, self.optimization_level)?; self.consume_ast_with_scope(scope, &ast) } diff --git a/src/fn_call.rs b/src/fn_call.rs index ae678448..c0329828 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -77,6 +77,7 @@ impl<'a> ArgBackup<'a> { /// This method blindly casts a reference to another lifetime, which saves allocation and string cloning. /// /// If `restore_first_arg` is called before the end of the scope, the shorter lifetime will not leak. + #[inline(always)] fn change_first_arg_to_copy(&mut self, normalize: bool, args: &mut FnCallArgs<'a>) { // Only do it for method calls with arguments. if !normalize || args.is_empty() { @@ -106,6 +107,7 @@ impl<'a> ArgBackup<'a> { /// /// If `change_first_arg_to_copy` has been called, this function **MUST** be called _BEFORE_ exiting /// the current scope. Otherwise it is undefined behavior as the shorter lifetime will leak. + #[inline(always)] fn restore_first_arg(&mut self, args: &mut FnCallArgs<'a>) { if let Some(this_pointer) = self.orig_mut.take() { args[0] = this_pointer; @@ -114,6 +116,7 @@ impl<'a> ArgBackup<'a> { } impl Drop for ArgBackup<'_> { + #[inline(always)] fn drop(&mut self) { // Panic if the shorter lifetime leaks. assert!( @@ -433,7 +436,7 @@ impl Engine { } // Has a system function an override? - #[inline] + #[inline(always)] pub(crate) fn has_override_by_name_and_arguments( &self, mods: Option<&Imports>, diff --git a/src/fn_native.rs b/src/fn_native.rs index ddfebb20..a1d32a37 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -65,6 +65,7 @@ impl<'e, 's, 'a, 'm, 'pm: 'm, M: AsRef<[&'pm Module]> + ?Sized> From<(&'e Engine, &'s Option, &'a Imports, &'m M)> for NativeCallContext<'e, 's, 'a, 'm, 'pm> { + #[inline(always)] fn from(value: (&'e Engine, &'s Option, &'a Imports, &'m M)) -> Self { Self { engine: value.0, @@ -78,6 +79,7 @@ impl<'e, 's, 'a, 'm, 'pm: 'm, M: AsRef<[&'pm Module]> + ?Sized> impl<'e, 'm, 'pm: 'm, M: AsRef<[&'pm Module]> + ?Sized> From<(&'e Engine, &'m M)> for NativeCallContext<'e, '_, '_, 'm, 'pm> { + #[inline(always)] fn from(value: (&'e Engine, &'m M)) -> Self { Self { engine: value.0, @@ -152,6 +154,7 @@ impl<'e, 's, 'a, 'm, 'pm> NativeCallContext<'e, 's, 'a, 'm, 'pm> { /// /// If `is_method` is [`true`], the first argument is assumed to be passed /// by reference and is not consumed. + #[inline(always)] pub fn call_fn_dynamic_raw( &self, fn_name: &str, @@ -225,6 +228,7 @@ pub struct FnPtr(ImmutableString, StaticVec); impl FnPtr { /// Create a new function pointer. + #[inline(always)] pub fn new(name: impl Into) -> Result> { name.into().try_into() } @@ -289,6 +293,7 @@ impl FnPtr { /// This is to avoid unnecessarily cloning the arguments. /// Do not use the arguments after this call. If they are needed afterwards, /// clone them _before_ calling this function. + #[inline(always)] pub fn call_dynamic( &self, ctx: NativeCallContext, @@ -306,13 +311,13 @@ impl FnPtr { let mut args = args_data.iter_mut().collect::>(); - let has_this = this_ptr.is_some(); + let is_method = this_ptr.is_some(); if let Some(obj) = this_ptr { args.insert(0, obj); } - ctx.call_fn_dynamic_raw(self.fn_name(), has_this, true, args.as_mut(), None) + ctx.call_fn_dynamic_raw(self.fn_name(), is_method, true, args.as_mut(), None) } } @@ -424,6 +429,7 @@ pub enum CallableFunction { } impl fmt::Debug for CallableFunction { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Pure(_) => write!(f, "NativePureFunction"), @@ -438,6 +444,7 @@ impl fmt::Debug for CallableFunction { } impl fmt::Display for CallableFunction { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Pure(_) => write!(f, "NativePureFunction"), @@ -453,6 +460,7 @@ impl fmt::Display for CallableFunction { impl CallableFunction { /// Is this a pure native Rust function? + #[inline(always)] pub fn is_pure(&self) -> bool { match self { Self::Pure(_) => true, @@ -465,6 +473,7 @@ impl CallableFunction { } } /// Is this a native Rust method function? + #[inline(always)] pub fn is_method(&self) -> bool { match self { Self::Method(_) => true, @@ -477,6 +486,7 @@ impl CallableFunction { } } /// Is this an iterator function? + #[inline(always)] pub fn is_iter(&self) -> bool { match self { Self::Iterator(_) => true, @@ -487,6 +497,7 @@ impl CallableFunction { } } /// Is this a Rhai-scripted function? + #[inline(always)] pub fn is_script(&self) -> bool { match self { #[cfg(not(feature = "no_function"))] @@ -496,6 +507,7 @@ impl CallableFunction { } } /// Is this a plugin function? + #[inline(always)] pub fn is_plugin_fn(&self) -> bool { match self { Self::Plugin(_) => true, @@ -506,6 +518,7 @@ impl CallableFunction { } } /// Is this a native Rust function? + #[inline(always)] pub fn is_native(&self) -> bool { match self { Self::Pure(_) | Self::Method(_) => true, @@ -517,6 +530,7 @@ impl CallableFunction { } } /// Get the access mode. + #[inline(always)] pub fn access(&self) -> FnAccess { match self { Self::Plugin(_) => FnAccess::Public, @@ -532,6 +546,7 @@ impl CallableFunction { /// /// Panics if the [`CallableFunction`] is not [`Pure`][CallableFunction::Pure] or /// [`Method`][CallableFunction::Method]. + #[inline(always)] pub fn get_native_fn(&self) -> &FnAny { match self { Self::Pure(f) | Self::Method(f) => f.as_ref(), @@ -547,6 +562,7 @@ impl CallableFunction { /// /// Panics if the [`CallableFunction`] is not [`Script`][CallableFunction::Script]. #[cfg(not(feature = "no_function"))] + #[inline(always)] pub fn get_fn_def(&self) -> &ScriptFnDef { match self { Self::Pure(_) | Self::Method(_) | Self::Iterator(_) | Self::Plugin(_) => { @@ -560,6 +576,7 @@ impl CallableFunction { /// # Panics /// /// Panics if the [`CallableFunction`] is not [`Iterator`][CallableFunction::Iterator]. + #[inline(always)] pub fn get_iter_fn(&self) -> IteratorFn { match self { Self::Iterator(f) => *f, @@ -576,6 +593,7 @@ impl CallableFunction { /// # Panics /// /// Panics if the [`CallableFunction`] is not [`Plugin`][CallableFunction::Plugin]. + #[inline(always)] pub fn get_plugin_fn<'s>(&'s self) -> &FnPlugin { match self { Self::Plugin(f) => f.as_ref(), diff --git a/src/parser.rs b/src/parser.rs index d9f37d74..b1c39d36 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -882,7 +882,7 @@ fn parse_switch( } }; - let stmt = parse_stmt(input, state, lib, settings.level_up()).map(Option::unwrap)?; + let stmt = parse_stmt(input, state, lib, settings.level_up())?; let need_comma = !stmt.is_self_terminated(); @@ -1014,10 +1014,11 @@ fn parse_primary( state.access_var(closure, *pos); }); - // Qualifiers (none) + function name + number of arguments. - let hash = calc_script_fn_hash(empty(), &func.name, func.params.len()).unwrap(); - - lib.insert(hash, func); + lib.insert( + // Qualifiers (none) + function name + number of arguments. + calc_script_fn_hash(empty(), &func.name, func.params.len()).unwrap(), + func, + ); expr } @@ -1372,7 +1373,28 @@ fn make_assignment_stmt<'a>( rhs: Expr, op_pos: Position, ) -> Result { + fn check_lvalue(expr: &Expr, parent_is_dot: bool) -> Position { + match expr { + Expr::Index(x, _) | Expr::Dot(x, _) if parent_is_dot => match x.lhs { + Expr::Property(_) => check_lvalue(&x.rhs, matches!(expr, Expr::Dot(_, _))), + ref e => e.position(), + }, + Expr::Index(x, _) | Expr::Dot(x, _) => match x.lhs { + Expr::Property(_) => unreachable!("unexpected Expr::Property in indexing"), + _ => check_lvalue(&x.rhs, matches!(expr, Expr::Dot(_, _))), + }, + Expr::Property(_) if parent_is_dot => Position::NONE, + Expr::Property(_) => unreachable!("unexpected Expr::Property in indexing"), + e if parent_is_dot => e.position(), + _ => Position::NONE, + } + } + match &lhs { + // const_expr = rhs + expr if expr.is_constant() => { + Err(PERR::AssignmentToConstant("".into()).into_err(lhs.position())) + } // var (non-indexed) = rhs Expr::Variable(x) if x.0.is_none() => Ok(Stmt::Assignment( Box::new((lhs, fn_name.into(), rhs)), @@ -1392,33 +1414,36 @@ fn make_assignment_stmt<'a>( } } } - // xxx[???] = rhs, xxx.??? = rhs - Expr::Index(x, _) | Expr::Dot(x, _) => match &x.lhs { - // var[???] (non-indexed) = rhs, var.??? (non-indexed) = rhs - Expr::Variable(x) if x.0.is_none() => Ok(Stmt::Assignment( - Box::new((lhs, fn_name.into(), rhs)), - op_pos, - )), - // var[???] (indexed) = rhs, var.??? (indexed) = rhs - Expr::Variable(x) => { - let (index, _, Ident { name, pos }) = x.as_ref(); - match state.stack[(state.stack.len() - index.unwrap().get())].1 { - AccessMode::ReadWrite => Ok(Stmt::Assignment( + // xxx[???]... = rhs, xxx.prop... = rhs + Expr::Index(x, _) | Expr::Dot(x, _) => { + match check_lvalue(&x.rhs, matches!(lhs, Expr::Dot(_, _))) { + Position::NONE => match &x.lhs { + // var[???] (non-indexed) = rhs, var.??? (non-indexed) = rhs + Expr::Variable(x) if x.0.is_none() => Ok(Stmt::Assignment( Box::new((lhs, fn_name.into(), rhs)), op_pos, )), - // Constant values cannot be assigned to - AccessMode::ReadOnly => { - Err(PERR::AssignmentToConstant(name.to_string()).into_err(*pos)) + // var[???] (indexed) = rhs, var.??? (indexed) = rhs + Expr::Variable(x) => { + let (index, _, Ident { name, pos }) = x.as_ref(); + match state.stack[(state.stack.len() - index.unwrap().get())].1 { + AccessMode::ReadWrite => Ok(Stmt::Assignment( + Box::new((lhs, fn_name.into(), rhs)), + op_pos, + )), + // Constant values cannot be assigned to + AccessMode::ReadOnly => { + Err(PERR::AssignmentToConstant(name.to_string()).into_err(*pos)) + } + } } - } + // expr[???] = rhs, expr.??? = rhs + expr => { + Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(expr.position())) + } + }, + pos => Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(pos)), } - // expr[???] = rhs, expr.??? = rhs - _ => Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(x.lhs.position())), - }, - // const_expr = rhs - expr if expr.is_constant() => { - Err(PERR::AssignmentToConstant("".into()).into_err(lhs.position())) } // ??? && ??? = rhs, ??? || ??? = rhs Expr::And(_, _) | Expr::Or(_, _) => Err(LexError::ImproperSymbol( @@ -2435,7 +2460,11 @@ fn parse_block( // Parse statements inside the block settings.is_global = false; - let stmt = parse_stmt(input, state, lib, settings.level_up()).map(Option::unwrap)?; + let stmt = parse_stmt(input, state, lib, settings.level_up())?; + + if stmt.is_noop() { + continue; + } // See if it needs a terminating semicolon let need_semicolon = !stmt.is_self_terminated(); @@ -2502,7 +2531,7 @@ fn parse_stmt( state: &mut ParseState, lib: &mut FunctionsLib, mut settings: ParseSettings, -) -> Result, ParseError> { +) -> Result { use AccessMode::{ReadOnly, ReadWrite}; let mut comments: Vec = Default::default(); @@ -2538,7 +2567,7 @@ fn parse_stmt( } let (token, token_pos) = match input.peek().unwrap() { - (Token::EOF, pos) => return Ok(Some(Stmt::Noop(*pos))), + (Token::EOF, pos) => return Ok(Stmt::Noop(*pos)), x => x, }; settings.pos = *token_pos; @@ -2548,10 +2577,10 @@ fn parse_stmt( match token { // ; - empty statement - Token::SemiColon => Ok(Some(Stmt::Noop(settings.pos))), + Token::SemiColon => Ok(Stmt::Noop(settings.pos)), // { - statements block - Token::LeftBrace => Ok(Some(parse_block(input, state, lib, settings.level_up())?)), + Token::LeftBrace => Ok(parse_block(input, state, lib, settings.level_up())?), // fn ... #[cfg(not(feature = "no_function"))] @@ -2591,12 +2620,13 @@ fn parse_stmt( let func = parse_fn(input, &mut new_state, lib, access, settings, comments)?; - // Qualifiers (none) + function name + number of arguments. - let hash = calc_script_fn_hash(empty(), &func.name, func.params.len()).unwrap(); + lib.insert( + // Qualifiers (none) + function name + number of arguments. + calc_script_fn_hash(empty(), &func.name, func.params.len()).unwrap(), + func, + ); - lib.insert(hash, func); - - Ok(None) + Ok(Stmt::Noop(settings.pos)) } (_, pos) => Err(PERR::MissingToken( @@ -2607,21 +2637,19 @@ fn parse_stmt( } } - Token::If => parse_if(input, state, lib, settings.level_up()).map(Some), - Token::Switch => parse_switch(input, state, lib, settings.level_up()).map(Some), - Token::While | Token::Loop => { - parse_while_loop(input, state, lib, settings.level_up()).map(Some) - } - Token::Do => parse_do(input, state, lib, settings.level_up()).map(Some), - Token::For => parse_for(input, state, lib, settings.level_up()).map(Some), + Token::If => parse_if(input, state, lib, settings.level_up()), + Token::Switch => parse_switch(input, state, lib, settings.level_up()), + Token::While | Token::Loop => parse_while_loop(input, state, lib, settings.level_up()), + Token::Do => parse_do(input, state, lib, settings.level_up()), + Token::For => parse_for(input, state, lib, settings.level_up()), Token::Continue if settings.is_breakable => { let pos = eat_token(input, Token::Continue); - Ok(Some(Stmt::Continue(pos))) + Ok(Stmt::Continue(pos)) } Token::Break if settings.is_breakable => { let pos = eat_token(input, Token::Break); - Ok(Some(Stmt::Break(pos))) + Ok(Stmt::Break(pos)) } Token::Continue | Token::Break => Err(PERR::LoopBreak.into_err(settings.pos)), @@ -2645,43 +2673,35 @@ fn parse_stmt( match input.peek().unwrap() { // `return`/`throw` at - (Token::EOF, pos) => Ok(Some(Stmt::Return((return_type, token_pos), None, *pos))), + (Token::EOF, pos) => Ok(Stmt::Return((return_type, token_pos), None, *pos)), // `return;` or `throw;` - (Token::SemiColon, _) => Ok(Some(Stmt::Return( - (return_type, token_pos), - None, - settings.pos, - ))), + (Token::SemiColon, _) => { + Ok(Stmt::Return((return_type, token_pos), None, settings.pos)) + } // `return` or `throw` with expression (_, _) => { let expr = parse_expr(input, state, lib, settings.level_up())?; let pos = expr.position(); - Ok(Some(Stmt::Return( - (return_type, token_pos), - Some(expr), - pos, - ))) + Ok(Stmt::Return((return_type, token_pos), Some(expr), pos)) } } } - Token::Try => parse_try_catch(input, state, lib, settings.level_up()).map(Some), + Token::Try => parse_try_catch(input, state, lib, settings.level_up()), - Token::Let => parse_let(input, state, lib, ReadWrite, false, settings.level_up()).map(Some), - Token::Const => { - parse_let(input, state, lib, ReadOnly, false, settings.level_up()).map(Some) - } + Token::Let => parse_let(input, state, lib, ReadWrite, false, settings.level_up()), + Token::Const => parse_let(input, state, lib, ReadOnly, false, settings.level_up()), #[cfg(not(feature = "no_module"))] - Token::Import => parse_import(input, state, lib, settings.level_up()).map(Some), + Token::Import => parse_import(input, state, lib, settings.level_up()), #[cfg(not(feature = "no_module"))] Token::Export if !settings.is_global => Err(PERR::WrongExport.into_err(settings.pos)), #[cfg(not(feature = "no_module"))] - Token::Export => parse_export(input, state, lib, settings.level_up()).map(Some), + Token::Export => parse_export(input, state, lib, settings.level_up()), - _ => parse_expr_stmt(input, state, lib, settings.level_up()).map(Some), + _ => parse_expr_stmt(input, state, lib, settings.level_up()), } } @@ -2951,7 +2971,7 @@ fn parse_anon_fn( // Parse function body settings.is_breakable = false; - let body = parse_stmt(input, state, lib, settings.level_up()).map(Option::unwrap)?; + let body = parse_stmt(input, state, lib, settings.level_up())?; // External variables may need to be processed in a consistent order, // so extract them into a list. @@ -3095,10 +3115,11 @@ impl Engine { pos: Position::NONE, }; - let stmt = match parse_stmt(input, &mut state, &mut functions, settings)? { - Some(s) => s, - None => continue, - }; + let stmt = parse_stmt(input, &mut state, &mut functions, settings)?; + + if stmt.is_noop() { + continue; + } let need_semicolon = !stmt.is_self_terminated(); diff --git a/src/scope.rs b/src/scope.rs index 3498dce3..4386bae7 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -55,6 +55,7 @@ pub struct Scope<'a> { } impl Default for Scope<'_> { + #[inline(always)] fn default() -> Self { Self { values: Vec::with_capacity(16), diff --git a/src/token.rs b/src/token.rs index c10c1ba4..752c1da4 100644 --- a/src/token.rs +++ b/src/token.rs @@ -6,7 +6,6 @@ use crate::engine::{ }; use crate::stdlib::{ borrow::Cow, - boxed::Box, char, fmt, format, iter::Peekable, str::{Chars, FromStr}, @@ -1126,14 +1125,14 @@ fn get_next_token_inner( 'x' | 'X' => is_hex_char, 'o' | 'O' => is_octal_char, 'b' | 'B' => is_binary_char, - _ => unreachable!("expecting 'x', 'o' or 'B', but gets {}", ch), + _ => unreachable!("expecting 'x', 'o' or 'b', but gets {}", ch), }; radix_base = Some(match ch { 'x' | 'X' => 16, 'o' | 'O' => 8, 'b' | 'B' => 2, - _ => unreachable!("expecting 'x', 'o' or 'B', but gets {}", ch), + _ => unreachable!("expecting 'x', 'o' or 'b', but gets {}", ch), }); while let Some(next_char_in_escape_seq) = stream.peek_next() { @@ -1184,9 +1183,14 @@ fn get_next_token_inner( } // letter or underscore ... + #[cfg(not(feature = "unicode-xid-ident"))] ('A'..='Z', _) | ('a'..='z', _) | ('_', _) => { return get_identifier(stream, pos, start_pos, c); } + #[cfg(feature = "unicode-xid-ident")] + (ch, _) if unicode_xid::UnicodeXID::is_xid_start(ch) => { + return get_identifier(stream, pos, start_pos, c); + } // " - string literal ('"', _) => { @@ -1495,10 +1499,7 @@ fn get_next_token_inner( ('$', _) => return Some((Token::Reserved("$".into()), start_pos)), (ch, _) if ch.is_whitespace() => (), - #[cfg(feature = "unicode-xid-ident")] - (ch, _) if unicode_xid::UnicodeXID::is_xid_start(ch) => { - return get_identifier(stream, pos, start_pos, c); - } + (ch, _) => { return Some(( Token::LexError(LERR::UnexpectedInput(ch.to_string())), @@ -1683,8 +1684,8 @@ pub struct TokenIterator<'a, 'e> { pos: Position, /// Input character stream. stream: MultiInputsStream<'a>, - /// A processor function (if any) that maps a token to another. - map: Option Token>>, + /// A processor function that maps a token to another. + map: fn(Token) -> Token, } impl<'a> Iterator for TokenIterator<'a, '_> { @@ -1760,24 +1761,26 @@ impl<'a> Iterator for TokenIterator<'a, '_> { match token { None => None, - Some((token, pos)) => { - if let Some(ref map) = self.map { - Some((map(token), pos)) - } else { - Some((token, pos)) - } - } + Some((token, pos)) => Some(((self.map)(token), pos)), } } } impl Engine { /// Tokenize an input text stream. - #[inline] + #[inline(always)] pub fn lex<'a, 'e>( &'e self, input: impl IntoIterator, - map: Option Token>>, + ) -> TokenIterator<'a, 'e> { + self.lex_with_map(input, |f| f) + } + /// Tokenize an input text stream with a mapping function. + #[inline] + pub fn lex_with_map<'a, 'e>( + &'e self, + input: impl IntoIterator, + map: fn(Token) -> Token, ) -> TokenIterator<'a, 'e> { TokenIterator { engine: self, diff --git a/src/utils.rs b/src/utils.rs index 52dfda99..9043bb4a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -54,6 +54,7 @@ impl BuildHasher for StraightHasherBuilder { } /// Create an instance of the default hasher. +#[inline(always)] pub fn get_hasher() -> impl Hasher { #[cfg(feature = "no_std")] let s: ahash::AHasher = Default::default(); diff --git a/tests/assignments.rs b/tests/assignments.rs new file mode 100644 index 00000000..d3936223 --- /dev/null +++ b/tests/assignments.rs @@ -0,0 +1,82 @@ +use rhai::{Engine, EvalAltResult, ParseErrorType, INT}; + +#[test] +fn test_assignments() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!(engine.eval::("let x = 42; x = 123; x")?, 123); + assert_eq!(engine.eval::("let x = 42; x += 123; x")?, 165); + + #[cfg(not(feature = "no_index"))] + assert_eq!(engine.eval::("let x = [42]; x[0] += 123; x[0]")?, 165); + + #[cfg(not(feature = "no_object"))] + assert_eq!(engine.eval::("let x = #{a:42}; x.a += 123; x.a")?, 165); + + Ok(()) +} + +#[test] +fn test_assignments_bad_lhs() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!( + *engine.compile(r"(x+y) = 42;").expect_err("should error").0, + ParseErrorType::AssignmentToInvalidLHS("".to_string()) + ); + assert_eq!( + *engine.compile(r"foo(x) = 42;").expect_err("should error").0, + ParseErrorType::AssignmentToInvalidLHS("".to_string()) + ); + assert_eq!( + *engine.compile(r"true = 42;").expect_err("should error").0, + ParseErrorType::AssignmentToConstant("".to_string()) + ); + assert_eq!( + *engine.compile(r"123 = 42;").expect_err("should error").0, + ParseErrorType::AssignmentToConstant("".to_string()) + ); + + #[cfg(not(feature = "no_object"))] + { + assert_eq!( + *engine + .compile(r"x.foo() = 42;") + .expect_err("should error") + .0, + ParseErrorType::AssignmentToInvalidLHS("".to_string()) + ); + assert_eq!( + *engine + .compile(r"x.foo().x.y = 42;") + .expect_err("should error") + .0, + ParseErrorType::AssignmentToInvalidLHS("".to_string()) + ); + assert_eq!( + *engine + .compile(r"x.y.z.foo() = 42;") + .expect_err("should error") + .0, + ParseErrorType::AssignmentToInvalidLHS("".to_string()) + ); + #[cfg(not(feature = "no_index"))] + assert_eq!( + *engine + .compile(r"x.foo()[0] = 42;") + .expect_err("should error") + .0, + ParseErrorType::AssignmentToInvalidLHS("".to_string()) + ); + #[cfg(not(feature = "no_index"))] + assert_eq!( + *engine + .compile(r"x[y].z.foo() = 42;") + .expect_err("should error") + .0, + ParseErrorType::AssignmentToInvalidLHS("".to_string()) + ); + } + + Ok(()) +}