diff --git a/src/engine.rs b/src/engine.rs index 1e35d21f..8ad61089 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -367,4 +367,10 @@ impl Engine { pub(crate) const fn is_debugger_registered(&self) -> bool { self.debugger_interface.is_some() } + + /// Imitation of std::hints::black_box which requires nightly. + #[inline(never)] + pub(crate) fn black_box() -> usize { + unsafe { core::ptr::read_volatile(&0_usize as *const usize) } + } } diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 0567e39e..91d944cf 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -239,17 +239,17 @@ impl Engine { // Coded this way for better branch prediction. // Popular branches are lifted out of the `match` statement into their own branches. + #[cfg(feature = "debugging")] + let reset = + self.run_debugger_with_reset(global, caches, scope, this_ptr.as_deref_mut(), expr)?; + #[cfg(feature = "debugging")] + auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); + + 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 { - #[cfg(feature = "debugging")] - let reset = - self.run_debugger_with_reset(global, caches, scope, this_ptr.as_deref_mut(), expr)?; - #[cfg(feature = "debugging")] - auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); - - self.track_operation(global, expr.position())?; - return self.eval_fn_call_expr(global, caches, scope, this_ptr, x, *pos); } @@ -257,11 +257,6 @@ impl Engine { // We shouldn't do this for too many variants because, soon or later, the added comparisons // will cost more than the mis-predicted `match` branch. if let Expr::Variable(x, index, var_pos) = expr { - #[cfg(feature = "debugging")] - self.run_debugger(global, caches, scope, this_ptr.as_deref_mut(), expr)?; - - self.track_operation(global, expr.position())?; - return if index.is_none() && x.0.is_none() && x.3 == KEYWORD_THIS { this_ptr .ok_or_else(|| ERR::ErrorUnboundThis(*var_pos).into()) @@ -272,25 +267,41 @@ impl Engine { }; } - #[cfg(feature = "debugging")] - let reset = - self.run_debugger_with_reset(global, caches, scope, this_ptr.as_deref_mut(), expr)?; - #[cfg(feature = "debugging")] - auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); + // Stop merging branches here! + Self::black_box(); - self.track_operation(global, expr.position())?; + // Constants + if let Expr::IntegerConstant(x, ..) = expr { + return Ok((*x).into()); + } + if let Expr::StringConstant(x, ..) = expr { + return Ok(x.clone().into()); + } + if let Expr::BoolConstant(x, ..) = expr { + return Ok((*x).into()); + } + + // Stop merging branches here! + Self::black_box(); + + #[cfg(not(feature = "no_float"))] + if let Expr::FloatConstant(x, ..) = expr { + return Ok((*x).into()); + } + if let Expr::CharConstant(x, ..) = expr { + return Ok((*x).into()); + } + if let Expr::Unit(..) = expr { + return Ok(Dynamic::UNIT); + } + if let Expr::DynamicConstant(x, ..) = expr { + return Ok(x.as_ref().clone()); + } + + // Stop merging branches here! + Self::black_box(); match expr { - // Constants - Expr::DynamicConstant(x, ..) => Ok(x.as_ref().clone()), - Expr::IntegerConstant(x, ..) => Ok((*x).into()), - #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(x, ..) => Ok((*x).into()), - Expr::StringConstant(x, ..) => Ok(x.clone().into()), - Expr::CharConstant(x, ..) => Ok((*x).into()), - Expr::BoolConstant(x, ..) => Ok((*x).into()), - Expr::Unit(..) => Ok(Dynamic::UNIT), - // `... ${...} ...` Expr::InterpolatedString(x, _) => { let mut concat = SmartString::new_const(); diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index 4846c9a7..4b863701 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -273,13 +273,14 @@ impl Engine { #[cfg(feature = "debugging")] auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); + self.track_operation(global, stmt.position())?; + // Coded this way for better branch prediction. // Popular branches are lifted out of the `match` statement into their own branches. + // Hopefully the compiler won't undo all this work! // Function calls should account for a relatively larger portion of statements. if let Stmt::FnCall(x, pos) = stmt { - self.track_operation(global, stmt.position())?; - return self.eval_fn_call_expr(global, caches, scope, this_ptr, x, *pos); } @@ -289,8 +290,6 @@ impl Engine { if let Stmt::Assignment(x, ..) = stmt { let (op_info, BinaryExpr { lhs, rhs }) = &**x; - self.track_operation(global, stmt.position())?; - if let Expr::Variable(x, ..) = lhs { let rhs_val = self .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), rhs)? @@ -354,47 +353,146 @@ impl Engine { } } - self.track_operation(global, stmt.position())?; + // Stop merging branches here! + 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), - Stmt::Block(statements, ..) => { + // Block scope + if let Stmt::Block(statements, ..) = stmt { + return if statements.is_empty() { + Ok(Dynamic::UNIT) + } else { self.eval_stmt_block(global, caches, scope, this_ptr, statements, true) + }; + } + + if let Stmt::Var(x, options, pos) = stmt { + if !self.allow_shadowing() && scope.contains(&x.0) { + return Err(ERR::ErrorVariableExists(x.0.to_string(), *pos).into()); } + // Let/const statement + let (var_name, expr, index) = &**x; - // If statement - Stmt::If(x, ..) => { - let FlowControl { - expr, - body: if_block, - branch: else_block, - } = &**x; + let access = if options.contains(ASTFlags::CONSTANT) { + AccessMode::ReadOnly + } else { + AccessMode::ReadWrite + }; + let export = options.contains(ASTFlags::EXPORTED); - let guard_val = self - .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), expr)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, expr.position()))?; + // Check variable definition filter + if let Some(ref filter) = self.def_var_filter { + let will_shadow = scope.contains(var_name); + let is_const = access == AccessMode::ReadOnly; + let info = VarDefInfo { + name: var_name, + is_const, + nesting_level: global.scope_level, + will_shadow, + }; + let orig_scope_len = scope.len(); + let context = + EvalContext::new(self, global, caches, scope, this_ptr.as_deref_mut()); + let filter_result = filter(true, info, context); - match guard_val { - true if !if_block.is_empty() => { - self.eval_stmt_block(global, caches, scope, this_ptr, if_block, true) - } - false if !else_block.is_empty() => { - self.eval_stmt_block(global, caches, scope, this_ptr, else_block, true) - } - _ => Ok(Dynamic::UNIT), + if orig_scope_len != scope.len() { + // The scope is changed, always search from now on + global.always_search_scope = true; + } + + if !filter_result? { + return Err(ERR::ErrorForbiddenVariable(var_name.to_string(), *pos).into()); } } + // Evaluate initial value + let mut value = self + .eval_expr(global, caches, scope, this_ptr, expr)? + .flatten() + .intern_string(self); + + let _alias = if !rewind_scope { + // Put global constants into global module + #[cfg(not(feature = "no_function"))] + #[cfg(not(feature = "no_module"))] + if global.scope_level == 0 + && access == AccessMode::ReadOnly + && global.lib.iter().any(|m| !m.is_empty()) + { + crate::func::locked_write(global.constants.get_or_insert_with(|| { + crate::Shared::new(crate::Locked::new(std::collections::BTreeMap::new())) + })) + .insert(var_name.name.clone(), value.clone()); + } + + if export { + Some(var_name) + } else { + None + } + } else if export { + unreachable!("exported variable not on global level"); + } else { + None + }; + + if let Some(index) = index { + value.set_access_mode(access); + *scope.get_mut_by_index(scope.len() - index.get()) = value; + } else { + scope.push_entry(var_name.name.clone(), access, value); + } + + #[cfg(not(feature = "no_module"))] + if let Some(alias) = _alias { + scope.add_alias_by_index(scope.len() - 1, alias.as_str().into()); + } + + return Ok(Dynamic::UNIT); + } + + // Stop merging branches here! + Self::black_box(); + + // If statement + if let Stmt::If(x, ..) = stmt { + let FlowControl { + expr, + body: if_block, + branch: else_block, + } = &**x; + + let guard_val = self + .eval_expr(global, caches, scope, this_ptr.as_deref_mut(), expr)? + .as_bool() + .map_err(|typ| self.make_type_mismatch_err::(typ, expr.position()))?; + + return match guard_val { + true if !if_block.is_empty() => { + self.eval_stmt_block(global, caches, scope, this_ptr, if_block, true) + } + false if !else_block.is_empty() => { + self.eval_stmt_block(global, caches, scope, this_ptr, else_block, true) + } + _ => Ok(Dynamic::UNIT), + }; + } + + // Expression as statement + if let Stmt::Expr(expr) = stmt { + return self + .eval_expr(global, caches, scope, this_ptr, expr) + .map(Dynamic::flatten); + } + + // No-op + if let Stmt::Noop(..) = stmt { + return Ok(Dynamic::UNIT); + } + + // Stop merging branches here! + Self::black_box(); + + match stmt { // Switch statement Stmt::Switch(x, ..) => { let ( @@ -766,94 +864,6 @@ impl Engine { // Empty return Stmt::Return(None, .., pos) => Err(ERR::Return(Dynamic::UNIT, *pos).into()), - // Let/const statement - shadowing disallowed - Stmt::Var(x, .., pos) if !self.allow_shadowing() && scope.contains(&x.0) => { - Err(ERR::ErrorVariableExists(x.0.to_string(), *pos).into()) - } - // Let/const statement - Stmt::Var(x, options, pos) => { - let (var_name, expr, index) = &**x; - - let access = if options.contains(ASTFlags::CONSTANT) { - AccessMode::ReadOnly - } else { - AccessMode::ReadWrite - }; - let export = options.contains(ASTFlags::EXPORTED); - - // Check variable definition filter - if let Some(ref filter) = self.def_var_filter { - let will_shadow = scope.contains(var_name); - let is_const = access == AccessMode::ReadOnly; - let info = VarDefInfo { - name: var_name, - is_const, - nesting_level: global.scope_level, - will_shadow, - }; - let orig_scope_len = scope.len(); - let context = - EvalContext::new(self, global, caches, scope, this_ptr.as_deref_mut()); - let filter_result = filter(true, info, context); - - if orig_scope_len != scope.len() { - // The scope is changed, always search from now on - global.always_search_scope = true; - } - - if !filter_result? { - return Err(ERR::ErrorForbiddenVariable(var_name.to_string(), *pos).into()); - } - } - - // Evaluate initial value - let mut value = self - .eval_expr(global, caches, scope, this_ptr, expr)? - .flatten() - .intern_string(self); - - let _alias = if !rewind_scope { - // Put global constants into global module - #[cfg(not(feature = "no_function"))] - #[cfg(not(feature = "no_module"))] - if global.scope_level == 0 - && access == AccessMode::ReadOnly - && global.lib.iter().any(|m| !m.is_empty()) - { - crate::func::locked_write(global.constants.get_or_insert_with(|| { - crate::Shared::new( - crate::Locked::new(std::collections::BTreeMap::new()), - ) - })) - .insert(var_name.name.clone(), value.clone()); - } - - if export { - Some(var_name) - } else { - None - } - } else if export { - unreachable!("exported variable not on global level"); - } else { - None - }; - - if let Some(index) = index { - value.set_access_mode(access); - *scope.get_mut_by_index(scope.len() - index.get()) = value; - } else { - scope.push_entry(var_name.name.clone(), access, value); - } - - #[cfg(not(feature = "no_module"))] - if let Some(alias) = _alias { - scope.add_alias_by_index(scope.len() - 1, alias.as_str().into()); - } - - Ok(Dynamic::UNIT) - } - // Import statement #[cfg(not(feature = "no_module"))] Stmt::Import(x, _pos) => {