From 246f5fbbe60b21a4b268e1636d5e3654165308ba Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 2 Apr 2020 12:18:22 +0800 Subject: [PATCH] Rename `downcast` to `try_cast` and add `cast` for Dynamic. --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++------ src/any.rs | 34 ++++++++++++++++++++++++---- src/api.rs | 36 ++++++++++++------------------ src/engine.rs | 48 +++++++++++++++++++-------------------- src/parser.rs | 44 ++++++++---------------------------- tests/maps.rs | 28 +++++++++++------------ 6 files changed, 145 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 4a168273..fa033138 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,6 @@ The following primitive types are supported natively: | **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | | **Nothing/void/nil/null** (or whatever you want to call it) | `()` | `"()"` | `""` _(empty string)_ | -[`Dynamic`]: #values-and-types [`()`]: #values-and-types All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different - they even cannot be added together. This is very similar to Rust. @@ -313,6 +312,55 @@ if type_of(x) == "string" { } ``` +Dynamic values +-------------- + +[`Dynamic`]: #dynamic-values + +A `Dynamic` value can be _any_ type. + +Because [`type_of()`] a `Dynamic` value returns the type of the actual value, it is usually used to perform type-specific +actions based on the actual value's type. + +```rust +let mystery = get_some_dynamic_value(); + +if type_of(mystery) == "i64" { + print("Hey, I got an integer here!"); +} else if type_of(mystery) == "f64" { + print("Hey, I got a float here!"); +} else if type_of(mystery) == "string" { + print("Hey, I got a string here!"); +} else if type_of(mystery) == "bool" { + print("Hey, I got a boolean here!"); +} else if type_of(mystery) == "array" { + print("Hey, I got an array here!"); +} else if type_of(mystery) == "map" { + print("Hey, I got an object map here!"); +} else if type_of(mystery) == "TestStruct" { + print("Hey, I got the TestStruct custom type here!"); +} else { + print("I don't know what this is: " + type_of(mystery)); +} +``` + +In Rust, sometimes a `Dynamic` forms part of the return value - a good example is elements within an `Array` which are `Dynamic`, +or property values in an object map. In order to get the _real_ value, the actual value type _must_ be known in advance. +There is no easy way for Rust to detect, at run-time, what type the `Dynamic` value is. + +To use a `Dynamic` value in Rust, use the `cast` function to convert the value into a specific, known type. +Alternatively, use the `try_cast` function which does not panic but returns an error when the cast fails. + +```rust +let list: Array = engine.eval("...")?; // return type is 'Array' +let item = list[0]; // an element in an 'Array' is 'Dynamic' + +let value = item.cast::(); // if the element is 'i64', this succeeds; otherwise it panics +let value: i64 = item.cast(); // type can also be inferred + +let value = item.try_cast::()?; // 'try_cast' does not panic when the cast fails, but returns an error +``` + Value conversions ----------------- @@ -325,11 +373,11 @@ That's about it. For other conversions, register custom conversion functions. ```rust let x = 42; -let y = x * 100.0; // <- error: cannot multiply i64 with f64 -let y = x.to_float() * 100.0; // works -let z = y.to_int() + x; // works +let y = x * 100.0; // <- error: cannot multiply i64 with f64 +let y = x.to_float() * 100.0; // works +let z = y.to_int() + x; // works -let c = 'X'; // character +let c = 'X'; // character print("c is '" + c + "' and its code is " + c.to_int()); // prints "c is 'X' and its code is 88" ``` @@ -1642,9 +1690,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! +environment and is 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 -it will eagerly run execute the function call. This causes the script to behave differently from the intended semantics because +it will eagerly 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/any.rs b/src/any.rs index e92b3e9d..befadcce 100644 --- a/src/any.rs +++ b/src/any.rs @@ -89,7 +89,14 @@ impl Clone for Dynamic { /// An extension trait that allows down-casting a `Dynamic` value to a specific type. pub trait AnyExt: Sized { /// Get a copy of a `Dynamic` value as a specific type. - fn downcast(self) -> Result, Self>; + fn try_cast(self) -> Result; + + /// Get a copy of a `Dynamic` value as a specific type. + /// + /// # Panics + /// + /// Panics if the cast fails (e.g. the type of the actual value is not the same as the specified type). + fn cast(self) -> T; /// This trait may only be implemented by `rhai`. #[doc(hidden)] @@ -106,19 +113,38 @@ impl AnyExt for Dynamic { /// /// let x: Dynamic = 42_u32.into_dynamic(); /// - /// assert_eq!(*x.downcast::().unwrap(), 42); + /// assert_eq!(x.try_cast::().unwrap(), 42); /// ``` - fn downcast(self) -> Result, Self> { + fn try_cast(self) -> Result { if self.is::() { unsafe { let raw: *mut Variant = Box::into_raw(self); - Ok(Box::from_raw(raw as *mut T)) + Ok(*Box::from_raw(raw as *mut T)) } } else { Err(self) } } + /// Get a copy of the `Dynamic` value as a specific type. + /// + /// # Panics + /// + /// Panics if the cast fails (e.g. the type of the actual value is not the same as the specified type). + /// + /// # Example + /// + /// ``` + /// use rhai::{Dynamic, Any, AnyExt}; + /// + /// let x: Dynamic = 42_u32.into_dynamic(); + /// + /// assert_eq!(x.cast::(), 42); + /// ``` + fn cast(self) -> T { + self.try_cast::().expect("cast failed") + } + fn _closed(&self) -> _Private { _Private } diff --git a/src/api.rs b/src/api.rs index 7e010192..b3acfa79 100644 --- a/src/api.rs +++ b/src/api.rs @@ -692,8 +692,7 @@ impl<'e> Engine<'e> { ast: &AST, ) -> Result { self.eval_ast_with_scope_raw(scope, false, ast)? - .downcast::() - .map(|v| *v) + .try_cast::() .map_err(|a| { EvalAltResult::ErrorMismatchOutputType( self.map_type_name((*a).type_name()).to_string(), @@ -735,9 +734,8 @@ impl<'e> Engine<'e> { /// Evaluate a file, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// # Note - /// - /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ and not cleared from run to run. + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. #[cfg(not(feature = "no_std"))] pub fn consume_file( &mut self, @@ -750,9 +748,8 @@ impl<'e> Engine<'e> { /// Evaluate a file with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// # Note - /// - /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ and not cleared from run to run. + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. #[cfg(not(feature = "no_std"))] pub fn consume_file_with_scope( &mut self, @@ -767,9 +764,8 @@ impl<'e> Engine<'e> { /// Evaluate a string, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// # Note - /// - /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. pub fn consume(&mut self, retain_functions: bool, input: &str) -> Result<(), EvalAltResult> { self.consume_with_scope(&mut Scope::new(), retain_functions, input) } @@ -777,9 +773,8 @@ impl<'e> Engine<'e> { /// Evaluate a string with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// # Note - /// - /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. pub fn consume_with_scope( &mut self, scope: &mut Scope, @@ -797,9 +792,8 @@ impl<'e> Engine<'e> { /// Evaluate an AST, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// # Note - /// - /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. pub fn consume_ast(&mut self, retain_functions: bool, ast: &AST) -> Result<(), EvalAltResult> { self.consume_ast_with_scope(&mut Scope::new(), retain_functions, ast) } @@ -807,9 +801,8 @@ impl<'e> Engine<'e> { /// Evaluate an `AST` with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// # Note - /// - /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. + /// If `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ + /// and not cleared from run to run. pub fn consume_ast_with_scope( &mut self, scope: &mut Scope, @@ -884,8 +877,7 @@ impl<'e> Engine<'e> { let mut arg_values: Vec<_> = values.iter_mut().map(Dynamic::as_mut).collect(); self.call_fn_raw(name, &mut arg_values, None, Position::none(), 0)? - .downcast() - .map(|b| *b) + .try_cast() .map_err(|a| { EvalAltResult::ErrorMismatchOutputType( self.map_type_name((*a).type_name()).into(), diff --git a/src/engine.rs b/src/engine.rs index 2d3e955d..5230f5b8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -628,9 +628,9 @@ impl Engine<'_> { // val_array[idx] if let Some(arr) = val.downcast_ref::() { - let idx = *self + let idx = self .eval_expr(scope, idx_expr, level)? - .downcast::() + .try_cast::() .map_err(|_| EvalAltResult::ErrorNumericIndexExpr(idx_expr.position()))?; return if idx >= 0 { @@ -647,9 +647,9 @@ impl Engine<'_> { { // val_map[idx] if let Some(map) = val.downcast_ref::() { - let idx = *self + let idx = self .eval_expr(scope, idx_expr, level)? - .downcast::() + .try_cast::() .map_err(|_| EvalAltResult::ErrorStringIndexExpr(idx_expr.position()))?; return Ok(( @@ -662,9 +662,9 @@ impl Engine<'_> { // val_string[idx] if let Some(s) = val.downcast_ref::() { - let idx = *self + let idx = self .eval_expr(scope, idx_expr, level)? - .downcast::() + .try_cast::() .map_err(|_| EvalAltResult::ErrorNumericIndexExpr(idx_expr.position()))?; return if idx >= 0 { @@ -795,9 +795,9 @@ impl Engine<'_> { let s = scope.get_mut_by_type::(src); let pos = new_val.1; // Value must be a character - let ch = *new_val + let ch = new_val .0 - .downcast::() + .try_cast::() .map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?; Self::str_replace_char(s, idx.as_num(), ch); Ok(().into_dynamic()) @@ -830,8 +830,8 @@ impl Engine<'_> { if let Some(s) = target.downcast_mut::() { // Value must be a character - let ch = *new_val - .downcast::() + let ch = new_val + .try_cast::() .map_err(|_| EvalAltResult::ErrorCharMismatch(pos))?; Self::str_replace_char(s, idx.as_num(), ch); return Ok(target); @@ -1258,32 +1258,32 @@ impl Engine<'_> { } Expr::And(lhs, rhs) => Ok(Box::new( - *self + self .eval_expr(scope, &*lhs, level)? - .downcast::() + .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), lhs.position()) })? && // Short-circuit using && - *self + self .eval_expr(scope, &*rhs, level)? - .downcast::() + .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), rhs.position()) })?, )), Expr::Or(lhs, rhs) => Ok(Box::new( - *self + self .eval_expr(scope, &*lhs, level)? - .downcast::() + .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), lhs.position()) })? || // Short-circuit using || - *self + self .eval_expr(scope, &*rhs, level)? - .downcast::() + .try_cast::() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), rhs.position()) })?, @@ -1334,10 +1334,10 @@ impl Engine<'_> { // If-else statement Stmt::IfThenElse(guard, if_body, else_body) => self .eval_expr(scope, guard, level)? - .downcast::() + .try_cast::() .map_err(|_| EvalAltResult::ErrorLogicGuard(guard.position())) .and_then(|guard_val| { - if *guard_val { + if guard_val { self.eval_stmt(scope, if_body, level) } else if let Some(stmt) = else_body { self.eval_stmt(scope, stmt.as_ref(), level) @@ -1348,8 +1348,8 @@ impl Engine<'_> { // While loop Stmt::While(guard, body) => loop { - match self.eval_expr(scope, guard, level)?.downcast::() { - Ok(guard_val) if *guard_val => match self.eval_stmt(scope, body, level) { + match self.eval_expr(scope, guard, level)?.try_cast::() { + Ok(guard_val) if guard_val => match self.eval_stmt(scope, body, level) { Ok(_) | Err(EvalAltResult::ErrorLoopBreak(false, _)) => (), Err(EvalAltResult::ErrorLoopBreak(true, _)) => return Ok(().into_dynamic()), Err(x) => return Err(x), @@ -1425,9 +1425,7 @@ impl Engine<'_> { Stmt::ReturnWithVal(Some(a), ReturnType::Exception, pos) => { let val = self.eval_expr(scope, a, level)?; Err(EvalAltResult::ErrorRuntime( - val.downcast::() - .map(|s| *s) - .unwrap_or_else(|_| "".to_string()), + val.try_cast::().unwrap_or_else(|_| "".to_string()), *pos, )) } diff --git a/src/parser.rs b/src/parser.rs index c3d16f2e..d164d35e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2673,41 +2673,21 @@ pub fn parse<'a, 'e>( pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> (Option, Dynamic) { if value.is::() { let value2 = value.clone(); - ( - Some(Expr::IntegerConstant( - *value.downcast::().expect("value should be INT"), - pos, - )), - value2, - ) + (Some(Expr::IntegerConstant(value.cast(), pos)), value2) } else if value.is::() { let value2 = value.clone(); - ( - Some(Expr::CharConstant( - *value.downcast::().expect("value should be char"), - pos, - )), - value2, - ) + (Some(Expr::CharConstant(value.cast(), pos)), value2) } else if value.is::() { let value2 = value.clone(); - ( - Some(Expr::StringConstant( - *value.downcast::().expect("value should be String"), - pos, - )), - value2, - ) + (Some(Expr::StringConstant(value.cast(), pos)), value2) } else if value.is::() { let value2 = value.clone(); ( - Some( - if *value.downcast::().expect("value should be bool") { - Expr::True(pos) - } else { - Expr::False(pos) - }, - ), + Some(if value.cast::() { + Expr::True(pos) + } else { + Expr::False(pos) + }), value2, ) } else { @@ -2715,13 +2695,7 @@ pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> (Option, Dyna { if value.is::() { let value2 = value.clone(); - return ( - Some(Expr::FloatConstant( - *value.downcast::().expect("value should be FLOAT"), - pos, - )), - value2, - ); + return (Some(Expr::FloatConstant(value.cast(), pos)), value2); } } diff --git a/tests/maps.rs b/tests/maps.rs index bb8c90cf..7a17f1a9 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -73,14 +73,14 @@ fn test_map_indexing() -> Result<(), EvalAltResult> { fn test_map_assign() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); - let x = engine.eval::(r#"let x = #{a: 1, b: true, "c#": "hello"}; x"#)?; - let a = x.get("a").cloned().unwrap(); - let b = x.get("b").cloned().unwrap(); - let c = x.get("c#").cloned().unwrap(); + let x = engine.eval::(r#"let x = #{a: 1, b: true, "c$": "hello"}; x"#)?; + let a = x.get("a").cloned().expect("should have property a"); + let b = x.get("b").cloned().expect("should have property b"); + let c = x.get("c$").cloned().expect("should have property c$"); - assert_eq!(*a.downcast::().unwrap(), 1); - assert_eq!(*b.downcast::().unwrap(), true); - assert_eq!(*c.downcast::().unwrap(), "hello"); + assert_eq!(a.cast::(), 1); + assert_eq!(b.cast::(), true); + assert_eq!(c.cast::(), "hello"); Ok(()) } @@ -89,14 +89,14 @@ fn test_map_assign() -> Result<(), EvalAltResult> { fn test_map_return() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); - let x = engine.eval::(r#"#{a: 1, b: true, c: "hello"}"#)?; - let a = x.get("a").cloned().unwrap(); - let b = x.get("b").cloned().unwrap(); - let c = x.get("c").cloned().unwrap(); + let x = engine.eval::(r#"#{a: 1, b: true, "c$": "hello"}"#)?; + let a = x.get("a").cloned().expect("should have property a"); + let b = x.get("b").cloned().expect("should have property b"); + let c = x.get("c$").cloned().expect("should have property c$"); - assert_eq!(*a.downcast::().unwrap(), 1); - assert_eq!(*b.downcast::().unwrap(), true); - assert_eq!(*c.downcast::().unwrap(), "hello"); + assert_eq!(a.cast::(), 1); + assert_eq!(b.cast::(), true); + assert_eq!(c.cast::(), "hello"); Ok(()) }