Allow expressions in constants.
This commit is contained in:
parent
d511aac7a4
commit
7ede299aae
@ -17,6 +17,7 @@ Breaking changes
|
|||||||
New features
|
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).
|
* `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.
|
* 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.
|
* `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
|
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
|
```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.
|
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
|
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
|
as a [_singleton_](../patterns/singleton.md).
|
||||||
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.
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use rhai::{Engine, Scope};
|
use rhai::{Engine, Scope};
|
||||||
@ -58,3 +54,11 @@ engine.consume_with_scope(&mut scope, r"
|
|||||||
print(MY_NUMBER.value); // prints 42
|
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
|
// Const statement
|
||||||
Stmt::Const(x) if x.1.is_constant() => {
|
Stmt::Const(x) => {
|
||||||
let ((var_name, _), expr, _) = x.as_ref();
|
let ((var_name, _), expr, _) = x.as_ref();
|
||||||
let val = self
|
let val = if let Some(expr) = expr { self
|
||||||
.eval_expr(scope, mods, state, lib, this_ptr, &expr, level)?
|
.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?
|
||||||
.flatten();
|
.flatten()
|
||||||
|
} else {
|
||||||
|
().into()
|
||||||
|
};
|
||||||
let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state);
|
let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state);
|
||||||
scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true);
|
scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true);
|
||||||
Ok(Default::default())
|
Ok(Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Const expression not constant
|
|
||||||
Stmt::Const(_) => unreachable!(),
|
|
||||||
|
|
||||||
// Import statement
|
// Import statement
|
||||||
#[cfg(not(feature = "no_module"))]
|
#[cfg(not(feature = "no_module"))]
|
||||||
Stmt::Import(x) => {
|
Stmt::Import(x) => {
|
||||||
|
@ -100,8 +100,6 @@ pub enum ParseErrorType {
|
|||||||
///
|
///
|
||||||
/// Never appears under the `no_object` feature.
|
/// Never appears under the `no_object` feature.
|
||||||
DuplicatedProperty(String),
|
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.
|
/// Missing a property name for custom types and maps.
|
||||||
///
|
///
|
||||||
/// Never appears under the `no_object` feature.
|
/// Never appears under the `no_object` feature.
|
||||||
@ -174,7 +172,6 @@ impl ParseErrorType {
|
|||||||
Self::MalformedInExpr(_) => "Invalid 'in' expression",
|
Self::MalformedInExpr(_) => "Invalid 'in' expression",
|
||||||
Self::MalformedCapture(_) => "Invalid capturing",
|
Self::MalformedCapture(_) => "Invalid capturing",
|
||||||
Self::DuplicatedProperty(_) => "Duplicated property in object map literal",
|
Self::DuplicatedProperty(_) => "Duplicated property in object map literal",
|
||||||
Self::ForbiddenConstantExpr(_) => "Expecting a constant",
|
|
||||||
Self::PropertyExpected => "Expecting name of a property",
|
Self::PropertyExpected => "Expecting name of a property",
|
||||||
Self::VariableExpected => "Expecting name of a variable",
|
Self::VariableExpected => "Expecting name of a variable",
|
||||||
Self::Reserved(_) => "Invalid use of reserved keyword",
|
Self::Reserved(_) => "Invalid use of reserved keyword",
|
||||||
@ -201,9 +198,6 @@ impl fmt::Display for ParseErrorType {
|
|||||||
Self::BadInput(s) | ParseErrorType::MalformedCallExpr(s) => {
|
Self::BadInput(s) | ParseErrorType::MalformedCallExpr(s) => {
|
||||||
f.write_str(if s.is_empty() { self.desc() } else { 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::UnknownOperator(s) => write!(f, "{}: '{}'", self.desc(), s),
|
||||||
|
|
||||||
Self::MalformedIndexExpr(s) | Self::MalformedInExpr(s) | Self::MalformedCapture(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.
|
/// Get an iterator to the functions in the module.
|
||||||
|
#[cfg(not(feature = "no_optimize"))]
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub(crate) fn iter_fn(&self) -> impl Iterator<Item = &FuncInfo> {
|
pub(crate) fn iter_fn(&self) -> impl Iterator<Item = &FuncInfo> {
|
||||||
self.functions.values()
|
self.functions.values()
|
||||||
|
@ -3,15 +3,15 @@
|
|||||||
use crate::any::Dynamic;
|
use crate::any::Dynamic;
|
||||||
use crate::calc_fn_hash;
|
use crate::calc_fn_hash;
|
||||||
use crate::engine::{
|
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,
|
KEYWORD_PRINT, KEYWORD_TYPE_OF,
|
||||||
};
|
};
|
||||||
use crate::fn_call::run_builtin_binary_op;
|
use crate::fn_call::run_builtin_binary_op;
|
||||||
use crate::fn_native::FnPtr;
|
|
||||||
use crate::module::Module;
|
use crate::module::Module;
|
||||||
use crate::parser::{map_dynamic_to_expr, Expr, ScriptFnDef, Stmt, AST};
|
use crate::parser::{map_dynamic_to_expr, Expr, ScriptFnDef, Stmt, AST};
|
||||||
use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope};
|
use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope};
|
||||||
use crate::utils::StaticVec;
|
use crate::utils::StaticVec;
|
||||||
|
use crate::token::is_valid_identifier;
|
||||||
|
|
||||||
#[cfg(not(feature = "no_function"))]
|
#[cfg(not(feature = "no_function"))]
|
||||||
use crate::parser::ReturnType;
|
use crate::parser::ReturnType;
|
||||||
@ -21,7 +21,6 @@ use crate::parser::CustomExpr;
|
|||||||
|
|
||||||
use crate::stdlib::{
|
use crate::stdlib::{
|
||||||
boxed::Box,
|
boxed::Box,
|
||||||
convert::TryFrom,
|
|
||||||
iter::empty,
|
iter::empty,
|
||||||
string::{String, ToString},
|
string::{String, ToString},
|
||||||
vec,
|
vec,
|
||||||
@ -282,12 +281,24 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt {
|
|||||||
let mut result: Vec<_> =
|
let mut result: Vec<_> =
|
||||||
x.0.into_iter()
|
x.0.into_iter()
|
||||||
.map(|stmt| match stmt {
|
.map(|stmt| match stmt {
|
||||||
// Add constant into the state
|
// Add constant literals into the state
|
||||||
Stmt::Const(v) => {
|
Stmt::Const(mut v) => {
|
||||||
let ((name, pos), expr, _) = *v;
|
if let Some(expr) = v.1 {
|
||||||
state.push_constant(&name, expr);
|
let expr = optimize_expr(expr, state);
|
||||||
state.set_dirty();
|
|
||||||
Stmt::Noop(pos) // No need to keep constants
|
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 the statement
|
||||||
_ => optimize_stmt(stmt, state, preserve_result),
|
_ => 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() {
|
while let Some(expr) = result.pop() {
|
||||||
match expr {
|
match expr {
|
||||||
Stmt::Let(x) if x.1.is_none() => removed = true,
|
Stmt::Let(x) => removed = x.1.as_ref().map(Expr::is_pure).unwrap_or(true),
|
||||||
Stmt::Let(x) if x.1.is_some() => removed = x.1.unwrap().is_pure(),
|
|
||||||
#[cfg(not(feature = "no_module"))]
|
#[cfg(not(feature = "no_module"))]
|
||||||
Stmt::Import(x) => removed = x.0.is_pure(),
|
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 {
|
match stmt {
|
||||||
Stmt::ReturnWithVal(_) | Stmt::Break(_) => {
|
Stmt::ReturnWithVal(_) | Stmt::Break(_) => dead_code = true,
|
||||||
dead_code = true;
|
|
||||||
}
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -569,30 +577,13 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
|
|||||||
Expr::FnCall(x)
|
Expr::FnCall(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fn("...")
|
// Call built-in operators
|
||||||
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
|
|
||||||
Expr::FnCall(mut x)
|
Expr::FnCall(mut x)
|
||||||
if x.1.is_none() // Non-qualified
|
if x.1.is_none() // Non-qualified
|
||||||
&& state.optimization_level == OptimizationLevel::Simple // simple optimizations
|
&& state.optimization_level == OptimizationLevel::Simple // simple optimizations
|
||||||
&& x.3.len() == 2 // binary call
|
&& x.3.len() == 2 // binary call
|
||||||
&& x.3.iter().all(Expr::is_constant) // all arguments are constants
|
&& 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();
|
let ((name, _, _, pos), _, _, args, _) = x.as_mut();
|
||||||
|
|
||||||
@ -733,12 +724,28 @@ fn optimize(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, stmt)| {
|
.map(|(i, stmt)| {
|
||||||
match &stmt {
|
match stmt {
|
||||||
Stmt::Const(v) => {
|
Stmt::Const(mut v) => {
|
||||||
// Load constants
|
// Load constants
|
||||||
let ((name, _), expr, _) = v.as_ref();
|
if let Some(expr) = v.1 {
|
||||||
state.push_constant(&name, expr.clone());
|
let expr = optimize_expr(expr, &mut state);
|
||||||
stmt // Keep it in the global scope
|
|
||||||
|
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
|
// Keep all variable declarations at this level
|
||||||
|
@ -740,7 +740,7 @@ pub enum Stmt {
|
|||||||
/// let id = expr
|
/// let id = expr
|
||||||
Let(Box<((String, Position), Option<Expr>, Position)>),
|
Let(Box<((String, Position), Option<Expr>, Position)>),
|
||||||
/// const id = expr
|
/// const id = expr
|
||||||
Const(Box<((String, Position), Expr, Position)>),
|
Const(Box<((String, Position), Option<Expr>, Position)>),
|
||||||
/// { stmt; ... }
|
/// { stmt; ... }
|
||||||
Block(Box<(StaticVec<Stmt>, Position)>),
|
Block(Box<(StaticVec<Stmt>, Position)>),
|
||||||
/// expr
|
/// 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?
|
/// Is the expression a constant?
|
||||||
pub fn is_constant(&self) -> bool {
|
pub fn is_constant(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
@ -2843,45 +2885,23 @@ fn parse_let(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// let name = ...
|
// let name = ...
|
||||||
if match_token(input, Token::Equals)? {
|
let init_value = if match_token(input, Token::Equals)? {
|
||||||
// let name = expr
|
// let name = expr
|
||||||
let init_value = parse_expr(input, state, lib, settings.level_up())?;
|
Some(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()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// let name
|
None
|
||||||
match var_type {
|
};
|
||||||
ScopeEntryType::Normal => {
|
|
||||||
state.stack.push((name.clone(), ScopeEntryType::Normal));
|
match var_type {
|
||||||
Ok(Stmt::Let(Box::new(((name, pos), None, token_pos))))
|
// let name = expr
|
||||||
}
|
ScopeEntryType::Normal => {
|
||||||
ScopeEntryType::Constant => {
|
state.stack.push((name.clone(), ScopeEntryType::Normal));
|
||||||
state.stack.push((name.clone(), ScopeEntryType::Constant));
|
Ok(Stmt::Let(Box::new(((name, pos), init_value, token_pos))))
|
||||||
Ok(Stmt::Const(Box::new((
|
}
|
||||||
(name, pos),
|
// const name = { expr:constant }
|
||||||
Expr::Unit(pos),
|
ScopeEntryType::Constant => {
|
||||||
token_pos,
|
state.stack.push((name.clone(), ScopeEntryType::Constant));
|
||||||
))))
|
Ok(Stmt::Const(Box::new(((name, pos), init_value, token_pos))))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user