Merge pull request #114 from schungx/master

Add evaluate expressions.
This commit is contained in:
Stephen Chung 2020-03-23 09:22:36 +08:00 committed by GitHub
commit 8cb8b89474
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 491 additions and 139 deletions

View File

@ -7,13 +7,14 @@ description = "Embedded scripting for Rust"
homepage = "https://github.com/jonathandturner/rhai"
repository = "https://github.com/jonathandturner/rhai"
readme = "README.md"
license = "MIT/Apache-2.0"
license = "MIT OR Apache-2.0"
include = [
"**/*.rs",
"scripts/*.rhai",
"Cargo.toml"
]
keywords = [ "scripting" ]
categories = [ "no-std", "embedded", "parser-implementations" ]
[dependencies]
num-traits = "0.2.11"

View File

@ -1,6 +1,13 @@
Rhai - Embedded Scripting for Rust
=================================
![GitHub last commit](https://img.shields.io/github/last-commit/jonathandturner/rhai)
[![Travis (.org)](https://img.shields.io/travis/jonathandturner/rhai)](http://travis-ci.org/jonathandturner/rhai)
[![license](https://img.shields.io/github/license/jonathandturner/rhai)](https://github.com/license/jonathandturner/rhai)
[![crates.io](https://img.shields.io/crates/v/rhai.svg)](https::/crates.io/crates/rhai/)
![crates.io](https://img.shields.io/crates/d/rhai)
[![API Docs](https://docs.rs/rhai/badge.svg)](https://docs.rs/rhai/)
Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way to add scripting to any application.
Rhai's current features set:
@ -176,10 +183,6 @@ let result = engine.eval_file::<i64>("hello_world.rhai".into())?; // 'eval
To repeatedly evaluate a script, _compile_ it first into an AST (abstract syntax tree) form:
```rust
use rhai::Engine;
let mut engine = Engine::new();
// Compile to an AST and store it for later evaluations
let ast = engine.compile("40 + 2")?;
@ -193,20 +196,12 @@ for _ in 0..42 {
Compiling a script file is also supported:
```rust
use rhai::Engine;
let mut engine = Engine::new();
let ast = engine.compile_file("hello_world.rhai".into())?;
```
Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust - via `call_fn`:
```rust
use rhai::Engine;
let mut engine = Engine::new();
// Define a function in a script and load it into the Engine.
// Pass true to 'retain_functions' otherwise these functions will be cleared at the end of consume()
engine.consume(true,
@ -234,6 +229,26 @@ let result: i64 = engine.call_fn("hello", 123_i64)?
// ^^^^^^^ calls 'hello' with one parameter (no need for tuple)
```
Evaluate expressions only
-------------------------
Sometimes a use case does not require a full-blown scripting _language_, but only needs to evaluate _expressions_.
In these cases, use the `compile_expression` and `eval_expression` methods or their `_with_scope` variants.
```rust
let result = engine.eval_expression::<i64>("2 + (10 + 10) * 2")?;
```
When evaluation _expressions_, no control-flow statement (e.g. `if`, `while`, `for`) is not supported and will be
parse errors when encountered - not even variable assignments.
```rust
// The following are all syntax errors because the script is not an expression.
engine.eval_expression::<()>("x = 42")?;
let ast = engine.compile_expression("let x = 42")?;
let result = engine.eval_expression_with_scope::<i64>(&mut scope, "if x { 42 } else { 123 }")?;
```
Values and types
----------------
@ -1464,9 +1479,9 @@ Function volatility considerations
Even if a custom function does not mutate state nor cause side effects, it may still be _volatile_, i.e. it _depends_ on the external
environment and not _pure_. A perfect example is a function that gets the current time - obviously each run will return a different value!
The optimizer, when using [`OptimizationLevel::Full`], _assumes_ that all functions are _pure_, so when it finds constant arguments.
This may cause the script to behave differently from the intended semantics because essentially the result of each function call will
always be the same value.
The optimizer, when using [`OptimizationLevel::Full`], _assumes_ that all functions are _pure_, so when it finds constant arguments
it will eagerly run execute the function call. This causes the script to behave differently from the intended semantics because
essentially the result of the function call will always be the same value.
Therefore, **avoid using [`OptimizationLevel::Full`]** if you intend to register non-_pure_ custom types and/or functions.

View File

@ -13,29 +13,26 @@ fn print_error(input: &str, err: EvalAltResult) {
iter::repeat(pad).take(len).collect::<String>()
}
let lines: Vec<_> = input.split("\n").collect();
let lines: Vec<_> = input.trim().split("\n").collect();
let line_no = if lines.len() > 1 {
match err.position() {
p if p.is_none() => "".to_string(),
p if p.is_eof() => format!("{}: ", lines.len()),
p => format!("{}: ", p.line().unwrap()),
}
} else {
"".to_string()
};
// Print error
let pos_text = format!(" ({})", err.position());
match err.position() {
p if p.is_eof() => {
// EOF
let last = lines[lines.len() - 2];
println!("{}", last);
println!("{}^ {}", padding(" ", last.len() - 1), err);
}
p if p.is_none() => {
// No position
println!("{}", err);
}
p => {
// Specific position
let pos_text = format!(
" (line {}, position {})",
p.line().unwrap(),
p.position().unwrap()
);
println!("{}", lines[p.line().unwrap() - 1]);
let last = lines[lines.len() - 1];
println!("{}{}", line_no, last);
let err_text = match err {
EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => {
@ -46,37 +43,103 @@ fn print_error(input: &str, err: EvalAltResult) {
println!(
"{}^ {}",
padding(" ", p.position().unwrap() - 1),
padding(" ", line_no.len() + last.len() - 1),
err_text.replace(&pos_text, "")
);
}
p if p.is_none() => {
// No position
println!("{}", err);
}
p => {
// Specific position
println!("{}{}", line_no, lines[p.line().unwrap() - 1]);
let err_text = match err {
EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => {
format!("Runtime error: {}", err)
}
_ => err.to_string(),
};
println!(
"{}^ {}",
padding(" ", line_no.len() + p.position().unwrap() - 1),
err_text.replace(&pos_text, "")
);
}
}
}
fn print_help() {
println!("help => print this help");
println!("quit, exit => quit");
println!("ast => print the last AST");
println!("astu => print the last raw, un-optimized AST");
println!(r"end a line with '\' to continue to the next line.");
println!();
}
fn main() {
let mut engine = Engine::new();
#[cfg(not(feature = "no_optimize"))]
engine.set_optimization_level(OptimizationLevel::Full);
engine.set_optimization_level(OptimizationLevel::None);
let mut scope = Scope::new();
let mut input = String::new();
let mut ast_u: Option<AST> = None;
let mut ast: Option<AST> = None;
println!("Rhai REPL tool");
println!("==============");
print_help();
loop {
print!("rhai> ");
stdout().flush().expect("couldn't flush stdout");
input.clear();
if let Err(err) = stdin().read_line(&mut input) {
println!("input error: {}", err);
loop {
if let Err(err) = stdin().read_line(&mut input) {
panic!("input error: {}", err);
}
let line = input.as_str().trim_end();
// Allow line continuation
if line.ends_with('\\') {
let len = line.len();
input.truncate(len - 1);
input.push('\n');
} else {
break;
}
print!("> ");
stdout().flush().expect("couldn't flush stdout");
}
let script = input.trim();
// Implement standard commands
match input.as_str().trim() {
match script {
"help" => {
print_help();
continue;
}
"exit" | "quit" => break, // quit
"astu" => {
if matches!(&ast_u, Some(_)) {
// print the last un-optimized AST
println!("{:#?}", ast_u.as_ref().unwrap());
} else {
println!("()");
}
continue;
}
"ast" => {
if matches!(&ast, Some(_)) {
// print the last AST
@ -90,10 +153,23 @@ fn main() {
}
if let Err(err) = engine
.compile_with_scope(&scope, &input)
.compile_with_scope(&scope, &script)
.map_err(EvalAltResult::ErrorParsing)
.and_then(|r| {
ast = Some(r);
ast_u = Some(r);
#[cfg(not(feature = "no_optimize"))]
{
engine.set_optimization_level(OptimizationLevel::Full);
ast = Some(engine.optimize_ast(&mut scope, ast_u.as_ref().unwrap()));
engine.set_optimization_level(OptimizationLevel::None);
}
#[cfg(feature = "no_optimize")]
{
ast = ast_u.clone();
}
engine
.consume_ast_with_scope(&mut scope, true, ast.as_ref().unwrap())
.or_else(|err| match err {

View File

@ -5,7 +5,7 @@ use crate::call::FuncArgs;
use crate::engine::{Engine, FnAny, FnSpec, FUNC_GETTER, FUNC_SETTER};
use crate::error::ParseError;
use crate::fn_register::RegisterFn;
use crate::parser::{lex, parse, FnDef, Position, AST};
use crate::parser::{lex, parse, parse_global_expr, FnDef, Position, AST};
use crate::result::EvalAltResult;
use crate::scope::Scope;
@ -50,8 +50,8 @@ impl<'e> Engine<'e> {
/// }
///
/// impl TestStruct {
/// fn new() -> Self { TestStruct { field: 1 } }
/// fn update(&mut self) { self.field += 41; }
/// fn new() -> Self { TestStruct { field: 1 } }
/// fn update(&mut self, offset: i64) { self.field += offset; }
/// }
///
/// # fn main() -> Result<(), rhai::EvalAltResult> {
@ -64,11 +64,11 @@ impl<'e> Engine<'e> {
///
/// engine.register_fn("new_ts", TestStruct::new);
///
/// // Register method on the type.
/// // Use `register_fn` to register methods on the type.
/// engine.register_fn("update", TestStruct::update);
///
/// assert_eq!(
/// engine.eval::<TestStruct>("let x = new_ts(); x.update(); x")?.field,
/// engine.eval::<TestStruct>("let x = new_ts(); x.update(41); x")?.field,
/// 42
/// );
/// # Ok(())
@ -138,6 +138,8 @@ impl<'e> Engine<'e> {
/// Register a getter function for a member of a registered type with the `Engine`.
///
/// The function signature must start with `&mut self` and not `&self`.
///
/// # Example
///
/// ```
@ -148,6 +150,8 @@ impl<'e> Engine<'e> {
///
/// impl TestStruct {
/// fn new() -> Self { TestStruct { field: 1 } }
///
/// // Even a getter must start with `&mut self` and not `&self`.
/// fn get_field(&mut self) -> i64 { self.field }
/// }
///
@ -222,6 +226,8 @@ impl<'e> Engine<'e> {
/// Shorthand for registering both getter and setter functions
/// of a registered type with the `Engine`.
///
/// All function signatures must start with `&mut self` and not `&self`.
///
/// # Example
///
/// ```
@ -231,8 +237,9 @@ impl<'e> Engine<'e> {
/// }
///
/// impl TestStruct {
/// fn new() -> Self { TestStruct { field: 1 } }
/// fn new() -> Self { TestStruct { field: 1 } }
/// fn get_field(&mut self) -> i64 { self.field }
/// // Even a getter must start with `&mut self` and not `&self`.
/// fn set_field(&mut self, new_val: i64) { self.field = new_val; }
/// }
///
@ -409,6 +416,78 @@ impl<'e> Engine<'e> {
})
}
/// Compile a string containing an expression into an `AST`,
/// which can be used later for evaluation.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// use rhai::Engine;
///
/// let mut engine = Engine::new();
///
/// // Compile a script to an AST and store it for later evaluation
/// let ast = engine.compile_expression("40 + 2")?;
///
/// for _ in 0..42 {
/// assert_eq!(engine.eval_ast::<i64>(&ast)?, 42);
/// }
/// # Ok(())
/// # }
/// ```
pub fn compile_expression(&self, input: &str) -> Result<AST, ParseError> {
self.compile_expression_with_scope(&Scope::new(), input)
}
/// Compile a string containing an expression into an `AST` using own scope,
/// which can be used later for evaluation.
///
/// The scope is useful for passing constants into the script for optimization
/// when using `OptimizationLevel::Full`.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// # #[cfg(not(feature = "no_optimize"))]
/// # {
/// use rhai::{Engine, Scope, OptimizationLevel};
///
/// let mut engine = Engine::new();
///
/// // Set optimization level to 'Full' so the Engine can fold constants
/// // into function calls and operators.
/// engine.set_optimization_level(OptimizationLevel::Full);
///
/// // Create initialized scope
/// let mut scope = Scope::new();
/// scope.push_constant("x", 10_i64); // 'x' is a constant
///
/// // Compile a script to an AST and store it for later evaluation.
/// // Notice that `Full` optimization is on, so constants are folded
/// // into function calls and operators.
/// let ast = engine.compile_expression_with_scope(&mut scope,
/// "2 + (x + x) * 2" // all 'x' are replaced with 10
/// )?;
///
/// // Normally this would have failed because no scope is passed into the 'eval_ast'
/// // call and so the variable 'x' does not exist. Here, it passes because the script
/// // has been optimized and all references to 'x' are already gone.
/// assert_eq!(engine.eval_ast::<i64>(&ast)?, 42);
/// # }
/// # Ok(())
/// # }
/// ```
pub fn compile_expression_with_scope(
&self,
scope: &Scope,
input: &str,
) -> Result<AST, ParseError> {
let tokens_stream = lex(input);
parse_global_expr(&mut tokens_stream.peekable(), self, scope)
}
/// Evaluate a script file.
///
/// # Example
@ -507,6 +586,55 @@ impl<'e> Engine<'e> {
self.eval_ast_with_scope(scope, &ast)
}
/// Evaluate a string containing an expression.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// use rhai::Engine;
///
/// let mut engine = Engine::new();
///
/// assert_eq!(engine.eval_expression::<i64>("40 + 2")?, 42);
/// # Ok(())
/// # }
/// ```
pub fn eval_expression<T: Any + Clone>(&mut self, input: &str) -> Result<T, EvalAltResult> {
let mut scope = Scope::new();
self.eval_expression_with_scope(&mut scope, input)
}
/// Evaluate a string containing an expression with own scope.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// use rhai::{Engine, Scope};
///
/// let mut engine = Engine::new();
///
/// // Create initialized scope
/// let mut scope = Scope::new();
/// scope.push("x", 40_i64);
///
/// assert_eq!(engine.eval_expression_with_scope::<i64>(&mut scope, "x + 2")?, 42);
/// # Ok(())
/// # }
/// ```
pub fn eval_expression_with_scope<T: Any + Clone>(
&mut self,
scope: &mut Scope,
input: &str,
) -> Result<T, EvalAltResult> {
let ast = self
.compile_expression(input)
.map_err(EvalAltResult::ErrorParsing)?;
self.eval_ast_with_scope(scope, &ast)
}
/// Evaluate an `AST`.
///
/// # Example

View File

@ -3,6 +3,7 @@
#![allow(non_snake_case)]
use crate::any::{Any, Dynamic};
use crate::parser::INT;
#[cfg(not(feature = "no_index"))]
use crate::engine::Array;
@ -36,11 +37,8 @@ impl_std_args!(Array);
#[cfg(not(feature = "only_i64"))]
impl_std_args!(u8, i8, u16, i16, u32, i32, u64, i64);
#[cfg(feature = "only_i32")]
impl_std_args!(i32);
#[cfg(feature = "only_i64")]
impl_std_args!(i64);
#[cfg(any(feature = "only_i32", feature = "only_i64"))]
impl_std_args!(INT);
#[cfg(not(feature = "no_float"))]
impl_std_args!(f32, f64);

View File

@ -1,16 +1,13 @@
//! Main module defining the script evaluation `Engine`.
use crate::any::{Any, AnyExt, Dynamic, Variant};
use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt};
use crate::parser::{Expr, FnDef, Position, ReturnType, Stmt, INT};
use crate::result::EvalAltResult;
use crate::scope::{Scope, ScopeSource, VariableType};
#[cfg(not(feature = "no_optimize"))]
use crate::optimize::OptimizationLevel;
#[cfg(not(feature = "no_index"))]
use crate::INT;
use crate::stdlib::{
any::{type_name, TypeId},
borrow::Cow,
@ -1156,7 +1153,7 @@ impl Engine<'_> {
}
// If-else statement
Stmt::IfElse(guard, if_body, else_body) => self
Stmt::IfThenElse(guard, if_body, else_body) => self
.eval_expr(scope, guard)?
.downcast::<bool>()
.map_err(|_| EvalAltResult::ErrorLogicGuard(guard.position()))

View File

@ -79,7 +79,7 @@ impl State<'_> {
fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -> Stmt {
match stmt {
// if expr { Noop }
Stmt::IfElse(expr, if_block, None) if matches!(*if_block, Stmt::Noop(_)) => {
Stmt::IfThenElse(expr, if_block, None) if matches!(*if_block, Stmt::Noop(_)) => {
state.set_dirty();
let pos = expr.position();
@ -94,7 +94,7 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -
}
}
// if expr { if_block }
Stmt::IfElse(expr, if_block, None) => match *expr {
Stmt::IfThenElse(expr, if_block, None) => match *expr {
// if false { if_block } -> Noop
Expr::False(pos) => {
state.set_dirty();
@ -103,20 +103,20 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -
// if true { if_block } -> if_block
Expr::True(_) => optimize_stmt(*if_block, state, true),
// if expr { if_block }
expr => Stmt::IfElse(
expr => Stmt::IfThenElse(
Box::new(optimize_expr(expr, state)),
Box::new(optimize_stmt(*if_block, state, true)),
None,
),
},
// if expr { if_block } else { else_block }
Stmt::IfElse(expr, if_block, Some(else_block)) => match *expr {
Stmt::IfThenElse(expr, if_block, Some(else_block)) => match *expr {
// if false { if_block } else { else_block } -> else_block
Expr::False(_) => optimize_stmt(*else_block, state, true),
// if true { if_block } else { else_block } -> if_block
Expr::True(_) => optimize_stmt(*if_block, state, true),
// if expr { if_block } else { else_block }
expr => Stmt::IfElse(
expr => Stmt::IfThenElse(
Box::new(optimize_expr(expr, state)),
Box::new(optimize_stmt(*if_block, state, true)),
match optimize_stmt(*else_block, state, true) {
@ -584,8 +584,9 @@ pub fn optimize_into_ast(
AST(
match engine.optimization_level {
OptimizationLevel::None => statements,
OptimizationLevel::Simple => optimize(statements, engine, &scope),
OptimizationLevel::Full => optimize(statements, engine, &scope),
OptimizationLevel::Simple | OptimizationLevel::Full => {
optimize(statements, engine, &scope)
}
},
functions
.into_iter()

View File

@ -204,7 +204,7 @@ pub enum Stmt {
/// No-op.
Noop(Position),
/// if expr { stmt } else { stmt }
IfElse(Box<Expr>, Box<Stmt>, Option<Box<Stmt>>),
IfThenElse(Box<Expr>, Box<Stmt>, Option<Box<Stmt>>),
/// while expr { stmt }
While(Box<Expr>, Box<Stmt>),
/// loop { stmt }
@ -235,7 +235,7 @@ impl Stmt {
| Stmt::Block(_, pos)
| Stmt::Break(pos)
| Stmt::ReturnWithVal(_, _, pos) => *pos,
Stmt::IfElse(expr, _, _) | Stmt::Expr(expr) => expr.position(),
Stmt::IfThenElse(expr, _, _) | Stmt::Expr(expr) => expr.position(),
Stmt::While(_, stmt) | Stmt::Loop(stmt) | Stmt::For(_, _, stmt) => stmt.position(),
}
}
@ -243,7 +243,7 @@ impl Stmt {
/// Is this statement self-terminated (i.e. no need for a semicolon terminator)?
pub fn is_self_terminated(&self) -> bool {
match self {
Stmt::IfElse(_, _, _)
Stmt::IfThenElse(_, _, _)
| Stmt::While(_, _)
| Stmt::Loop(_)
| Stmt::For(_, _, _)
@ -265,10 +265,10 @@ impl Stmt {
match self {
Stmt::Noop(_) => true,
Stmt::Expr(expr) => expr.is_pure(),
Stmt::IfElse(guard, if_block, Some(else_block)) => {
Stmt::IfThenElse(guard, if_block, Some(else_block)) => {
guard.is_pure() && if_block.is_pure() && else_block.is_pure()
}
Stmt::IfElse(guard, block, None) | Stmt::While(guard, block) => {
Stmt::IfThenElse(guard, block, None) | Stmt::While(guard, block) => {
guard.is_pure() && block.is_pure()
}
Stmt::Loop(block) => block.is_pure(),
@ -1346,6 +1346,7 @@ pub fn lex(input: &str) -> TokenIterator<'_> {
fn parse_paren_expr<'a>(
input: &mut Peekable<TokenIterator<'a>>,
begin: Position,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
match input.peek() {
// ()
@ -1356,7 +1357,7 @@ fn parse_paren_expr<'a>(
_ => (),
}
let expr = parse_expr(input)?;
let expr = parse_expr(input, allow_stmt_expr)?;
match input.next() {
// ( xxx )
@ -1381,6 +1382,7 @@ fn parse_call_expr<'a>(
id: String,
input: &mut Peekable<TokenIterator<'a>>,
begin: Position,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
let mut args_expr_list = Vec::new();
@ -1399,7 +1401,7 @@ fn parse_call_expr<'a>(
}
loop {
args_expr_list.push(parse_expr(input)?);
args_expr_list.push(parse_expr(input, allow_stmt_expr)?);
match input.peek().ok_or_else(|| {
ParseError::new(
@ -1436,8 +1438,9 @@ fn parse_index_expr<'a>(
lhs: Box<Expr>,
input: &mut Peekable<TokenIterator<'a>>,
pos: Position,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
let idx_expr = parse_expr(input)?;
let idx_expr = parse_expr(input, allow_stmt_expr)?;
// Check type of indexing - must be integer
match &idx_expr {
@ -1529,24 +1532,30 @@ fn parse_ident_expr<'a>(
id: String,
input: &mut Peekable<TokenIterator<'a>>,
begin: Position,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
match input.peek() {
// id(...) - function call
Some((Token::LeftParen, _)) => {
input.next();
parse_call_expr(id, input, begin)
parse_call_expr(id, input, begin, allow_stmt_expr)
}
// id[...] - indexing
#[cfg(not(feature = "no_index"))]
Some((Token::LeftBracket, pos)) => {
let pos = *pos;
input.next();
parse_index_expr(Box::new(Expr::Variable(id, begin)), input, pos)
parse_index_expr(
Box::new(Expr::Variable(id, begin)),
input,
pos,
allow_stmt_expr,
)
}
// id - variable
Some(_) => Ok(Expr::Variable(id, begin)),
// EOF
None => Ok(Expr::Variable(id, Position::eof())),
None => Ok(Expr::Variable(id, begin)),
}
}
@ -1555,12 +1564,13 @@ fn parse_ident_expr<'a>(
fn parse_array_literal<'a>(
input: &mut Peekable<TokenIterator<'a>>,
begin: Position,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
let mut arr = Vec::new();
if !matches!(input.peek(), Some((Token::RightBracket, _))) {
while input.peek().is_some() {
arr.push(parse_expr(input)?);
arr.push(parse_expr(input, allow_stmt_expr)?);
match input.peek().ok_or_else(|| {
ParseError(
@ -1600,15 +1610,19 @@ fn parse_array_literal<'a>(
}
/// Parse a primary expression.
fn parse_primary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, ParseError> {
fn parse_primary<'a>(
input: &mut Peekable<TokenIterator<'a>>,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
let token = match input
.peek()
.ok_or_else(|| ParseError::new(PERR::UnexpectedEOF, Position::eof()))?
{
// { - block statement as expression
(Token::LeftBrace, pos) => {
(Token::LeftBrace, pos) if allow_stmt_expr => {
let pos = *pos;
return parse_block(input, false).map(|block| Expr::Stmt(Box::new(block), pos));
return parse_block(input, false, allow_stmt_expr)
.map(|block| Expr::Stmt(Box::new(block), pos));
}
_ => input.next().expect("should be a token"),
};
@ -1627,16 +1641,16 @@ fn parse_primary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Pa
}
(Token::Identifier(s), pos) => {
can_be_indexed = true;
parse_ident_expr(s, input, pos)
parse_ident_expr(s, input, pos, allow_stmt_expr)
}
(Token::LeftParen, pos) => {
can_be_indexed = true;
parse_paren_expr(input, pos)
parse_paren_expr(input, pos, allow_stmt_expr)
}
#[cfg(not(feature = "no_index"))]
(Token::LeftBracket, pos) => {
can_be_indexed = true;
parse_array_literal(input, pos)
parse_array_literal(input, pos, allow_stmt_expr)
}
(Token::True, pos) => Ok(Expr::True(pos)),
(Token::False, pos) => Ok(Expr::False(pos)),
@ -1653,7 +1667,7 @@ fn parse_primary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Pa
while let Some((Token::LeftBracket, pos)) = input.peek() {
let pos = *pos;
input.next();
root_expr = parse_index_expr(Box::new(root_expr), input, pos)?;
root_expr = parse_index_expr(Box::new(root_expr), input, pos, allow_stmt_expr)?;
}
}
@ -1661,7 +1675,10 @@ fn parse_primary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Pa
}
/// Parse a potential unary operator.
fn parse_unary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, ParseError> {
fn parse_unary<'a>(
input: &mut Peekable<TokenIterator<'a>>,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
match input
.peek()
.ok_or_else(|| ParseError::new(PERR::UnexpectedEOF, Position::eof()))?
@ -1672,7 +1689,7 @@ fn parse_unary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Pars
input.next();
match parse_unary(input)? {
match parse_unary(input, allow_stmt_expr)? {
// Negative integer
Expr::IntegerConstant(i, _) => i
.checked_neg()
@ -1702,7 +1719,7 @@ fn parse_unary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Pars
// +expr
(Token::UnaryPlus, _) => {
input.next();
parse_unary(input)
parse_unary(input, allow_stmt_expr)
}
// !expr
(Token::Bang, pos) => {
@ -1712,13 +1729,13 @@ fn parse_unary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Pars
Ok(Expr::FunctionCall(
"!".into(),
vec![parse_primary(input)?],
vec![parse_primary(input, allow_stmt_expr)?],
Some(Box::new(false)), // NOT operator, when operating on invalid operand, defaults to false
pos,
))
}
// All other tokens
_ => parse_primary(input),
_ => parse_primary(input, allow_stmt_expr),
}
}
@ -1821,6 +1838,7 @@ fn parse_binary_op<'a>(
input: &mut Peekable<TokenIterator<'a>>,
parent_precedence: u8,
lhs: Expr,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
let mut current_lhs = lhs;
@ -1842,7 +1860,7 @@ fn parse_binary_op<'a>(
if let Some((op_token, pos)) = input.next() {
input.peek();
let rhs = parse_unary(input)?;
let rhs = parse_unary(input, allow_stmt_expr)?;
let next_precedence = if let Some((next_op, _)) = input.peek() {
next_op.precedence()
@ -1855,7 +1873,7 @@ fn parse_binary_op<'a>(
let rhs = if (current_precedence == next_precedence && bind_right)
|| current_precedence < next_precedence
{
parse_binary_op(input, current_precedence, rhs)?
parse_binary_op(input, current_precedence, rhs, allow_stmt_expr)?
} else {
// Otherwise bind to left (even if next operator has the same precedence)
rhs
@ -1971,9 +1989,12 @@ fn parse_binary_op<'a>(
}
/// Parse an expression.
fn parse_expr<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, ParseError> {
let lhs = parse_unary(input)?;
parse_binary_op(input, 1, lhs)
fn parse_expr<'a>(
input: &mut Peekable<TokenIterator<'a>>,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
let lhs = parse_unary(input, allow_stmt_expr)?;
parse_binary_op(input, 1, lhs, allow_stmt_expr)
}
/// Make sure that the expression is not a statement expression (i.e. wrapped in {})
@ -1996,14 +2017,15 @@ fn ensure_not_statement_expr<'a>(
fn parse_if<'a>(
input: &mut Peekable<TokenIterator<'a>>,
breakable: bool,
allow_stmt_expr: bool,
) -> Result<Stmt, ParseError> {
// if ...
input.next();
// if guard { if_body }
ensure_not_statement_expr(input, "a boolean")?;
let guard = parse_expr(input)?;
let if_body = parse_block(input, breakable)?;
let guard = parse_expr(input, allow_stmt_expr)?;
let if_body = parse_block(input, breakable, allow_stmt_expr)?;
// if guard { if_body } else ...
let else_body = if matches!(input.peek(), Some((Token::Else, _))) {
@ -2011,44 +2033,57 @@ fn parse_if<'a>(
Some(Box::new(if matches!(input.peek(), Some((Token::If, _))) {
// if guard { if_body } else if ...
parse_if(input, breakable)?
parse_if(input, breakable, allow_stmt_expr)?
} else {
// if guard { if_body } else { else-body }
parse_block(input, breakable)?
parse_block(input, breakable, allow_stmt_expr)?
}))
} else {
None
};
Ok(Stmt::IfElse(Box::new(guard), Box::new(if_body), else_body))
Ok(Stmt::IfThenElse(
Box::new(guard),
Box::new(if_body),
else_body,
))
}
/// Parse a while loop.
fn parse_while<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseError> {
fn parse_while<'a>(
input: &mut Peekable<TokenIterator<'a>>,
allow_stmt_expr: bool,
) -> Result<Stmt, ParseError> {
// while ...
input.next();
// while guard { body }
ensure_not_statement_expr(input, "a boolean")?;
let guard = parse_expr(input)?;
let body = parse_block(input, true)?;
let guard = parse_expr(input, allow_stmt_expr)?;
let body = parse_block(input, true, allow_stmt_expr)?;
Ok(Stmt::While(Box::new(guard), Box::new(body)))
}
/// Parse a loop statement.
fn parse_loop<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseError> {
fn parse_loop<'a>(
input: &mut Peekable<TokenIterator<'a>>,
allow_stmt_expr: bool,
) -> Result<Stmt, ParseError> {
// loop ...
input.next();
// loop { body }
let body = parse_block(input, true)?;
let body = parse_block(input, true, allow_stmt_expr)?;
Ok(Stmt::Loop(Box::new(body)))
}
/// Parse a for loop.
fn parse_for<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseError> {
fn parse_for<'a>(
input: &mut Peekable<TokenIterator<'a>>,
allow_stmt_expr: bool,
) -> Result<Stmt, ParseError> {
// for ...
input.next();
@ -2078,8 +2113,8 @@ fn parse_for<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseE
// for name in expr { body }
ensure_not_statement_expr(input, "a boolean")?;
let expr = parse_expr(input)?;
let body = parse_block(input, true)?;
let expr = parse_expr(input, allow_stmt_expr)?;
let body = parse_block(input, true, allow_stmt_expr)?;
Ok(Stmt::For(name, Box::new(expr), Box::new(body)))
}
@ -2088,6 +2123,7 @@ fn parse_for<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseE
fn parse_let<'a>(
input: &mut Peekable<TokenIterator<'a>>,
var_type: VariableType,
allow_stmt_expr: bool,
) -> Result<Stmt, ParseError> {
// let/const... (specified in `var_type`)
input.next();
@ -2109,7 +2145,7 @@ fn parse_let<'a>(
input.next();
// let name = expr
let init_value = parse_expr(input)?;
let init_value = parse_expr(input, allow_stmt_expr)?;
match var_type {
// let name = expr
@ -2134,6 +2170,7 @@ fn parse_let<'a>(
fn parse_block<'a>(
input: &mut Peekable<TokenIterator<'a>>,
breakable: bool,
allow_stmt_expr: bool,
) -> Result<Stmt, ParseError> {
// Must start with {
let pos = match input
@ -2148,7 +2185,7 @@ fn parse_block<'a>(
while !matches!(input.peek(), Some((Token::RightBrace, _))) {
// Parse statements inside the block
let stmt = parse_stmt(input, breakable)?;
let stmt = parse_stmt(input, breakable, allow_stmt_expr)?;
// See if it needs a terminating semicolon
let need_semicolon = !stmt.is_self_terminated();
@ -2197,14 +2234,18 @@ fn parse_block<'a>(
}
/// Parse an expression as a statement.
fn parse_expr_stmt<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseError> {
Ok(Stmt::Expr(Box::new(parse_expr(input)?)))
fn parse_expr_stmt<'a>(
input: &mut Peekable<TokenIterator<'a>>,
allow_stmt_expr: bool,
) -> Result<Stmt, ParseError> {
Ok(Stmt::Expr(Box::new(parse_expr(input, allow_stmt_expr)?)))
}
/// Parse a single statement.
fn parse_stmt<'a>(
input: &mut Peekable<TokenIterator<'a>>,
breakable: bool,
allow_stmt_expr: bool,
) -> Result<Stmt, ParseError> {
let token = match input.peek() {
Some(token) => token,
@ -2219,10 +2260,10 @@ fn parse_stmt<'a>(
#[cfg(not(feature = "no_function"))]
(Token::Fn, pos) => return Err(ParseError::new(PERR::WrongFnDefinition, *pos)),
(Token::If, _) => parse_if(input, breakable),
(Token::While, _) => parse_while(input),
(Token::Loop, _) => parse_loop(input),
(Token::For, _) => parse_for(input),
(Token::If, _) => parse_if(input, breakable, allow_stmt_expr),
(Token::While, _) => parse_while(input, allow_stmt_expr),
(Token::Loop, _) => parse_loop(input, allow_stmt_expr),
(Token::For, _) => parse_for(input, allow_stmt_expr),
(Token::Break, pos) if breakable => {
let pos = *pos;
input.next();
@ -2246,23 +2287,26 @@ fn parse_stmt<'a>(
Some((Token::SemiColon, _)) => Ok(Stmt::ReturnWithVal(None, return_type, pos)),
// `return` or `throw` with expression
Some((_, _)) => {
let expr = parse_expr(input)?;
let expr = parse_expr(input, allow_stmt_expr)?;
let pos = expr.position();
Ok(Stmt::ReturnWithVal(Some(Box::new(expr)), return_type, pos))
}
}
}
(Token::LeftBrace, _) => parse_block(input, breakable),
(Token::Let, _) => parse_let(input, VariableType::Normal),
(Token::Const, _) => parse_let(input, VariableType::Constant),
_ => parse_expr_stmt(input),
(Token::LeftBrace, _) => parse_block(input, breakable, allow_stmt_expr),
(Token::Let, _) => parse_let(input, VariableType::Normal, allow_stmt_expr),
(Token::Const, _) => parse_let(input, VariableType::Constant, allow_stmt_expr),
_ => parse_expr_stmt(input, allow_stmt_expr),
}
}
/// Parse a function definition.
#[cfg(not(feature = "no_function"))]
fn parse_fn<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<FnDef, ParseError> {
fn parse_fn<'a>(
input: &mut Peekable<TokenIterator<'a>>,
allow_stmt_expr: bool,
) -> Result<FnDef, ParseError> {
let pos = input.next().expect("should be fn").1;
let name = match input
@ -2346,7 +2390,7 @@ fn parse_fn<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<FnDef, ParseE
}
let body = match input.peek() {
Some((Token::LeftBrace, _)) => parse_block(input, false)?,
Some((Token::LeftBrace, _)) => parse_block(input, false, allow_stmt_expr)?,
Some((_, pos)) => return Err(ParseError::new(PERR::FnMissingBody(name), *pos)),
None => return Err(ParseError::new(PERR::FnMissingBody(name), Position::eof())),
};
@ -2359,8 +2403,34 @@ fn parse_fn<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<FnDef, ParseE
})
}
pub fn parse_global_expr<'a, 'e>(
input: &mut Peekable<TokenIterator<'a>>,
engine: &Engine<'e>,
scope: &Scope,
) -> Result<AST, ParseError> {
let expr = parse_expr(input, false)?;
if let Some((token, pos)) = input.peek() {
// Return error if the expression doesn't end
return Err(ParseError::new(
PERR::BadInput(format!("Unexpected '{}'", token.syntax())),
*pos,
));
}
Ok(
// Optimize AST
#[cfg(not(feature = "no_optimize"))]
optimize_into_ast(engine, scope, vec![Stmt::Expr(Box::new(expr))], vec![]),
//
// Do not optimize AST if `no_optimize`
#[cfg(feature = "no_optimize")]
AST(vec![Stmt::Expr(Box::new(expr))], vec![]),
)
}
/// Parse the global level statements.
fn parse_global_level<'a, 'e>(
fn parse_global_level<'a>(
input: &mut Peekable<TokenIterator<'a>>,
) -> Result<(Vec<Stmt>, Vec<FnDef>), ParseError> {
let mut statements = Vec::<Stmt>::new();
@ -2371,7 +2441,7 @@ fn parse_global_level<'a, 'e>(
{
// Collect all the function definitions
if matches!(input.peek().expect("should not be None"), (Token::Fn, _)) {
let f = parse_fn(input)?;
let f = parse_fn(input, true)?;
// Ensure list is sorted
match functions.binary_search_by(|fn_def| fn_def.compare(&f.name, f.params.len())) {
@ -2384,7 +2454,7 @@ fn parse_global_level<'a, 'e>(
}
// Actual statement
let stmt = parse_stmt(input, false)?;
let stmt = parse_stmt(input, false, true)?;
let need_semicolon = !stmt.is_self_terminated();

25
tests/expressions.rs Normal file
View File

@ -0,0 +1,25 @@
use rhai::{Engine, EvalAltResult, Scope, INT};
#[test]
fn test_expressions() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
let mut scope = Scope::new();
scope.push("x", 10 as INT);
assert_eq!(engine.eval_expression::<INT>("2 + (10 + 10) * 2")?, 42);
assert_eq!(
engine.eval_expression_with_scope::<INT>(&mut scope, "2 + (x + 10) * 2")?,
42
);
assert!(engine.eval_expression::<()>("40 + 2;").is_err());
assert!(engine.eval_expression::<()>("40 + { 2 }").is_err());
assert!(engine.eval_expression::<()>("x = 42").is_err());
assert!(engine.compile_expression("let x = 42").is_err());
assert!(engine
.eval_expression_with_scope::<INT>(&mut scope, "if x > 0 { 42 } else { 123 }")
.is_err());
Ok(())
}

View File

@ -1,16 +1,41 @@
use rhai::{Engine, EvalAltResult, RegisterFn, Scope};
use std::cell::Cell;
///! This test simulates an external command object that is driven by a script.
use rhai::{Engine, EvalAltResult, RegisterFn, Scope, INT};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone)]
/// External command.
struct Command {
state: i64,
}
impl Command {
/// Do some action.
pub fn action(&mut self, val: i64) {
self.state = val;
}
/// Get current value.
pub fn get(&self) -> i64 {
self.state
}
}
/// Wrapper object to wrap a command object.
#[derive(Clone)]
struct CommandWrapper {
value: Rc<Cell<i64>>,
command: Rc<RefCell<Command>>,
}
impl CommandWrapper {
pub fn set_value(&mut self, x: i64) {
let val = self.value.get();
self.value.set(val + x);
/// Delegate command action.
pub fn do_action(&mut self, x: i64) {
let mut command = self.command.borrow_mut();
let val = command.get();
command.action(val + x);
}
/// Delegate get value action.
pub fn get_value(&mut self) -> i64 {
let command = self.command.borrow();
command.get()
}
}
@ -19,21 +44,37 @@ fn test_side_effects() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
let mut scope = Scope::new();
let payload = Rc::new(Cell::new(12));
assert_eq!(payload.get(), 12);
// Create the command object with initial state, handled by an `Rc`.
let command = Rc::new(RefCell::new(Command { state: 12 }));
assert_eq!(command.borrow().get(), 12);
let command = CommandWrapper {
value: payload.clone(),
// Create the wrapper.
let wrapper = CommandWrapper {
command: command.clone(), // Notice this clones the `Rc` only
};
scope.push_constant("Command", command);
// Make the wrapper a singleton in the script environment.
scope.push_constant("Command", wrapper);
// Register type.
engine.register_type_with_name::<CommandWrapper>("CommandType");
engine.register_fn("action", CommandWrapper::set_value);
engine.register_fn("action", CommandWrapper::do_action);
engine.register_get("value", CommandWrapper::get_value);
engine.eval_with_scope::<()>(&mut scope, "Command.action(30)")?;
assert_eq!(
engine.eval_with_scope::<INT>(
&mut scope,
r"
// Drive the command object via the wrapper
Command.action(30);
Command.value
"
)?,
42
);
assert_eq!(payload.get(), 42);
// Make sure the actions are properly performed
assert_eq!(command.borrow().get(), 42);
Ok(())
}