Merge pull request #715 from schungx/master

Fix bugs.
This commit is contained in:
Stephen Chung 2023-04-27 13:40:37 +08:00 committed by GitHub
commit aa47dd2407
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 632 additions and 307 deletions

View File

@ -6,18 +6,23 @@ 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.
Buf fixes
Bug fixes
----------
* `is_shared` is a reserved keyword and is now handled properly (e.g. it cannot be the target of a function pointer).
* Re-optimizing an AST via `optimize_ast` with constants now works correctly for closures. Previously the hidden `Share` nodes are not removed and causes variable-not-found errors during runtime if the constants are not available in the scope.
* Expressions such as `(v[0].func()).prop` now parse correctly.
* Shadowed variable exports are now handled correctly.
* Shadowed constant definitions are now optimized correctly when propagated (e.g. `const X = 1; const X = 1 + 1 + 1; X` now evaluates to 3 instead of 0).
* Identifiers and comma's in the middle of custom syntax now register correctly.
* Exporting an object map from a module with closures defined on properties now works correctly when those properties are called to mimic method calls in OOP-style.
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.
* `is_def_fn` is extended to support checking for typed methods, with syntax `is_def_fn(this_type, fn_name, arity)`
* `Dynamic::take` is added as a short-cut for `std::mem::take(&mut value)`.
Enhancements
------------

View File

