Allow expressions in constants.
This commit is contained in:
parent
d511aac7a4
commit
7ede299aae
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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<Item = &FuncInfo> {
|
||||
self.functions.values()
|
||||
|
@ -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);
|
||||
// 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
|
||||
|
@ -740,7 +740,7 @@ pub enum Stmt {
|
||||
/// let id = expr
|
||||
Let(Box<((String, Position), Option<Expr>, Position)>),
|
||||
/// const id = expr
|
||||
Const(Box<((String, Position), Expr, Position)>),
|
||||
Const(Box<((String, Position), Option<Expr>, Position)>),
|
||||
/// { stmt; ... }
|
||||
Block(Box<(StaticVec<Stmt>, 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,46 +2885,24 @@ 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())?;
|
||||
Some(parse_expr(input, state, lib, settings.level_up())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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,
|
||||
))))
|
||||
Ok(Stmt::Let(Box::new(((name, pos), init_value, token_pos))))
|
||||
}
|
||||
// const name = { expr:constant }
|
||||
ScopeEntryType::Constant if init_value.is_constant() => {
|
||||
ScopeEntryType::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()))
|
||||
}
|
||||
}
|
||||
} 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,
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user