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