@ -232,11 +232,9 @@ impl Engine {
}
let token = Token::lookup_symbol_from_syntax(s).or_else(|| {
if is_reserved_keyword_or_symbol(s).0 {
Some(Token::Reserved(Box::new(s.into())))
} else {
None
}
is_reserved_keyword_or_symbol(s)
.0
.then(|| Token::Reserved(Box::new(s.into())))
});
let seg = match s {
@ -256,6 +254,9 @@ impl Engine {
#[cfg(not(feature = "no_float"))]
CUSTOM_SYNTAX_MARKER_FLOAT if !segments.is_empty() => s.into(),
// Identifier not in first position
_ if !segments.is_empty() && is_valid_identifier(s) => s.into(),
// Keyword/symbol not in first position
_ if !segments.is_empty() && token.is_some() => {
// Make it a custom keyword/symbol if it is disabled or reserved
@ -277,10 +278,7 @@ impl Engine {
{
return Err(LexError::ImproperSymbol(
s.to_string(),
format!(
"Improper symbol for custom syntax at position #{}: '{s}'",
segments.len() + 1,
),
format!("Improper symbol for custom syntax at position #0: '{s}'"),
)
.into_err(Position::NONE));
}

View File

@ -124,7 +124,7 @@ impl Engine {
///
/// To access a primary argument value (i.e. cloning is cheap), use: `args[n].as_xxx().unwrap()`
///
/// To access an argument value and avoid cloning, use `std::mem::take(args[n]).cast::<T>()`.
/// To access an argument value and avoid cloning, use `args[n].take().cast::<T>()`.
/// Notice that this will _consume_ the argument, replacing it with `()`.
///
/// To access the first mutable parameter, use `args.get_mut(0).unwrap()`

View File

@ -16,6 +16,7 @@ use std::{
fmt::Write,
hash::Hash,
iter::once,
mem,
num::{NonZeroU8, NonZeroUsize},
};
@ -833,6 +834,11 @@ impl Expr {
Self::Property(..) => matches!(token, Token::LeftParen),
}
}
/// Return this [`Expr`], replacing it with [`Expr::Unit`].
#[inline(always)]
pub fn take(&mut self) -> Self {
mem::take(self)
}
/// Recursively walk this expression.
/// Return `false` from the callback to terminate the walk.
pub fn walk<'a>(

View File

@ -1036,6 +1036,11 @@ impl Stmt {
pub const fn is_control_flow_break(&self) -> bool {
matches!(self, Self::Return(..) | Self::BreakLoop(..))
}
/// Return this [`Stmt`], replacing it with [`Stmt::Noop`].
#[inline(always)]
pub fn take(&mut self) -> Self {
mem::take(self)
}
/// Recursively walk this statement.
/// Return `false` from the callback to terminate the walk.
pub fn walk<'a>(

View File

@ -939,7 +939,7 @@ impl Engine {
if !val.is_shared() {
// Replace the variable with a shared value.
*val = std::mem::take(val).into_shared();
*val = val.take().into_shared();
}
}

View File

@ -748,7 +748,7 @@ pub fn get_builtin_op_assignment_fn(op: &Token, x: &Dynamic, y: &Dynamic) -> Opt
return match op {
PlusAssign => Some((
|_ctx, args| {
let x = std::mem::take(args[1]).into_array().unwrap();
let x = args[1].take().into_array().unwrap();
if x.is_empty() {
return Ok(Dynamic::UNIT);
@ -783,7 +783,7 @@ pub fn get_builtin_op_assignment_fn(op: &Token, x: &Dynamic, y: &Dynamic) -> Opt
return match op {
PlusAssign => Some((
|_ctx, args| {
let blob2 = std::mem::take(args[1]).into_blob().unwrap();
let blob2 = args[1].take().into_blob().unwrap();
let blob1 = &mut *args[0].write_lock::<Blob>().unwrap();
#[cfg(not(feature = "unchecked"))]
@ -931,7 +931,7 @@ pub fn get_builtin_op_assignment_fn(op: &Token, x: &Dynamic, y: &Dynamic) -> Opt
PlusAssign => Some((
|_ctx, args| {
{
let x = std::mem::take(args[1]);
let x = args[1].take();
let array = &mut *args[0].write_lock::<Array>().unwrap();
push(array, x);
}

View File

@ -768,51 +768,55 @@ impl Engine {
// Linked to scripted function?
#[cfg(not(feature = "no_function"))]
if let Some(fn_def) = fn_ptr.fn_def() {
if fn_def.params.len() == args.len() {
return self
.call_script_fn(
global,
caches,
&mut Scope::new(),
None,
fn_ptr.encapsulated_environ(),
fn_def,
args,
true,
fn_call_pos,
)
.map(|v| (v, false));
let fn_def = fn_ptr.fn_def();
#[cfg(feature = "no_function")]
let fn_def = ();
match fn_def {
#[cfg(not(feature = "no_function"))]
Some(fn_def) if fn_def.params.len() == args.len() => self
.call_script_fn(
global,
caches,
&mut Scope::new(),
None,
fn_ptr.encapsulated_environ().map(|r| r.as_ref()),
fn_def,
args,
true,
fn_call_pos,
)
.map(|v| (v, false)),
_ => {
#[cfg(not(feature = "no_function"))]
let is_anon = fn_ptr.is_anonymous();
#[cfg(feature = "no_function")]
let is_anon = false;
// Redirect function name
let fn_name = fn_ptr.fn_name();
// Recalculate hashes
let new_hash = if !is_anon && !is_valid_function_name(fn_name) {
FnCallHashes::from_native_only(calc_fn_hash(None, fn_name, args.len()))
} else {
FnCallHashes::from_hash(calc_fn_hash(None, fn_name, args.len()))
};
// Map it to name(args) in function-call style
self.exec_fn_call(
global,
caches,
None,
fn_name,
None,
new_hash,
args,
false,
false,
fn_call_pos,
)
}
}
#[cfg(not(feature = "no_function"))]
let is_anon = fn_ptr.is_anonymous();
#[cfg(feature = "no_function")]
let is_anon = false;
// Redirect function name
let fn_name = fn_ptr.fn_name();
// Recalculate hashes
let new_hash = if !is_anon && !is_valid_function_name(fn_name) {
FnCallHashes::from_native_only(calc_fn_hash(None, fn_name, args.len()))
} else {
FnCallHashes::from_hash(calc_fn_hash(None, fn_name, args.len()))
};
// Map it to name(args) in function-call style
self.exec_fn_call(
global,
caches,
None,
fn_name,
None,
new_hash,
args,
false,
false,
fn_call_pos,
)
}
// Handle obj.call(fn_ptr, ...)
@ -831,18 +835,29 @@ impl Engine {
}
// FnPtr call on object
let fn_ptr = mem::take(&mut call_args[0]).cast::<FnPtr>();
let fn_ptr = call_args[0].take().cast::<FnPtr>();
#[cfg(not(feature = "no_function"))]
let (fn_name, is_anon, fn_curry, _environ, fn_def) = {
#[cfg(not(feature = "no_function"))]
let is_anon = fn_ptr.is_anonymous();
#[cfg(feature = "no_function")]
let is_anon = false;
#[cfg(not(feature = "no_function"))]
let (fn_name, fn_curry, environ, fn_def) = fn_ptr.take_data();
(fn_name, is_anon, fn_curry, environ, fn_def)
};
#[cfg(feature = "no_function")]
let (fn_name, is_anon, fn_curry, _environ) = {
#[cfg(feature = "no_function")]
let (fn_name, fn_curry, environ) = fn_ptr.take_data();
(fn_name, false, fn_curry, environ)
(
fn_name,
is_anon,
fn_curry,
environ,
#[cfg(not(feature = "no_function"))]
fn_def,
#[cfg(feature = "no_function")]
(),
)
};
// Replace the first argument with the object pointer, adding the curried arguments
@ -855,59 +870,67 @@ impl Engine {
args.extend(call_args.iter_mut());
// Linked to scripted function?
#[cfg(not(feature = "no_function"))]
if let Some(fn_def) = fn_def {
if fn_def.params.len() == args.len() {
match fn_def {
#[cfg(not(feature = "no_function"))]
Some(fn_def) if fn_def.params.len() == args.len() => {
// Check for data race.
#[cfg(not(feature = "no_closure"))]
ensure_no_data_race(&fn_def.name, args, false)?;
return self
.call_script_fn(
global,
caches,
&mut Scope::new(),
Some(target),
_environ.as_deref(),
&fn_def,
args,
true,
fn_call_pos,
)
.map(|v| (v, false));
self.call_script_fn(
global,
caches,
&mut Scope::new(),
Some(target),
_environ.as_deref(),
&fn_def,
args,
true,
fn_call_pos,
)
.map(|v| (v, false))
}
_ => {
// Add the first argument with the object pointer
args.insert(0, target.as_mut());
// Recalculate hash
let new_hash = match is_anon {
false if !is_valid_function_name(&fn_name) => {
FnCallHashes::from_native_only(calc_fn_hash(
None,
&fn_name,
args.len(),
))
}
#[cfg(not(feature = "no_function"))]
_ => FnCallHashes::from_script_and_native(
calc_fn_hash(None, &fn_name, args.len() - 1),
calc_fn_hash(None, &fn_name, args.len()),
),
#[cfg(feature = "no_function")]
_ => FnCallHashes::from_native_only(calc_fn_hash(
None,
&fn_name,
args.len(),
)),
};
// Map it to name(args) in function-call style
self.exec_fn_call(
global,
caches,
None,
&fn_name,
None,
new_hash,
args,
is_ref_mut,
true,
fn_call_pos,
)
}
}
// Add the first argument with the object pointer
args.insert(0, target.as_mut());
// Recalculate hash
let new_hash = match is_anon {
false if !is_valid_function_name(&fn_name) => {
FnCallHashes::from_native_only(calc_fn_hash(None, &fn_name, args.len()))
}
#[cfg(not(feature = "no_function"))]
_ => FnCallHashes::from_script_and_native(
calc_fn_hash(None, &fn_name, args.len() - 1),
calc_fn_hash(None, &fn_name, args.len()),
),
#[cfg(feature = "no_function")]
_ => FnCallHashes::from_native_only(calc_fn_hash(None, &fn_name, args.len())),
};
// Map it to name(args) in function-call style
self.exec_fn_call(
global,
caches,
None,
&fn_name,
None,
new_hash,
args,
is_ref_mut,
true,
fn_call_pos,
)
}
KEYWORD_FN_PTR_CURRY => {
if !target.is_fnptr() {
@ -936,19 +959,16 @@ impl Engine {
_ => {
let mut fn_name = fn_name;
let _redirected;
let mut _linked = None;
let mut _arg_values: FnArgsVec<_>;
let mut call_args = call_args;
// Check if it is a map method call in OOP style
#[cfg(not(feature = "no_object"))]
if let Some(map) = target.read_lock::<crate::Map>() {
if let Some(val) = map.get(fn_name) {
if let Some(fn_ptr) = val.read_lock::<FnPtr>() {
#[cfg(not(feature = "no_function"))]
let is_anon = fn_ptr.is_anonymous();
#[cfg(feature = "no_function")]
let is_anon = false;
// Remap the function name
_redirected = fn_ptr.fn_name_raw().clone();
fn_name = &_redirected;
@ -962,45 +982,90 @@ impl Engine {
.collect();
call_args = &mut _arg_values;
}
// Recalculate the hash based on the new function name and new arguments
let args_len = call_args.len() + 1;
hash = match is_anon {
false if !is_valid_function_name(fn_name) => {
FnCallHashes::from_native_only(calc_fn_hash(
None, fn_name, args_len,
// Linked to scripted function?
#[cfg(not(feature = "no_function"))]
let fn_def = fn_ptr.fn_def();
#[cfg(feature = "no_function")]
let fn_def = ();
match fn_def {
#[cfg(not(feature = "no_function"))]
Some(fn_def) if fn_def.params.len() == call_args.len() => {
_linked = Some((
fn_def.clone(),
fn_ptr.encapsulated_environ().cloned(),
))
}
#[cfg(not(feature = "no_function"))]
_ => FnCallHashes::from_script_and_native(
calc_fn_hash(None, fn_name, args_len - 1),
calc_fn_hash(None, fn_name, args_len),
),
#[cfg(feature = "no_function")]
_ => FnCallHashes::from_native_only(calc_fn_hash(
None, fn_name, args_len,
)),
};
_ => {
#[cfg(not(feature = "no_function"))]
let is_anon = fn_ptr.is_anonymous();
#[cfg(feature = "no_function")]
let is_anon = false;
// Recalculate the hash based on the new function name and new arguments
let args_len = call_args.len() + 1;
hash = match is_anon {
false if !is_valid_function_name(fn_name) => {
FnCallHashes::from_native_only(calc_fn_hash(
None, fn_name, args_len,
))
}
#[cfg(not(feature = "no_function"))]
_ => FnCallHashes::from_script_and_native(
calc_fn_hash(None, fn_name, args_len - 1),
calc_fn_hash(None, fn_name, args_len),
),
#[cfg(feature = "no_function")]
_ => FnCallHashes::from_native_only(calc_fn_hash(
None, fn_name, args_len,
)),
};
}
}
}
}
};
}
// Attached object pointer in front of the arguments
let mut args = FnArgsVec::with_capacity(call_args.len() + 1);
args.push(target.as_mut());
args.extend(call_args.iter_mut());
match _linked {
#[cfg(not(feature = "no_function"))]
Some((fn_def, environ)) => {
// Linked to scripted function
self.call_script_fn(
global,
caches,
&mut Scope::new(),
Some(target),
environ.as_deref(),
&*fn_def,
&mut call_args.iter_mut().collect::<FnArgsVec<_>>(),
true,
fn_call_pos,
)
.map(|v| (v, false))
}
#[cfg(feature = "no_function")]
Some(()) => unreachable!(),
None => {
// Attached object pointer in front of the arguments
let mut args = FnArgsVec::with_capacity(call_args.len() + 1);
args.push(target.as_mut());
args.extend(call_args.iter_mut());
self.exec_fn_call(
global,
caches,
None,
fn_name,
None,
hash,
&mut args,
is_ref_mut,
true,
fn_call_pos,
)
self.exec_fn_call(
global,
caches,
None,
fn_name,
None,
hash,
&mut args,
is_ref_mut,
true,
fn_call_pos,
)
}
}
}
}?;

View File

@ -57,12 +57,12 @@ pub fn by_value<T: Variant + Clone>(data: &mut Dynamic) -> T {
}
if TypeId::of::<T>() == TypeId::of::<String>() {
// If T is `String`, data must be `ImmutableString`, so map directly to it
return reify! { mem::take(data).into_string().expect("`ImmutableString`") => T };
return reify! { data.take().into_string().expect("`ImmutableString`") => T };
}
// We consume the argument and then replace it with () - the argument is not supposed to be used again.
// This way, we avoid having to clone the argument again, because it is already a clone when passed here.
mem::take(data).cast::<T>()
data.take().cast::<T>()
}
/// Trait to register custom Rust functions.

View File

@ -67,7 +67,7 @@ impl Engine {
// Put arguments into scope as variables
scope.extend(fn_def.params.iter().cloned().zip(args.iter_mut().map(|v| {
// Actually consume the arguments instead of cloning them
mem::take(*v)
v.take()
})));
// Push a new call stack frame

View File

@ -1204,7 +1204,7 @@ impl Module {
///
/// To access a primary argument value (i.e. cloning is cheap), use: `args[n].as_xxx().unwrap()`
///
/// To access an argument value and avoid cloning, use `std::mem::take(args[n]).cast::<T>()`.
/// To access an argument value and avoid cloning, use `args[n].take().cast::<T>()`.
/// Notice that this will _consume_ the argument, replacing it with `()`.
///
/// To access the first mutable argument, use `args.get_mut(0).unwrap()`
@ -1227,7 +1227,7 @@ impl Module {
/// // 'args' is guaranteed to be the right length and of the correct types
///
/// // Get the second parameter by 'consuming' it
/// let double = std::mem::take(args[1]).cast::<bool>();
/// let double = args[1].take().cast::<bool>();
/// // Since it is a primary type, it can also be cheaply copied
/// let double = args[1].clone_cast::<bool>();
/// // Get a mutable reference to the first argument.
@ -2211,8 +2211,17 @@ impl Module {
});
// Variables with an alias left in the scope become module variables
while scope.len() > orig_scope_len {
let (_name, mut value, mut aliases) = scope.pop_entry().expect("not empty");
let mut i = scope.len();
while i > 0 {
i -= 1;
let (mut value, mut aliases) = if i >= orig_scope_len {
let (_, v, a) = scope.pop_entry().expect("not empty");
(v, a)
} else {
let (_, v, a) = scope.get_entry_by_index(i);
(v.clone(), a.to_vec())
};
value.deep_scan(|v| {
if let Some(fn_ptr) = v.downcast_mut::<crate::FnPtr>() {
@ -2224,15 +2233,28 @@ impl Module {
0 => (),
1 => {
let alias = aliases.pop().unwrap();
module.set_var(alias, value);
if !module.contains_var(&alias) {
module.set_var(alias, value);
}
}
_ => {
let last_alias = aliases.pop().unwrap();
for alias in aliases {
module.set_var(alias, value.clone());
}
// Avoid cloning the last value
module.set_var(last_alias, value);
let mut first_alias = None;
for alias in aliases {
if module.contains_var(&alias) {
continue;
}
if first_alias.is_none() {
first_alias = Some(alias);
} else {
module.set_var(alias, value.clone());
}
}
if let Some(alias) = first_alias {
module.set_var(alias, value);
}
}
}
}

View File

@ -14,10 +14,12 @@ use crate::func::builtin::get_builtin_binary_op_fn;
use crate::func::hashing::get_hasher;
use crate::module::ModuleFlags;
use crate::tokenizer::Token;
use crate::types::scope::SCOPE_ENTRIES_INLINED;
use crate::{
calc_fn_hash, calc_fn_hash_full, Dynamic, Engine, FnPtr, ImmutableString, Position, Scope,
StaticVec, AST,
calc_fn_hash, calc_fn_hash_full, Dynamic, Engine, FnArgsVec, FnPtr, ImmutableString, Position,
Scope, AST,
};
use smallvec::SmallVec;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
use std::{
@ -52,12 +54,12 @@ impl Default for OptimizationLevel {
#[derive(Debug, Clone)]
struct OptimizerState<'a> {
/// Has the [`AST`] been changed during this pass?
changed: bool,
/// Collection of variables/constants to use for eager function evaluations.
variables: StaticVec<(ImmutableString, Option<Dynamic>)>,
is_dirty: bool,
/// Stack of variables/constants for constants propagation.
variables: SmallVec<[(ImmutableString, Option<Dynamic>); SCOPE_ENTRIES_INLINED]>,
/// Activate constants propagation?
propagate_constants: bool,
/// An [`Engine`] instance for eager function evaluation.
/// [`Engine`] instance for eager function evaluation.
engine: &'a Engine,
/// The global runtime state.
global: GlobalRuntimeState,
@ -84,8 +86,8 @@ impl<'a> OptimizerState<'a> {
}
Self {
changed: false,
variables: StaticVec::new_const(),
is_dirty: false,
variables: SmallVec::new_const(),
propagate_constants: true,
engine,
global: _global,
@ -96,48 +98,42 @@ impl<'a> OptimizerState<'a> {
/// Set the [`AST`] state to be dirty (i.e. changed).
#[inline(always)]
pub fn set_dirty(&mut self) {
self.changed = true;
self.is_dirty = true;
}
/// Set the [`AST`] state to be not dirty (i.e. unchanged).
#[inline(always)]
pub fn clear_dirty(&mut self) {
self.changed = false;
self.is_dirty = false;
}
/// Is the [`AST`] dirty (i.e. changed)?
#[inline(always)]
pub const fn is_dirty(&self) -> bool {
self.changed
self.is_dirty
}
/// Prune the list of constants back to a specified size.
/// Rewind the variables stack back to a specified size.
#[inline(always)]
pub fn restore_var(&mut self, len: usize) {
pub fn rewind_var(&mut self, len: usize) {
self.variables.truncate(len);
}
/// Add a new variable to the list.
/// Add a new variable to the stack.
///
/// `Some(value)` if constant, `None` or otherwise.
/// `Some(value)` if literal constant (which can be used for constants propagation), `None` otherwise.
#[inline(always)]
pub fn push_var(&mut self, name: ImmutableString, value: Option<Dynamic>) {
self.variables.push((name, value));
}
/// Look up a constant from the list.
/// Look up a literal constant from the variables stack.
#[inline]
pub fn find_constant(&self, name: &str) -> Option<&Dynamic> {
if !self.propagate_constants {
return None;
}
for (n, value) in self.variables.iter().rev() {
if n.as_str() == name {
return value.as_ref();
}
}
None
pub fn find_literal_constant(&self, name: &str) -> Option<&Dynamic> {
self.variables
.iter()
.rev()
.find(|(n, _)| n.as_str() == name)
.and_then(|(_, value)| value.as_ref())
}
/// Call a registered function
#[inline]
pub fn call_fn_with_constant_arguments(
pub fn call_fn_with_const_args(
&mut self,
fn_name: &str,
op_token: Option<&Token>,
@ -150,7 +146,7 @@ impl<'a> OptimizerState<'a> {
fn_name,
op_token,
calc_fn_hash(None, fn_name, arg_values.len()),
&mut arg_values.iter_mut().collect::<StaticVec<_>>(),
&mut arg_values.iter_mut().collect::<FnArgsVec<_>>(),
false,
Position::NONE,
)
@ -184,7 +180,7 @@ fn optimize_stmt_block(
|s| matches!(s, Stmt::Block(block, ..) if !block.iter().any(Stmt::is_block_dependent)),
) {
let (first, second) = statements.split_at_mut(n);
let stmt = mem::take(&mut second[0]);
let stmt = second[0].take();
let mut stmts = match stmt {
Stmt::Block(block, ..) => block,
stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt),
@ -225,19 +221,16 @@ fn optimize_stmt_block(
statements.iter_mut().for_each(|stmt| {
match stmt {
Stmt::Var(x, options, ..) => {
if options.contains(ASTFlags::CONSTANT) {
// Add constant literals into the state
optimize_expr(&mut x.1, state, false);
optimize_expr(&mut x.1, state, false);
if x.1.is_constant() {
state
.push_var(x.0.name.clone(), Some(x.1.get_literal_value().unwrap()));
}
let value = if options.contains(ASTFlags::CONSTANT) && x.1.is_constant() {
// constant literal
Some(x.1.get_literal_value().unwrap())
} else {
// Add variables into the state
optimize_expr(&mut x.1, state, false);
state.push_var(x.0.name.clone(), None);
}
// variable
None
};
state.push_var(x.0.name.clone(), value);
}
// Optimize the statement
_ => optimize_stmt(stmt, state, preserve_result),
@ -371,7 +364,7 @@ fn optimize_stmt_block(
}
// Pop the stack and remove all the local constants
state.restore_var(orig_constants_len);
state.rewind_var(orig_constants_len);
state.propagate_constants = orig_propagate_constants;
if !state.is_dirty() {
@ -406,7 +399,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
Expr::FnCall(ref mut x2, pos) => {
state.set_dirty();
x.0 = OpAssignment::new_op_assignment_from_base(&x2.name, pos);
x.1.rhs = mem::take(&mut x2.args[1]);
x.1.rhs = x2.args[1].take();
}
ref expr => unreachable!("Expr::FnCall expected but gets {:?}", expr),
}
@ -426,7 +419,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
state.set_dirty();
let pos = condition.start_position();
let mut expr = mem::take(condition);
let mut expr = condition.take();
optimize_expr(&mut expr, state, false);
*stmt = if preserve_result {
@ -456,7 +449,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
// if false { if_block } else { else_block } -> else_block
Stmt::If(x, ..) if matches!(x.expr, Expr::BoolConstant(false, ..)) => {
state.set_dirty();
let body = mem::take(&mut *x.branch);
let body = x.branch.take_statements();
*stmt = match optimize_stmt_block(body, state, preserve_result, true, false) {
statements if statements.is_empty() => Stmt::Noop(x.branch.position()),
statements => (statements, x.branch.span()).into(),
@ -465,7 +458,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
// if true { if_block } else { else_block } -> if_block
Stmt::If(x, ..) if matches!(x.expr, Expr::BoolConstant(true, ..)) => {
state.set_dirty();
let body = mem::take(&mut *x.body);
let body = x.body.take_statements();
*stmt = match optimize_stmt_block(body, state, preserve_result, true, false) {
statements if statements.is_empty() => Stmt::Noop(x.body.position()),
statements => (statements, x.body.span()).into(),
@ -475,9 +468,9 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
Stmt::If(x, ..) => {
let FlowControl { expr, body, branch } = &mut **x;
optimize_expr(expr, state, false);
let statements = mem::take(&mut **body);
let statements = body.take_statements();
**body = optimize_stmt_block(statements, state, preserve_result, true, false);
let statements = mem::take(&mut **branch);
let statements = branch.take_statements();
**branch = optimize_stmt_block(statements, state, preserve_result, true, false);
}
@ -508,7 +501,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
if b.is_always_true() {
// Promote the matched case
let mut statements = Stmt::Expr(mem::take(&mut b.expr).into());
let mut statements = Stmt::Expr(b.expr.take().into());
optimize_stmt(&mut statements, state, true);
*stmt = statements;
} else {
@ -518,14 +511,14 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
let branch = match def_case {
Some(index) => {
let mut def_stmt =
Stmt::Expr(mem::take(&mut expressions[*index].expr).into());
Stmt::Expr(expressions[*index].expr.take().into());
optimize_stmt(&mut def_stmt, state, true);
def_stmt.into()
}
_ => StmtBlock::NONE,
};
let body = Stmt::Expr(mem::take(&mut b.expr).into()).into();
let expr = mem::take(&mut b.condition);
let body = Stmt::Expr(b.expr.take().into()).into();
let expr = b.condition.take();
*stmt = Stmt::If(
FlowControl { expr, body, branch }.into(),
@ -542,7 +535,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
if b.is_always_true() {
// Promote the matched case
let mut statements = Stmt::Expr(mem::take(&mut b.expr).into());
let mut statements = Stmt::Expr(b.expr.take().into());
optimize_stmt(&mut statements, state, true);
*stmt = statements;
state.set_dirty();
@ -567,11 +560,11 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
if range_block.is_always_true() {
// Promote the matched case
let block = &mut expressions[r.index()];
let mut statements = Stmt::Expr(mem::take(&mut block.expr).into());
let mut statements = Stmt::Expr(block.expr.take().into());
optimize_stmt(&mut statements, state, true);
*stmt = statements;
} else {
let mut expr = mem::take(&mut range_block.condition);
let mut expr = range_block.condition.take();
// switch const { range if condition => stmt, _ => def } => if condition { stmt } else { def }
optimize_expr(&mut expr, state, false);
@ -579,16 +572,14 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
let branch = match def_case {
Some(index) => {
let mut def_stmt =
Stmt::Expr(mem::take(&mut expressions[*index].expr).into());
Stmt::Expr(expressions[*index].expr.take().into());
optimize_stmt(&mut def_stmt, state, true);
def_stmt.into()
}
_ => StmtBlock::NONE,
};
let body =
Stmt::Expr(mem::take(&mut expressions[r.index()].expr).into())
.into();
let body = Stmt::Expr(expressions[r.index()].expr.take().into()).into();
*stmt = Stmt::If(
FlowControl { expr, body, branch }.into(),
@ -628,7 +619,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
match def_case {
Some(index) => {
let mut def_stmt = Stmt::Expr(mem::take(&mut expressions[*index].expr).into());
let mut def_stmt = Stmt::Expr(expressions[*index].expr.take().into());
optimize_stmt(&mut def_stmt, state, true);
*stmt = def_stmt;
}
@ -738,17 +729,17 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
if let Expr::BoolConstant(true, pos) = expr {
*expr = Expr::Unit(*pos);
}
**body = optimize_stmt_block(mem::take(&mut **body), state, false, true, false);
**body = optimize_stmt_block(body.take_statements(), state, false, true, false);
}
// do { block } while|until expr
Stmt::Do(x, ..) => {
optimize_expr(&mut x.expr, state, false);
*x.body = optimize_stmt_block(mem::take(&mut *x.body), state, false, true, false);
*x.body = optimize_stmt_block(x.body.take_statements(), state, false, true, false);
}
// for id in expr { block }
Stmt::For(x, ..) => {
optimize_expr(&mut x.2.expr, state, false);
*x.2.body = optimize_stmt_block(mem::take(&mut *x.2.body), state, false, true, false);
*x.2.body = optimize_stmt_block(x.2.body.take_statements(), state, false, true, false);
}
// let id = expr;
Stmt::Var(x, options, ..) if !options.contains(ASTFlags::CONSTANT) => {
@ -771,7 +762,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
// Only one statement which is not block-dependent - promote
[s] if !s.is_block_dependent() => {
state.set_dirty();
*stmt = mem::take(s);
*stmt = s.take();
}
_ => *stmt = (block, span).into(),
}
@ -781,15 +772,15 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
// If try block is pure, there will never be any exceptions
state.set_dirty();
*stmt = (
optimize_stmt_block(mem::take(&mut *x.body), state, false, true, false),
optimize_stmt_block(x.body.take_statements(), state, false, true, false),
x.body.span(),
)
.into();
}
// try { try_block } catch ( var ) { catch_block }
Stmt::TryCatch(x, ..) => {
*x.body = optimize_stmt_block(mem::take(&mut *x.body), state, false, true, false);
*x.branch = optimize_stmt_block(mem::take(&mut *x.branch), state, false, true, false);
*x.body = optimize_stmt_block(x.body.take_statements(), state, false, true, false);
*x.branch = optimize_stmt_block(x.branch.take_statements(), state, false, true, false);
}
// expr(stmt)
@ -799,7 +790,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
Expr::Stmt(block) if !block.is_empty() => {
let mut stmt_block = *mem::take(block);
*stmt_block =
optimize_stmt_block(mem::take(&mut *stmt_block), state, true, true, false);
optimize_stmt_block(stmt_block.take_statements(), state, true, true, false);
*stmt = stmt_block.into();
}
Expr::Stmt(..) => *stmt = Stmt::Noop(expr.position()),
@ -810,7 +801,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
// expr(func())
Stmt::Expr(expr) if matches!(**expr, Expr::FnCall(..)) => {
state.set_dirty();
match mem::take(expr.as_mut()) {
match expr.take() {
Expr::FnCall(x, pos) => *stmt = Stmt::FnCall(x, pos),
_ => unreachable!(),
}
@ -820,7 +811,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
// func(...)
Stmt::FnCall(..) => {
if let Stmt::FnCall(x, pos) = mem::take(stmt) {
if let Stmt::FnCall(x, pos) = stmt.take() {
let mut expr = Expr::FnCall(x, pos);
optimize_expr(&mut expr, state, false);
*stmt = match expr {
@ -847,10 +838,14 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b
// Share constants
#[cfg(not(feature = "no_closure"))]
Stmt::Share(x) => {
let len = x.len();
x.retain(|(v, _)| !state.find_constant(v).is_some());
if x.len() != len {
state.set_dirty();
let orig_len = x.len();
if state.propagate_constants {
x.retain(|(v, _)| state.find_literal_constant(v).is_none());
if x.len() != orig_len {
state.set_dirty();
}
}
}
@ -883,16 +878,16 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
}
// { stmt; ... } - do not count promotion as dirty because it gets turned back into an array
Expr::Stmt(x) => {
***x = optimize_stmt_block(mem::take(&mut **x), state, true, true, false);
***x = optimize_stmt_block(x.take_statements(), state, true, true, false);
// { Stmt(Expr) } - promote
if let [ Stmt::Expr(e) ] = &mut ****x { state.set_dirty(); *expr = mem::take(e); }
if let [ Stmt::Expr(e) ] = &mut ****x { state.set_dirty(); *expr = e.take(); }
}
// ()?.rhs
#[cfg(not(feature = "no_object"))]
Expr::Dot(x, options, ..) if options.contains(ASTFlags::NEGATED) && matches!(x.lhs, Expr::Unit(..)) => {
state.set_dirty();
*expr = mem::take(&mut x.lhs);
*expr = x.lhs.take();
}
// lhs.rhs
#[cfg(not(feature = "no_object"))]
@ -935,7 +930,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
#[cfg(not(feature = "no_index"))]
Expr::Index(x, options, ..) if options.contains(ASTFlags::NEGATED) && matches!(x.lhs, Expr::Unit(..)) => {
state.set_dirty();
*expr = mem::take(&mut x.lhs);
*expr = x.lhs.take();
}
// lhs[rhs]
#[cfg(not(feature = "no_index"))]
@ -946,7 +941,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
// Array literal where everything is pure - promote the indexed item.
// All other items can be thrown away.
state.set_dirty();
let mut result = mem::take(&mut a[*i as usize]);
let mut result = a[*i as usize].take();
result.set_position(*pos);
*expr = result;
}
@ -956,7 +951,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
// All other items can be thrown away.
state.set_dirty();
let index = a.len() - i.unsigned_abs() as usize;
let mut result = mem::take(&mut a[index]);
let mut result = a[index].take();
result.set_position(*pos);
*expr = result;
}
@ -1018,7 +1013,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
// Merge consecutive strings
while n < x.len() - 1 {
match (mem::take(&mut x[n]), mem::take(&mut x[n+1])) {
match (x[n].take(),x[n+1].take()) {
(Expr::StringConstant(mut s1, pos), Expr::StringConstant(s2, ..)) => { s1 += s2; x[n] = Expr::StringConstant(s1, pos); x.remove(n+1); state.set_dirty(); }
(expr1, Expr::Unit(..)) => { x[n] = expr1; x.remove(n+1); state.set_dirty(); }
(Expr::Unit(..), expr2) => { x[n+1] = expr2; x.remove(n); state.set_dirty(); }
@ -1051,34 +1046,34 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
// lhs && rhs
Expr::And(x, ..) => match (&mut x.lhs, &mut x.rhs) {
// true && rhs -> rhs
(Expr::BoolConstant(true, ..), rhs) => { state.set_dirty(); optimize_expr(rhs, state, false); *expr = mem::take(rhs); }
(Expr::BoolConstant(true, ..), rhs) => { state.set_dirty(); optimize_expr(rhs, state, false); *expr = rhs.take(); }
// false && rhs -> false
(Expr::BoolConstant(false, pos), ..) => { state.set_dirty(); *expr = Expr::BoolConstant(false, *pos); }
// lhs && true -> lhs
(lhs, Expr::BoolConstant(true, ..)) => { state.set_dirty(); optimize_expr(lhs, state, false); *expr = mem::take(lhs); }
(lhs, Expr::BoolConstant(true, ..)) => { state.set_dirty(); optimize_expr(lhs, state, false); *expr = lhs.take(); }
// lhs && rhs
(lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, false); }
},
// lhs || rhs
Expr::Or(ref mut x, ..) => match (&mut x.lhs, &mut x.rhs) {
// false || rhs -> rhs
(Expr::BoolConstant(false, ..), rhs) => { state.set_dirty(); optimize_expr(rhs, state, false); *expr = mem::take(rhs); }
(Expr::BoolConstant(false, ..), rhs) => { state.set_dirty(); optimize_expr(rhs, state, false); *expr = rhs.take(); }
// true || rhs -> true
(Expr::BoolConstant(true, pos), ..) => { state.set_dirty(); *expr = Expr::BoolConstant(true, *pos); }
// lhs || false
(lhs, Expr::BoolConstant(false, ..)) => { state.set_dirty(); optimize_expr(lhs, state, false); *expr = mem::take(lhs); }
(lhs, Expr::BoolConstant(false, ..)) => { state.set_dirty(); optimize_expr(lhs, state, false); *expr = lhs.take(); }
// lhs || rhs
(lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, false); }
},
// () ?? rhs -> rhs
Expr::Coalesce(x, ..) if matches!(x.lhs, Expr::Unit(..)) => {
state.set_dirty();
*expr = mem::take(&mut x.rhs);
*expr = x.rhs.take();
},
// lhs:constant ?? rhs -> lhs
Expr::Coalesce(x, ..) if x.lhs.is_constant() => {
state.set_dirty();
*expr = mem::take(&mut x.lhs);
*expr = x.lhs.take();
},
// !true or !false
@ -1143,8 +1138,8 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
&& state.optimization_level == OptimizationLevel::Simple // simple optimizations
&& x.constant_args() // all arguments are constants
=> {
let arg_values = &mut x.args.iter().map(|arg_expr| arg_expr.get_literal_value().unwrap()).collect::<StaticVec<_>>();
let arg_types: StaticVec<_> = arg_values.iter().map(Dynamic::type_id).collect();
let arg_values = &mut x.args.iter().map(|arg_expr| arg_expr.get_literal_value().unwrap()).collect::<FnArgsVec<_>>();
let arg_types = arg_values.iter().map(Dynamic::type_id).collect::<FnArgsVec<_>>();
match x.name.as_str() {
KEYWORD_TYPE_OF if arg_values.len() == 1 => {
@ -1206,13 +1201,13 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
let has_script_fn = false;
if !has_script_fn {
let arg_values = &mut x.args.iter().map(Expr::get_literal_value).collect::<Option<StaticVec<_>>>().unwrap();
let arg_values = &mut x.args.iter().map(Expr::get_literal_value).collect::<Option<FnArgsVec<_>>>().unwrap();
let result = match x.name.as_str() {
KEYWORD_TYPE_OF if arg_values.len() == 1 => Some(state.engine.map_type_name(arg_values[0].type_name()).into()),
#[cfg(not(feature = "no_closure"))]
crate::engine::KEYWORD_IS_SHARED if arg_values.len() == 1 => Some(Dynamic::FALSE),
_ => state.call_fn_with_constant_arguments(&x.name, x.op_token.as_ref(), arg_values)
_ => state.call_fn_with_const_args(&x.name, x.op_token.as_ref(), arg_values)
};
if let Some(r) = result {
@ -1248,9 +1243,9 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
// constant-name
#[cfg(not(feature = "no_module"))]
Expr::Variable(x, ..) if !x.1.is_empty() => (),
Expr::Variable(x, .., pos) if state.find_constant(&x.3).is_some() => {
Expr::Variable(x, .., pos) if state.propagate_constants && state.find_literal_constant(&x.3).is_some() => {
// Replace constant with value
*expr = Expr::from_dynamic(state.find_constant(&x.3).unwrap().clone(), *pos);
*expr = Expr::from_dynamic(state.find_literal_constant(&x.3).unwrap().clone(), *pos);
state.set_dirty();
}
@ -1343,7 +1338,7 @@ impl Engine {
&self,
scope: Option<&Scope>,
statements: StmtBlockContainer,
#[cfg(not(feature = "no_function"))] functions: StaticVec<
#[cfg(not(feature = "no_function"))] functions: crate::StaticVec<
crate::Shared<crate::ast::ScriptFnDef>,
>,
optimization_level: OptimizationLevel,
@ -1383,7 +1378,7 @@ impl Engine {
functions.into_iter().for_each(|fn_def| {
let mut fn_def = crate::func::shared_take_or_clone(fn_def);
// Optimize the function body
let body = mem::take(&mut *fn_def.body);
let body = fn_def.body.take_statements();
*fn_def.body = self.optimize_top_level(body, scope, lib2, optimization_level);

View File

@ -55,7 +55,7 @@ pub struct ParseState<'e, 's> {
/// Global runtime state.
pub global: Option<Box<GlobalRuntimeState>>,
/// Encapsulates a local stack with variable names to simulate an actual runtime scope.
pub stack: Option<Box<Scope<'e>>>,
pub stack: Option<Scope<'e>>,
/// Size of the local variables stack upon entry of the current block scope.
pub block_stack_len: usize,
/// Tracks a list of external variables (variables that are not explicitly declared in the scope).
@ -143,7 +143,7 @@ impl<'e, 's> ParseState<'e, 's> {
let index = self
.stack
.as_deref()
.as_ref()
.into_iter()
.flat_map(Scope::iter_rev_raw)
.enumerate()
@ -539,6 +539,82 @@ fn parse_var_name(input: &mut TokenStream) -> ParseResult<(SmartString, Position
}
}
/// Optimize the structure of a chained expression where the root expression is another chained expression.
///
/// # Panics
///
/// Panics if the expression is not a combo chain.
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
fn optimize_combo_chain(expr: &mut Expr) {
let (mut x, x_options, x_pos, mut root, mut root_options, root_pos, make_sub, make_root): (
_,
_,
_,
_,
_,
_,
fn(_, _, _) -> Expr,
fn(_, _, _) -> Expr,
) = match expr.take() {
#[cfg(not(feature = "no_index"))]
Expr::Index(mut x, opt, pos) => match x.lhs.take() {
Expr::Index(x2, opt2, pos2) => (x, opt, pos, x2, opt2, pos2, Expr::Index, Expr::Index),
#[cfg(not(feature = "no_object"))]
Expr::Dot(x2, opt2, pos2) => (x, opt, pos, x2, opt2, pos2, Expr::Index, Expr::Dot),
_ => panic!("combo chain expected"),
},
#[cfg(not(feature = "no_object"))]
Expr::Dot(mut x, opt, pos) => match x.lhs.take() {
#[cfg(not(feature = "no_index"))]
Expr::Index(x2, opt2, pos2) => (x, opt, pos, x2, opt2, pos2, Expr::Dot, Expr::Index),
Expr::Dot(x2, opt2, pos2) => (x, opt, pos, x2, opt2, pos2, Expr::Dot, Expr::Index),
_ => panic!("combo chain expected"),
},
_ => panic!("combo chain expected"),
};
// Rewrite the chains like this:
//
// Source: ( x[y].prop_a )[z].prop_b
// ^ ^
// parentheses that generated the combo chain
//
// From: Index( Index( x, Dot(y, prop_a) ), Dot(z, prop_b) )
// ^ ^ ^
// x root tail
//
// To: Index( x, Dot(y, Index(prop_a, Dot(z, prop_b) ) ) )
//
// Equivalent to: x[y].prop_a[z].prop_b
// Find the end of the root chain.
let mut tail = root.as_mut();
let mut tail_options = &mut root_options;
while !tail_options.contains(ASTFlags::BREAK) {
match tail.rhs {
Expr::Index(ref mut x, ref mut options2, ..) => {
tail = x.as_mut();
tail_options = options2;
}
#[cfg(not(feature = "no_object"))]
Expr::Dot(ref mut x, ref mut options2, ..) => {
tail = x.as_mut();
tail_options = options2;
}
_ => break,
}
}
// Since we attach the outer chain to the root chain, we no longer terminate at the end of the
// root chain, so remove the ASTFlags::BREAK flag.
tail_options.remove(ASTFlags::BREAK);
x.lhs = tail.rhs.take(); // remove tail and insert it into head of outer chain
tail.rhs = make_sub(x, x_options, x_pos); // attach outer chain to tail
*expr = make_root(root, root_options, root_pos);
}
impl Engine {
/// Parse a function call.
fn parse_fn_call(
@ -1857,6 +1933,13 @@ impl Engine {
parent_options = ASTFlags::empty();
}
// Optimize chain where the root expression is another chain
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
if matches!(lhs, Expr::Index(ref x, ..) | Expr::Dot(ref x, ..) if matches!(x.lhs, Expr::Index(..) | Expr::Dot(..)))
{
optimize_combo_chain(&mut lhs)
}
// Cache the hash key for namespace-qualified variables
#[cfg(not(feature = "no_module"))]
let namespaced_variable = match lhs {
@ -2890,7 +2973,7 @@ impl Engine {
settings.flags |= ParseSettingFlags::BREAKABLE;
let body = self.parse_block(input, state, lib, settings)?.into();
state.stack.as_deref_mut().unwrap().rewind(prev_stack_len);
state.stack.as_mut().unwrap().rewind(prev_stack_len);
let branch = StmtBlock::NONE;
@ -2971,15 +3054,16 @@ impl Engine {
let (existing, hit_barrier) = state.find_var(&name);
let stack = state.stack.as_deref_mut().unwrap();
let stack = state.stack.as_mut().unwrap();
let existing = if !hit_barrier && existing > 0 {
let offset = stack.len() - existing;
if offset < state.block_stack_len {
match stack.len() - existing {
// Variable has been aliased
#[cfg(not(feature = "no_module"))]
offset if !stack.get_entry_by_index(offset).2.is_empty() => None,
// Defined in parent block
None
} else {
Some(offset)
offset if offset < state.block_stack_len => None,
offset => Some(offset),
}
} else {
None
@ -2993,6 +3077,11 @@ impl Engine {
None
};
#[cfg(not(feature = "no_module"))]
if is_export {
stack.add_alias_by_index(stack.len() - 1, name.clone());
}
let var_def = (Ident { name, pos }, expr, idx).into();
Ok(match access {
@ -3077,18 +3166,25 @@ impl Engine {
let (id, id_pos) = parse_var_name(input)?;
let (alias, alias_pos) = if match_token(input, Token::As).0 {
parse_var_name(input).map(|(name, pos)| (Some(name), pos))?
parse_var_name(input).map(|(name, pos)| (state.get_interned_string(name), pos))?
} else {
(None, Position::NONE)
(state.get_interned_string(""), Position::NONE)
};
let (existing, hit_barrier) = state.find_var(&id);
if !hit_barrier && existing > 0 {
let stack = state.stack.as_mut().unwrap();
stack.add_alias_by_index(stack.len() - existing, alias.clone());
}
let export = (
Ident {
name: state.get_interned_string(id),
pos: id_pos,
},
Ident {
name: state.get_interned_string(alias.as_deref().unwrap_or("")),
name: alias,
pos: alias_pos,
},
);
@ -3137,7 +3233,7 @@ impl Engine {
}
let prev_entry_stack_len = state.block_stack_len;
state.block_stack_len = state.stack.as_deref().map_or(0, Scope::len);
state.block_stack_len = state.stack.as_ref().map_or(0, Scope::len);
#[cfg(not(feature = "no_module"))]
let orig_imports_len = state.imports.as_deref().map_or(0, StaticVec::len);
@ -3580,7 +3676,7 @@ impl Engine {
Expr::Unit(catch_var.pos)
} else {
// Remove the error variable from the stack
state.stack.as_deref_mut().unwrap().pop();
state.stack.as_mut().unwrap().pop();
Expr::Variable(
(None, Namespace::default(), 0, catch_var.name).into(),

View File

@ -472,7 +472,7 @@ static KEYWORDS_LIST: [(&str, Token); 153] = [
("", Token::EOF),
("", Token::EOF),
("", Token::EOF),
(",Token::", Token::Comma),
(",", Token::Comma),
("do", Token::Do),
("", Token::EOF),
("", Token::EOF),

View File

@ -1146,6 +1146,11 @@ impl Dynamic {
)),
}
}
/// Return this [`Dynamic`], replacing it with [`Dynamic::UNIT`].
#[inline(always)]
pub fn take(&mut self) -> Self {
mem::take(self)
}
/// Convert the [`Dynamic`] value into specific type.
///
/// Casting to a [`Dynamic`] just returns as is, but if it contains a shared value,

View File

@ -49,16 +49,19 @@ impl fmt::Debug for FnPtr {
#[cold]
#[inline(never)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_curried() {
self.curry
.iter()
.fold(f.debug_tuple("Fn").field(&self.name), |f, curry| {
f.field(curry)
})
.finish()
} else {
write!(f, "Fn({})", self.fn_name())
let ff = &mut f.debug_tuple("Fn");
ff.field(&self.name);
self.curry.iter().for_each(|curry| {
ff.field(curry);
});
ff.finish()?;
#[cfg(not(feature = "no_function"))]
if let Some(ref fn_def) = self.fn_def {
write!(f, ": {fn_def}")?;
}
Ok(())
}
}
@ -310,7 +313,7 @@ impl FnPtr {
caches,
&mut crate::Scope::new(),
this_ptr,
self.encapsulated_environ(),
self.encapsulated_environ().map(|r| r.as_ref()),
fn_def,
args,
true,
@ -331,8 +334,8 @@ impl FnPtr {
#[inline(always)]
#[must_use]
#[allow(dead_code)]
pub(crate) fn encapsulated_environ(&self) -> Option<&EncapsulatedEnviron> {
self.environ.as_deref()
pub(crate) fn encapsulated_environ(&self) -> Option<&Shared<EncapsulatedEnviron>> {
self.environ.as_ref()
}
/// Set a reference to the [encapsulated environment][EncapsulatedEnviron].
#[inline(always)]
@ -347,8 +350,8 @@ impl FnPtr {
#[cfg(not(feature = "no_function"))]
#[inline(always)]
#[must_use]
pub(crate) fn fn_def(&self) -> Option<&crate::ast::ScriptFnDef> {
self.fn_def.as_deref()
pub(crate) fn fn_def(&self) -> Option<&Shared<crate::ast::ScriptFnDef>> {
self.fn_def.as_ref()
}
/// Set a reference to the linked [`ScriptFnDef`][crate::ast::ScriptFnDef].
#[cfg(not(feature = "no_function"))]

View File

@ -12,7 +12,7 @@ use std::{
};
/// Keep a number of entries inline (since [`Dynamic`] is usually small enough).
const SCOPE_ENTRIES_INLINED: usize = 8;
pub const SCOPE_ENTRIES_INLINED: usize = 8;
/// Type containing information about the current scope. Useful for keeping state between
/// [`Engine`][crate::Engine] evaluation runs.
@ -635,6 +635,24 @@ impl Scope<'_> {
pub fn get(&self, name: &str) -> Option<&Dynamic> {
self.search(name).map(|index| &self.values[index])
}
/// Get a reference to an entry in the [`Scope`] based on the index.
///
/// # Panics
///
/// Panics if the index is out of bounds.
#[inline(always)]
#[must_use]
#[allow(dead_code)]
pub(crate) fn get_entry_by_index(
&mut self,
index: usize,
) -> (&Identifier, &Dynamic, &[ImmutableString]) {
(
&self.names[index],
&self.values[index],
&self.aliases[index],
)
}
/// Remove the last entry in the [`Scope`] by the specified name and return its value.
///
/// If the entry by the specified name is not found, [`None`] is returned.
@ -670,7 +688,7 @@ impl Scope<'_> {
self.values.remove(index).try_cast()
})
}
/// Get a mutable reference to an entry in the [`Scope`].
/// Get a mutable reference to the value of an entry in the [`Scope`].
///
/// If the entry by the specified name is not found, or if it is read-only,
/// [`None`] is returned.
@ -702,14 +720,14 @@ impl Scope<'_> {
AccessMode::ReadOnly => None,
})
}
/// Get a mutable reference to an entry in the [`Scope`] based on the index.
/// Get a mutable reference to the value of an entry in the [`Scope`] based on the index.
///
/// # Panics
///
/// Panics if the index is out of bounds.
#[inline]
#[inline(always)]
pub(crate) fn get_mut_by_index(&mut self, index: usize) -> &mut Dynamic {
self.values.get_mut(index).unwrap()
&mut self.values[index]
}
/// Add an alias to an entry in the [`Scope`].
///
@ -728,12 +746,14 @@ impl Scope<'_> {
/// Add an alias to a variable in the [`Scope`] so that it is exported under that name.
/// This is an advanced API.
///
/// Variable aliases are used, for example, in [`Module::eval_ast_as_new`][crate::Module::eval_ast_as_new]
/// to create a new module with exported variables under different names.
///
/// If the alias is empty, then the variable is exported under its original name.
///
/// Multiple aliases can be added to any variable.
///
/// Only the last variable matching the name (and not other shadowed versions) is aliased by
/// this call.
/// Only the last variable matching the name (and not other shadowed versions) is aliased by this call.
#[cfg(not(feature = "no_module"))]
#[inline]
pub fn set_alias(

View File

@ -175,7 +175,7 @@ fn test_fn_ptr_raw() -> Result<(), Box<EvalAltResult>> {
TypeId::of::<INT>(),
],
move |context, args| {
let fp = std::mem::take(args[1]).cast::<FnPtr>();
let fp = args[1].take().cast::<FnPtr>();
let value = args[2].clone();
let this_ptr = args.get_mut(0).unwrap();

View File

@ -16,8 +16,8 @@ fn test_fn_ptr_curry_call() -> Result<(), Box<EvalAltResult>> {
"call_with_arg",
[TypeId::of::<FnPtr>(), TypeId::of::<INT>()],
|context, args| {
let fn_ptr = std::mem::take(args[0]).cast::<FnPtr>();
fn_ptr.call_raw(&context, None, [std::mem::take(args[1])])
let fn_ptr = args[0].take().cast::<FnPtr>();
fn_ptr.call_raw(&context, None, [args[1].take()])
},
);

View File

@ -251,6 +251,65 @@ fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[test]
fn test_custom_syntax_scope() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.register_custom_syntax(
[
"with", "offset", "(", "$expr$", ",", "$expr$", ")", "$block$",
],
true,
|context, inputs| {
let x = context
.eval_expression_tree(&inputs[0])?
.as_int()
.map_err(|typ| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"integer".to_string(),
typ.to_string(),
inputs[0].position(),
))
})?;
let y = context
.eval_expression_tree(&inputs[1])?
.as_int()
.map_err(|typ| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"integer".to_string(),
typ.to_string(),
inputs[1].position(),
))
})?;
let orig_len = context.scope().len();
context.scope_mut().push_constant("x", x);
context.scope_mut().push_constant("y", y);
let result = context.eval_expression_tree(&inputs[2]);
context.scope_mut().rewind(orig_len);
result
},
)?;
assert_eq!(
engine.eval::<INT>(
"
let y = 1;
let x = 0;
with offset(44, 2) { x - y }
"
)?,
42
);
Ok(())
}
#[test]
fn test_custom_syntax_matrix() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();

View File

@ -5,15 +5,14 @@ use rhai::{Engine, EvalAltResult, Module, OptimizationLevel, Scope, INT};
#[test]
fn test_optimizer() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.set_optimization_level(OptimizationLevel::Full);
engine.set_optimization_level(OptimizationLevel::Simple);
#[cfg(not(feature = "no_function"))]
assert_eq!(
engine.eval::<INT>(
"
fn foo(x) { print(x); return; }
fn foo2(x) { if x > 0 {} return; }
42
const X = 0;
const X = 40 + 2 - 1 + 1;
X
"
)?,
42
@ -202,6 +201,18 @@ fn test_optimizer_full() -> Result<(), Box<EvalAltResult>> {
engine.set_optimization_level(OptimizationLevel::Full);
#[cfg(not(feature = "no_function"))]
assert_eq!(
engine.eval::<INT>(
"
fn foo(x) { print(x); return; }
fn foo2(x) { if x > 0 {} return; }
42
"
)?,
42
);
engine
.register_type_with_name::<TestStruct>("TestStruct")
.register_fn("ts", |n: INT| TestStruct(n))

View File

@ -1,4 +1,4 @@
use rhai::{Dynamic, Engine, EvalAltResult, ParseErrorType, Position, Scope, INT};
use rhai::{Dynamic, Engine, EvalAltResult, Module, ParseErrorType, Position, Scope, INT};
#[test]
fn test_var_scope() -> Result<(), Box<EvalAltResult>> {
@ -93,6 +93,41 @@ fn test_var_scope() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[cfg(not(feature = "no_module"))]
#[test]
fn test_var_scope_alias() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
let mut scope = Scope::new();
scope.push("x", 42 as INT);
scope.set_alias("x", "a");
scope.set_alias("x", "b");
scope.set_alias("x", "y");
scope.push("x", 123 as INT);
scope.set_alias("x", "b");
scope.set_alias("x", "c");
let ast = engine.compile(
"
let x = 999;
export x as a;
export x as c;
let x = 0;
export x as z;
",
)?;
let m = Module::eval_ast_as_new(scope, &ast, &engine)?;
assert_eq!(m.get_var_value::<INT>("a").unwrap(), 999);
assert_eq!(m.get_var_value::<INT>("b").unwrap(), 123);
assert_eq!(m.get_var_value::<INT>("c").unwrap(), 999);
assert_eq!(m.get_var_value::<INT>("y").unwrap(), 42);
assert_eq!(m.get_var_value::<INT>("z").unwrap(), 0);
Ok(())
}
#[test]
fn test_var_is_def() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();

View File

@ -42,7 +42,7 @@ struct keyword;
::, Token::DoubleColon
=>, Token::DoubleArrow
_, Token::Underscore
,, Token::Comma
",", Token::Comma
., Token::Period
?., Token::Elvis
??, Token::DoubleQuestion
@ -94,8 +94,8 @@ catch, Token::Catch
<<, Token::LeftShift
>>, Token::RightShift
^, Token::XOr
%, Token::Modulo
%=, Token::ModuloAssign
"%", Token::Modulo
"%=", Token::ModuloAssign
**, Token::PowerOf
**=, Token::PowerOfAssign
fn, Token::Fn