Allow expressions in constants.

This commit is contained in:
Stephen Chung 2020-10-09 11:15:25 +08:00
parent d511aac7a4
commit 7ede299aae
7 changed files with 124 additions and 97 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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()

View File

@ -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

View File

@ -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,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))))
}
}
}