diff --git a/RELEASES.md b/RELEASES.md index 038d3b2a..8c7a98b7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -17,6 +17,7 @@ Breaking changes New features ------------ +* `const` statements can now take any expression (or none at all) instead of only constant values. * `OptimizationLevel::Simple` now eagerly evaluates built-in binary operators of primary types (if not overloaded). * Added `is_def_var()` to detect if variable is defined, and `is_def_fn()` to detect if script function is defined. * `Dynamic::from(&str)` now constructs a `Dynamic` with a copy of the string as value. diff --git a/doc/src/language/constants.md b/doc/src/language/constants.md index 81ad1ce6..da2a13be 100644 --- a/doc/src/language/constants.md +++ b/doc/src/language/constants.md @@ -15,12 +15,10 @@ print(x * 2); // prints 84 x = 123; // <- syntax error: cannot assign to constant ``` -Unlike variables which need not have initial values (default to [`()`]), -constants must be assigned one, and it must be a [_literal value_](../appendix/literals.md), -not an expression. - ```rust -const x = 40 + 2; // <- syntax error: cannot assign expression to constant +const x; // 'x' is a constant '()' + +const x = 40 + 2; // 'x' is a constant 42 ``` @@ -33,9 +31,7 @@ running with that [`Scope`]. When added to a custom [`Scope`], a constant can hold any value, not just a literal value. It is very useful to have a constant value hold a [custom type], which essentially acts -as a [_singleton_](../patterns/singleton.md). The singleton object can be modified via its -registered API - being a constant only prevents it from being re-assigned or operated upon by Rhai; -mutating it via a Rust function is still allowed. +as a [_singleton_](../patterns/singleton.md). ```rust use rhai::{Engine, Scope}; @@ -58,3 +54,11 @@ engine.consume_with_scope(&mut scope, r" print(MY_NUMBER.value); // prints 42 ")?; ``` + + +Constants Can be Modified, Just Not Reassigned +--------------------------------------------- + +A custom type stored as a constant can be modified via its registered API - +being a constant only prevents it from being re-assigned or operated upon by Rhai; +mutating it via a Rust function is still allowed. diff --git a/src/engine.rs b/src/engine.rs index 156551a9..06bd9c47 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1800,19 +1800,19 @@ impl Engine { } // Const statement - Stmt::Const(x) if x.1.is_constant() => { + Stmt::Const(x) => { let ((var_name, _), expr, _) = x.as_ref(); - let val = self - .eval_expr(scope, mods, state, lib, this_ptr, &expr, level)? - .flatten(); + let val = if let Some(expr) = expr { self + .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .flatten() + } else { + ().into() + }; let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state); scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true); Ok(Default::default()) } - // Const expression not constant - Stmt::Const(_) => unreachable!(), - // Import statement #[cfg(not(feature = "no_module"))] Stmt::Import(x) => { diff --git a/src/error.rs b/src/error.rs index 4d39e76e..1cdfb7aa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -100,8 +100,6 @@ pub enum ParseErrorType { /// /// Never appears under the `no_object` feature. DuplicatedProperty(String), - /// Invalid expression assigned to constant. Wrapped value is the name of the constant. - ForbiddenConstantExpr(String), /// Missing a property name for custom types and maps. /// /// Never appears under the `no_object` feature. @@ -174,7 +172,6 @@ impl ParseErrorType { Self::MalformedInExpr(_) => "Invalid 'in' expression", Self::MalformedCapture(_) => "Invalid capturing", Self::DuplicatedProperty(_) => "Duplicated property in object map literal", - Self::ForbiddenConstantExpr(_) => "Expecting a constant", Self::PropertyExpected => "Expecting name of a property", Self::VariableExpected => "Expecting name of a variable", Self::Reserved(_) => "Invalid use of reserved keyword", @@ -201,9 +198,6 @@ impl fmt::Display for ParseErrorType { Self::BadInput(s) | ParseErrorType::MalformedCallExpr(s) => { f.write_str(if s.is_empty() { self.desc() } else { s }) } - Self::ForbiddenConstantExpr(s) => { - write!(f, "Expecting a constant to assign to '{}'", s) - } Self::UnknownOperator(s) => write!(f, "{}: '{}'", self.desc(), s), Self::MalformedIndexExpr(s) | Self::MalformedInExpr(s) | Self::MalformedCapture(s) => { diff --git a/src/module/mod.rs b/src/module/mod.rs index f1fc5738..d9c1a356 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1285,6 +1285,7 @@ impl Module { } /// Get an iterator to the functions in the module. + #[cfg(not(feature = "no_optimize"))] #[inline(always)] pub(crate) fn iter_fn(&self) -> impl Iterator { self.functions.values() diff --git a/src/optimize.rs b/src/optimize.rs index 746acc9a..e616799a 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -3,15 +3,15 @@ use crate::any::Dynamic; use crate::calc_fn_hash; use crate::engine::{ - Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_IS_DEF_FN, KEYWORD_IS_DEF_VAR, + Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_IS_DEF_FN, KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_TYPE_OF, }; use crate::fn_call::run_builtin_binary_op; -use crate::fn_native::FnPtr; use crate::module::Module; use crate::parser::{map_dynamic_to_expr, Expr, ScriptFnDef, Stmt, AST}; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; use crate::utils::StaticVec; +use crate::token::is_valid_identifier; #[cfg(not(feature = "no_function"))] use crate::parser::ReturnType; @@ -21,7 +21,6 @@ use crate::parser::CustomExpr; use crate::stdlib::{ boxed::Box, - convert::TryFrom, iter::empty, string::{String, ToString}, vec, @@ -282,12 +281,24 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { let mut result: Vec<_> = x.0.into_iter() .map(|stmt| match stmt { - // Add constant into the state - Stmt::Const(v) => { - let ((name, pos), expr, _) = *v; - state.push_constant(&name, expr); - state.set_dirty(); - Stmt::Noop(pos) // No need to keep constants + // Add constant literals into the state + Stmt::Const(mut v) => { + if let Some(expr) = v.1 { + let expr = optimize_expr(expr, state); + + if expr.is_literal() { + state.set_dirty(); + state.push_constant(&v.0.0, expr); + Stmt::Noop(pos) // No need to keep constants + } else { + v.1 = Some(expr); + Stmt::Const(v) + } + } else { + state.set_dirty(); + state.push_constant(&v.0.0, Expr::Unit(v.0.1)); + Stmt::Noop(pos) // No need to keep constants + } } // Optimize the statement _ => optimize_stmt(stmt, state, preserve_result), @@ -310,8 +321,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { while let Some(expr) = result.pop() { match expr { - Stmt::Let(x) if x.1.is_none() => removed = true, - Stmt::Let(x) if x.1.is_some() => removed = x.1.unwrap().is_pure(), + Stmt::Let(x) => removed = x.1.as_ref().map(Expr::is_pure).unwrap_or(true), #[cfg(not(feature = "no_module"))] Stmt::Import(x) => removed = x.0.is_pure(), _ => { @@ -345,9 +355,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { } match stmt { - Stmt::ReturnWithVal(_) | Stmt::Break(_) => { - dead_code = true; - } + Stmt::ReturnWithVal(_) | Stmt::Break(_) => dead_code = true, _ => (), } @@ -569,30 +577,13 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { Expr::FnCall(x) } - // Fn("...") - Expr::FnCall(x) - if x.1.is_none() - && (x.0).0 == KEYWORD_FN_PTR - && x.3.len() == 1 - && matches!(x.3[0], Expr::StringConstant(_)) - => { - if let Expr::StringConstant(s) = &x.3[0] { - if let Ok(fn_ptr) = FnPtr::try_from(s.0.as_str()) { - Expr::FnPointer(Box::new((fn_ptr.take_data().0, s.1))) - } else { - Expr::FnCall(x) - } - } else { - unreachable!() - } - } - - // Call built-in functions + // Call built-in operators Expr::FnCall(mut x) if x.1.is_none() // Non-qualified && state.optimization_level == OptimizationLevel::Simple // simple optimizations && x.3.len() == 2 // binary call && x.3.iter().all(Expr::is_constant) // all arguments are constants + && !is_valid_identifier(x.0.0.chars()) // cannot be scripted => { let ((name, _, _, pos), _, _, args, _) = x.as_mut(); @@ -733,12 +724,28 @@ fn optimize( .into_iter() .enumerate() .map(|(i, stmt)| { - match &stmt { - Stmt::Const(v) => { + match stmt { + Stmt::Const(mut v) => { // Load constants - let ((name, _), expr, _) = v.as_ref(); - state.push_constant(&name, expr.clone()); - stmt // Keep it in the global scope + if let Some(expr) = v.1 { + let expr = optimize_expr(expr, &mut state); + + if expr.is_literal() { + state.push_constant(&v.0.0, expr.clone()); + } + + v.1 = if expr.is_unit() { + state.set_dirty(); + None + } else { + Some(expr) + }; + } else { + state.push_constant(&v.0.0, Expr::Unit(v.0.1)); + } + + // Keep it in the global scope + Stmt::Const(v) } _ => { // Keep all variable declarations at this level diff --git a/src/parser.rs b/src/parser.rs index 863901aa..04699a3f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -740,7 +740,7 @@ pub enum Stmt { /// let id = expr Let(Box<((String, Position), Option, Position)>), /// const id = expr - Const(Box<((String, Position), Expr, Position)>), + Const(Box<((String, Position), Option, Position)>), /// { stmt; ... } Block(Box<(StaticVec, Position)>), /// expr @@ -1164,6 +1164,48 @@ impl Expr { } } + /// Is the expression the unit `()` literal? + #[inline(always)] + pub fn is_unit(&self) -> bool { + match self { + Self::Unit(_) => true, + _ => false, + } + } + + /// Is the expression a simple constant literal? + pub fn is_literal(&self) -> bool { + match self { + Self::Expr(x) => x.is_literal(), + + #[cfg(not(feature = "no_float"))] + Self::FloatConstant(_) => true, + + Self::IntegerConstant(_) + | Self::CharConstant(_) + | Self::StringConstant(_) + | Self::FnPointer(_) + | Self::True(_) + | Self::False(_) + | Self::Unit(_) => true, + + // An array literal is literal if all items are literals + Self::Array(x) => x.0.iter().all(Self::is_literal), + + // An map literal is literal if all items are literals + Self::Map(x) => x.0.iter().map(|(_, expr)| expr).all(Self::is_literal), + + // Check in expression + Self::In(x) => match (&x.0, &x.1) { + (Self::StringConstant(_), Self::StringConstant(_)) + | (Self::CharConstant(_), Self::StringConstant(_)) => true, + _ => false, + }, + + _ => false, + } + } + /// Is the expression a constant? pub fn is_constant(&self) -> bool { match self { @@ -2843,45 +2885,23 @@ fn parse_let( }; // let name = ... - if match_token(input, Token::Equals)? { + let init_value = if match_token(input, Token::Equals)? { // let name = expr - let init_value = parse_expr(input, state, lib, settings.level_up())?; - - match var_type { - // let name = expr - ScopeEntryType::Normal => { - state.stack.push((name.clone(), ScopeEntryType::Normal)); - Ok(Stmt::Let(Box::new(( - (name, pos), - Some(init_value), - token_pos, - )))) - } - // const name = { expr:constant } - ScopeEntryType::Constant if init_value.is_constant() => { - state.stack.push((name.clone(), ScopeEntryType::Constant)); - Ok(Stmt::Const(Box::new(((name, pos), init_value, token_pos)))) - } - // const name = expr: error - ScopeEntryType::Constant => { - Err(PERR::ForbiddenConstantExpr(name).into_err(init_value.position())) - } - } + Some(parse_expr(input, state, lib, settings.level_up())?) } else { - // let name - match var_type { - ScopeEntryType::Normal => { - state.stack.push((name.clone(), ScopeEntryType::Normal)); - Ok(Stmt::Let(Box::new(((name, pos), None, token_pos)))) - } - ScopeEntryType::Constant => { - state.stack.push((name.clone(), ScopeEntryType::Constant)); - Ok(Stmt::Const(Box::new(( - (name, pos), - Expr::Unit(pos), - token_pos, - )))) - } + None + }; + + match var_type { + // let name = expr + ScopeEntryType::Normal => { + state.stack.push((name.clone(), ScopeEntryType::Normal)); + Ok(Stmt::Let(Box::new(((name, pos), init_value, token_pos)))) + } + // const name = { expr:constant } + ScopeEntryType::Constant => { + state.stack.push((name.clone(), ScopeEntryType::Constant)); + Ok(Stmt::Const(Box::new(((name, pos), init_value, token_pos)))) } } }