From b29c3ba6a9050a1147c0e95a4ab946ef531eb10b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 21 Mar 2020 17:14:31 +0800 Subject: [PATCH 1/5] Add multi-line support to repl example. --- examples/repl.rs | 64 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/examples/repl.rs b/examples/repl.rs index 953ee2a9..b37a50b3 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -13,15 +13,25 @@ fn print_error(input: &str, err: EvalAltResult) { iter::repeat(pad).take(len).collect::() } - 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 match err.position() { p if p.is_eof() => { // EOF - let last = lines[lines.len() - 2]; - println!("{}", last); - println!("{}^ {}", padding(" ", last.len() - 1), err); + let last = lines[lines.len() - 1]; + println!("{}{}", line_no, last); + println!("{}^ {}", padding(" ", line_no.len() + last.len() - 1), err); } p if p.is_none() => { // No position @@ -35,7 +45,7 @@ fn print_error(input: &str, err: EvalAltResult) { p.position().unwrap() ); - println!("{}", lines[p.line().unwrap() - 1]); + println!("{}{}", line_no, lines[p.line().unwrap() - 1]); let err_text = match err { EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { @@ -46,13 +56,21 @@ fn print_error(input: &str, err: EvalAltResult) { println!( "{}^ {}", - padding(" ", p.position().unwrap() - 1), + 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!(r"end a line with '\' to continue to the next line."); + println!(); +} + fn main() { let mut engine = Engine::new(); @@ -64,18 +82,44 @@ fn main() { let mut input = String::new(); let mut ast: Option = 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 "ast" => { if matches!(&ast, Some(_)) { @@ -90,7 +134,7 @@ 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); From 3a2413c88fefbdf0bac4be6cc28dfe534a1620e6 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 22 Mar 2020 00:32:07 +0800 Subject: [PATCH 2/5] Add LICENSE. --- LICENSE.txt | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From f51864e190d259c934bcba0678ea1da7290b1ce9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 22 Mar 2020 09:25:41 +0800 Subject: [PATCH 3/5] Allow line continuations in repl example. --- examples/repl.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/examples/repl.rs b/examples/repl.rs index b37a50b3..b5f59a4f 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -67,6 +67,7 @@ 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!(); } @@ -75,11 +76,12 @@ 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 = None; let mut ast: Option = None; println!("Rhai REPL tool"); @@ -121,6 +123,15 @@ fn main() { 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 @@ -137,7 +148,12 @@ fn main() { .compile_with_scope(&scope, &script) .map_err(EvalAltResult::ErrorParsing) .and_then(|r| { - ast = Some(r); + ast_u = Some(r); + + engine.set_optimization_level(OptimizationLevel::Full); + ast = Some(engine.optimize_ast(&mut scope, ast_u.as_ref().unwrap())); + engine.set_optimization_level(OptimizationLevel::None); + engine .consume_ast_with_scope(&mut scope, true, ast.as_ref().unwrap()) .or_else(|err| match err { From b6320c0eef68c676ecd509acec8b64f72c8e1ca8 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 22 Mar 2020 10:18:16 +0800 Subject: [PATCH 4/5] Minor fine tuning. --- Cargo.toml | 3 +- README.md | 13 ++++++-- src/api.rs | 17 ++++++++--- src/call.rs | 8 ++--- src/engine.rs | 7 ++--- src/optimize.rs | 15 ++++----- src/parser.rs | 16 ++++++---- tests/side_effects.rs | 71 ++++++++++++++++++++++++++++++++++--------- 8 files changed, 103 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 593091cc..1164a869 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 41fdc227..4a18aff4 100644 --- a/README.md +++ b/README.md @@ -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: @@ -1464,9 +1471,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. diff --git a/src/api.rs b/src/api.rs index 850ff357..6a223b9b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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::("let x = new_ts(); x.update(); x")?.field, + /// engine.eval::("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; } /// } /// diff --git a/src/call.rs b/src/call.rs index cf7356fe..f4dadb4b 100644 --- a/src/call.rs +++ b/src/call.rs @@ -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); diff --git a/src/engine.rs b/src/engine.rs index 8735d842..24430ea1 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -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::() .map_err(|_| EvalAltResult::ErrorLogicGuard(guard.position())) diff --git a/src/optimize.rs b/src/optimize.rs index 43664681..e350cba2 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -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() diff --git a/src/parser.rs b/src/parser.rs index d068f8d4..3447aa53 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -204,7 +204,7 @@ pub enum Stmt { /// No-op. Noop(Position), /// if expr { stmt } else { stmt } - IfElse(Box, Box, Option>), + IfThenElse(Box, Box, Option>), /// while expr { stmt } While(Box, Box), /// 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(), @@ -2020,7 +2020,11 @@ fn parse_if<'a>( 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. diff --git a/tests/side_effects.rs b/tests/side_effects.rs index edfc0009..69b82e64 100644 --- a/tests/side_effects.rs +++ b/tests/side_effects.rs @@ -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>, + command: Rc>, } 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::("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::( + &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(()) } From 1b4bcbcfdf15dbefb7242313a50b72c210caf91d Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 22 Mar 2020 21:03:58 +0800 Subject: [PATCH 5/5] Add evaluate expressions. --- README.md | 32 +++++--- examples/repl.rs | 36 ++++++--- src/api.rs | 123 +++++++++++++++++++++++++++++- src/parser.rs | 174 +++++++++++++++++++++++++++++-------------- tests/expressions.rs | 25 +++++++ 5 files changed, 313 insertions(+), 77 deletions(-) create mode 100644 tests/expressions.rs diff --git a/README.md b/README.md index 4a18aff4..f36e305b 100644 --- a/README.md +++ b/README.md @@ -183,10 +183,6 @@ let result = engine.eval_file::("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")?; @@ -200,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, @@ -241,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::("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::(&mut scope, "if x { 42 } else { 123 }")?; +``` + Values and types ---------------- diff --git a/examples/repl.rs b/examples/repl.rs index b5f59a4f..91bfcfa0 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -26,12 +26,26 @@ fn print_error(input: &str, err: EvalAltResult) { }; // Print error + let pos_text = format!(" ({})", err.position()); + match err.position() { p if p.is_eof() => { // EOF let last = lines[lines.len() - 1]; println!("{}{}", line_no, last); - println!("{}^ {}", padding(" ", line_no.len() + last.len() - 1), err); + + let err_text = match err { + EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { + format!("Runtime error: {}", err) + } + _ => err.to_string(), + }; + + println!( + "{}^ {}", + padding(" ", line_no.len() + last.len() - 1), + err_text.replace(&pos_text, "") + ); } p if p.is_none() => { // No position @@ -39,12 +53,6 @@ fn print_error(input: &str, err: EvalAltResult) { } p => { // Specific position - let pos_text = format!( - " (line {}, position {})", - p.line().unwrap(), - p.position().unwrap() - ); - println!("{}{}", line_no, lines[p.line().unwrap() - 1]); let err_text = match err { @@ -150,9 +158,17 @@ fn main() { .and_then(|r| { ast_u = Some(r); - 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(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()) diff --git a/src/api.rs b/src/api.rs index 6a223b9b..208f311c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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; @@ -416,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::(&ast)?, 42); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn compile_expression(&self, input: &str) -> Result { + 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::(&ast)?, 42); + /// # } + /// # Ok(()) + /// # } + /// ``` + pub fn compile_expression_with_scope( + &self, + scope: &Scope, + input: &str, + ) -> Result { + let tokens_stream = lex(input); + parse_global_expr(&mut tokens_stream.peekable(), self, scope) + } + /// Evaluate a script file. /// /// # Example @@ -514,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::("40 + 2")?, 42); + /// # Ok(()) + /// # } + /// ``` + pub fn eval_expression(&mut self, input: &str) -> Result { + 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::(&mut scope, "x + 2")?, 42); + /// # Ok(()) + /// # } + /// ``` + pub fn eval_expression_with_scope( + &mut self, + scope: &mut Scope, + input: &str, + ) -> Result { + let ast = self + .compile_expression(input) + .map_err(EvalAltResult::ErrorParsing)?; + + self.eval_ast_with_scope(scope, &ast) + } + /// Evaluate an `AST`. /// /// # Example diff --git a/src/parser.rs b/src/parser.rs index 3447aa53..5d3b8407 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1346,6 +1346,7 @@ pub fn lex(input: &str) -> TokenIterator<'_> { fn parse_paren_expr<'a>( input: &mut Peekable>, begin: Position, + allow_stmt_expr: bool, ) -> Result { 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>, begin: Position, + allow_stmt_expr: bool, ) -> Result { 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, input: &mut Peekable>, pos: Position, + allow_stmt_expr: bool, ) -> Result { - 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>, begin: Position, + allow_stmt_expr: bool, ) -> Result { 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>, begin: Position, + allow_stmt_expr: bool, ) -> Result { 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>) -> Result { +fn parse_primary<'a>( + input: &mut Peekable>, + allow_stmt_expr: bool, +) -> Result { 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>) -> Result { 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>) -> Result(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { +fn parse_unary<'a>( + input: &mut Peekable>, + allow_stmt_expr: bool, +) -> Result { match input .peek() .ok_or_else(|| ParseError::new(PERR::UnexpectedEOF, Position::eof()))? @@ -1672,7 +1689,7 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result i .checked_neg() @@ -1702,7 +1719,7 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result { 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>) -> Result parse_primary(input), + _ => parse_primary(input, allow_stmt_expr), } } @@ -1821,6 +1838,7 @@ fn parse_binary_op<'a>( input: &mut Peekable>, parent_precedence: u8, lhs: Expr, + allow_stmt_expr: bool, ) -> Result { 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>) -> Result { - let lhs = parse_unary(input)?; - parse_binary_op(input, 1, lhs) +fn parse_expr<'a>( + input: &mut Peekable>, + allow_stmt_expr: bool, +) -> Result { + 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>, breakable: bool, + allow_stmt_expr: bool, ) -> Result { // 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,10 +2033,10 @@ 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 @@ -2028,31 +2050,40 @@ fn parse_if<'a>( } /// Parse a while loop. -fn parse_while<'a>(input: &mut Peekable>) -> Result { +fn parse_while<'a>( + input: &mut Peekable>, + allow_stmt_expr: bool, +) -> Result { // 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>) -> Result { +fn parse_loop<'a>( + input: &mut Peekable>, + allow_stmt_expr: bool, +) -> Result { // 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>) -> Result { +fn parse_for<'a>( + input: &mut Peekable>, + allow_stmt_expr: bool, +) -> Result { // for ... input.next(); @@ -2082,8 +2113,8 @@ fn parse_for<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result( input: &mut Peekable>, var_type: VariableType, + allow_stmt_expr: bool, ) -> Result { // let/const... (specified in `var_type`) input.next(); @@ -2113,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 @@ -2138,6 +2170,7 @@ fn parse_let<'a>( fn parse_block<'a>( input: &mut Peekable>, breakable: bool, + allow_stmt_expr: bool, ) -> Result { // Must start with { let pos = match input @@ -2152,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(); @@ -2201,14 +2234,18 @@ fn parse_block<'a>( } /// Parse an expression as a statement. -fn parse_expr_stmt<'a>(input: &mut Peekable>) -> Result { - Ok(Stmt::Expr(Box::new(parse_expr(input)?))) +fn parse_expr_stmt<'a>( + input: &mut Peekable>, + allow_stmt_expr: bool, +) -> Result { + Ok(Stmt::Expr(Box::new(parse_expr(input, allow_stmt_expr)?))) } /// Parse a single statement. fn parse_stmt<'a>( input: &mut Peekable>, breakable: bool, + allow_stmt_expr: bool, ) -> Result { let token = match input.peek() { Some(token) => token, @@ -2223,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(); @@ -2250,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>) -> Result { +fn parse_fn<'a>( + input: &mut Peekable>, + allow_stmt_expr: bool, +) -> Result { let pos = input.next().expect("should be fn").1; let name = match input @@ -2350,7 +2390,7 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result 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())), }; @@ -2363,8 +2403,34 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result( + input: &mut Peekable>, + engine: &Engine<'e>, + scope: &Scope, +) -> Result { + 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>, ) -> Result<(Vec, Vec), ParseError> { let mut statements = Vec::::new(); @@ -2375,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())) { @@ -2388,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(); diff --git a/tests/expressions.rs b/tests/expressions.rs new file mode 100644 index 00000000..d43d990a --- /dev/null +++ b/tests/expressions.rs @@ -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::("2 + (10 + 10) * 2")?, 42); + assert_eq!( + engine.eval_expression_with_scope::(&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::(&mut scope, "if x > 0 { 42 } else { 123 }") + .is_err()); + + Ok(()) +}