From 760c13d36e5fb08946ca36996e6fe646cefb139c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 1 Jul 2020 22:21:43 +0800 Subject: [PATCH 01/46] Fix features. --- Cargo.toml | 2 +- RELEASES.md | 10 ++++++++++ doc/src/context.json | 2 +- src/engine.rs | 16 ++++++++++++++-- src/packages/fn_basic.rs | 3 ++- src/parser.rs | 3 +++ tests/data_size.rs | 3 +++ tests/for.rs | 2 +- tests/functions.rs | 1 + 9 files changed, 36 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2ffbd617..835fb830 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.16.0" +version = "0.16.1" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" diff --git a/RELEASES.md b/RELEASES.md index 6f38bb72..e60a0b1e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,11 +1,21 @@ Rhai Release Notes ================== +Version 0.16.1 +============== + +Bug fix release to fix errors when compiling with features. + + Version 0.16.0 ============== The major new feature in this version is OOP - well, poor man's OOP, that is. +The `README` is officially transferred to [The Rhai Book](https://schungx.github.io/rhai). + +An online [Playground](https://alvinhochun.github.io/rhai-demo/) is available. + Breaking changes ---------------- diff --git a/doc/src/context.json b/doc/src/context.json index 6da8d6bb..2cc332fb 100644 --- a/doc/src/context.json +++ b/doc/src/context.json @@ -1,5 +1,5 @@ { - "version": "0.16.0", + "version": "0.16.1", "rootUrl": "", "rootUrlX": "/rhai" } \ No newline at end of file diff --git a/src/engine.rs b/src/engine.rs index 65c91735..96514f48 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2407,7 +2407,13 @@ impl Engine { let mut maps = 0; arr.iter().for_each(|value| match value { - Dynamic(Union::Array(_)) | Dynamic(Union::Map(_)) => { + Dynamic(Union::Array(_)) => { + let (a, m, _) = calc_size(value); + arrays += a; + maps += m; + } + #[cfg(not(feature = "no_object"))] + Dynamic(Union::Map(_)) => { let (a, m, _) = calc_size(value); arrays += a; maps += m; @@ -2423,7 +2429,13 @@ impl Engine { let mut maps = 0; map.values().for_each(|value| match value { - Dynamic(Union::Array(_)) | Dynamic(Union::Map(_)) => { + #[cfg(not(feature = "no_index"))] + Dynamic(Union::Array(_)) => { + let (a, m, _) = calc_size(value); + arrays += a; + maps += m; + } + Dynamic(Union::Map(_)) => { let (a, m, _) = calc_size(value); arrays += a; maps += m; diff --git a/src/packages/fn_basic.rs b/src/packages/fn_basic.rs index 01807c9e..dde08fd5 100644 --- a/src/packages/fn_basic.rs +++ b/src/packages/fn_basic.rs @@ -3,6 +3,7 @@ use crate::fn_native::FnPtr; def_package!(crate:BasicFnPackage:"Basic Fn functions.", lib, { lib.set_fn_1_mut("name", |f: &mut FnPtr| Ok(f.get_fn_name().clone())); - lib.set_getter_fn("name", |f: &mut FnPtr| Ok(f.get_fn_name().clone())); + #[cfg(not(feature = "no_object"))] + lib.set_getter_fn("name", |f: &mut FnPtr| Ok(f.get_fn_name().clone())); }); diff --git a/src/parser.rs b/src/parser.rs index 906d99de..3ff3798f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2194,6 +2194,7 @@ fn parse_let( } /// Parse an import statement. +#[cfg(not(feature = "no_module"))] fn parse_import( input: &mut TokenStream, state: &mut ParseState, @@ -2444,6 +2445,8 @@ fn parse_stmt( Token::Let => parse_let(input, state, Normal, settings.level_up()), Token::Const => parse_let(input, state, Constant, settings.level_up()), + + #[cfg(not(feature = "no_module"))] Token::Import => parse_import(input, state, settings.level_up()), #[cfg(not(feature = "no_module"))] diff --git a/tests/data_size.rs b/tests/data_size.rs index e073d443..33257cfd 100644 --- a/tests/data_size.rs +++ b/tests/data_size.rs @@ -35,6 +35,7 @@ fn test_max_string_size() -> Result<(), Box> { EvalAltResult::ErrorDataTooLarge(_, 10, 13, _) )); + #[cfg(not(feature = "no_object"))] assert!(matches!( *engine .eval::( @@ -92,6 +93,8 @@ fn test_max_array_size() -> Result<(), Box> { .expect_err("should error"), EvalAltResult::ErrorDataTooLarge(_, 10, 12, _) )); + + #[cfg(not(feature = "no_object"))] assert!(matches!( *engine .eval::( diff --git a/tests/for.rs b/tests/for.rs index 4f99c06f..e3b561d6 100644 --- a/tests/for.rs +++ b/tests/for.rs @@ -39,7 +39,7 @@ fn test_for_string() -> Result<(), Box> { let sum = 0; for ch in s { - sum += ch.to_int(); + sum += to_int(ch); } sum diff --git a/tests/functions.rs b/tests/functions.rs index 49789da4..ed3eeb73 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -52,6 +52,7 @@ fn test_functions() -> Result<(), Box> { } #[test] +#[cfg(not(feature = "no_object"))] fn test_function_pointers() -> Result<(), Box> { let engine = Engine::new(); From a9c6014edc275b4d671e29ba419f0018bcfaed21 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 1 Jul 2020 23:14:11 +0800 Subject: [PATCH 02/46] Add vnext link. --- doc/src/about/index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/src/about/index.md b/doc/src/about/index.md index 85c55824..76603bde 100644 --- a/doc/src/about/index.md +++ b/doc/src/about/index.md @@ -5,3 +5,8 @@ What is 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. + + +This Book is for version {{version}} of Rhai. + +For the latest development version, see [here]({{rootUrl}}/vnext/). From a4af0b0e135ce4d50eac1a05c8aae3f9a5b8df10 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 1 Jul 2020 23:29:36 +0800 Subject: [PATCH 03/46] Bump version. --- Cargo.toml | 2 +- RELEASES.md | 4 ++++ doc/src/context.json | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 835fb830..4b1706ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.16.1" +version = "0.17.0" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" diff --git a/RELEASES.md b/RELEASES.md index e60a0b1e..c8821ac9 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,6 +1,10 @@ Rhai Release Notes ================== +Version 0.17.0 +============== + + Version 0.16.1 ============== diff --git a/doc/src/context.json b/doc/src/context.json index 2cc332fb..506b72e1 100644 --- a/doc/src/context.json +++ b/doc/src/context.json @@ -1,5 +1,6 @@ { - "version": "0.16.1", + "version": "0.17.0", "rootUrl": "", - "rootUrlX": "/rhai" + "rootUrlX": "/rhai", + "rootUrlXX": "/rhai/vnext" } \ No newline at end of file From 9c9f550200a9e8b939161934529f9c3dc104cdf4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 2 Jul 2020 21:46:08 +0800 Subject: [PATCH 04/46] Minor refactor. --- README.md | 2 +- doc/src/about/features.md | 2 +- src/error.rs | 14 ++++++-------- src/module.rs | 8 ++------ src/result.rs | 14 +++++++------- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 952d9bf3..681ae280 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Features * Freely pass Rust variables/constants into a script via an external [`Scope`](https://schungx.github.io/rhai/rust/scope.html). * Easily [call a script-defined function](https://schungx.github.io/rhai/engine/call-fn.html) from Rust. * Fairly low compile-time overhead. -* Fairly efficient evaluation (1 million iterations in 0.25 sec on a single core, 2.3 GHz Linux VM). +* Fairly efficient evaluation (1 million iterations in 0.4 sec on a single core, 2.3 GHz Linux VM). * Relatively little `unsafe` code (yes there are some for performance reasons, and most `unsafe` code is limited to one single source file, all with names starting with `"unsafe_"`). * Re-entrant scripting engine can be made `Send + Sync` (via the [`sync`] feature). diff --git a/doc/src/about/features.md b/doc/src/about/features.md index c012353a..0768115f 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -22,7 +22,7 @@ Fast * Fairly low compile-time overhead. -* Fairly efficient evaluation (1 million iterations in 0.25 sec on a single core, 2.3 GHz Linux VM). +* Fairly efficient evaluation (1 million iterations in 0.4 sec on a single core, 2.3 GHz Linux VM). * Scripts are [optimized][script optimization] (useful for template-based machine-generated scripts) for repeated evaluations. diff --git a/src/error.rs b/src/error.rs index 58423ef9..61f2c58f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -49,7 +49,7 @@ impl fmt::Display for LexError { "Length of string literal exceeds the maximum limit ({})", max ), - Self::ImproperSymbol(s) => write!(f, "{}", s), + Self::ImproperSymbol(s) => f.write_str(s), } } } @@ -185,18 +185,16 @@ impl fmt::Display for ParseErrorType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::BadInput(s) | ParseErrorType::MalformedCallExpr(s) => { - write!(f, "{}", if s.is_empty() { self.desc() } else { s }) + f.write_str(if s.is_empty() { self.desc() } else { s }) } Self::ForbiddenConstantExpr(s) => { write!(f, "Expecting a constant to assign to '{}'", s) } Self::UnknownOperator(s) => write!(f, "{}: '{}'", self.desc(), s), - Self::MalformedIndexExpr(s) => { - write!(f, "{}", if s.is_empty() { self.desc() } else { s }) - } + Self::MalformedIndexExpr(s) => f.write_str(if s.is_empty() { self.desc() } else { s }), - Self::MalformedInExpr(s) => write!(f, "{}", if s.is_empty() { self.desc() } else { s }), + Self::MalformedInExpr(s) => f.write_str(if s.is_empty() { self.desc() } else { s }), Self::DuplicatedProperty(s) => { write!(f, "Duplicated property '{}' for object map literal", s) @@ -222,12 +220,12 @@ impl fmt::Display for ParseErrorType { Self::MissingToken(token, s) => write!(f, "Expecting '{}' {}", token, s), - Self::AssignmentToConstant(s) if s.is_empty() => write!(f, "{}", self.desc()), + Self::AssignmentToConstant(s) if s.is_empty() => f.write_str(self.desc()), Self::AssignmentToConstant(s) => write!(f, "Cannot assign to constant '{}'", s), Self::LiteralTooLarge(typ, max) => { write!(f, "{} exceeds the maximum limit ({})", typ, max) } - _ => write!(f, "{}", self.desc()), + _ => f.write_str(self.desc()), } } } diff --git a/src/module.rs b/src/module.rs index 3cdeb711..c7198d83 100644 --- a/src/module.rs +++ b/src/module.rs @@ -876,12 +876,10 @@ impl Module { .functions .iter() .filter(|(_, (_, _, _, v))| match v { - CallableFunction::Pure(_) - | CallableFunction::Method(_) - | CallableFunction::Iterator(_) => true, CallableFunction::Script(ref f) => { filter(f.access, f.name.as_str(), f.params.len()) } + _ => true, }) .map(|(&k, v)| (k, v.clone())), ); @@ -897,10 +895,8 @@ impl Module { /// Filter out the functions, retaining only some based on a filter predicate. pub(crate) fn retain_functions(&mut self, filter: impl Fn(FnAccess, &str, usize) -> bool) { self.functions.retain(|_, (_, _, _, v)| match v { - CallableFunction::Pure(_) - | CallableFunction::Method(_) - | CallableFunction::Iterator(_) => true, CallableFunction::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()), + _ => true, }); self.all_functions.clear(); diff --git a/src/result.rs b/src/result.rs index 06bc80d3..d6351b25 100644 --- a/src/result.rs +++ b/src/result.rs @@ -197,16 +197,16 @@ impl fmt::Display for EvalAltResult { | Self::ErrorTooManyOperations(_) | Self::ErrorTooManyModules(_) | Self::ErrorStackOverflow(_) - | Self::ErrorTerminated(_) => write!(f, "{}", desc)?, + | Self::ErrorTerminated(_) => f.write_str(desc)?, - Self::ErrorRuntime(s, _) => write!(f, "{}", if s.is_empty() { desc } else { s })?, + Self::ErrorRuntime(s, _) => f.write_str(if s.is_empty() { desc } else { s })?, Self::ErrorAssignmentToConstant(s, _) => write!(f, "{}: '{}'", desc, s)?, Self::ErrorMismatchOutputType(s, _) => write!(f, "{}: {}", desc, s)?, - Self::ErrorArithmetic(s, _) => write!(f, "{}", s)?, + Self::ErrorArithmetic(s, _) => f.write_str(s)?, - Self::ErrorLoopBreak(_, _) => write!(f, "{}", desc)?, - Self::Return(_, _) => write!(f, "{}", desc)?, + Self::ErrorLoopBreak(_, _) => f.write_str(desc)?, + Self::Return(_, _) => f.write_str(desc)?, Self::ErrorBooleanArgMismatch(op, _) => { write!(f, "{} operator expects boolean operands", op)? @@ -215,7 +215,7 @@ impl fmt::Display for EvalAltResult { Self::ErrorArrayBounds(_, index, _) if *index < 0 => { write!(f, "{}: {} < 0", desc, index)? } - Self::ErrorArrayBounds(0, _, _) => write!(f, "{}", desc)?, + Self::ErrorArrayBounds(0, _, _) => f.write_str(desc)?, Self::ErrorArrayBounds(1, index, _) => write!( f, "Array index {} is out of bounds: only one element in the array", @@ -229,7 +229,7 @@ impl fmt::Display for EvalAltResult { Self::ErrorStringBounds(_, index, _) if *index < 0 => { write!(f, "{}: {} < 0", desc, index)? } - Self::ErrorStringBounds(0, _, _) => write!(f, "{}", desc)?, + Self::ErrorStringBounds(0, _, _) => f.write_str(desc)?, Self::ErrorStringBounds(1, index, _) => write!( f, "String index {} is out of bounds: only one character in the string", From bdc7b692665212894998daca2879303fac57baee Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 2 Jul 2020 22:16:09 +0800 Subject: [PATCH 05/46] Remove feature gate for Instant. --- src/any.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/any.rs b/src/any.rs index 7e8b8072..6a14cb4e 100644 --- a/src/any.rs +++ b/src/any.rs @@ -196,7 +196,6 @@ impl Dynamic { Union::FnPtr(_) => "Fn", #[cfg(not(feature = "no_std"))] - #[cfg(not(target_arch = "wasm32"))] Union::Variant(value) if value.is::() => "timestamp", Union::Variant(value) => (***value).type_name(), } @@ -220,7 +219,6 @@ impl fmt::Display for Dynamic { Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] - #[cfg(not(target_arch = "wasm32"))] Union::Variant(value) if value.is::() => write!(f, ""), Union::Variant(_) => write!(f, "?"), } @@ -244,7 +242,6 @@ impl fmt::Debug for Dynamic { Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] - #[cfg(not(target_arch = "wasm32"))] Union::Variant(value) if value.is::() => write!(f, ""), Union::Variant(_) => write!(f, ""), } From 1b7ffdf4083cd5f5b621520d0953dc5a6cc47ce4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 3 Jul 2020 10:45:01 +0800 Subject: [PATCH 06/46] Better type display. --- RELEASES.md | 4 ++++ src/any.rs | 21 +++++++++++++++++++++ src/api.rs | 10 ++++++---- src/engine.rs | 29 ++++++++++++++++++----------- src/result.rs | 14 ++++++++------ tests/mismatched_op.rs | 19 +++++++------------ 6 files changed, 64 insertions(+), 33 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index c8821ac9..57645f5f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,10 @@ Rhai Release Notes Version 0.17.0 ============== +Breaking changes +---------------- + +* `EvalAltResult::ErrorMismatchOutputType` has an extra argument containing the name of the requested type. Version 0.16.1 ============== diff --git a/src/any.rs b/src/any.rs index 6a14cb4e..0a89e57a 100644 --- a/src/any.rs +++ b/src/any.rs @@ -202,6 +202,27 @@ impl Dynamic { } } +/// Map the name of a standard type into a friendly form. +pub(crate) fn map_std_type_name(name: &str) -> &str { + if name == type_name::() { + "string" + } else if name == type_name::() { + "string" + } else if name == type_name::<&str>() { + "string" + } else if name == type_name::() { + "map" + } else if name == type_name::() { + "array" + } else if name == type_name::() { + "Fn" + } else if name == type_name::() { + "timestamp" + } else { + name + } +} + impl fmt::Display for Dynamic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { diff --git a/src/api.rs b/src/api.rs index 2f0e3e19..190e69db 100644 --- a/src/api.rs +++ b/src/api.rs @@ -976,11 +976,12 @@ impl Engine { let mut mods = Imports::new(); let (result, _) = self.eval_ast_with_scope_raw(scope, &mut mods, ast)?; - let return_type = self.map_type_name(result.type_name()); + let typ = self.map_type_name(result.type_name()); return result.try_cast::().ok_or_else(|| { Box::new(EvalAltResult::ErrorMismatchOutputType( - return_type.into(), + self.map_type_name(type_name::()).into(), + typ.into(), Position::none(), )) }); @@ -1123,11 +1124,12 @@ impl Engine { let mut arg_values = args.into_vec(); let result = self.call_fn_dynamic_raw(scope, ast, name, arg_values.as_mut())?; - let return_type = self.map_type_name(result.type_name()); + let typ = self.map_type_name(result.type_name()); return result.try_cast().ok_or_else(|| { Box::new(EvalAltResult::ErrorMismatchOutputType( - return_type.into(), + self.map_type_name(type_name::()).into(), + typ.into(), Position::none(), )) }); diff --git a/src/engine.rs b/src/engine.rs index 96514f48..4f6ec6f3 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,6 +1,6 @@ //! Main module defining the script evaluation `Engine`. -use crate::any::{Dynamic, Union, Variant}; +use crate::any::{map_std_type_name, Dynamic, Union, Variant}; use crate::calc_fn_hash; use crate::error::ParseErrorType; use crate::fn_native::{CallableFunction, Callback, FnCallArgs, FnPtr}; @@ -18,7 +18,7 @@ use crate::utils::StaticVec; use crate::parser::FLOAT; use crate::stdlib::{ - any::TypeId, + any::{type_name, TypeId}, borrow::Cow, boxed::Box, collections::HashMap, @@ -816,9 +816,10 @@ impl Engine { // See if the function match print/debug (which requires special processing) return Ok(match fn_name { KEYWORD_PRINT => ( - (self.print)(result.as_str().map_err(|type_name| { + (self.print)(result.as_str().map_err(|typ| { Box::new(EvalAltResult::ErrorMismatchOutputType( - type_name.into(), + self.map_type_name(type_name::()).into(), + typ.into(), Position::none(), )) })?) @@ -826,9 +827,10 @@ impl Engine { false, ), KEYWORD_DEBUG => ( - (self.debug)(result.as_str().map_err(|type_name| { + (self.debug)(result.as_str().map_err(|typ| { Box::new(EvalAltResult::ErrorMismatchOutputType( - type_name.into(), + self.map_type_name(type_name::()).into(), + typ.into(), Position::none(), )) })?) @@ -1064,8 +1066,12 @@ impl Engine { lib: &Module, script: &Dynamic, ) -> Result> { - let script = script.as_str().map_err(|type_name| { - EvalAltResult::ErrorMismatchOutputType(type_name.into(), Position::none()) + let script = script.as_str().map_err(|typ| { + EvalAltResult::ErrorMismatchOutputType( + self.map_type_name(type_name::()).into(), + typ.into(), + Position::none(), + ) })?; // Compile the script text @@ -1873,9 +1879,10 @@ impl Engine { self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; return arg_value .take_immutable_string() - .map_err(|type_name| { + .map_err(|typ| { Box::new(EvalAltResult::ErrorMismatchOutputType( - type_name.into(), + self.map_type_name(type_name::()).into(), + typ.into(), expr.position(), )) }) @@ -2524,7 +2531,7 @@ impl Engine { self.type_names .get(name) .map(String::as_str) - .unwrap_or(name) + .unwrap_or(map_std_type_name(name)) } } diff --git a/src/result.rs b/src/result.rs index d6351b25..7dd3d6fa 100644 --- a/src/result.rs +++ b/src/result.rs @@ -74,8 +74,8 @@ pub enum EvalAltResult { /// Assignment to a constant variable. ErrorAssignmentToConstant(String, Position), /// Returned type is not the same as the required output type. - /// Wrapped value is the type of the actual result. - ErrorMismatchOutputType(String, Position), + /// Wrapped values are the type requested and type of the actual result. + ErrorMismatchOutputType(String, String, Position), /// Inappropriate member access. ErrorDotExpr(String, Position), /// Arithmetic error encountered. Wrapped value is the error message. @@ -141,7 +141,7 @@ impl EvalAltResult { "Assignment to an unsupported left-hand side expression" } Self::ErrorAssignmentToConstant(_, _) => "Assignment to a constant variable", - Self::ErrorMismatchOutputType(_, _) => "Output type is incorrect", + Self::ErrorMismatchOutputType(_, _, _) => "Output type is incorrect", Self::ErrorInExpr(_) => "Malformed 'in' expression", Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorArithmetic(_, _) => "Arithmetic error", @@ -202,7 +202,9 @@ impl fmt::Display for EvalAltResult { Self::ErrorRuntime(s, _) => f.write_str(if s.is_empty() { desc } else { s })?, Self::ErrorAssignmentToConstant(s, _) => write!(f, "{}: '{}'", desc, s)?, - Self::ErrorMismatchOutputType(s, _) => write!(f, "{}: {}", desc, s)?, + Self::ErrorMismatchOutputType(r, s, _) => { + write!(f, "{} (expecting {}): {}", desc, s, r)? + } Self::ErrorArithmetic(s, _) => f.write_str(s)?, Self::ErrorLoopBreak(_, _) => f.write_str(desc)?, @@ -289,7 +291,7 @@ impl EvalAltResult { | Self::ErrorModuleNotFound(_, pos) | Self::ErrorAssignmentToUnknownLHS(pos) | Self::ErrorAssignmentToConstant(_, pos) - | Self::ErrorMismatchOutputType(_, pos) + | Self::ErrorMismatchOutputType(_, _, pos) | Self::ErrorInExpr(pos) | Self::ErrorDotExpr(_, pos) | Self::ErrorArithmetic(_, pos) @@ -329,7 +331,7 @@ impl EvalAltResult { | Self::ErrorModuleNotFound(_, pos) | Self::ErrorAssignmentToUnknownLHS(pos) | Self::ErrorAssignmentToConstant(_, pos) - | Self::ErrorMismatchOutputType(_, pos) + | Self::ErrorMismatchOutputType(_, _, pos) | Self::ErrorInExpr(pos) | Self::ErrorDotExpr(_, pos) | Self::ErrorArithmetic(_, pos) diff --git a/tests/mismatched_op.rs b/tests/mismatched_op.rs index b129f29b..1199bf94 100644 --- a/tests/mismatched_op.rs +++ b/tests/mismatched_op.rs @@ -6,14 +6,14 @@ fn test_mismatched_op() { assert!(matches!( *engine.eval::(r#""hello, " + "world!""#).expect_err("expects error"), - EvalAltResult::ErrorMismatchOutputType(err, _) if err == "string" + EvalAltResult::ErrorMismatchOutputType(need, actual, _) if need == std::any::type_name::() && actual == "string" )); } #[test] #[cfg(not(feature = "no_object"))] fn test_mismatched_op_custom_type() { - #[derive(Clone)] + #[derive(Debug, Clone)] struct TestStruct { x: INT, } @@ -28,19 +28,14 @@ fn test_mismatched_op_custom_type() { engine.register_type_with_name::("TestStruct"); engine.register_fn("new_ts", TestStruct::new); - let r = engine - .eval::("60 + new_ts()") - .expect_err("expects error"); - - #[cfg(feature = "only_i32")] assert!(matches!( - *r, - EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i32, TestStruct)" + *engine.eval::("60 + new_ts()").expect_err("should error"), + EvalAltResult::ErrorFunctionNotFound(err, _) if err == format!("+ ({}, TestStruct)", std::any::type_name::()) )); - #[cfg(not(feature = "only_i32"))] assert!(matches!( - *r, - EvalAltResult::ErrorFunctionNotFound(err, _) if err == "+ (i64, TestStruct)" + *engine.eval::("42").expect_err("should error"), + EvalAltResult::ErrorMismatchOutputType(need, actual, _) + if need == "TestStruct" && actual == std::any::type_name::() )); } From fa84e5c5023f705a96bca339391583737ae3ae4f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 3 Jul 2020 17:19:55 +0800 Subject: [PATCH 07/46] Add serde deserialization. --- Cargo.toml | 8 +- README.md | 1 + doc/src/SUMMARY.md | 5 +- doc/src/about/features.md | 2 + doc/src/links.md | 1 + doc/src/rust/serde.md | 63 +++++++ doc/src/start/features.md | 1 + src/engine.rs | 2 +- src/lib.rs | 7 + src/serde/de.rs | 375 ++++++++++++++++++++++++++++++++++++++ src/serde/mod.rs | 2 + src/serde/str.rs | 152 +++++++++++++++ tests/serde.rs | 108 +++++++++++ 13 files changed, 723 insertions(+), 4 deletions(-) create mode 100644 doc/src/rust/serde.md create mode 100644 src/serde/de.rs create mode 100644 src/serde/mod.rs create mode 100644 src/serde/str.rs create mode 100644 tests/serde.rs diff --git a/Cargo.toml b/Cargo.toml index 4b1706ae..ed5ba5b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] -default = [] +default = ["serde"] plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync @@ -65,5 +65,11 @@ default-features = false features = ["compile-time-rng"] optional = true +[dependencies.serde] +package = "serde" +version = "1.0.111" +features = ["derive"] +optional = true + [target.'cfg(target_arch = "wasm32")'.dependencies] instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant diff --git a/README.md b/README.md index 681ae280..c170099d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Features * Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html). * Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). +* Serialization/deserialization support via [serde](https://crates.io/crates/serde) * Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations. * Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html). diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index f9491dd3..a7c3946b 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -96,14 +96,15 @@ The Rhai Scripting Language 9. [Maximum Statement Depth](safety/max-stmt-depth.md) 8. [Advanced Topics](advanced.md) 1. [Object-Oriented Programming (OOP)](language/oop.md) - 2. [Script Optimization](engine/optimize/index.md) + 2. [Serialization/Deserialization of `Dynamic` with `serde`](rust/serde.md) + 3. [Script Optimization](engine/optimize/index.md) 1. [Optimization Levels](engine/optimize/optimize-levels.md) 2. [Re-Optimize an AST](engine/optimize/reoptimize.md) 3. [Eager Function Evaluation](engine/optimize/eager.md) 4. [Side-Effect Considerations](engine/optimize/side-effects.md) 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) - 3. [Eval Statement](language/eval.md) + 4. [Eval Statement](language/eval.md) 9. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) 2. [Operators](appendix/operators.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 0768115f..3f19bf63 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -39,6 +39,8 @@ Dynamic * Some support for [object-oriented programming (OOP)][OOP]. +* Serialization/deserialization support via [`serde`]. + Safe ---- diff --git a/doc/src/links.md b/doc/src/links.md index b7147a50..cf367206 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -29,6 +29,7 @@ [package]: {{rootUrl}}/rust/packages/index.md [packages]: {{rootUrl}}/rust/packages/index.md [`Scope`]: {{rootUrl}}/rust/scope.md +[`serde`]: {{rootUrl}}/rust/serde.md [`type_of()`]: {{rootUrl}}/language/type-of.md [`to_string()`]: {{rootUrl}}/language/values-and-types.md diff --git a/doc/src/rust/serde.md b/doc/src/rust/serde.md new file mode 100644 index 00000000..bf3743b2 --- /dev/null +++ b/doc/src/rust/serde.md @@ -0,0 +1,63 @@ +Serialization and Deserialization of `Dynamic` with `serde` +========================================================= + +{{#include ../links.md}} + +Rhai's [`Dynamic`] type supports serialization and deserialization by [`serde`](https://crates.io/crates/serde) +via the [`serde`][features] feature. + +A [`Dynamic`] can be seamlessly converted to and from a type that implements `serde::Serialize` and/or +`serde::Deserialize`. + + +Serialization +------------- + +Serialization by [`serde`](https://crates.io/crates/serde) is not yet implemented. + +It is simple to serialize a Rust type to `JSON` via `serde`, then use [`Engine::parse_json`]({{rootUrl}}/language/json.md) to convert +it into an [object map]. + + +Deserialization +--------------- + +The function `rhai::de::from_dynamic` automatically converts a [`Dynamic`] value into any Rust type +that implements `serde::Deserialize`. + +In particular, [object maps] are converted into Rust `struct`'s (or any type that is marked as +a `serde` map) while [arrays] are converted into Rust `Vec`'s (or any type that is marked +as a `serde` sequence). + +```rust +use rhai::{Engine, Dynamic}; +use rhai::de::from_dynamic; + +#[derive(Debug, serde::Deserialize)] +struct Point { + x: f64, + y: f64 +} + +#[derive(Debug, serde::Deserialize)] +struct MyStruct { + a: i64, + b: Vec, + c: bool, + d: Point +} + +let engine = Engine::new(); + +let result: Dynamic = engine.eval(r#" + #{ + a: 42, + b: [ "hello", "world" ], + c: true, + d: #{ x: 123.456, y: 999.0 } + } + "#)?; + +// Convert the 'Dynamic' object map into 'MyStruct' +let x: MyStruct = from_dynamic(&result)?; +``` diff --git a/doc/src/start/features.md b/doc/src/start/features.md index b4a415a9..77a17e10 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -24,6 +24,7 @@ more control over what a script can (or cannot) do. | `no_function` | Disable script-defined [functions]. | | `no_module` | Disable loading external [modules]. | | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | +| `serde` | Enable serialization/deserialization via [`serde`]. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | | `internals` | Expose internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version. | diff --git a/src/engine.rs b/src/engine.rs index 4f6ec6f3..27d35222 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -35,7 +35,7 @@ use crate::stdlib::{ #[cfg(not(feature = "no_index"))] pub type Array = Vec; -/// Hash map of `Dynamic` values with `String` keys. +/// Hash map of `Dynamic` values with `ImmutableString` keys. /// /// Not available under the `no_object` feature. #[cfg(not(feature = "no_object"))] diff --git a/src/lib.rs b/src/lib.rs index aa205dec..9d60ecea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,7 @@ //! | `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | //! | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | //! | `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, `Engine`, `Scope` and [`AST`] are all `Send + Sync`. | +//! | `serde` | Enable serialization/deserialization via [`serde`]. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | //! | `internals` | Expose internal data structures (beware they may be volatile from version to version). | //! //! See [The Rhai Book](https://schungx.github.io/rhai) for details on the Rhai script engine and language. @@ -86,6 +87,7 @@ pub mod packages; mod parser; mod result; mod scope; +mod serde; mod stdlib; mod token; mod r#unsafe; @@ -127,6 +129,11 @@ pub mod module_resolvers { pub use crate::module::resolvers::*; } +#[cfg(feature = "serde")] +pub mod de { + pub use crate::serde::de::from_dynamic; +} + #[cfg(not(feature = "no_optimize"))] pub use optimize::OptimizationLevel; diff --git a/src/serde/de.rs b/src/serde/de.rs new file mode 100644 index 00000000..63924dd1 --- /dev/null +++ b/src/serde/de.rs @@ -0,0 +1,375 @@ +use super::str::ImmutableStringDeserializer; +use crate::any::{Dynamic, Union}; +use crate::result::EvalAltResult; +use crate::token::Position; +use crate::utils::ImmutableString; + +use serde::de::{DeserializeSeed, Deserializer, Error, MapAccess, SeqAccess, Visitor}; +use serde::Deserialize; + +#[cfg(not(feature = "no_index"))] +use crate::engine::Array; +#[cfg(not(feature = "no_object"))] +use crate::engine::Map; + +use crate::stdlib::{any::type_name, fmt}; + +#[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +use crate::stdlib::time::Instant; + +#[cfg(not(feature = "no_std"))] +#[cfg(target_arch = "wasm32")] +use instant::Instant; + +pub struct DynamicDeserializer<'a> { + value: &'a Dynamic, +} + +impl<'a> DynamicDeserializer<'a> { + pub fn from_dynamic(value: &'a Dynamic) -> Self { + Self { value } + } + pub fn type_error(&self) -> Result> { + self.type_error_str(type_name::()) + } + pub fn type_error_str(&self, name: &str) -> Result> { + Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + name.into(), + self.value.type_name().into(), + Position::none(), + ))) + } +} + +pub fn from_dynamic<'de, T: Deserialize<'de>>( + value: &'de Dynamic, +) -> Result> { + T::deserialize(&mut DynamicDeserializer::from_dynamic(value)) +} + +impl Error for Box { + fn custom(err: T) -> Self { + Box::new(EvalAltResult::ErrorRuntime( + err.to_string(), + Position::none(), + )) + } +} + +impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { + type Error = Box; + + fn deserialize_any>(self, visitor: V) -> Result> { + match &self.value.0 { + Union::Unit(_) => self.deserialize_unit(visitor), + Union::Bool(_) => self.deserialize_bool(visitor), + Union::Str(_) => self.deserialize_str(visitor), + Union::Char(_) => self.deserialize_char(visitor), + #[cfg(not(feature = "only_i32"))] + Union::Int(_) => self.deserialize_i64(visitor), + #[cfg(feature = "only_i32")] + Union::Int(_) => self.deserialize_i32(visitor), + #[cfg(not(feature = "no_float"))] + Union::Float(_) => self.deserialize_f64(visitor), + #[cfg(not(feature = "no_index"))] + Union::Array(_) => self.deserialize_seq(visitor), + #[cfg(not(feature = "no_object"))] + Union::Map(_) => self.deserialize_map(visitor), + Union::FnPtr(_) => unimplemented!(), + + #[cfg(not(feature = "no_std"))] + Union::Variant(value) if value.is::() => unimplemented!(), + + Union::Variant(value) if value.is::() => self.deserialize_i8(visitor), + Union::Variant(value) if value.is::() => self.deserialize_i16(visitor), + Union::Variant(value) if value.is::() => self.deserialize_i32(visitor), + Union::Variant(value) if value.is::() => self.deserialize_i64(visitor), + Union::Variant(value) if value.is::() => self.deserialize_u8(visitor), + Union::Variant(value) if value.is::() => self.deserialize_u16(visitor), + Union::Variant(value) if value.is::() => self.deserialize_u32(visitor), + Union::Variant(value) if value.is::() => self.deserialize_u64(visitor), + + Union::Variant(_) => self.type_error_str("any"), + } + } + + fn deserialize_bool>(self, visitor: V) -> Result> { + visitor.visit_bool( + self.value + .as_bool() + .or_else(|_| self.type_error::())?, + ) + } + + fn deserialize_i8>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_i8(x)) + } + + fn deserialize_i16>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_i16(x)) + } + + fn deserialize_i32>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_i32(x)) + } + + fn deserialize_i64>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_i64(x)) + } + + fn deserialize_u8>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_u8(x)) + } + + fn deserialize_u16>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_u16(x)) + } + + fn deserialize_u32>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_u32(x)) + } + + fn deserialize_u64>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_u64(x)) + } + + fn deserialize_f32>(self, visitor: V) -> Result> { + #[cfg(not(feature = "no_float"))] + { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_f32(x)) + } + #[cfg(feature = "no_float")] + self.type_error_str("f32") + } + + fn deserialize_f64>(self, visitor: V) -> Result> { + #[cfg(not(feature = "no_float"))] + { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_f64(x)) + } + #[cfg(feature = "no_float")] + self.type_error_str("f64") + } + + fn deserialize_char>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_char(x)) + } + + fn deserialize_str>(self, visitor: V) -> Result> { + self.value.downcast_ref::().map_or_else( + || self.type_error::(), + |x| visitor.visit_borrowed_str(x.as_str()), + ) + } + + fn deserialize_string>( + self, + visitor: V, + ) -> Result> { + self.deserialize_str(visitor) + } + + fn deserialize_bytes>(self, _: V) -> Result> { + self.type_error_str("bytes array") + } + + fn deserialize_byte_buf>(self, _: V) -> Result> { + self.type_error_str("bytes array") + } + + fn deserialize_option>(self, _: V) -> Result> { + self.type_error_str("bytes array") + } + + fn deserialize_unit>(self, visitor: V) -> Result> { + self.value + .downcast_ref::<()>() + .map_or_else(|| self.type_error::<(), _>(), |_| visitor.visit_unit()) + } + + fn deserialize_unit_struct>( + self, + _name: &'static str, + visitor: V, + ) -> Result> { + self.deserialize_unit(visitor) + } + + fn deserialize_newtype_struct>( + self, + _name: &'static str, + visitor: V, + ) -> Result> { + visitor.visit_newtype_struct(self) + } + + fn deserialize_seq>(self, visitor: V) -> Result> { + #[cfg(not(feature = "no_index"))] + { + self.value.downcast_ref::().map_or_else( + || self.type_error::(), + |arr| visitor.visit_seq(IterateArray::new(arr.iter())), + ) + } + #[cfg(feature = "no_index")] + self.type_error_str("array") + } + + fn deserialize_tuple>( + self, + _len: usize, + visitor: V, + ) -> Result> { + self.deserialize_seq(visitor) + } + + fn deserialize_tuple_struct>( + self, + _name: &'static str, + _len: usize, + visitor: V, + ) -> Result> { + self.deserialize_seq(visitor) + } + + fn deserialize_map>(self, visitor: V) -> Result> { + #[cfg(not(feature = "no_object"))] + { + self.value.downcast_ref::().map_or_else( + || self.type_error::(), + |map| visitor.visit_map(IterateMap::new(map.keys(), map.values())), + ) + } + #[cfg(feature = "no_object")] + self.type_error_str("map") + } + + fn deserialize_struct>( + self, + _name: &'static str, + _fields: &'static [&'static str], + visitor: V, + ) -> Result> { + self.deserialize_map(visitor) + } + + fn deserialize_enum>( + self, + _name: &'static str, + _variants: &'static [&'static str], + _: V, + ) -> Result> { + self.type_error_str("num") + } + + fn deserialize_identifier>( + self, + visitor: V, + ) -> Result> { + self.deserialize_str(visitor) + } + + fn deserialize_ignored_any>( + self, + visitor: V, + ) -> Result> { + self.deserialize_any(visitor) + } +} + +struct IterateArray<'a, ITER: Iterator> { + iter: ITER, +} + +impl<'a, ITER: Iterator> IterateArray<'a, ITER> { + pub fn new(iter: ITER) -> Self { + Self { iter } + } +} + +impl<'a: 'de, 'de, ITER: Iterator> SeqAccess<'de> for IterateArray<'a, ITER> { + type Error = Box; + + fn next_element_seed>( + &mut self, + seed: T, + ) -> Result, Box> { + match self.iter.next() { + None => Ok(None), + Some(item) => seed + .deserialize(&mut DynamicDeserializer::from_dynamic(item)) + .map(Some), + } + } +} + +struct IterateMap< + 'a, + KEYS: Iterator, + VALUES: Iterator, +> { + keys: KEYS, + values: VALUES, +} + +impl<'a, KEYS: Iterator, VALUES: Iterator> + IterateMap<'a, KEYS, VALUES> +{ + pub fn new(keys: KEYS, values: VALUES) -> Self { + Self { keys, values } + } +} + +impl< + 'a: 'de, + 'de, + KEYS: Iterator, + VALUES: Iterator, + > MapAccess<'de> for IterateMap<'a, KEYS, VALUES> +{ + type Error = Box; + + fn next_key_seed>( + &mut self, + seed: K, + ) -> Result, Box> { + match self.keys.next() { + None => Ok(None), + Some(item) => seed + .deserialize(&mut ImmutableStringDeserializer::from_str(item)) + .map(Some), + } + } + + fn next_value_seed>( + &mut self, + seed: V, + ) -> Result> { + seed.deserialize(&mut DynamicDeserializer::from_dynamic( + self.values.next().unwrap(), + )) + } +} diff --git a/src/serde/mod.rs b/src/serde/mod.rs new file mode 100644 index 00000000..a4cf45de --- /dev/null +++ b/src/serde/mod.rs @@ -0,0 +1,2 @@ +pub mod de; +mod str; diff --git a/src/serde/str.rs b/src/serde/str.rs new file mode 100644 index 00000000..131cc0fe --- /dev/null +++ b/src/serde/str.rs @@ -0,0 +1,152 @@ +use crate::result::EvalAltResult; +use crate::token::Position; +use crate::utils::ImmutableString; + +use serde::de::{Deserializer, Visitor}; + +use crate::stdlib::any::type_name; + +pub struct ImmutableStringDeserializer<'a> { + value: &'a ImmutableString, +} + +impl<'a> ImmutableStringDeserializer<'a> { + pub fn from_str(value: &'a ImmutableString) -> Self { + Self { value } + } + pub fn type_error(&self) -> Result> { + self.type_error_str(type_name::()) + } + pub fn type_error_str(&self, name: &str) -> Result> { + Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + name.into(), + "string".into(), + Position::none(), + ))) + } +} + +impl<'de> Deserializer<'de> for &mut ImmutableStringDeserializer<'de> { + type Error = Box; + + fn deserialize_any>(self, v: V) -> Result> { + self.deserialize_str(v) + } + fn deserialize_bool>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_i8>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_i16>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_i32>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_i64>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_u8>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_u16>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_u32>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_u64>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_f32>(self, _: V) -> Result> { + self.type_error_str("f32") + } + fn deserialize_f64>(self, _: V) -> Result> { + self.type_error_str("f64") + } + fn deserialize_char>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_str>(self, v: V) -> Result> { + v.visit_borrowed_str(self.value.as_str()) + } + fn deserialize_string>( + self, + visitor: V, + ) -> Result> { + self.deserialize_str(visitor) + } + fn deserialize_bytes>(self, _: V) -> Result> { + self.type_error_str("bytes array") + } + fn deserialize_byte_buf>(self, _: V) -> Result> { + self.type_error_str("bytes array") + } + fn deserialize_option>(self, _: V) -> Result> { + self.type_error_str("option") + } + fn deserialize_unit>(self, _: V) -> Result> { + self.type_error::<(), _>() + } + fn deserialize_unit_struct>( + self, + _name: &'static str, + v: V, + ) -> Result> { + self.deserialize_unit(v) + } + fn deserialize_newtype_struct>( + self, + _name: &'static str, + v: V, + ) -> Result> { + v.visit_newtype_struct(self) + } + fn deserialize_seq>(self, _: V) -> Result> { + self.type_error_str("array") + } + fn deserialize_tuple>( + self, + _len: usize, + v: V, + ) -> Result> { + self.deserialize_seq(v) + } + fn deserialize_tuple_struct>( + self, + _name: &'static str, + _len: usize, + v: V, + ) -> Result> { + self.deserialize_seq(v) + } + fn deserialize_map>(self, _: V) -> Result> { + self.type_error_str("map") + } + fn deserialize_struct>( + self, + _name: &'static str, + _fields: &'static [&'static str], + v: V, + ) -> Result> { + self.deserialize_map(v) + } + fn deserialize_enum>( + self, + _name: &'static str, + _variants: &'static [&'static str], + _: V, + ) -> Result> { + self.type_error_str("enum") + } + fn deserialize_identifier>(self, v: V) -> Result> { + self.deserialize_str(v) + } + fn deserialize_ignored_any>( + self, + v: V, + ) -> Result> { + self.deserialize_any(v) + } +} diff --git a/tests/serde.rs b/tests/serde.rs new file mode 100644 index 00000000..daf712f0 --- /dev/null +++ b/tests/serde.rs @@ -0,0 +1,108 @@ +#![cfg(feature = "serde")] + +use rhai::{de::from_dynamic, Dynamic, Engine, EvalAltResult, INT}; +use serde::Deserialize; + +#[cfg(not(feature = "no_index"))] +use rhai::Array; +#[cfg(not(feature = "no_object"))] +use rhai::Map; + +#[test] +fn test_serde_de_primary_types() { + assert_eq!(42_u16, from_dynamic(&Dynamic::from(42_u16)).unwrap()); + assert_eq!(42 as INT, from_dynamic(&(42 as INT).into()).unwrap()); + assert_eq!(true, from_dynamic(&true.into()).unwrap()); + assert_eq!((), from_dynamic(&().into()).unwrap()); + + #[cfg(not(feature = "no_float"))] + { + assert_eq!(123.456_f64, from_dynamic(&123.456_f64.into()).unwrap()); + assert_eq!( + 123.456_f32, + from_dynamic(&Dynamic::from(123.456_f32)).unwrap() + ); + } + + assert_eq!( + "hello", + from_dynamic::(&"hello".to_string().into()).unwrap() + ); +} + +#[test] +#[cfg(not(feature = "no_index"))] +fn test_serde_de_array() { + let arr: Vec = vec![123, 456, 42, 999]; + assert_eq!(arr, from_dynamic::>(&arr.clone().into()).unwrap()); +} + +#[test] +fn test_serde_de_struct() { + #[derive(Debug, Deserialize, PartialEq)] + struct Hello { + a: INT, + b: bool, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct Test { + int: u32, + seq: Vec, + obj: Hello, + } + + let mut map = Map::new(); + map.insert("int".into(), Dynamic::from(42_u32)); + + let mut map2 = Map::new(); + map2.insert("a".into(), (123 as INT).into()); + map2.insert("b".into(), true.into()); + + map.insert("obj".into(), map2.into()); + + let arr: Array = vec!["hello".into(), "kitty".into(), "world".into()]; + map.insert("seq".into(), arr.into()); + + let expected = Test { + int: 42, + seq: vec!["hello".into(), "kitty".into(), "world".into()], + obj: Hello { a: 123, b: true }, + }; + assert_eq!(expected, from_dynamic(&map.into()).unwrap()); +} + +#[test] +fn test_serde_de_script() -> Result<(), Box> { + #[derive(Debug, Deserialize)] + struct Point { + x: f64, + y: f64, + } + + #[derive(Debug, Deserialize)] + struct MyStruct { + a: i64, + b: Vec, + c: bool, + d: Point, + } + + let engine = Engine::new(); + + let result: Dynamic = engine.eval( + r#" + #{ + a: 42, + b: [ "hello", "world" ], + c: true, + d: #{ x: 123.456, y: 999.0 } + } + "#, + )?; + + // Convert the 'Dynamic' object map into 'MyStruct' + let x: MyStruct = from_dynamic(&result)?; + + Ok(()) +} From 78c94daf4611a6cea783182e565ba63226b596f9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 3 Jul 2020 22:42:56 +0800 Subject: [PATCH 08/46] Add ser::to_dynamic. --- doc/src/rust/serde.md | 42 +++- src/any.rs | 13 +- src/lib.rs | 4 + src/parser.rs | 2 +- src/serde/de.rs | 4 +- src/serde/mod.rs | 1 + src/serde/ser.rs | 517 ++++++++++++++++++++++++++++++++++++++++++ tests/serde.rs | 126 ++++++++-- 8 files changed, 678 insertions(+), 31 deletions(-) create mode 100644 src/serde/ser.rs diff --git a/doc/src/rust/serde.md b/doc/src/rust/serde.md index bf3743b2..5b12ddc6 100644 --- a/doc/src/rust/serde.md +++ b/doc/src/rust/serde.md @@ -13,10 +13,46 @@ A [`Dynamic`] can be seamlessly converted to and from a type that implements `se Serialization ------------- -Serialization by [`serde`](https://crates.io/crates/serde) is not yet implemented. +While it is simple to serialize a Rust type to `JSON` via `serde`, +then use [`Engine::parse_json`]({{rootUrl}}/language/json.md) to convert it into an [object map], +Rhai supports serializing a [`Dynamic`] directly via `serde` without going through the `JSON` step. -It is simple to serialize a Rust type to `JSON` via `serde`, then use [`Engine::parse_json`]({{rootUrl}}/language/json.md) to convert -it into an [object map]. +The function `rhai::see::to_dynamic` automatically converts any Rust type that implements `serde::Serialize` +into a [`Dynamic`]. + +In particular, Rust `struct`'s (or any type that is marked as a `serde` map) are converted into [object maps] +while Rust `Vec`'s (or any type that is marked as a `serde` sequence) are converted into [arrays]. + +```rust +use rhai::{Dynamic, Map}; +use rhai::ser::to_dynamic; + +#[derive(Debug, serde::Serialize)] +struct Point { + x: f64, + y: f64 +} + +#[derive(Debug, serde::Serialize)] +struct MyStruct { + a: i64, + b: Vec, + c: bool, + d: Point +} + +let x = MyStruct { + a: 42, + b: vec![ "hello".into(), "world".into() ], + c: true, + d: Point { x: 123.456, y: 999.0 } +}; + +// Convert the 'MyStruct' into a 'Dynamic' +let map: Dynamic = to_dynamic(x); + +map.is::() == true; +``` Deserialization diff --git a/src/any.rs b/src/any.rs index 0a89e57a..2a2ae5a9 100644 --- a/src/any.rs +++ b/src/any.rs @@ -210,15 +210,20 @@ pub(crate) fn map_std_type_name(name: &str) -> &str { "string" } else if name == type_name::<&str>() { "string" - } else if name == type_name::() { - "map" - } else if name == type_name::() { - "array" } else if name == type_name::() { "Fn" } else if name == type_name::() { "timestamp" } else { + #[cfg(not(feature = "no_index"))] + if name == type_name::() { + return "array"; + } + #[cfg(not(feature = "no_object"))] + if name == type_name::() { + return "map"; + } + name } } diff --git a/src/lib.rs b/src/lib.rs index 9d60ecea..77c39460 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,6 +129,10 @@ pub mod module_resolvers { pub use crate::module::resolvers::*; } +#[cfg(feature = "serde")] +pub mod ser { + pub use crate::serde::ser::to_dynamic; +} #[cfg(feature = "serde")] pub mod de { pub use crate::serde::de::from_dynamic; diff --git a/src/parser.rs b/src/parser.rs index 3ff3798f..8dc1af84 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1701,7 +1701,7 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result Expr::Dot(Box::new((lhs, func, op_pos))), // lhs.rhs - _ => unreachable!(), + (lhs, rhs) => return Err(PERR::PropertyExpected.into_err(rhs.position())), }) } diff --git a/src/serde/de.rs b/src/serde/de.rs index 63924dd1..2b4b7bd9 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -26,8 +26,8 @@ pub struct DynamicDeserializer<'a> { value: &'a Dynamic, } -impl<'a> DynamicDeserializer<'a> { - pub fn from_dynamic(value: &'a Dynamic) -> Self { +impl<'de> DynamicDeserializer<'de> { + pub fn from_dynamic(value: &'de Dynamic) -> Self { Self { value } } pub fn type_error(&self) -> Result> { diff --git a/src/serde/mod.rs b/src/serde/mod.rs index a4cf45de..9d2e0281 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -1,2 +1,3 @@ pub mod de; +pub mod ser; mod str; diff --git a/src/serde/ser.rs b/src/serde/ser.rs new file mode 100644 index 00000000..95471e33 --- /dev/null +++ b/src/serde/ser.rs @@ -0,0 +1,517 @@ +use crate::any::Dynamic; +use crate::result::EvalAltResult; +use crate::token::Position; + +#[cfg(not(feature = "no_index"))] +use crate::engine::Array; +#[cfg(not(feature = "no_object"))] +use crate::engine::Map; + +use serde::ser::{ + Error, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, + SerializeTupleStruct, SerializeTupleVariant, Serializer, +}; +use serde::Serialize; + +use crate::stdlib::{any::type_name, fmt, mem}; + +pub struct DynamicSerializer { + key: Dynamic, + value: Dynamic, +} + +impl DynamicSerializer { + pub fn new(value: Dynamic) -> Self { + Self { + key: Default::default(), + value, + } + } + pub fn type_error(&self) -> Result> { + self.type_error_str(type_name::()) + } + pub fn type_error_str(&self, name: &str) -> Result> { + Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + name.into(), + self.value.type_name().into(), + Position::none(), + ))) + } +} + +pub fn to_dynamic(value: T) -> Result> { + let mut s = DynamicSerializer::new(Default::default()); + value.serialize(&mut s) +} + +impl Error for Box { + fn custom(err: T) -> Self { + Box::new(EvalAltResult::ErrorRuntime( + err.to_string(), + Position::none(), + )) + } +} + +impl Serializer for &mut DynamicSerializer { + type Ok = Dynamic; + type Error = Box; + type SerializeSeq = DynamicSerializer; + type SerializeTuple = DynamicSerializer; + type SerializeTupleStruct = DynamicSerializer; + type SerializeTupleVariant = DynamicSerializer; + type SerializeMap = DynamicSerializer; + type SerializeStruct = DynamicSerializer; + type SerializeStructVariant = DynamicSerializer; + + fn serialize_bool(self, v: bool) -> Result> { + Ok(v.into()) + } + + fn serialize_i8(self, v: i8) -> Result> { + #[cfg(not(feature = "only_i32"))] + return self.serialize_i64(i64::from(v)); + #[cfg(feature = "only_i32")] + return self.serialize_i32(i32::from(v)); + } + + fn serialize_i16(self, v: i16) -> Result> { + #[cfg(not(feature = "only_i32"))] + return self.serialize_i64(i64::from(v)); + #[cfg(feature = "only_i32")] + return self.serialize_i32(i32::from(v)); + } + + fn serialize_i32(self, v: i32) -> Result> { + #[cfg(not(feature = "only_i32"))] + return self.serialize_i64(i64::from(v)); + #[cfg(feature = "only_i32")] + return Ok(v.into()); + } + + fn serialize_i64(self, v: i64) -> Result> { + #[cfg(not(feature = "only_i32"))] + return Ok(v.into()); + #[cfg(feature = "only_i32")] + if v > i32::MAX as i64 { + return Ok(Dynamic::from(v)); + } else { + return self.serialize_i32(v as i32); + } + } + + fn serialize_u8(self, v: u8) -> Result> { + #[cfg(not(feature = "only_i32"))] + return self.serialize_i64(i64::from(v)); + #[cfg(feature = "only_i32")] + return self.serialize_i32(i32::from(v)); + } + + fn serialize_u16(self, v: u16) -> Result> { + #[cfg(not(feature = "only_i32"))] + return self.serialize_i64(i64::from(v)); + #[cfg(feature = "only_i32")] + return self.serialize_i32(i32::from(v)); + } + + fn serialize_u32(self, v: u32) -> Result> { + #[cfg(not(feature = "only_i32"))] + return self.serialize_i64(i64::from(v)); + #[cfg(feature = "only_i32")] + if v > i32::MAX as u64 { + return Ok(Dynamic::from(v)); + } else { + return self.serialize_i32(v as i32); + } + } + + fn serialize_u64(self, v: u64) -> Result> { + #[cfg(not(feature = "only_i32"))] + if v > i64::MAX as u64 { + return Ok(Dynamic::from(v)); + } else { + return self.serialize_i64(v as i64); + } + #[cfg(feature = "only_i32")] + if v > i32::MAX as u64 { + return Ok(Dynamic::from(v)); + } else { + return self.serialize_i32(v as i32); + } + } + + fn serialize_f32(self, v: f32) -> Result> { + #[cfg(not(feature = "no_float"))] + return Ok(Dynamic::from(v)); + #[cfg(feature = "no_float")] + return self.type_error_str("f32"); + } + + fn serialize_f64(self, v: f64) -> Result> { + #[cfg(not(feature = "no_float"))] + return Ok(v.into()); + #[cfg(feature = "no_float")] + return self.type_error_str("f64"); + } + + fn serialize_char(self, v: char) -> Result> { + Ok(v.into()) + } + + fn serialize_str(self, v: &str) -> Result> { + Ok(v.to_string().into()) + } + + fn serialize_bytes(self, _: &[u8]) -> Result> { + self.type_error_str("bytes array") + } + + fn serialize_none(self) -> Result> { + Ok(().into()) + } + + fn serialize_some( + self, + value: &T, + ) -> Result> { + value.serialize(&mut *self) + } + + fn serialize_unit(self) -> Result> { + Ok(().into()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result> { + self.serialize_unit() + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result> { + self.serialize_str(variant) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result> { + value.serialize(&mut *self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + value: &T, + ) -> Result> { + value.serialize(&mut *self) + } + + fn serialize_seq(self, _len: Option) -> Result> { + #[cfg(not(feature = "no_index"))] + return Ok(DynamicSerializer::new(Array::new().into())); + #[cfg(feature = "no_index")] + return self.type_error_str("array"); + } + + fn serialize_tuple(self, len: usize) -> Result> { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + len: usize, + ) -> Result> { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + len: usize, + ) -> Result> { + self.serialize_seq(Some(len)) + } + + fn serialize_map(self, _len: Option) -> Result> { + #[cfg(not(feature = "no_object"))] + return Ok(DynamicSerializer::new(Map::new().into())); + #[cfg(feature = "no_object")] + return self.type_error_str("map"); + } + + fn serialize_struct( + self, + _name: &'static str, + len: usize, + ) -> Result> { + self.serialize_map(Some(len)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + len: usize, + ) -> Result> { + self.serialize_map(Some(len)) + } +} + +impl SerializeSeq for DynamicSerializer { + type Ok = Dynamic; + type Error = Box; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Box> { + #[cfg(not(feature = "no_index"))] + { + let value = value.serialize(&mut *self)?; + if let Some(arr) = self.value.downcast_mut::() { + arr.push(value); + Ok(()) + } else { + self.type_error::() + } + } + #[cfg(feature = "no_index")] + self.type_error_str("array") + } + + // Close the sequence. + fn end(self) -> Result> { + #[cfg(not(feature = "no_index"))] + return Ok(self.value); + #[cfg(feature = "no_index")] + return self.type_error_str("array"); + } +} + +impl SerializeTuple for DynamicSerializer { + type Ok = Dynamic; + type Error = Box; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Box> { + #[cfg(not(feature = "no_index"))] + { + let value = value.serialize(&mut *self)?; + if let Some(arr) = self.value.downcast_mut::() { + arr.push(value); + Ok(()) + } else { + self.type_error::() + } + } + #[cfg(feature = "no_index")] + self.type_error_str("array") + } + + fn end(self) -> Result> { + #[cfg(not(feature = "no_index"))] + return Ok(self.value); + #[cfg(feature = "no_index")] + return self.type_error_str("array"); + } +} + +impl SerializeTupleStruct for DynamicSerializer { + type Ok = Dynamic; + type Error = Box; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Box> { + #[cfg(not(feature = "no_index"))] + { + let value = value.serialize(&mut *self)?; + if let Some(arr) = self.value.downcast_mut::() { + arr.push(value); + Ok(()) + } else { + self.type_error::() + } + } + #[cfg(feature = "no_index")] + self.type_error_str("array") + } + + fn end(self) -> Result> { + #[cfg(not(feature = "no_index"))] + return Ok(self.value); + #[cfg(feature = "no_index")] + return self.type_error_str("array"); + } +} + +impl SerializeTupleVariant for DynamicSerializer { + type Ok = Dynamic; + type Error = Box; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Box> { + #[cfg(not(feature = "no_index"))] + { + let value = value.serialize(&mut *self)?; + if let Some(arr) = self.value.downcast_mut::() { + arr.push(value); + Ok(()) + } else { + self.type_error::() + } + } + #[cfg(feature = "no_index")] + self.type_error_str("array") + } + + fn end(self) -> Result> { + #[cfg(not(feature = "no_index"))] + return Ok(self.value); + #[cfg(feature = "no_index")] + return self.type_error_str("array"); + } +} + +impl SerializeMap for DynamicSerializer { + type Ok = Dynamic; + type Error = Box; + + fn serialize_key(&mut self, key: &T) -> Result<(), Box> { + #[cfg(not(feature = "no_object"))] + { + self.key = key.serialize(&mut *self)?; + Ok(()) + } + #[cfg(feature = "no_object")] + self.type_error_str("map") + } + + fn serialize_value( + &mut self, + value: &T, + ) -> Result<(), Box> { + #[cfg(not(feature = "no_object"))] + { + let key = mem::take(&mut self.key) + .take_immutable_string() + .or_else(|_| self.type_error::())?; + let value = value.serialize(&mut *self)?; + if let Some(map) = self.value.downcast_mut::() { + map.insert(key, value); + Ok(()) + } else { + self.type_error::() + } + } + #[cfg(feature = "no_object")] + self.type_error_str("map") + } + + fn serialize_entry( + &mut self, + key: &K, + value: &T, + ) -> Result<(), Box> { + #[cfg(not(feature = "no_object"))] + { + let key: Dynamic = key.serialize(&mut *self)?; + let key = key + .take_immutable_string() + .or_else(|_| self.type_error::())?; + let value = value.serialize(&mut *self)?; + if let Some(map) = self.value.downcast_mut::() { + map.insert(key, value); + Ok(()) + } else { + self.type_error::() + } + } + #[cfg(feature = "no_object")] + self.type_error_str("map") + } + + fn end(self) -> Result> { + #[cfg(not(feature = "no_object"))] + return Ok(self.value); + #[cfg(feature = "no_object")] + return self.type_error_str("map"); + } +} + +impl SerializeStruct for DynamicSerializer { + type Ok = Dynamic; + type Error = Box; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Box> { + #[cfg(not(feature = "no_object"))] + { + let value = value.serialize(&mut *self)?; + if let Some(map) = self.value.downcast_mut::() { + map.insert(key.into(), value); + Ok(()) + } else { + self.type_error::() + } + } + #[cfg(feature = "no_object")] + self.type_error_str("map") + } + + fn end(self) -> Result> { + #[cfg(not(feature = "no_object"))] + return Ok(self.value); + #[cfg(feature = "no_object")] + return self.type_error_str("map"); + } +} + +impl SerializeStructVariant for DynamicSerializer { + type Ok = Dynamic; + type Error = Box; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Box> { + #[cfg(not(feature = "no_object"))] + { + let value = value.serialize(&mut *self)?; + if let Some(map) = self.value.downcast_mut::() { + map.insert(key.into(), value); + Ok(()) + } else { + self.type_error::() + } + } + #[cfg(feature = "no_object")] + self.type_error_str("map") + } + + fn end(self) -> Result> { + #[cfg(not(feature = "no_object"))] + return Ok(self.value); + #[cfg(feature = "no_object")] + return self.type_error_str("map"); + } +} diff --git a/tests/serde.rs b/tests/serde.rs index daf712f0..82ec330b 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -1,7 +1,7 @@ #![cfg(feature = "serde")] -use rhai::{de::from_dynamic, Dynamic, Engine, EvalAltResult, INT}; -use serde::Deserialize; +use rhai::{de::from_dynamic, ser::to_dynamic, Dynamic, Engine, EvalAltResult, INT}; +use serde::{Deserialize, Serialize}; #[cfg(not(feature = "no_index"))] use rhai::Array; @@ -9,36 +9,115 @@ use rhai::Array; use rhai::Map; #[test] -fn test_serde_de_primary_types() { - assert_eq!(42_u16, from_dynamic(&Dynamic::from(42_u16)).unwrap()); - assert_eq!(42 as INT, from_dynamic(&(42 as INT).into()).unwrap()); - assert_eq!(true, from_dynamic(&true.into()).unwrap()); - assert_eq!((), from_dynamic(&().into()).unwrap()); +fn test_serde_ser_primary_types() -> Result<(), Box> { + assert_eq!( + to_dynamic(42_u64)?.type_name(), + std::any::type_name::() + ); + assert_eq!(to_dynamic(u64::MAX)?.type_name(), "u64"); + assert_eq!( + to_dynamic(42 as INT)?.type_name(), + std::any::type_name::() + ); + assert_eq!(to_dynamic(true)?.type_name(), "bool"); + assert_eq!(to_dynamic(())?.type_name(), "()"); #[cfg(not(feature = "no_float"))] { - assert_eq!(123.456_f64, from_dynamic(&123.456_f64.into()).unwrap()); - assert_eq!( - 123.456_f32, - from_dynamic(&Dynamic::from(123.456_f32)).unwrap() - ); + assert_eq!(to_dynamic(123.456_f64)?.type_name(), "f64"); + assert_eq!(to_dynamic(123.456_f32)?.type_name(), "f32"); } - assert_eq!( - "hello", - from_dynamic::(&"hello".to_string().into()).unwrap() - ); + assert_eq!(to_dynamic("hello".to_string())?.type_name(), "string"); + + Ok(()) } #[test] #[cfg(not(feature = "no_index"))] -fn test_serde_de_array() { +fn test_serde_ser_array() -> Result<(), Box> { let arr: Vec = vec![123, 456, 42, 999]; - assert_eq!(arr, from_dynamic::>(&arr.clone().into()).unwrap()); + + let d = to_dynamic(arr)?; + assert!(d.is::()); + assert_eq!(d.cast::().len(), 4); + + Ok(()) } #[test] -fn test_serde_de_struct() { +#[cfg(not(feature = "no_index"))] +#[cfg(not(feature = "no_object"))] +fn test_serde_ser_struct() -> Result<(), Box> { + #[derive(Debug, Serialize, PartialEq)] + struct Hello { + a: INT, + b: bool, + } + + #[derive(Debug, Serialize, PartialEq)] + struct Test { + int: u32, + seq: Vec, + obj: Hello, + } + + let x = Test { + int: 42, + seq: vec!["hello".into(), "kitty".into(), "world".into()], + obj: Hello { a: 123, b: true }, + }; + + let d = to_dynamic(x)?; + + assert!(d.is::()); + + let mut map = d.cast::(); + let mut obj = map.remove("obj").unwrap().cast::(); + let mut seq = map.remove("seq").unwrap().cast::(); + + assert_eq!(obj.remove("a").unwrap().cast::(), 123); + assert!(obj.remove("b").unwrap().cast::()); + assert_eq!(map.remove("int").unwrap().cast::(), 42); + assert_eq!(seq.len(), 3); + assert_eq!(seq.remove(1).cast::(), "kitty"); + + Ok(()) +} + +#[test] +fn test_serde_de_primary_types() -> Result<(), Box> { + assert_eq!(42_u16, from_dynamic(&Dynamic::from(42_u16))?); + assert_eq!(42 as INT, from_dynamic(&(42 as INT).into())?); + assert_eq!(true, from_dynamic(&true.into())?); + assert_eq!((), from_dynamic(&().into())?); + + #[cfg(not(feature = "no_float"))] + { + assert_eq!(123.456_f64, from_dynamic(&123.456_f64.into())?); + assert_eq!(123.456_f32, from_dynamic(&Dynamic::from(123.456_f32))?); + } + + assert_eq!( + "hello", + from_dynamic::(&"hello".to_string().into())? + ); + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_index"))] +fn test_serde_de_array() -> Result<(), Box> { + let arr: Vec = vec![123, 456, 42, 999]; + assert_eq!(arr, from_dynamic::>(&arr.clone().into())?); + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_index"))] +#[cfg(not(feature = "no_object"))] +fn test_serde_de_struct() -> Result<(), Box> { #[derive(Debug, Deserialize, PartialEq)] struct Hello { a: INT, @@ -69,10 +148,15 @@ fn test_serde_de_struct() { seq: vec!["hello".into(), "kitty".into(), "world".into()], obj: Hello { a: 123, b: true }, }; - assert_eq!(expected, from_dynamic(&map.into()).unwrap()); + assert_eq!(expected, from_dynamic(&map.into())?); + + Ok(()) } #[test] +#[cfg(not(feature = "no_index"))] +#[cfg(not(feature = "no_object"))] +#[cfg(not(feature = "no_float"))] fn test_serde_de_script() -> Result<(), Box> { #[derive(Debug, Deserialize)] struct Point { @@ -102,7 +186,7 @@ fn test_serde_de_script() -> Result<(), Box> { )?; // Convert the 'Dynamic' object map into 'MyStruct' - let x: MyStruct = from_dynamic(&result)?; + let _: MyStruct = from_dynamic(&result)?; Ok(()) } From 2b2deba5e074dcbaa23509c5c774d69f1243a862 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 3 Jul 2020 22:48:33 +0800 Subject: [PATCH 09/46] Remove serde from default feature. --- Cargo.toml | 2 +- src/lib.rs | 1 + src/parser.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed5ba5b2..fa8f940c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] -default = ["serde"] +default = [] plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync diff --git a/src/lib.rs b/src/lib.rs index 77c39460..c6626b98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,6 +87,7 @@ pub mod packages; mod parser; mod result; mod scope; +#[cfg(feature = "serde")] mod serde; mod stdlib; mod token; diff --git a/src/parser.rs b/src/parser.rs index 8dc1af84..505c8e7e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1701,7 +1701,7 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result Expr::Dot(Box::new((lhs, func, op_pos))), // lhs.rhs - (lhs, rhs) => return Err(PERR::PropertyExpected.into_err(rhs.position())), + (_, rhs) => return Err(PERR::PropertyExpected.into_err(rhs.position())), }) } From cf2461651cfcce9908d54776953bea310ce1d083 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 4 Jul 2020 10:52:17 +0800 Subject: [PATCH 10/46] Undelete benchmark.yml. --- .github/benchmark.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/benchmark.yml diff --git a/.github/benchmark.yml b/.github/benchmark.yml new file mode 100644 index 00000000..df310705 --- /dev/null +++ b/.github/benchmark.yml @@ -0,0 +1,29 @@ +name: Benchmark +on: + push: + branches: + - master + +jobs: + benchmark: + name: Run Rust benchmark + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: rustup toolchain update nightly && rustup default nightly + - name: Run benchmark + run: cargo +nightly bench | tee output.txt + - name: Store benchmark result + uses: rhysd/github-action-benchmark@v1 + with: + name: Rust Benchmark + tool: 'cargo' + output-file-path: output.txt + # Use personal access token instead of GITHUB_TOKEN due to https://github.community/t5/GitHub-Actions/Github-action-not-triggering-gh-pages-upon-push/td-p/26869/highlight/false + github-token: ${{ secrets.RHAI }} + auto-push: true + # Show alert with commit comment on detecting possible performance regression + alert-threshold: '200%' + comment-on-alert: true + fail-on-alert: true + alert-comment-cc-users: '@schungx' From b3b3a083b8c4318df9ab5e68deca24f06f84f935 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 4 Jul 2020 15:39:40 +0800 Subject: [PATCH 11/46] Add docs on serde feature. --- Cargo.toml | 8 +- RELEASES.md | 9 ++ doc/src/rust/serde.md | 15 ++- src/lib.rs | 4 +- src/serde/de.rs | 173 ++++++++++++++++++++++++---------- src/serde/mod.rs | 2 + src/serde/ser.rs | 212 ++++++++++++++++++++++++------------------ src/serde/str.rs | 51 +++++----- 8 files changed, 303 insertions(+), 171 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fa8f940c..6380d11b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] -default = [] +default = ["serde"] plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync @@ -73,3 +73,9 @@ optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant + +[package.metadata.docs.rs] +features = ["serde"] + +[package.metadata.playground] +features = ["serde"] diff --git a/RELEASES.md b/RELEASES.md index 57645f5f..0a1ead2b 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,11 +4,20 @@ Rhai Release Notes Version 0.17.0 ============== +This version adds [`serde`](https://crates.io/crates/serde) support for working with `Dynamic` values (particularly _object maps_). + Breaking changes ---------------- * `EvalAltResult::ErrorMismatchOutputType` has an extra argument containing the name of the requested type. +New features +------------ + +* New `serde` feature to allow serializating/deserializating to/from `Dynamic` values using [`serde`](https://crates.io/crates/serde). + This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back. + + Version 0.16.1 ============== diff --git a/doc/src/rust/serde.md b/doc/src/rust/serde.md index 5b12ddc6..de59dc34 100644 --- a/doc/src/rust/serde.md +++ b/doc/src/rust/serde.md @@ -13,16 +13,21 @@ A [`Dynamic`] can be seamlessly converted to and from a type that implements `se Serialization ------------- -While it is simple to serialize a Rust type to `JSON` via `serde`, -then use [`Engine::parse_json`]({{rootUrl}}/language/json.md) to convert it into an [object map], -Rhai supports serializing a [`Dynamic`] directly via `serde` without going through the `JSON` step. - -The function `rhai::see::to_dynamic` automatically converts any Rust type that implements `serde::Serialize` +The function `rhai::ser::to_dynamic` automatically converts any Rust type that implements `serde::Serialize` into a [`Dynamic`]. +This is usually not necessary because using [`Dynamic::from`][`Dynamic`] is much easier and is essentially +the same thing. The only difference is treatment for integer values. `Dynamic::from` will keep the different +integer types intact, while `rhai::ser::to_dynamic` will convert them all into [`INT`][standard types] +(i.e. the system integer type which is `i64` or `i32` depending on the [`only_i32`] feature). + In particular, Rust `struct`'s (or any type that is marked as a `serde` map) are converted into [object maps] while Rust `Vec`'s (or any type that is marked as a `serde` sequence) are converted into [arrays]. +While it is also simple to serialize a Rust type to `JSON` via `serde`, +then use [`Engine::parse_json`]({{rootUrl}}/language/json.md) to convert it into an [object map], +`rhai::ser::to_dynamic` serializes it to [`Dynamic`] directly via `serde` without going through the `JSON` step. + ```rust use rhai::{Dynamic, Map}; use rhai::ser::to_dynamic; diff --git a/src/lib.rs b/src/lib.rs index c6626b98..50c574f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,7 +63,7 @@ //! | `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | //! | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | //! | `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, `Engine`, `Scope` and [`AST`] are all `Send + Sync`. | -//! | `serde` | Enable serialization/deserialization via [`serde`]. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | +//! | `serde` | Enable serialization/deserialization via `serde`. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | //! | `internals` | Expose internal data structures (beware they may be volatile from version to version). | //! //! See [The Rhai Book](https://schungx.github.io/rhai) for details on the Rhai script engine and language. @@ -130,10 +130,12 @@ pub mod module_resolvers { pub use crate::module::resolvers::*; } +/// Serialization support for [`serde`](https://crates.io/crates/serde). #[cfg(feature = "serde")] pub mod ser { pub use crate::serde::ser::to_dynamic; } +/// Deserialization support for [`serde`](https://crates.io/crates/serde). #[cfg(feature = "serde")] pub mod de { pub use crate::serde::de::from_dynamic; diff --git a/src/serde/de.rs b/src/serde/de.rs index 2b4b7bd9..be466508 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -1,5 +1,8 @@ +//! Implement deserialization support of `Dynamic` for [`serde`](https://crates.io/crates/serde). + use super::str::ImmutableStringDeserializer; use crate::any::{Dynamic, Union}; +use crate::error::ParseErrorType; use crate::result::EvalAltResult; use crate::token::Position; use crate::utils::ImmutableString; @@ -22,26 +25,83 @@ use crate::stdlib::time::Instant; #[cfg(target_arch = "wasm32")] use instant::Instant; +/// Deserializer for `Dynamic` which is kept as a reference. +/// +/// The reference is necessary because the deserialized type may hold references +/// (especially `&str`) to the source `Dynamic`. pub struct DynamicDeserializer<'a> { value: &'a Dynamic, } impl<'de> DynamicDeserializer<'de> { + /// Create a `DynamicDeserializer` from a reference to a `Dynamic` value. + /// + /// The reference is necessary because the deserialized type may hold references + /// (especially `&str`) to the source `Dynamic`. pub fn from_dynamic(value: &'de Dynamic) -> Self { Self { value } } - pub fn type_error(&self) -> Result> { - self.type_error_str(type_name::()) - } - pub fn type_error_str(&self, name: &str) -> Result> { + /// Shortcut for a type conversion error. + fn type_error(&self) -> Result> { Err(Box::new(EvalAltResult::ErrorMismatchOutputType( - name.into(), + type_name::().into(), self.value.type_name().into(), Position::none(), ))) } } +/// Deserialize a `Dynamic` value into a Rust type that implements `serde::Deserialize`. +/// +/// # Examples +/// +/// ``` +/// # fn main() -> Result<(), Box> { +/// # #[cfg(not(feature = "no_index"))] +/// # #[cfg(not(feature = "no_object"))] +/// # { +/// use rhai::{Dynamic, Array, Map, INT}; +/// use rhai::de::from_dynamic; +/// use serde::Deserialize; +/// +/// #[derive(Debug, Deserialize, PartialEq)] +/// struct Hello { +/// a: INT, +/// b: bool, +/// } +/// +/// #[derive(Debug, Deserialize, PartialEq)] +/// struct Test { +/// int: u32, +/// seq: Vec, +/// obj: Hello, +/// } +/// +/// let mut map = Map::new(); +/// map.insert("int".into(), Dynamic::from(42_u32)); +/// +/// let mut map2 = Map::new(); +/// map2.insert("a".into(), (123 as INT).into()); +/// map2.insert("b".into(), true.into()); +/// +/// map.insert("obj".into(), map2.into()); +/// +/// let arr: Array = vec!["foo".into(), "bar".into(), "baz".into()]; +/// map.insert("seq".into(), arr.into()); +/// +/// let value: Test = from_dynamic(&map.into())?; +/// +/// let expected = Test { +/// int: 42, +/// seq: vec!["foo".into(), "bar".into(), "baz".into()], +/// obj: Hello { a: 123, b: true }, +/// }; +/// +/// assert_eq!(value, expected); +/// # } +/// # Ok(()) +/// # } +/// ``` pub fn from_dynamic<'de, T: Deserialize<'de>>( value: &'de Dynamic, ) -> Result> { @@ -50,8 +110,8 @@ pub fn from_dynamic<'de, T: Deserialize<'de>>( impl Error for Box { fn custom(err: T) -> Self { - Box::new(EvalAltResult::ErrorRuntime( - err.to_string(), + Box::new(EvalAltResult::ErrorParsing( + ParseErrorType::BadInput(err.to_string()), Position::none(), )) } @@ -76,10 +136,10 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { Union::Array(_) => self.deserialize_seq(visitor), #[cfg(not(feature = "no_object"))] Union::Map(_) => self.deserialize_map(visitor), - Union::FnPtr(_) => unimplemented!(), + Union::FnPtr(_) => self.type_error(), #[cfg(not(feature = "no_std"))] - Union::Variant(value) if value.is::() => unimplemented!(), + Union::Variant(value) if value.is::() => self.type_error(), Union::Variant(value) if value.is::() => self.deserialize_i8(visitor), Union::Variant(value) if value.is::() => self.deserialize_i16(visitor), @@ -90,64 +150,60 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { Union::Variant(value) if value.is::() => self.deserialize_u32(visitor), Union::Variant(value) if value.is::() => self.deserialize_u64(visitor), - Union::Variant(_) => self.type_error_str("any"), + Union::Variant(_) => self.type_error(), } } fn deserialize_bool>(self, visitor: V) -> Result> { - visitor.visit_bool( - self.value - .as_bool() - .or_else(|_| self.type_error::())?, - ) + visitor.visit_bool(self.value.as_bool().or_else(|_| self.type_error())?) } fn deserialize_i8>(self, visitor: V) -> Result> { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_i8(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_i8(x)) } fn deserialize_i16>(self, visitor: V) -> Result> { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_i16(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_i16(x)) } fn deserialize_i32>(self, visitor: V) -> Result> { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_i32(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_i32(x)) } fn deserialize_i64>(self, visitor: V) -> Result> { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_i64(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_i64(x)) } fn deserialize_u8>(self, visitor: V) -> Result> { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_u8(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_u8(x)) } fn deserialize_u16>(self, visitor: V) -> Result> { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_u16(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_u16(x)) } fn deserialize_u32>(self, visitor: V) -> Result> { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_u32(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_u32(x)) } fn deserialize_u64>(self, visitor: V) -> Result> { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_u64(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_u64(x)) } fn deserialize_f32>(self, visitor: V) -> Result> { @@ -155,7 +211,7 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_f32(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_f32(x)) } #[cfg(feature = "no_float")] self.type_error_str("f32") @@ -166,7 +222,7 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_f64(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_f64(x)) } #[cfg(feature = "no_float")] self.type_error_str("f64") @@ -175,12 +231,12 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { fn deserialize_char>(self, visitor: V) -> Result> { self.value .downcast_ref::() - .map_or_else(|| self.type_error::(), |&x| visitor.visit_char(x)) + .map_or_else(|| self.type_error(), |&x| visitor.visit_char(x)) } fn deserialize_str>(self, visitor: V) -> Result> { self.value.downcast_ref::().map_or_else( - || self.type_error::(), + || self.type_error(), |x| visitor.visit_borrowed_str(x.as_str()), ) } @@ -193,21 +249,21 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { } fn deserialize_bytes>(self, _: V) -> Result> { - self.type_error_str("bytes array") + self.type_error() } fn deserialize_byte_buf>(self, _: V) -> Result> { - self.type_error_str("bytes array") + self.type_error() } fn deserialize_option>(self, _: V) -> Result> { - self.type_error_str("bytes array") + self.type_error() } fn deserialize_unit>(self, visitor: V) -> Result> { self.value .downcast_ref::<()>() - .map_or_else(|| self.type_error::<(), _>(), |_| visitor.visit_unit()) + .map_or_else(|| self.type_error(), |_| visitor.visit_unit()) } fn deserialize_unit_struct>( @@ -230,12 +286,12 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { #[cfg(not(feature = "no_index"))] { self.value.downcast_ref::().map_or_else( - || self.type_error::(), + || self.type_error(), |arr| visitor.visit_seq(IterateArray::new(arr.iter())), ) } #[cfg(feature = "no_index")] - self.type_error_str("array") + self.type_error() } fn deserialize_tuple>( @@ -259,12 +315,12 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { #[cfg(not(feature = "no_object"))] { self.value.downcast_ref::().map_or_else( - || self.type_error::(), + || self.type_error(), |map| visitor.visit_map(IterateMap::new(map.keys(), map.values())), ) } #[cfg(feature = "no_object")] - self.type_error_str("map") + self.type_error() } fn deserialize_struct>( @@ -282,7 +338,7 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { _variants: &'static [&'static str], _: V, ) -> Result> { - self.type_error_str("num") + self.type_error() } fn deserialize_identifier>( @@ -300,23 +356,35 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { } } -struct IterateArray<'a, ITER: Iterator> { +/// `SeqAccess` implementation for arrays. +struct IterateArray<'a, ITER> +where + ITER: Iterator, +{ + /// Iterator for a stream of `Dynamic` values. iter: ITER, } -impl<'a, ITER: Iterator> IterateArray<'a, ITER> { +impl<'a, ITER> IterateArray<'a, ITER> +where + ITER: Iterator, +{ pub fn new(iter: ITER) -> Self { Self { iter } } } -impl<'a: 'de, 'de, ITER: Iterator> SeqAccess<'de> for IterateArray<'a, ITER> { +impl<'a: 'de, 'de, ITER> SeqAccess<'de> for IterateArray<'a, ITER> +where + ITER: Iterator, +{ type Error = Box; fn next_element_seed>( &mut self, seed: T, ) -> Result, Box> { + // Deserialize each item coming out of the iterator. match self.iter.next() { None => Ok(None), Some(item) => seed @@ -326,29 +394,32 @@ impl<'a: 'de, 'de, ITER: Iterator> SeqAccess<'de> for Iterat } } -struct IterateMap< - 'a, +/// `MapAccess` implementation for maps. +struct IterateMap<'a, KEYS, VALUES> +where KEYS: Iterator, VALUES: Iterator, -> { +{ + // Iterator for a stream of `Dynamic` keys. keys: KEYS, + // Iterator for a stream of `Dynamic` values. values: VALUES, } -impl<'a, KEYS: Iterator, VALUES: Iterator> - IterateMap<'a, KEYS, VALUES> +impl<'a, KEYS, VALUES> IterateMap<'a, KEYS, VALUES> +where + KEYS: Iterator, + VALUES: Iterator, { pub fn new(keys: KEYS, values: VALUES) -> Self { Self { keys, values } } } -impl< - 'a: 'de, - 'de, - KEYS: Iterator, - VALUES: Iterator, - > MapAccess<'de> for IterateMap<'a, KEYS, VALUES> +impl<'a: 'de, 'de, KEYS, VALUES> MapAccess<'de> for IterateMap<'a, KEYS, VALUES> +where + KEYS: Iterator, + VALUES: Iterator, { type Error = Box; @@ -356,6 +427,7 @@ impl< &mut self, seed: K, ) -> Result, Box> { + // Deserialize each `ImmutableString` key coming out of the keys iterator. match self.keys.next() { None => Ok(None), Some(item) => seed @@ -368,6 +440,7 @@ impl< &mut self, seed: V, ) -> Result> { + // Deserialize each value item coming out of the iterator. seed.deserialize(&mut DynamicDeserializer::from_dynamic( self.values.next().unwrap(), )) diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 9d2e0281..2ed95bb4 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -1,3 +1,5 @@ +//! Helper module defining serialization/deserialization support for [`serde`](https://crates.io/crates/serde). + pub mod de; pub mod ser; mod str; diff --git a/src/serde/ser.rs b/src/serde/ser.rs index 95471e33..1477da2d 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -1,3 +1,5 @@ +//! Implement serialization support of `Dynamic` for [`serde`](https://crates.io/crates/serde). + use crate::any::Dynamic; use crate::result::EvalAltResult; use crate::token::Position; @@ -15,30 +17,72 @@ use serde::Serialize; use crate::stdlib::{any::type_name, fmt, mem}; +/// Serializer for `Dynamic` which is kept as a reference. pub struct DynamicSerializer { + /// Buffer to hold a temporary key. key: Dynamic, + /// Buffer to hold a temporary value. value: Dynamic, } impl DynamicSerializer { + /// Create a `DynamicSerializer` from a `Dynamic` value. pub fn new(value: Dynamic) -> Self { Self { key: Default::default(), value, } } - pub fn type_error(&self) -> Result> { - self.type_error_str(type_name::()) - } - pub fn type_error_str(&self, name: &str) -> Result> { - Err(Box::new(EvalAltResult::ErrorMismatchOutputType( - name.into(), - self.value.type_name().into(), - Position::none(), - ))) - } } +/// Serialize a Rust type that implements `serde::Serialize` into a `Dynamic`. +/// +/// # Examples +/// +/// ``` +/// # fn main() -> Result<(), Box> { +/// # #[cfg(not(feature = "no_index"))] +/// # #[cfg(not(feature = "no_object"))] +/// # #[cfg(not(feature = "no_float"))] +/// # { +/// use rhai::{Dynamic, Array, Map, INT}; +/// use rhai::ser::to_dynamic; +/// use serde::Serialize; +/// +/// #[derive(Debug, serde::Serialize, PartialEq)] +/// struct Point { +/// x: f64, +/// y: f64 +/// } +/// +/// #[derive(Debug, serde::Serialize, PartialEq)] +/// struct MyStruct { +/// a: i64, +/// b: Vec, +/// c: bool, +/// d: Point +/// } +/// +/// let x = MyStruct { +/// a: 42, +/// b: vec![ "hello".into(), "world".into() ], +/// c: true, +/// d: Point { x: 123.456, y: 999.0 } +/// }; +/// +/// // Convert the 'MyStruct' into a 'Dynamic' +/// let value = to_dynamic(x)?; +/// +/// assert!(value.is::()); +/// +/// let map = value.cast::(); +/// let point = map.get("d").unwrap().downcast_ref::().unwrap(); +/// assert_eq!(*point.get("x").unwrap().downcast_ref::().unwrap(), 123.456); +/// assert_eq!(*point.get("y").unwrap().downcast_ref::().unwrap(), 999.0); +/// # } +/// # Ok(()) +/// # } +/// ``` pub fn to_dynamic(value: T) -> Result> { let mut s = DynamicSerializer::new(Default::default()); value.serialize(&mut s) @@ -141,17 +185,11 @@ impl Serializer for &mut DynamicSerializer { } fn serialize_f32(self, v: f32) -> Result> { - #[cfg(not(feature = "no_float"))] - return Ok(Dynamic::from(v)); - #[cfg(feature = "no_float")] - return self.type_error_str("f32"); + Ok(Dynamic::from(v)) } fn serialize_f64(self, v: f64) -> Result> { - #[cfg(not(feature = "no_float"))] - return Ok(v.into()); - #[cfg(feature = "no_float")] - return self.type_error_str("f64"); + Ok(Dynamic::from(v)) } fn serialize_char(self, v: char) -> Result> { @@ -162,8 +200,8 @@ impl Serializer for &mut DynamicSerializer { Ok(v.to_string().into()) } - fn serialize_bytes(self, _: &[u8]) -> Result> { - self.type_error_str("bytes array") + fn serialize_bytes(self, v: &[u8]) -> Result> { + Ok(Dynamic::from(v.to_vec())) } fn serialize_none(self) -> Result> { @@ -216,7 +254,11 @@ impl Serializer for &mut DynamicSerializer { #[cfg(not(feature = "no_index"))] return Ok(DynamicSerializer::new(Array::new().into())); #[cfg(feature = "no_index")] - return self.type_error_str("array"); + return Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + "Dynamic".into(), + "array".into(), + Position::none(), + ))); } fn serialize_tuple(self, len: usize) -> Result> { @@ -245,7 +287,11 @@ impl Serializer for &mut DynamicSerializer { #[cfg(not(feature = "no_object"))] return Ok(DynamicSerializer::new(Map::new().into())); #[cfg(feature = "no_object")] - return self.type_error_str("map"); + return Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + "Dynamic".into(), + "map".into(), + Position::none(), + ))); } fn serialize_struct( @@ -278,15 +324,12 @@ impl SerializeSeq for DynamicSerializer { #[cfg(not(feature = "no_index"))] { let value = value.serialize(&mut *self)?; - if let Some(arr) = self.value.downcast_mut::() { - arr.push(value); - Ok(()) - } else { - self.type_error::() - } + let arr = self.value.downcast_mut::().unwrap(); + arr.push(value); + Ok(()) } #[cfg(feature = "no_index")] - self.type_error_str("array") + unreachable!() } // Close the sequence. @@ -294,7 +337,7 @@ impl SerializeSeq for DynamicSerializer { #[cfg(not(feature = "no_index"))] return Ok(self.value); #[cfg(feature = "no_index")] - return self.type_error_str("array"); + unreachable!() } } @@ -309,22 +352,19 @@ impl SerializeTuple for DynamicSerializer { #[cfg(not(feature = "no_index"))] { let value = value.serialize(&mut *self)?; - if let Some(arr) = self.value.downcast_mut::() { - arr.push(value); - Ok(()) - } else { - self.type_error::() - } + let arr = self.value.downcast_mut::().unwrap(); + arr.push(value); + Ok(()) } #[cfg(feature = "no_index")] - self.type_error_str("array") + unreachable!() } fn end(self) -> Result> { #[cfg(not(feature = "no_index"))] return Ok(self.value); #[cfg(feature = "no_index")] - return self.type_error_str("array"); + unreachable!() } } @@ -339,22 +379,19 @@ impl SerializeTupleStruct for DynamicSerializer { #[cfg(not(feature = "no_index"))] { let value = value.serialize(&mut *self)?; - if let Some(arr) = self.value.downcast_mut::() { - arr.push(value); - Ok(()) - } else { - self.type_error::() - } + let arr = self.value.downcast_mut::().unwrap(); + arr.push(value); + Ok(()) } #[cfg(feature = "no_index")] - self.type_error_str("array") + unreachable!() } fn end(self) -> Result> { #[cfg(not(feature = "no_index"))] return Ok(self.value); #[cfg(feature = "no_index")] - return self.type_error_str("array"); + unreachable!() } } @@ -369,22 +406,19 @@ impl SerializeTupleVariant for DynamicSerializer { #[cfg(not(feature = "no_index"))] { let value = value.serialize(&mut *self)?; - if let Some(arr) = self.value.downcast_mut::() { - arr.push(value); - Ok(()) - } else { - self.type_error::() - } + let arr = self.value.downcast_mut::().unwrap(); + arr.push(value); + Ok(()) } #[cfg(feature = "no_index")] - self.type_error_str("array") + unreachable!() } fn end(self) -> Result> { #[cfg(not(feature = "no_index"))] return Ok(self.value); #[cfg(feature = "no_index")] - return self.type_error_str("array"); + unreachable!() } } @@ -399,7 +433,7 @@ impl SerializeMap for DynamicSerializer { Ok(()) } #[cfg(feature = "no_object")] - self.type_error_str("map") + unreachable!() } fn serialize_value( @@ -410,17 +444,20 @@ impl SerializeMap for DynamicSerializer { { let key = mem::take(&mut self.key) .take_immutable_string() - .or_else(|_| self.type_error::())?; + .map_err(|typ| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + "string".into(), + typ.into(), + Position::none(), + )) + })?; let value = value.serialize(&mut *self)?; - if let Some(map) = self.value.downcast_mut::() { - map.insert(key, value); - Ok(()) - } else { - self.type_error::() - } + let map = self.value.downcast_mut::().unwrap(); + map.insert(key, value); + Ok(()) } #[cfg(feature = "no_object")] - self.type_error_str("map") + unreachable!() } fn serialize_entry( @@ -431,26 +468,27 @@ impl SerializeMap for DynamicSerializer { #[cfg(not(feature = "no_object"))] { let key: Dynamic = key.serialize(&mut *self)?; - let key = key - .take_immutable_string() - .or_else(|_| self.type_error::())?; + let key = key.take_immutable_string().map_err(|typ| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + "string".into(), + typ.into(), + Position::none(), + )) + })?; let value = value.serialize(&mut *self)?; - if let Some(map) = self.value.downcast_mut::() { - map.insert(key, value); - Ok(()) - } else { - self.type_error::() - } + let map = self.value.downcast_mut::().unwrap(); + map.insert(key, value); + Ok(()) } #[cfg(feature = "no_object")] - self.type_error_str("map") + unreachable!() } fn end(self) -> Result> { #[cfg(not(feature = "no_object"))] return Ok(self.value); #[cfg(feature = "no_object")] - return self.type_error_str("map"); + unreachable!() } } @@ -466,22 +504,19 @@ impl SerializeStruct for DynamicSerializer { #[cfg(not(feature = "no_object"))] { let value = value.serialize(&mut *self)?; - if let Some(map) = self.value.downcast_mut::() { - map.insert(key.into(), value); - Ok(()) - } else { - self.type_error::() - } + let map = self.value.downcast_mut::().unwrap(); + map.insert(key.into(), value); + Ok(()) } #[cfg(feature = "no_object")] - self.type_error_str("map") + unreachable!() } fn end(self) -> Result> { #[cfg(not(feature = "no_object"))] return Ok(self.value); #[cfg(feature = "no_object")] - return self.type_error_str("map"); + unreachable!() } } @@ -497,21 +532,18 @@ impl SerializeStructVariant for DynamicSerializer { #[cfg(not(feature = "no_object"))] { let value = value.serialize(&mut *self)?; - if let Some(map) = self.value.downcast_mut::() { - map.insert(key.into(), value); - Ok(()) - } else { - self.type_error::() - } + let map = self.value.downcast_mut::().unwrap(); + map.insert(key.into(), value); + Ok(()) } #[cfg(feature = "no_object")] - self.type_error_str("map") + unreachable!() } fn end(self) -> Result> { #[cfg(not(feature = "no_object"))] return Ok(self.value); #[cfg(feature = "no_object")] - return self.type_error_str("map"); + unreachable!() } } diff --git a/src/serde/str.rs b/src/serde/str.rs index 131cc0fe..23e2c3d2 100644 --- a/src/serde/str.rs +++ b/src/serde/str.rs @@ -1,3 +1,5 @@ +//! Implement deserialization support of `ImmutableString` for [`serde`](https://crates.io/crates/serde). + use crate::result::EvalAltResult; use crate::token::Position; use crate::utils::ImmutableString; @@ -6,20 +8,20 @@ use serde::de::{Deserializer, Visitor}; use crate::stdlib::any::type_name; +/// Deserializer for `ImmutableString`. pub struct ImmutableStringDeserializer<'a> { value: &'a ImmutableString, } impl<'a> ImmutableStringDeserializer<'a> { + /// Create an `ImmutableStringDeserializer` from an `ImmutableString` reference. pub fn from_str(value: &'a ImmutableString) -> Self { Self { value } } - pub fn type_error(&self) -> Result> { - self.type_error_str(type_name::()) - } - pub fn type_error_str(&self, name: &str) -> Result> { + /// Shortcut for a type conversion error. + fn type_error(&self) -> Result> { Err(Box::new(EvalAltResult::ErrorMismatchOutputType( - name.into(), + type_name::().into(), "string".into(), Position::none(), ))) @@ -33,42 +35,43 @@ impl<'de> Deserializer<'de> for &mut ImmutableStringDeserializer<'de> { self.deserialize_str(v) } fn deserialize_bool>(self, _: V) -> Result> { - self.type_error::() + self.type_error() } fn deserialize_i8>(self, _: V) -> Result> { - self.type_error::() + self.type_error() } fn deserialize_i16>(self, _: V) -> Result> { - self.type_error::() + self.type_error() } fn deserialize_i32>(self, _: V) -> Result> { - self.type_error::() + self.type_error() } fn deserialize_i64>(self, _: V) -> Result> { - self.type_error::() + self.type_error() } fn deserialize_u8>(self, _: V) -> Result> { - self.type_error::() + self.type_error() } fn deserialize_u16>(self, _: V) -> Result> { - self.type_error::() + self.type_error() } fn deserialize_u32>(self, _: V) -> Result> { - self.type_error::() + self.type_error() } fn deserialize_u64>(self, _: V) -> Result> { - self.type_error::() + self.type_error() } fn deserialize_f32>(self, _: V) -> Result> { - self.type_error_str("f32") + self.type_error() } fn deserialize_f64>(self, _: V) -> Result> { - self.type_error_str("f64") + self.type_error() } fn deserialize_char>(self, _: V) -> Result> { - self.type_error::() + self.type_error() } fn deserialize_str>(self, v: V) -> Result> { + // Only allow deserialization into a string. v.visit_borrowed_str(self.value.as_str()) } fn deserialize_string>( @@ -78,16 +81,16 @@ impl<'de> Deserializer<'de> for &mut ImmutableStringDeserializer<'de> { self.deserialize_str(visitor) } fn deserialize_bytes>(self, _: V) -> Result> { - self.type_error_str("bytes array") + self.type_error() } fn deserialize_byte_buf>(self, _: V) -> Result> { - self.type_error_str("bytes array") + self.type_error() } fn deserialize_option>(self, _: V) -> Result> { - self.type_error_str("option") + self.type_error() } fn deserialize_unit>(self, _: V) -> Result> { - self.type_error::<(), _>() + self.type_error() } fn deserialize_unit_struct>( self, @@ -104,7 +107,7 @@ impl<'de> Deserializer<'de> for &mut ImmutableStringDeserializer<'de> { v.visit_newtype_struct(self) } fn deserialize_seq>(self, _: V) -> Result> { - self.type_error_str("array") + self.type_error() } fn deserialize_tuple>( self, @@ -122,7 +125,7 @@ impl<'de> Deserializer<'de> for &mut ImmutableStringDeserializer<'de> { self.deserialize_seq(v) } fn deserialize_map>(self, _: V) -> Result> { - self.type_error_str("map") + self.type_error() } fn deserialize_struct>( self, @@ -138,7 +141,7 @@ impl<'de> Deserializer<'de> for &mut ImmutableStringDeserializer<'de> { _variants: &'static [&'static str], _: V, ) -> Result> { - self.type_error_str("enum") + self.type_error() } fn deserialize_identifier>(self, v: V) -> Result> { self.deserialize_str(v) From 467b109c23d4283392f17ba6acccc21855d8d05e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 4 Jul 2020 15:43:48 +0800 Subject: [PATCH 12/46] Move benchmark.yml to correct location. --- .github/{ => workflows}/benchmark.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ => workflows}/benchmark.yml (100%) diff --git a/.github/benchmark.yml b/.github/workflows/benchmark.yml similarity index 100% rename from .github/benchmark.yml rename to .github/workflows/benchmark.yml From d626bf9f5bacfd8d8fb082b03f12fe33c60055b8 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 4 Jul 2020 16:21:15 +0800 Subject: [PATCH 13/46] Refine no_function feature. --- Cargo.toml | 2 +- README.md | 4 +-- src/api.rs | 13 ++++--- src/engine.rs | 91 +++++++++++++++++++++++++----------------------- src/fn_native.rs | 35 ++++++++++++++++--- src/lib.rs | 6 ++++ src/module.rs | 32 ++++++++++------- src/optimize.rs | 10 ++++-- 8 files changed, 122 insertions(+), 71 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6380d11b..6d48bc8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] -default = ["serde"] +default = [] plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync diff --git a/README.md b/README.md index c170099d..c8363ac2 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Features * Fairly efficient evaluation (1 million iterations in 0.4 sec on a single core, 2.3 GHz Linux VM). * Relatively little `unsafe` code (yes there are some for performance reasons, and most `unsafe` code is limited to one single source file, all with names starting with `"unsafe_"`). -* Re-entrant scripting engine can be made `Send + Sync` (via the [`sync`] feature). +* Re-entrant scripting engine can be made `Send + Sync` (via the `sync` feature). * Sand-boxed - the scripting engine, if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`). * Rugged - protected against malicious attacks (such as [stack-overflow](https://schungx.github.io/rhai/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/rhai/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/rhai/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts. * Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run. @@ -38,7 +38,7 @@ Features * Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html). * Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). -* Serialization/deserialization support via [serde](https://crates.io/crates/serde) +* Serialization/deserialization support via [serde](https://crates.io/crates/serde) (requires the `serde` feature). * Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations. * Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html). diff --git a/src/api.rs b/src/api.rs index 190e69db..49c7b486 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,10 +1,7 @@ //! Module that defines the extern API of `Engine`. use crate::any::{Dynamic, Variant}; -use crate::engine::{ - get_script_function_by_signature, make_getter, make_setter, Engine, Imports, State, FN_IDX_GET, - FN_IDX_SET, -}; +use crate::engine::{make_getter, make_setter, Engine, Imports, State, FN_IDX_GET, FN_IDX_SET}; use crate::error::ParseError; use crate::fn_call::FuncArgs; use crate::fn_native::{IteratorFn, SendSync}; @@ -19,6 +16,9 @@ use crate::utils::StaticVec; #[cfg(not(feature = "no_object"))] use crate::engine::Map; +#[cfg(not(feature = "no_function"))] +use crate::engine::get_script_function_by_signature; + use crate::stdlib::{ any::{type_name, TypeId}, boxed::Box, @@ -1189,6 +1189,7 @@ impl Engine { /// This is to avoid unnecessarily cloning the arguments. /// Do not use the arguments after this call. If they are needed afterwards, /// clone them _before_ calling this function. + #[cfg(not(feature = "no_function"))] pub(crate) fn call_fn_dynamic_raw( &self, scope: &mut Scope, @@ -1240,6 +1241,7 @@ impl Engine { mut ast: AST, optimization_level: OptimizationLevel, ) -> AST { + #[cfg(not(feature = "no_function"))] let lib = ast .lib() .iter_fn() @@ -1247,6 +1249,9 @@ impl Engine { .map(|(_, _, _, f)| f.get_fn_def().clone()) .collect(); + #[cfg(feature = "no_function")] + let lib = Default::default(); + let stmt = mem::take(ast.statements_mut()); optimize_into_ast(self, scope, stmt, lib, optimization_level) } diff --git a/src/engine.rs b/src/engine.rs index 27d35222..1de5d3d4 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -216,6 +216,7 @@ impl State { } /// Get a script-defined function definition from a module. +#[cfg(not(feature = "no_function"))] pub fn get_script_function_by_signature<'a>( module: &'a Module, name: &str, @@ -767,22 +768,23 @@ impl Engine { .or_else(|| self.packages.get_fn(hash_fn)); if let Some(func) = func { - // Calling pure function but the first argument is a reference? - normalize_first_arg( - is_ref && (func.is_pure() || (func.is_script() && !is_method)), - &mut this_copy, - &mut old_this_ptr, - args, - ); + #[cfg(not(feature = "no_function"))] + let need_normalize = is_ref && (func.is_pure() || (func.is_script() && !is_method)); + #[cfg(feature = "no_function")] + let need_normalize = is_ref && func.is_pure(); + // Calling pure function but the first argument is a reference? + normalize_first_arg(need_normalize, &mut this_copy, &mut old_this_ptr, args); + + #[cfg(not(feature = "no_function"))] if func.is_script() { // Run scripted function let fn_def = func.get_fn_def(); // Method call of script function - map first argument to `this` - if is_method { + return if is_method { let (first, rest) = args.split_at_mut(1); - return Ok(( + Ok(( self.call_script_fn( scope, mods, @@ -795,7 +797,7 @@ impl Engine { level, )?, false, - )); + )) } else { let result = self.call_script_fn( scope, mods, state, lib, &mut None, fn_name, fn_def, args, level, @@ -804,42 +806,42 @@ impl Engine { // Restore the original reference restore_first_arg(old_this_ptr, args); - return Ok((result, false)); + Ok((result, false)) }; - } else { - // Run external function - let result = func.get_native_fn()(self, args)?; - - // Restore the original reference - restore_first_arg(old_this_ptr, args); - - // See if the function match print/debug (which requires special processing) - return Ok(match fn_name { - KEYWORD_PRINT => ( - (self.print)(result.as_str().map_err(|typ| { - Box::new(EvalAltResult::ErrorMismatchOutputType( - self.map_type_name(type_name::()).into(), - typ.into(), - Position::none(), - )) - })?) - .into(), - false, - ), - KEYWORD_DEBUG => ( - (self.debug)(result.as_str().map_err(|typ| { - Box::new(EvalAltResult::ErrorMismatchOutputType( - self.map_type_name(type_name::()).into(), - typ.into(), - Position::none(), - )) - })?) - .into(), - false, - ), - _ => (result, func.is_method()), - }); } + + // Run external function + let result = func.get_native_fn()(self, args)?; + + // Restore the original reference + restore_first_arg(old_this_ptr, args); + + // See if the function match print/debug (which requires special processing) + return Ok(match fn_name { + KEYWORD_PRINT => ( + (self.print)(result.as_str().map_err(|typ| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + self.map_type_name(type_name::()).into(), + typ.into(), + Position::none(), + )) + })?) + .into(), + false, + ), + KEYWORD_DEBUG => ( + (self.debug)(result.as_str().map_err(|typ| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + self.map_type_name(type_name::()).into(), + typ.into(), + Position::none(), + )) + })?) + .into(), + false, + ), + _ => (result, func.is_method()), + }); } // See if it is built in. @@ -2016,6 +2018,7 @@ impl Engine { }; match func { + #[cfg(not(feature = "no_function"))] Ok(f) if f.is_script() => { let args = args.as_mut(); let fn_def = f.get_fn_def(); diff --git a/src/fn_native.rs b/src/fn_native.rs index e41ae7db..a1110fd3 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -114,6 +114,7 @@ pub enum CallableFunction { /// An iterator function. Iterator(IteratorFn), /// A script-defined function. + #[cfg(not(feature = "no_function"))] Script(Shared), } @@ -123,6 +124,8 @@ impl fmt::Debug for CallableFunction { Self::Pure(_) => write!(f, "NativePureFunction"), Self::Method(_) => write!(f, "NativeMethod"), Self::Iterator(_) => write!(f, "NativeIterator"), + + #[cfg(not(feature = "no_function"))] Self::Script(fn_def) => fmt::Debug::fmt(fn_def, f), } } @@ -134,6 +137,8 @@ impl fmt::Display for CallableFunction { Self::Pure(_) => write!(f, "NativePureFunction"), Self::Method(_) => write!(f, "NativeMethod"), Self::Iterator(_) => write!(f, "NativeIterator"), + + #[cfg(not(feature = "no_function"))] CallableFunction::Script(s) => fmt::Display::fmt(s, f), } } @@ -144,24 +149,34 @@ impl CallableFunction { pub fn is_pure(&self) -> bool { match self { Self::Pure(_) => true, - Self::Method(_) | Self::Iterator(_) | Self::Script(_) => false, + Self::Method(_) | Self::Iterator(_) => false, + + #[cfg(not(feature = "no_function"))] + Self::Script(_) => false, } } /// Is this a native Rust method function? pub fn is_method(&self) -> bool { match self { Self::Method(_) => true, - Self::Pure(_) | Self::Iterator(_) | Self::Script(_) => false, + Self::Pure(_) | Self::Iterator(_) => false, + + #[cfg(not(feature = "no_function"))] + Self::Script(_) => false, } } /// Is this an iterator function? pub fn is_iter(&self) -> bool { match self { Self::Iterator(_) => true, - Self::Pure(_) | Self::Method(_) | Self::Script(_) => false, + Self::Pure(_) | Self::Method(_) => false, + + #[cfg(not(feature = "no_function"))] + Self::Script(_) => false, } } /// Is this a Rhai-scripted function? + #[cfg(not(feature = "no_function"))] pub fn is_script(&self) -> bool { match self { Self::Script(_) => true, @@ -176,7 +191,10 @@ impl CallableFunction { pub fn get_native_fn(&self) -> &FnAny { match self { Self::Pure(f) | Self::Method(f) => f.as_ref(), - Self::Iterator(_) | Self::Script(_) => unreachable!(), + Self::Iterator(_) => unreachable!(), + + #[cfg(not(feature = "no_function"))] + Self::Script(_) => unreachable!(), } } /// Get a shared reference to a script-defined function definition. @@ -184,6 +202,7 @@ impl CallableFunction { /// # Panics /// /// Panics if the `CallableFunction` is not `Script`. + #[cfg(not(feature = "no_function"))] pub fn get_shared_fn_def(&self) -> Shared { match self { Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => unreachable!(), @@ -195,6 +214,7 @@ impl CallableFunction { /// # Panics /// /// Panics if the `CallableFunction` is not `Script`. + #[cfg(not(feature = "no_function"))] pub fn get_fn_def(&self) -> &ScriptFnDef { match self { Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => unreachable!(), @@ -209,7 +229,10 @@ impl CallableFunction { pub fn get_iter_fn(&self) -> IteratorFn { match self { Self::Iterator(f) => *f, - Self::Pure(_) | Self::Method(_) | Self::Script(_) => unreachable!(), + Self::Pure(_) | Self::Method(_) => unreachable!(), + + #[cfg(not(feature = "no_function"))] + Self::Script(_) => unreachable!(), } } /// Create a new `CallableFunction::Pure`. @@ -228,12 +251,14 @@ impl From for CallableFunction { } } +#[cfg(not(feature = "no_function"))] impl From for CallableFunction { fn from(func: ScriptFnDef) -> Self { Self::Script(func.into()) } } +#[cfg(not(feature = "no_function"))] impl From> for CallableFunction { fn from(func: Shared) -> Self { Self::Script(func) diff --git a/src/lib.rs b/src/lib.rs index 50c574f7..0b825113 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,17 +125,23 @@ pub use parser::FLOAT; pub use module::ModuleResolver; /// Module containing all built-in _module resolvers_ available to Rhai. +/// +/// Not available under the `no_module` feature. #[cfg(not(feature = "no_module"))] pub mod module_resolvers { pub use crate::module::resolvers::*; } /// Serialization support for [`serde`](https://crates.io/crates/serde). +/// +/// Requires the `serde` feature. #[cfg(feature = "serde")] pub mod ser { pub use crate::serde::ser::to_dynamic; } /// Deserialization support for [`serde`](https://crates.io/crates/serde). +/// +/// Requires the `serde` feature. #[cfg(feature = "serde")] pub mod de { pub use crate::serde::de::from_dynamic; diff --git a/src/module.rs b/src/module.rs index c7198d83..7ae8d355 100644 --- a/src/module.rs +++ b/src/module.rs @@ -236,6 +236,7 @@ impl Module { /// Set a script-defined function into the module. /// /// If there is an existing function of the same name and number of arguments, it is replaced. + #[cfg(not(feature = "no_function"))] pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) { // None + function name + number of arguments. let hash_script = calc_fn_hash(empty(), &fn_def.name, fn_def.params.len(), empty()); @@ -876,6 +877,7 @@ impl Module { .functions .iter() .filter(|(_, (_, _, _, v))| match v { + #[cfg(not(feature = "no_function"))] CallableFunction::Script(ref f) => { filter(f.access, f.name.as_str(), f.params.len()) } @@ -893,6 +895,7 @@ impl Module { } /// Filter out the functions, retaining only some based on a filter predicate. + #[cfg(not(feature = "no_function"))] pub(crate) fn retain_functions(&mut self, filter: impl Fn(FnAccess, &str, usize) -> bool) { self.functions.retain(|_, (_, _, _, v)| match v { CallableFunction::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()), @@ -930,6 +933,7 @@ impl Module { } /// Get an iterator over all script-defined functions in the module. + #[cfg(not(feature = "no_function"))] pub fn iter_script_fn<'a>(&'a self) -> impl Iterator> + 'a { self.functions .values() @@ -1014,6 +1018,7 @@ impl Module { Public => (), } + #[cfg(not(feature = "no_function"))] if func.is_script() { let fn_def = func.get_shared_fn_def(); // Qualifiers + function name + number of arguments. @@ -1024,20 +1029,21 @@ impl Module { empty(), ); functions.push((hash_qualified_script, fn_def.into())); - } else { - // Qualified Rust functions are indexed in two steps: - // 1) Calculate a hash in a similar manner to script-defined functions, - // i.e. qualifiers + function name + number of arguments. - let hash_qualified_script = - calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty()); - // 2) Calculate a second hash with no qualifiers, empty function name, - // zero number of arguments, and the actual list of argument `TypeId`'.s - let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned()); - // 3) The final hash is the XOR of the two hashes. - let hash_qualified_fn = hash_qualified_script ^ hash_fn_args; - - functions.push((hash_qualified_fn, func.clone())); + continue; } + + // Qualified Rust functions are indexed in two steps: + // 1) Calculate a hash in a similar manner to script-defined functions, + // i.e. qualifiers + function name + number of arguments. + let hash_qualified_script = + calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty()); + // 2) Calculate a second hash with no qualifiers, empty function name, + // zero number of arguments, and the actual list of argument `TypeId`'.s + let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned()); + // 3) The final hash is the XOR of the two hashes. + let hash_qualified_fn = hash_qualified_script ^ hash_fn_args; + + functions.push((hash_qualified_fn, func.clone())); } } diff --git a/src/optimize.rs b/src/optimize.rs index c7a1ed71..5676bad4 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -551,11 +551,17 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { // First search in functions lib (can override built-in) // Cater for both normal function call style and method call style (one additional arguments) - if state.lib.iter_fn().find(|(_, _, _, f)| { + #[cfg(not(feature = "no_function"))] + let has_script_fn = state.lib.iter_fn().find(|(_, _, _, f)| { if !f.is_script() { return false; } let fn_def = f.get_fn_def(); &fn_def.name == name && (args.len()..=args.len() + 1).contains(&fn_def.params.len()) - }).is_some() { + }).is_some(); + + #[cfg(feature = "no_function")] + const has_script_fn: bool = false; + + if has_script_fn { // A script-defined function overrides the built-in function - do not make the call x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect(); return Expr::FnCall(x); From a2ae052c82dd7165184c031242d0be8f6b67e5ce Mon Sep 17 00:00:00 2001 From: Alvin Wong Date: Sat, 4 Jul 2020 22:32:18 +0800 Subject: [PATCH 14/46] Add `serde` example --- examples/serde.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 examples/serde.rs diff --git a/examples/serde.rs b/examples/serde.rs new file mode 100644 index 00000000..ae3ae6a9 --- /dev/null +++ b/examples/serde.rs @@ -0,0 +1,74 @@ +#[cfg(not(feature = "serde"))] +fn main() { + println!("This example requires the feature `serde`. Use `cargo run --features serde --example serde`."); +} + +#[cfg(feature = "serde")] +fn main() { + example::ser(); + println!(); + example::de(); +} + +#[cfg(feature = "serde")] +mod example { + use rhai::{de::from_dynamic, ser::to_dynamic}; + use rhai::{Dynamic, Engine, Map}; + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + struct Point { + x: f64, + y: f64, + } + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + struct MyStruct { + a: i64, + b: Vec, + c: bool, + d: Point, + } + + pub fn ser() { + let x = MyStruct { + a: 42, + b: vec!["hello".into(), "world".into()], + c: true, + d: Point { + x: 123.456, + y: 999.0, + }, + }; + + println!("Source struct: {:#?}", x); + + // Convert the 'MyStruct' into a 'Dynamic' + let map: Dynamic = to_dynamic(x).unwrap(); + + assert!(map.is::()); + println!("Serialized to Dynamic: {:#?}", map); + } + + pub fn de() { + let engine = Engine::new(); + let result: Dynamic = engine + .eval( + r#" + #{ + a: 42, + b: [ "hello", "world" ], + c: true, + d: #{ x: 123.456, y: 999.0 } + } + "#, + ) + .unwrap(); + + println!("Source Dynamic: {:#?}", result); + + // Convert the 'Dynamic' object map into 'MyStruct' + let x: MyStruct = from_dynamic(&result).unwrap(); + + println!("Deserialized to struct: {:#?}", x); + } +} From 23f21c7808dd5b8dbe9580746b1c1c9b2391c19e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 4 Jul 2020 22:52:45 +0800 Subject: [PATCH 15/46] Add doc to serde example. --- doc/src/start/examples/rust.md | 23 ++++++++++++----------- examples/serde.rs | 8 +++++--- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/doc/src/start/examples/rust.md b/doc/src/start/examples/rust.md index 1803c49b..412589e5 100644 --- a/doc/src/start/examples/rust.md +++ b/doc/src/start/examples/rust.md @@ -5,17 +5,18 @@ Rust Examples A number of examples can be found in the `examples` folder: -| Example | Description | -| ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| [`arrays_and_structs`](https://github.com/jonathandturner/rhai/tree/master/examples/arrays_and_structs.rs) | Shows how to register a custom Rust type and using [arrays] on it. | -| [`custom_types_and_methods`](https://github.com/jonathandturner/rhai/tree/master/examples/custom_types_and_methods.rs) | Shows how to register a custom Rust type and methods for it. | -| [`hello`](https://github.com/jonathandturner/rhai/tree/master/examples/hello.rs) | Simple example that evaluates an expression and prints the result. | -| [`no_std`](https://github.com/jonathandturner/rhai/tree/master/examples/no_std.rs) | Example to test out `no-std` builds. | -| [`reuse_scope`](https://github.com/jonathandturner/rhai/tree/master/examples/reuse_scope.rs) | Evaluates two pieces of code in separate runs, but using a common [`Scope`]. | -| [`rhai_runner`](https://github.com/jonathandturner/rhai/tree/master/examples/rhai_runner.rs) | Runs each filename passed to it as a Rhai script. | -| [`simple_fn`](https://github.com/jonathandturner/rhai/tree/master/examples/simple_fn.rs) | Shows how to register a simple function. | -| [`strings`](https://github.com/jonathandturner/rhai/tree/master/examples/strings.rs) | Shows different ways to register functions taking string arguments. | -| [`repl`](https://github.com/jonathandturner/rhai/tree/master/examples/repl.rs) | A simple REPL, interactively evaluate statements from stdin. | +| Example | Description | +| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [`arrays_and_structs`](https://github.com/jonathandturner/rhai/tree/master/examples/arrays_and_structs.rs) | Shows how to register a custom Rust type and using [arrays] on it. | +| [`custom_types_and_methods`](https://github.com/jonathandturner/rhai/tree/master/examples/custom_types_and_methods.rs) | Shows how to register a custom Rust type and methods for it. | +| [`hello`](https://github.com/jonathandturner/rhai/tree/master/examples/hello.rs) | Simple example that evaluates an expression and prints the result. | +| [`no_std`](https://github.com/jonathandturner/rhai/tree/master/examples/no_std.rs) | Example to test out `no-std` builds. | +| [`reuse_scope`](https://github.com/jonathandturner/rhai/tree/master/examples/reuse_scope.rs) | Evaluates two pieces of code in separate runs, but using a common [`Scope`]. | +| [`rhai_runner`](https://github.com/jonathandturner/rhai/tree/master/examples/rhai_runner.rs) | Runs each filename passed to it as a Rhai script. | +| [`serde`](https://github.com/jonathandturner/rhai/tree/master/examples/serde.rs) | Example to serialize and deserialize Rust types with [`serde`](https://crates.io/crates/serde).
The [`serde`] feature is required to run. | +| [`simple_fn`](https://github.com/jonathandturner/rhai/tree/master/examples/simple_fn.rs) | Shows how to register a simple function. | +| [`strings`](https://github.com/jonathandturner/rhai/tree/master/examples/strings.rs) | Shows different ways to register functions taking string arguments. | +| [`repl`](https://github.com/jonathandturner/rhai/tree/master/examples/repl.rs) | A simple REPL, interactively evaluate statements from stdin. | The `repl` example is a particularly good one as it allows one to interactively try out Rhai's language features in a standard REPL (**R**ead-**E**val-**P**rint **L**oop). diff --git a/examples/serde.rs b/examples/serde.rs index ae3ae6a9..3cb68459 100644 --- a/examples/serde.rs +++ b/examples/serde.rs @@ -1,6 +1,7 @@ #[cfg(not(feature = "serde"))] fn main() { - println!("This example requires the feature `serde`. Use `cargo run --features serde --example serde`."); + println!(r#"This example requires the "serde" feature which is not enabled by default."#); + println!("Try: cargo run --features serde --example serde"); } #[cfg(feature = "serde")] @@ -14,14 +15,15 @@ fn main() { mod example { use rhai::{de::from_dynamic, ser::to_dynamic}; use rhai::{Dynamic, Engine, Map}; + use serde::{Deserialize, Serialize}; - #[derive(Debug, serde::Serialize, serde::Deserialize)] + #[derive(Debug, Serialize, Deserialize)] struct Point { x: f64, y: f64, } - #[derive(Debug, serde::Serialize, serde::Deserialize)] + #[derive(Debug, Serialize, Deserialize)] struct MyStruct { a: i64, b: Vec, From 368b4a480ba91b31fcd447b5c0b8973640e9b617 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 4 Jul 2020 22:53:00 +0800 Subject: [PATCH 16/46] Reformat code. --- README.md | 2 +- doc/src/about/features.md | 2 +- src/any.rs | 6 +- src/engine.rs | 136 ++++++++++++++++-------------------- src/fn_native.rs | 16 ++--- src/packages/arithmetic.rs | 58 +++++++-------- src/packages/array_basic.rs | 16 ++--- src/packages/string_more.rs | 16 ++--- src/packages/time_basic.rs | 45 ++++++------ src/parser.rs | 9 +-- src/serde/de.rs | 50 +++++++------ 11 files changed, 156 insertions(+), 200 deletions(-) diff --git a/README.md b/README.md index c8363ac2..bb4a9256 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Features * Freely pass Rust variables/constants into a script via an external [`Scope`](https://schungx.github.io/rhai/rust/scope.html). * Easily [call a script-defined function](https://schungx.github.io/rhai/engine/call-fn.html) from Rust. * Fairly low compile-time overhead. -* Fairly efficient evaluation (1 million iterations in 0.4 sec on a single core, 2.3 GHz Linux VM). +* Fairly efficient evaluation (1 million iterations in 0.3 sec on a single core, 2.3 GHz Linux VM). * Relatively little `unsafe` code (yes there are some for performance reasons, and most `unsafe` code is limited to one single source file, all with names starting with `"unsafe_"`). * Re-entrant scripting engine can be made `Send + Sync` (via the `sync` feature). diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 3f19bf63..7d3ff3cd 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -22,7 +22,7 @@ Fast * Fairly low compile-time overhead. -* Fairly efficient evaluation (1 million iterations in 0.4 sec on a single core, 2.3 GHz Linux VM). +* Fairly efficient evaluation (1 million iterations in 0.3 sec on a single core, 2.3 GHz Linux VM). * Scripts are [optimized][script optimization] (useful for template-based machine-generated scripts) for repeated evaluations. diff --git a/src/any.rs b/src/any.rs index 2a2ae5a9..bd21404b 100644 --- a/src/any.rs +++ b/src/any.rs @@ -346,10 +346,8 @@ impl Dynamic { } #[cfg(not(feature = "no_float"))] - { - if let Some(result) = ::downcast_ref::(&value) { - return result.clone().into(); - } + if let Some(result) = ::downcast_ref::(&value) { + return result.clone().into(); } let mut boxed = Box::new(value); diff --git a/src/engine.rs b/src/engine.rs index 1de5d3d4..2bea2ab7 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -353,17 +353,14 @@ pub fn make_getter(id: &str) -> String { /// Extract the property name from a getter function name. fn extract_prop_from_getter(fn_name: &str) -> Option<&str> { #[cfg(not(feature = "no_object"))] - { - if fn_name.starts_with(FN_GET) { - Some(&fn_name[FN_GET.len()..]) - } else { - None - } - } - #[cfg(feature = "no_object")] - { + if fn_name.starts_with(FN_GET) { + Some(&fn_name[FN_GET.len()..]) + } else { None } + + #[cfg(feature = "no_object")] + None } /// Make setter function @@ -374,17 +371,14 @@ pub fn make_setter(id: &str) -> String { /// Extract the property name from a setter function name. fn extract_prop_from_setter(fn_name: &str) -> Option<&str> { #[cfg(not(feature = "no_object"))] - { - if fn_name.starts_with(FN_SET) { - Some(&fn_name[FN_SET.len()..]) - } else { - None - } - } - #[cfg(feature = "no_object")] - { + if fn_name.starts_with(FN_SET) { + Some(&fn_name[FN_SET.len()..]) + } else { None } + + #[cfg(feature = "no_object")] + None } /// Print/debug to stdout @@ -701,12 +695,10 @@ impl Engine { // Check for stack overflow #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "unchecked"))] - { - if level > self.max_call_stack_depth { - return Err(Box::new( - EvalAltResult::ErrorStackOverflow(Position::none()), - )); - } + if level > self.max_call_stack_depth { + return Err(Box::new( + EvalAltResult::ErrorStackOverflow(Position::none()), + )); } let mut this_copy: Dynamic = Default::default(); @@ -2344,21 +2336,19 @@ impl Engine { .try_cast::() { #[cfg(not(feature = "no_module"))] - { - if let Some(resolver) = &self.module_resolver { - let mut module = resolver.resolve(self, &path, expr.position())?; - module.index_all_sub_modules(); - mods.push((name.clone().into(), module)); + if let Some(resolver) = &self.module_resolver { + let mut module = resolver.resolve(self, &path, expr.position())?; + module.index_all_sub_modules(); + mods.push((name.clone().into(), module)); - state.modules += 1; + state.modules += 1; - Ok(Default::default()) - } else { - Err(Box::new(EvalAltResult::ErrorModuleNotFound( - path.to_string(), - expr.position(), - ))) - } + Ok(Default::default()) + } else { + Err(Box::new(EvalAltResult::ErrorModuleNotFound( + path.to_string(), + expr.position(), + ))) } #[cfg(feature = "no_module")] @@ -2509,13 +2499,11 @@ impl Engine { state.operations += 1; #[cfg(not(feature = "unchecked"))] - { - // Guard against too many operations - if self.max_operations > 0 && state.operations > self.max_operations { - return Err(Box::new(EvalAltResult::ErrorTooManyOperations( - Position::none(), - ))); - } + // Guard against too many operations + if self.max_operations > 0 && state.operations > self.max_operations { + return Err(Box::new(EvalAltResult::ErrorTooManyOperations( + Position::none(), + ))); } // Report progress - only in steps @@ -2641,26 +2629,24 @@ fn run_builtin_binary_op( } #[cfg(not(feature = "no_float"))] - { - if args_type == TypeId::of::() { - let x = *x.downcast_ref::().unwrap(); - let y = *y.downcast_ref::().unwrap(); + if args_type == TypeId::of::() { + let x = *x.downcast_ref::().unwrap(); + let y = *y.downcast_ref::().unwrap(); - match op { - "+" => return Ok(Some((x + y).into())), - "-" => return Ok(Some((x - y).into())), - "*" => return Ok(Some((x * y).into())), - "/" => return Ok(Some((x / y).into())), - "%" => return Ok(Some((x % y).into())), - "~" => return pow_f_f(x, y).map(Into::into).map(Some), - "==" => return Ok(Some((x == y).into())), - "!=" => return Ok(Some((x != y).into())), - ">" => return Ok(Some((x > y).into())), - ">=" => return Ok(Some((x >= y).into())), - "<" => return Ok(Some((x < y).into())), - "<=" => return Ok(Some((x <= y).into())), - _ => (), - } + match op { + "+" => return Ok(Some((x + y).into())), + "-" => return Ok(Some((x - y).into())), + "*" => return Ok(Some((x * y).into())), + "/" => return Ok(Some((x / y).into())), + "%" => return Ok(Some((x % y).into())), + "~" => return pow_f_f(x, y).map(Into::into).map(Some), + "==" => return Ok(Some((x == y).into())), + "!=" => return Ok(Some((x != y).into())), + ">" => return Ok(Some((x > y).into())), + ">=" => return Ok(Some((x >= y).into())), + "<" => return Ok(Some((x < y).into())), + "<=" => return Ok(Some((x <= y).into())), + _ => (), } } @@ -2737,20 +2723,18 @@ fn run_builtin_op_assignment( } #[cfg(not(feature = "no_float"))] - { - if args_type == TypeId::of::() { - let x = x.downcast_mut::().unwrap(); - let y = *y.downcast_ref::().unwrap(); + if args_type == TypeId::of::() { + let x = x.downcast_mut::().unwrap(); + let y = *y.downcast_ref::().unwrap(); - match op { - "+=" => return Ok(Some(*x += y)), - "-=" => return Ok(Some(*x -= y)), - "*=" => return Ok(Some(*x *= y)), - "/=" => return Ok(Some(*x /= y)), - "%=" => return Ok(Some(*x %= y)), - "~=" => return Ok(Some(*x = pow_f_f(*x, y)?)), - _ => (), - } + match op { + "+=" => return Ok(Some(*x += y)), + "-=" => return Ok(Some(*x -= y)), + "*=" => return Ok(Some(*x *= y)), + "/=" => return Ok(Some(*x /= y)), + "%=" => return Ok(Some(*x %= y)), + "~=" => return Ok(Some(*x = pow_f_f(*x, y)?)), + _ => (), } } diff --git a/src/fn_native.rs b/src/fn_native.rs index a1110fd3..1279e430 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -27,13 +27,9 @@ pub type Shared = Arc; /// If the resource is shared (i.e. has other outstanding references), a cloned copy is used. pub fn shared_make_mut(value: &mut Shared) -> &mut T { #[cfg(not(feature = "sync"))] - { - Rc::make_mut(value) - } + return Rc::make_mut(value); #[cfg(feature = "sync")] - { - Arc::make_mut(value) - } + return Arc::make_mut(value); } /// Consume a `Shared` resource, assuming that it is unique (i.e. not shared). @@ -43,13 +39,9 @@ pub fn shared_make_mut(value: &mut Shared) -> &mut T { /// Panics if the resource is shared (i.e. has other outstanding references). pub fn shared_take(value: Shared) -> T { #[cfg(not(feature = "sync"))] - { - Rc::try_unwrap(value).map_err(|_| ()).unwrap() - } + return Rc::try_unwrap(value).map_err(|_| ()).unwrap(); #[cfg(feature = "sync")] - { - Arc::try_unwrap(value).map_err(|_| ()).unwrap() - } + return Arc::try_unwrap(value).map_err(|_| ()).unwrap(); } pub type FnCallArgs<'a> = [&'a mut Dynamic]; diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index 31808a2a..7a548ff0 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -190,42 +190,38 @@ fn modulo_u(x: T, y: T) -> FuncReturn<::Output> { // Checked power pub(crate) fn pow_i_i(x: INT, y: INT) -> FuncReturn { #[cfg(not(feature = "only_i32"))] - { - if y > (u32::MAX as INT) { - Err(Box::new(EvalAltResult::ErrorArithmetic( - format!("Integer raised to too large an index: {} ~ {}", x, y), + if y > (u32::MAX as INT) { + Err(Box::new(EvalAltResult::ErrorArithmetic( + format!("Integer raised to too large an index: {} ~ {}", x, y), + Position::none(), + ))) + } else if y < 0 { + Err(Box::new(EvalAltResult::ErrorArithmetic( + format!("Integer raised to a negative index: {} ~ {}", x, y), + Position::none(), + ))) + } else { + x.checked_pow(y as u32).ok_or_else(|| { + Box::new(EvalAltResult::ErrorArithmetic( + format!("Power overflow: {} ~ {}", x, y), Position::none(), - ))) - } else if y < 0 { - Err(Box::new(EvalAltResult::ErrorArithmetic( - format!("Integer raised to a negative index: {} ~ {}", x, y), - Position::none(), - ))) - } else { - x.checked_pow(y as u32).ok_or_else(|| { - Box::new(EvalAltResult::ErrorArithmetic( - format!("Power overflow: {} ~ {}", x, y), - Position::none(), - )) - }) - } + )) + }) } #[cfg(feature = "only_i32")] - { - if y < 0 { - Err(Box::new(EvalAltResult::ErrorArithmetic( - format!("Integer raised to a negative index: {} ~ {}", x, y), + if y < 0 { + Err(Box::new(EvalAltResult::ErrorArithmetic( + format!("Integer raised to a negative index: {} ~ {}", x, y), + Position::none(), + ))) + } else { + x.checked_pow(y as u32).ok_or_else(|| { + Box::new(EvalAltResult::ErrorArithmetic( + format!("Power overflow: {} ~ {}", x, y), Position::none(), - ))) - } else { - x.checked_pow(y as u32).ok_or_else(|| { - Box::new(EvalAltResult::ErrorArithmetic( - format!("Power overflow: {} ~ {}", x, y), - Position::none(), - )) - }) - } + )) + }) } } // Unchecked integer power - may panic on overflow or if the power index is too high (> u32::MAX) diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index bf06ed47..7c7b5681 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -30,15 +30,13 @@ fn pad(engine: &Engine, args: &mut [&mut Dynamic]) -> FuncRe // Check if array will be over max size limit #[cfg(not(feature = "unchecked"))] - { - if engine.max_array_size > 0 && len > 0 && (len as usize) > engine.max_array_size { - return Err(Box::new(EvalAltResult::ErrorDataTooLarge( - "Size of array".to_string(), - engine.max_array_size, - len as usize, - Position::none(), - ))); - } + if engine.max_array_size > 0 && len > 0 && (len as usize) > engine.max_array_size { + return Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Size of array".to_string(), + engine.max_array_size, + len as usize, + Position::none(), + ))); } if len > 0 { diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 7bc10012..e6acbbb9 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -231,15 +231,13 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str // Check if string will be over max size limit #[cfg(not(feature = "unchecked"))] - { - if engine.max_string_size > 0 && len > 0 && (len as usize) > engine.max_string_size { - return Err(Box::new(EvalAltResult::ErrorDataTooLarge( - "Length of string".to_string(), - engine.max_string_size, - len as usize, - Position::none(), - ))); - } + if engine.max_string_size > 0 && len > 0 && (len as usize) > engine.max_string_size { + return Err(Box::new(EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + engine.max_string_size, + len as usize, + Position::none(), + ))); } if len > 0 { diff --git a/src/packages/time_basic.rs b/src/packages/time_basic.rs index 9fd4e21f..cbf13f1b 100644 --- a/src/packages/time_basic.rs +++ b/src/packages/time_basic.rs @@ -33,17 +33,16 @@ def_package!(crate:BasicTimePackage:"Basic timing utilities.", lib, { let seconds = (ts2 - ts1).as_secs(); #[cfg(not(feature = "unchecked"))] - { - if seconds > (MAX_INT as u64) { - return Err(Box::new(EvalAltResult::ErrorArithmetic( - format!( - "Integer overflow for timestamp duration: {}", - -(seconds as i64) - ), - Position::none(), - ))); - } + if seconds > (MAX_INT as u64) { + return Err(Box::new(EvalAltResult::ErrorArithmetic( + format!( + "Integer overflow for timestamp duration: {}", + -(seconds as i64) + ), + Position::none(), + ))); } + return Ok(-(seconds as INT)); } } else { @@ -55,14 +54,13 @@ def_package!(crate:BasicTimePackage:"Basic timing utilities.", lib, { let seconds = (ts1 - ts2).as_secs(); #[cfg(not(feature = "unchecked"))] - { - if seconds > (MAX_INT as u64) { - return Err(Box::new(EvalAltResult::ErrorArithmetic( - format!("Integer overflow for timestamp duration: {}", seconds), - Position::none(), - ))); - } + if seconds > (MAX_INT as u64) { + return Err(Box::new(EvalAltResult::ErrorArithmetic( + format!("Integer overflow for timestamp duration: {}", seconds), + Position::none(), + ))); } + return Ok(seconds as INT); } } @@ -86,14 +84,13 @@ def_package!(crate:BasicTimePackage:"Basic timing utilities.", lib, { let seconds = timestamp.elapsed().as_secs(); #[cfg(not(feature = "unchecked"))] - { - if seconds > (MAX_INT as u64) { - return Err(Box::new(EvalAltResult::ErrorArithmetic( - format!("Integer overflow for timestamp.elapsed: {}", seconds), - Position::none(), - ))); - } + if seconds > (MAX_INT as u64) { + return Err(Box::new(EvalAltResult::ErrorArithmetic( + format!("Integer overflow for timestamp.elapsed: {}", seconds), + Position::none(), + ))); } + Ok(seconds as INT) } diff --git a/src/parser.rs b/src/parser.rs index 505c8e7e..37743e0f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1495,13 +1495,9 @@ fn parse_unary( .map(|i| Expr::IntegerConstant(Box::new((i, pos)))) .or_else(|| { #[cfg(not(feature = "no_float"))] - { - Some(Expr::FloatConstant(Box::new((-(x.0 as FLOAT), pos)))) - } + return Some(Expr::FloatConstant(Box::new((-(x.0 as FLOAT), pos)))); #[cfg(feature = "no_float")] - { - None - } + return None; }) .ok_or_else(|| LexError::MalformedNumber(format!("-{}", x.0)).into_err(pos)) } @@ -2618,7 +2614,6 @@ impl Engine { }; match input.peek().unwrap() { - #[cfg(not(feature = "no_function"))] (Token::Fn, pos) => { let mut state = ParseState::new( self.max_function_expr_depth, diff --git a/src/serde/de.rs b/src/serde/de.rs index be466508..82910073 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -208,24 +208,24 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { fn deserialize_f32>(self, visitor: V) -> Result> { #[cfg(not(feature = "no_float"))] - { - self.value - .downcast_ref::() - .map_or_else(|| self.type_error(), |&x| visitor.visit_f32(x)) - } + return self + .value + .downcast_ref::() + .map_or_else(|| self.type_error(), |&x| visitor.visit_f32(x)); + #[cfg(feature = "no_float")] - self.type_error_str("f32") + return self.type_error_str("f32"); } fn deserialize_f64>(self, visitor: V) -> Result> { #[cfg(not(feature = "no_float"))] - { - self.value - .downcast_ref::() - .map_or_else(|| self.type_error(), |&x| visitor.visit_f64(x)) - } + return self + .value + .downcast_ref::() + .map_or_else(|| self.type_error(), |&x| visitor.visit_f64(x)); + #[cfg(feature = "no_float")] - self.type_error_str("f64") + return self.type_error_str("f64"); } fn deserialize_char>(self, visitor: V) -> Result> { @@ -284,14 +284,13 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { fn deserialize_seq>(self, visitor: V) -> Result> { #[cfg(not(feature = "no_index"))] - { - self.value.downcast_ref::().map_or_else( - || self.type_error(), - |arr| visitor.visit_seq(IterateArray::new(arr.iter())), - ) - } + return self.value.downcast_ref::().map_or_else( + || self.type_error(), + |arr| visitor.visit_seq(IterateArray::new(arr.iter())), + ); + #[cfg(feature = "no_index")] - self.type_error() + return self.type_error(); } fn deserialize_tuple>( @@ -313,14 +312,13 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { fn deserialize_map>(self, visitor: V) -> Result> { #[cfg(not(feature = "no_object"))] - { - self.value.downcast_ref::().map_or_else( - || self.type_error(), - |map| visitor.visit_map(IterateMap::new(map.keys(), map.values())), - ) - } + return self.value.downcast_ref::().map_or_else( + || self.type_error(), + |map| visitor.visit_map(IterateMap::new(map.keys(), map.values())), + ); + #[cfg(feature = "no_object")] - self.type_error() + return self.type_error(); } fn deserialize_struct>( From 936a3ff44aabb261f133ba780c4890159d4bba83 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 5 Jul 2020 15:23:51 +0800 Subject: [PATCH 17/46] Add feature to disable symbols. --- README.md | 1 + RELEASES.md | 6 +- doc/src/SUMMARY.md | 17 ++-- doc/src/about/features.md | 2 + doc/src/engine/disable.md | 28 ++++++ doc/src/links.md | 2 + src/api.rs | 15 ++- src/engine.rs | 172 +++----------------------------- src/error.rs | 7 +- src/lib.rs | 1 + src/parser.rs | 4 +- src/settings.rs | 203 ++++++++++++++++++++++++++++++++++++++ src/token.rs | 101 ++++++++++++++++--- tests/tokens.rs | 20 ++++ 14 files changed, 386 insertions(+), 193 deletions(-) create mode 100644 doc/src/engine/disable.md create mode 100644 src/settings.rs create mode 100644 tests/tokens.rs diff --git a/README.md b/README.md index bb4a9256..a88c574d 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Features * Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). * Serialization/deserialization support via [serde](https://crates.io/crates/serde) (requires the `serde` feature). +* Surgically disable keywords and operators to restrict the language. * Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations. * Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html). diff --git a/RELEASES.md b/RELEASES.md index 0a1ead2b..4306c015 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,7 +4,10 @@ Rhai Release Notes Version 0.17.0 ============== -This version adds [`serde`](https://crates.io/crates/serde) support for working with `Dynamic` values (particularly _object maps_). +This version adds: + +* [`serde`](https://crates.io/crates/serde) support for working with `Dynamic` values (particularly _object maps_) +* Ability to surgically disable keywords and/or operators in the language Breaking changes ---------------- @@ -16,6 +19,7 @@ New features * New `serde` feature to allow serializating/deserializating to/from `Dynamic` values using [`serde`](https://crates.io/crates/serde). This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back. +* `Engine::disable_symbol` to surgically disable keywords and/or operators. Version 0.16.1 diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index a7c3946b..39c9fb68 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -95,17 +95,18 @@ The Rhai Scripting Language 8. [Maximum Call Stack Depth](safety/max-call-stack.md) 9. [Maximum Statement Depth](safety/max-stmt-depth.md) 8. [Advanced Topics](advanced.md) - 1. [Object-Oriented Programming (OOP)](language/oop.md) - 2. [Serialization/Deserialization of `Dynamic` with `serde`](rust/serde.md) - 3. [Script Optimization](engine/optimize/index.md) + 1. [Disable Keywords and/or Operators](engine/disable.md) + 2. [Object-Oriented Programming (OOP)](language/oop.md) + 3. [Serialization/Deserialization of `Dynamic` with `serde`](rust/serde.md) + 4. [Script Optimization](engine/optimize/index.md) 1. [Optimization Levels](engine/optimize/optimize-levels.md) 2. [Re-Optimize an AST](engine/optimize/reoptimize.md) 3. [Eager Function Evaluation](engine/optimize/eager.md) 4. [Side-Effect Considerations](engine/optimize/side-effects.md) 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) - 4. [Eval Statement](language/eval.md) -9. [Appendix](appendix/index.md) - 1. [Keywords](appendix/keywords.md) - 2. [Operators](appendix/operators.md) - 3. [Literals](appendix/literals.md) + 5. [Eval Statement](language/eval.md) +9. [Appendix](appendix/index.md) + 6. [Keywords](appendix/keywords.md) + 7. [Operators](appendix/operators.md) + 8. [Literals](appendix/literals.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 7d3ff3cd..28f20aef 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -64,3 +64,5 @@ Flexible * Support for [minimal builds] by excluding unneeded language [features]. * Supports [most build targets](targets.md) including `no-std` and [WASM]. + +* Surgically [disable keywords and operators] to restrict the language. diff --git a/doc/src/engine/disable.md b/doc/src/engine/disable.md new file mode 100644 index 00000000..f34e763c --- /dev/null +++ b/doc/src/engine/disable.md @@ -0,0 +1,28 @@ +Disable Certain Keywords and/or Operators +======================================== + +{{#include ../links.md}} + +For certain embedded usage, it is sometimes necessary to restrict the language to a strict subset of Rhai +to prevent usage of certain language features. + +Rhai supports surgically disabling a keyword or operator via the `Engine::disable_symbol` method. + +```rust +use rhai::Engine; + +let mut engine = Engine::new(); + +engine.disable_symbol("if"); // disable the 'if' keyword +engine.disable_symbol("+="); // disable the '+=' operator + +// The following all return parse errors. + +engine.compile("let x = if true { 42 } else { 0 };")?; +// ^ missing ';' after statement end +// ^ 'if' is parsed as a variable name + +engine.compile("let x = 40 + 2; x += 1;")?; +// ^ '+=' is not recognized as an operator +// ^ other operators are not affected +``` diff --git a/doc/src/links.md b/doc/src/links.md index cf367206..7ef02a10 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -102,3 +102,5 @@ [`OptimizationLevel::Full`]: {{rootUrl}}/engine/optimize/optimize-levels.md [`OptimizationLevel::Simple`]: {{rootUrl}}/engine/optimize/optimize-levels.md [`OptimizationLevel::None`]: {{rootUrl}}/engine/optimize/optimize-levels.md + +[disable keywords and operators]: {{rootUrl}}/engine/disable.md diff --git a/src/api.rs b/src/api.rs index 49c7b486..5b6acdb5 100644 --- a/src/api.rs +++ b/src/api.rs @@ -118,8 +118,13 @@ impl Engine { /// ``` #[cfg(not(feature = "no_object"))] pub fn register_type_with_name(&mut self, name: &str) { + if self.type_names.is_none() { + self.type_names = Some(Default::default()); + } // Add the pretty-print type name into the map self.type_names + .as_mut() + .unwrap() .insert(type_name::().to_string(), name.to_string()); } @@ -548,7 +553,7 @@ impl Engine { scripts: &[&str], optimization_level: OptimizationLevel, ) -> Result { - let stream = lex(scripts, self.max_string_size); + let stream = lex(scripts, self.max_string_size, self.disable_tokens.as_ref()); self.parse(&mut stream.peekable(), scope, optimization_level) } @@ -673,7 +678,7 @@ impl Engine { // Trims the JSON string and add a '#' in front let scripts = ["#", json.trim()]; - let stream = lex(&scripts, self.max_string_size); + let stream = lex(&scripts, self.max_string_size, self.disable_tokens.as_ref()); let ast = self.parse_global_expr(&mut stream.peekable(), &scope, OptimizationLevel::None)?; @@ -754,7 +759,7 @@ impl Engine { script: &str, ) -> Result { let scripts = [script]; - let stream = lex(&scripts, self.max_string_size); + let stream = lex(&scripts, self.max_string_size, self.disable_tokens.as_ref()); { let mut peekable = stream.peekable(); self.parse_global_expr(&mut peekable, scope, self.optimization_level) @@ -909,7 +914,7 @@ impl Engine { script: &str, ) -> Result> { let scripts = [script]; - let stream = lex(&scripts, self.max_string_size); + let stream = lex(&scripts, self.max_string_size, self.disable_tokens.as_ref()); // No need to optimize a lone expression let ast = self.parse_global_expr(&mut stream.peekable(), scope, OptimizationLevel::None)?; @@ -1042,7 +1047,7 @@ impl Engine { script: &str, ) -> Result<(), Box> { let scripts = [script]; - let stream = lex(&scripts, self.max_string_size); + let stream = lex(&scripts, self.max_string_size, self.disable_tokens.as_ref()); let ast = self.parse(&mut stream.peekable(), scope, self.optimization_level)?; self.consume_ast_with_scope(scope, &ast) } diff --git a/src/engine.rs b/src/engine.rs index 2bea2ab7..9dff48e4 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -6,7 +6,7 @@ use crate::error::ParseErrorType; use crate::fn_native::{CallableFunction, Callback, FnCallArgs, FnPtr}; use crate::module::{resolvers, Module, ModuleRef, ModuleResolver}; use crate::optimize::OptimizationLevel; -use crate::packages::{Package, PackageLibrary, PackagesCollection, StandardPackage}; +use crate::packages::{Package, PackagesCollection, StandardPackage}; use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, Stmt, AST, INT}; use crate::r#unsafe::unsafe_cast_var_name_to_lifetime; use crate::result::EvalAltResult; @@ -21,7 +21,7 @@ use crate::stdlib::{ any::{type_name, TypeId}, borrow::Cow, boxed::Box, - collections::HashMap, + collections::{HashMap, HashSet}, format, iter::{empty, once}, mem, @@ -266,7 +266,10 @@ pub struct Engine { pub(crate) module_resolver: Option>, /// A hashmap mapping type names to pretty-print names. - pub(crate) type_names: HashMap, + pub(crate) type_names: Option>, + + /// A hash-set containing tokens to disable. + pub(crate) disable_tokens: Option>, /// Callback closure for implementing the `print` command. pub(crate) print: Callback, @@ -313,7 +316,8 @@ impl Default for Engine { #[cfg(any(feature = "no_module", feature = "no_std", target_arch = "wasm32",))] module_resolver: None, - type_names: Default::default(), + type_names: None, + disable_tokens: None, // default print/debug implementations print: Box::new(default_print), @@ -492,7 +496,9 @@ impl Engine { global_module: Default::default(), module_resolver: None, - type_names: Default::default(), + type_names: None, + disable_tokens: None, + print: Box::new(|_| {}), debug: Box::new(|_| {}), progress: None, @@ -514,158 +520,6 @@ impl Engine { } } - /// Load a new package into the `Engine`. - /// - /// When searching for functions, packages loaded later are preferred. - /// In other words, loaded packages are searched in reverse order. - pub fn load_package(&mut self, package: PackageLibrary) { - // Push the package to the top - packages are searched in reverse order - self.packages.push(package); - } - - /// Load a new package into the `Engine`. - /// - /// When searching for functions, packages loaded later are preferred. - /// In other words, loaded packages are searched in reverse order. - pub fn load_packages(&mut self, package: PackageLibrary) { - // Push the package to the top - packages are searched in reverse order - self.packages.push(package); - } - - /// Control whether and how the `Engine` will optimize an AST after compilation. - /// - /// Not available under the `no_optimize` feature. - #[cfg(not(feature = "no_optimize"))] - pub fn set_optimization_level(&mut self, optimization_level: OptimizationLevel) { - self.optimization_level = optimization_level - } - - /// The current optimization level. - /// It controls whether and how the `Engine` will optimize an AST after compilation. - /// - /// Not available under the `no_optimize` feature. - #[cfg(not(feature = "no_optimize"))] - pub fn optimization_level(&self) -> OptimizationLevel { - self.optimization_level - } - - /// Set the maximum levels of function calls allowed for a script in order to avoid - /// infinite recursion and stack overflows. - #[cfg(not(feature = "unchecked"))] - pub fn set_max_call_levels(&mut self, levels: usize) { - self.max_call_stack_depth = levels - } - - /// The maximum levels of function calls allowed for a script. - #[cfg(not(feature = "unchecked"))] - pub fn max_call_levels(&self) -> usize { - self.max_call_stack_depth - } - - /// Set the maximum number of operations allowed for a script to run to avoid - /// consuming too much resources (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - pub fn set_max_operations(&mut self, operations: u64) { - self.max_operations = if operations == u64::MAX { - 0 - } else { - operations - }; - } - - /// The maximum number of operations allowed for a script to run (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - pub fn max_operations(&self) -> u64 { - self.max_operations - } - - /// Set the maximum number of imported modules allowed for a script. - #[cfg(not(feature = "unchecked"))] - pub fn set_max_modules(&mut self, modules: usize) { - self.max_modules = modules; - } - - /// The maximum number of imported modules allowed for a script. - #[cfg(not(feature = "unchecked"))] - pub fn max_modules(&self) -> usize { - self.max_modules - } - - /// Set the depth limits for expressions (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - pub fn set_max_expr_depths(&mut self, max_expr_depth: usize, max_function_expr_depth: usize) { - self.max_expr_depth = if max_expr_depth == usize::MAX { - 0 - } else { - max_expr_depth - }; - self.max_function_expr_depth = if max_function_expr_depth == usize::MAX { - 0 - } else { - max_function_expr_depth - }; - } - - /// The depth limit for expressions (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - pub fn max_expr_depth(&self) -> usize { - self.max_expr_depth - } - - /// The depth limit for expressions in functions (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - pub fn max_function_expr_depth(&self) -> usize { - self.max_function_expr_depth - } - - /// Set the maximum length of strings (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - pub fn set_max_string_size(&mut self, max_size: usize) { - self.max_string_size = if max_size == usize::MAX { 0 } else { max_size }; - } - - /// The maximum length of strings (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - pub fn max_string_size(&self) -> usize { - self.max_string_size - } - - /// Set the maximum length of arrays (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - #[cfg(not(feature = "no_index"))] - pub fn set_max_array_size(&mut self, max_size: usize) { - self.max_array_size = if max_size == usize::MAX { 0 } else { max_size }; - } - - /// The maximum length of arrays (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - #[cfg(not(feature = "no_index"))] - pub fn max_array_size(&self) -> usize { - self.max_array_size - } - - /// Set the maximum length of object maps (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - #[cfg(not(feature = "no_object"))] - pub fn set_max_map_size(&mut self, max_size: usize) { - self.max_map_size = if max_size == usize::MAX { 0 } else { max_size }; - } - - /// The maximum length of object maps (0 for unlimited). - #[cfg(not(feature = "unchecked"))] - #[cfg(not(feature = "no_object"))] - pub fn max_map_size(&self) -> usize { - self.max_map_size - } - - /// Set the module resolution service used by the `Engine`. - /// - /// Not available under the `no_module` feature. - #[cfg(not(feature = "no_module"))] - pub fn set_module_resolver(&mut self, resolver: Option) { - self.module_resolver = resolver.map(|f| Box::new(f) as Box); - } - /// Universal method for calling functions either registered with the `Engine` or written in Rhai. /// Position in `EvalAltResult` is None and must be set afterwards. /// @@ -2520,8 +2374,8 @@ impl Engine { /// Map a type_name into a pretty-print name pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str { self.type_names - .get(name) - .map(String::as_str) + .as_ref() + .and_then(|t| t.get(name).map(String::as_str)) .unwrap_or(map_std_type_name(name)) } } diff --git a/src/error.rs b/src/error.rs index 61f2c58f..6b3308ba 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,6 @@ use crate::token::Position; use crate::stdlib::{ boxed::Box, - char, error::Error, fmt, string::{String, ToString}, @@ -15,8 +14,8 @@ use crate::stdlib::{ #[derive(Debug, Eq, PartialEq, Clone, Hash)] #[non_exhaustive] pub enum LexError { - /// An unexpected character is encountered when tokenizing the script text. - UnexpectedChar(char), + /// An unexpected symbol is encountered when tokenizing the script text. + UnexpectedInput(String), /// A string literal is not terminated before a new-line or EOF. UnterminatedString, /// An identifier is in an invalid format. @@ -38,7 +37,7 @@ impl Error for LexError {} impl fmt::Display for LexError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::UnexpectedChar(c) => write!(f, "Unexpected '{}'", c), + Self::UnexpectedInput(s) => write!(f, "Unexpected '{}'", s), Self::MalformedEscapeSequence(s) => write!(f, "Invalid escape sequence: '{}'", s), Self::MalformedNumber(s) => write!(f, "Invalid number: '{}'", s), Self::MalformedChar(s) => write!(f, "Invalid character: '{}'", s), diff --git a/src/lib.rs b/src/lib.rs index 0b825113..c3563e38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ mod result; mod scope; #[cfg(feature = "serde")] mod serde; +mod settings; mod stdlib; mod token; mod r#unsafe; diff --git a/src/parser.rs b/src/parser.rs index 37743e0f..797e28f4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1272,7 +1272,7 @@ fn parse_map_literal( _ => { let (name, pos) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s, pos), - (Token::StringConst(s), pos) => (s, pos), + (Token::StringConstant(s), pos) => (s, pos), (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) if map.is_empty() => { return Err(PERR::MissingToken( @@ -1380,7 +1380,7 @@ fn parse_primary( #[cfg(not(feature = "no_float"))] Token::FloatConstant(x) => Expr::FloatConstant(Box::new((x, settings.pos))), Token::CharConstant(c) => Expr::CharConstant(Box::new((c, settings.pos))), - Token::StringConst(s) => Expr::StringConstant(Box::new((s.into(), settings.pos))), + Token::StringConstant(s) => Expr::StringConstant(Box::new((s.into(), settings.pos))), Token::Identifier(s) => { let index = state.find_var(&s); Expr::Variable(Box::new(((s, settings.pos), None, 0, index))) diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 00000000..a4379d79 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,203 @@ +use crate::engine::Engine; +use crate::module::ModuleResolver; +use crate::optimize::OptimizationLevel; +use crate::packages::PackageLibrary; + +impl Engine { + /// Load a new package into the `Engine`. + /// + /// When searching for functions, packages loaded later are preferred. + /// In other words, loaded packages are searched in reverse order. + pub fn load_package(&mut self, package: PackageLibrary) { + // Push the package to the top - packages are searched in reverse order + self.packages.push(package); + } + + /// Load a new package into the `Engine`. + /// + /// When searching for functions, packages loaded later are preferred. + /// In other words, loaded packages are searched in reverse order. + pub fn load_packages(&mut self, package: PackageLibrary) { + // Push the package to the top - packages are searched in reverse order + self.packages.push(package); + } + + /// Control whether and how the `Engine` will optimize an AST after compilation. + /// + /// Not available under the `no_optimize` feature. + #[cfg(not(feature = "no_optimize"))] + pub fn set_optimization_level(&mut self, optimization_level: OptimizationLevel) { + self.optimization_level = optimization_level + } + + /// The current optimization level. + /// It controls whether and how the `Engine` will optimize an AST after compilation. + /// + /// Not available under the `no_optimize` feature. + #[cfg(not(feature = "no_optimize"))] + pub fn optimization_level(&self) -> OptimizationLevel { + self.optimization_level + } + + /// Set the maximum levels of function calls allowed for a script in order to avoid + /// infinite recursion and stack overflows. + #[cfg(not(feature = "unchecked"))] + pub fn set_max_call_levels(&mut self, levels: usize) { + self.max_call_stack_depth = levels + } + + /// The maximum levels of function calls allowed for a script. + #[cfg(not(feature = "unchecked"))] + pub fn max_call_levels(&self) -> usize { + self.max_call_stack_depth + } + + /// Set the maximum number of operations allowed for a script to run to avoid + /// consuming too much resources (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn set_max_operations(&mut self, operations: u64) { + self.max_operations = if operations == u64::MAX { + 0 + } else { + operations + }; + } + + /// The maximum number of operations allowed for a script to run (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_operations(&self) -> u64 { + self.max_operations + } + + /// Set the maximum number of imported modules allowed for a script. + #[cfg(not(feature = "unchecked"))] + pub fn set_max_modules(&mut self, modules: usize) { + self.max_modules = modules; + } + + /// The maximum number of imported modules allowed for a script. + #[cfg(not(feature = "unchecked"))] + pub fn max_modules(&self) -> usize { + self.max_modules + } + + /// Set the depth limits for expressions (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn set_max_expr_depths(&mut self, max_expr_depth: usize, max_function_expr_depth: usize) { + self.max_expr_depth = if max_expr_depth == usize::MAX { + 0 + } else { + max_expr_depth + }; + self.max_function_expr_depth = if max_function_expr_depth == usize::MAX { + 0 + } else { + max_function_expr_depth + }; + } + + /// The depth limit for expressions (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_expr_depth(&self) -> usize { + self.max_expr_depth + } + + /// The depth limit for expressions in functions (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_function_expr_depth(&self) -> usize { + self.max_function_expr_depth + } + + /// Set the maximum length of strings (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn set_max_string_size(&mut self, max_size: usize) { + self.max_string_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of strings (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_string_size(&self) -> usize { + self.max_string_size + } + + /// Set the maximum length of arrays (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_index"))] + pub fn set_max_array_size(&mut self, max_size: usize) { + self.max_array_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of arrays (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_index"))] + pub fn max_array_size(&self) -> usize { + self.max_array_size + } + + /// Set the maximum length of object maps (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_object"))] + pub fn set_max_map_size(&mut self, max_size: usize) { + self.max_map_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of object maps (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_object"))] + pub fn max_map_size(&self) -> usize { + self.max_map_size + } + + /// Set the module resolution service used by the `Engine`. + /// + /// Not available under the `no_module` feature. + #[cfg(not(feature = "no_module"))] + pub fn set_module_resolver(&mut self, resolver: Option) { + self.module_resolver = resolver.map(|f| Box::new(f) as Box); + } + + /// Disable a particular keyword or operator in the language. + /// + /// # Examples + /// + /// The following will raise an error during parsing because the `if` keyword is disabled + /// and is recognized as a variable name! + /// + /// ```rust,should_panic + /// # fn main() -> Result<(), rhai::ParseError> { + /// use rhai::Engine; + /// + /// let mut engine = Engine::new(); + /// + /// engine.disable_symbol("if"); // disable the 'if' keyword + /// + /// engine.compile("let x = if true { 42 } else { 0 };")?; + /// // ^ 'if' is parsed as a variable name + /// // ^ missing ';' after statement end + /// # Ok(()) + /// # } + /// ``` + /// + /// The following will raise an error during parsing because the `+=` operator is disabled. + /// + /// ```rust,should_panic + /// # fn main() -> Result<(), rhai::ParseError> { + /// use rhai::Engine; + /// + /// let mut engine = Engine::new(); + /// + /// engine.disable_symbol("+="); // disable the '+=' operator + /// + /// engine.compile("let x = 42; x += 1;")?; + /// // ^ unknown operator + /// # Ok(()) + /// # } + /// ``` + pub fn disable_symbol(&mut self, symbol: &str) { + if self.disable_tokens.is_none() { + self.disable_tokens = Some(Default::default()); + } + + self.disable_tokens.as_mut().unwrap().insert(symbol.into()); + } +} diff --git a/src/token.rs b/src/token.rs index 4b19ab49..fc527917 100644 --- a/src/token.rs +++ b/src/token.rs @@ -10,7 +10,9 @@ use crate::parser::FLOAT; use crate::stdlib::{ borrow::Cow, boxed::Box, - char, fmt, + char, + collections::HashSet, + fmt, iter::Peekable, str::{Chars, FromStr}, string::{String, ToString}, @@ -19,7 +21,7 @@ use crate::stdlib::{ type LERR = LexError; -pub type TokenStream<'a> = Peekable>; +pub type TokenStream<'a, 't> = Peekable>; /// A location (line number + character position) in the input script. /// @@ -137,7 +139,7 @@ pub enum Token { FloatConstant(FLOAT), Identifier(String), CharConstant(char), - StringConst(String), + StringConstant(String), LeftBrace, RightBrace, LeftParen, @@ -226,8 +228,8 @@ impl Token { CharConstant(c) => c.to_string().into(), LexError(err) => err.to_string().into(), - token => (match token { - StringConst(_) => "string", + token => match token { + StringConstant(_) => "string", LeftBrace => "{", RightBrace => "}", LeftParen => "(", @@ -292,13 +294,15 @@ impl Token { PowerOfAssign => "~=", #[cfg(not(feature = "no_function"))] Private => "private", + #[cfg(not(feature = "no_module"))] Import => "import", #[cfg(not(feature = "no_module"))] Export => "export", + #[cfg(not(feature = "no_module"))] As => "as", EOF => "{EOF}", _ => unreachable!("operator should be match in outer scope"), - }) + } .into(), } } @@ -422,6 +426,41 @@ impl Token { _ => false, } } + + /// Is this token an operator? + pub fn is_operator(&self) -> bool { + use Token::*; + + match self { + LeftBrace | RightBrace | LeftParen | RightParen | LeftBracket | RightBracket | Plus + | UnaryPlus | Minus | UnaryMinus | Multiply | Divide | Modulo | PowerOf | LeftShift + | RightShift | SemiColon | Colon | DoubleColon | Comma | Period | MapStart | Equals + | LessThan | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo + | NotEqualsTo | Bang | Pipe | Or | XOr | Ampersand | And | PlusAssign | MinusAssign + | MultiplyAssign | DivideAssign | LeftShiftAssign | RightShiftAssign | AndAssign + | OrAssign | XOrAssign | ModuloAssign | PowerOfAssign => true, + + _ => false, + } + } + + /// Is this token a keyword? + pub fn is_keyword(&self) -> bool { + use Token::*; + + match self { + #[cfg(not(feature = "no_function"))] + Fn | Private => true, + + #[cfg(not(feature = "no_module"))] + Import | Export | As => true, + + True | False | Let | Const | If | Else | While | Loop | For | In | Continue | Break + | Return | Throw => true, + + _ => false, + } + } } impl From for String { @@ -431,7 +470,7 @@ impl From for String { } /// State of the tokenizer. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)] +#[derive(Debug, Clone, Eq, PartialEq, Default)] pub struct TokenizeState { /// Maximum length of a string (0 = unlimited). pub max_string_size: usize, @@ -644,7 +683,7 @@ pub fn get_next_token( let result = get_next_token_inner(stream, state, pos); // Save the last token's state - if let Some((token, _)) = &result { + if let Some((ref token, _)) = result { state.non_unary = !token.is_next_unary(); } @@ -848,7 +887,7 @@ fn get_next_token_inner( ('"', _) => return parse_string_literal(stream, state, pos, '"') .map_or_else( |err| Some((Token::LexError(Box::new(err.0)), err.1)), - |out| Some((Token::StringConst(out), start_pos)), + |out| Some((Token::StringConstant(out), start_pos)), ), // ' - character literal @@ -1118,7 +1157,7 @@ fn get_next_token_inner( ('\0', _) => unreachable!(), (ch, _) if ch.is_whitespace() => (), - (ch, _) => return Some((Token::LexError(Box::new(LERR::UnexpectedChar(ch))), start_pos)), + (ch, _) => return Some((Token::LexError(Box::new(LERR::UnexpectedInput(ch.to_string()))), start_pos)), } } @@ -1172,7 +1211,9 @@ impl InputStream for MultiInputsStream<'_> { } /// An iterator on a `Token` stream. -pub struct TokenIterator<'a> { +pub struct TokenIterator<'a, 't> { + /// Disable certain tokens. + pub disable_tokens: Option<&'t HashSet>, /// Current state. state: TokenizeState, /// Current position. @@ -1181,17 +1222,49 @@ pub struct TokenIterator<'a> { stream: MultiInputsStream<'a>, } -impl<'a> Iterator for TokenIterator<'a> { +impl<'a> Iterator for TokenIterator<'a, '_> { type Item = (Token, Position); fn next(&mut self) -> Option { - get_next_token(&mut self.stream, &mut self.state, &mut self.pos) + match get_next_token(&mut self.stream, &mut self.state, &mut self.pos) { + None => None, + r @ Some(_) if self.disable_tokens.is_none() => r, + Some((token, pos)) + if token.is_operator() + && self + .disable_tokens + .unwrap() + .contains(token.syntax().as_ref()) => + { + // Convert disallowed operators into lex errors + Some(( + Token::LexError(Box::new(LexError::UnexpectedInput(token.syntax().into()))), + pos, + )) + } + Some((token, pos)) + if token.is_keyword() + && self + .disable_tokens + .unwrap() + .contains(token.syntax().as_ref()) => + { + // Convert disallowed keywords into identifiers + Some((Token::Identifier(token.syntax().into()), pos)) + } + r => r, + } } } /// Tokenize an input text stream. -pub fn lex<'a>(input: &'a [&'a str], max_string_size: usize) -> TokenIterator<'a> { +pub fn lex<'a, 't>( + input: &'a [&'a str], + max_string_size: usize, + disable_tokens: Option<&'t HashSet>, +) -> TokenIterator<'a, 't> { TokenIterator { + disable_tokens, state: TokenizeState { max_string_size, non_unary: false, diff --git a/tests/tokens.rs b/tests/tokens.rs new file mode 100644 index 00000000..5fa0b9b6 --- /dev/null +++ b/tests/tokens.rs @@ -0,0 +1,20 @@ +use rhai::{Engine, ParseErrorType}; + +#[test] +fn test_tokens_disabled() { + let mut engine = Engine::new(); + + engine.disable_symbol("if"); // disable the 'if' keyword + + assert!(matches!( + *engine.compile("let x = if true { 42 } else { 0 };").expect_err("should error").0, + ParseErrorType::MissingToken(ref token, _) if token == ";" + )); + + engine.disable_symbol("+="); // disable the '+=' operator + + assert!(matches!( + *engine.compile("let x = 40 + 2; x += 1;").expect_err("should error").0, + ParseErrorType::BadInput(ref s) if s == "Unexpected '+='" + )); +} From e390dd73e6f759b7be9088942295aa4b370efca5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 5 Jul 2020 17:41:45 +0800 Subject: [PATCH 18/46] Add custom operators. --- README.md | 2 +- RELEASES.md | 6 +- doc/src/SUMMARY.md | 19 +++--- doc/src/about/features.md | 2 + doc/src/appendix/operators.md | 50 ++++++++-------- doc/src/engine/custom-op.md | 105 ++++++++++++++++++++++++++++++++++ doc/src/links.md | 2 + src/api.rs | 10 ++-- src/engine.rs | 12 ++-- src/parser.rs | 83 ++++++++++++--------------- src/settings.rs | 57 +++++++++++++++++- src/token.rs | 86 ++++++++++++++-------------- tests/tokens.rs | 31 +++++++++- 13 files changed, 326 insertions(+), 139 deletions(-) create mode 100644 doc/src/engine/custom-op.md diff --git a/README.md b/README.md index a88c574d..e448ba69 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Features * Rugged - protected against malicious attacks (such as [stack-overflow](https://schungx.github.io/rhai/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/rhai/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/rhai/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts. * Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run. * [Function overloading](https://schungx.github.io/rhai/language/overload.html). -* [Operator overloading](https://schungx.github.io/rhai/rust/operators.html). +* [Operator overloading](https://schungx.github.io/rhai/rust/operators.html) and [custom operators](https://schungx.github.io/rhai/engine/custom-op.html). * Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html). * Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). diff --git a/RELEASES.md b/RELEASES.md index 4306c015..7b9d8230 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,8 +6,9 @@ Version 0.17.0 This version adds: -* [`serde`](https://crates.io/crates/serde) support for working with `Dynamic` values (particularly _object maps_) -* Ability to surgically disable keywords and/or operators in the language +* [`serde`](https://crates.io/crates/serde) support for working with `Dynamic` values (particularly _object maps_). +* Ability to surgically disable keywords and/or operators in the language. +* Ability to define custom operators (which must be valid identifiers). Breaking changes ---------------- @@ -20,6 +21,7 @@ New features * New `serde` feature to allow serializating/deserializating to/from `Dynamic` values using [`serde`](https://crates.io/crates/serde). This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back. * `Engine::disable_symbol` to surgically disable keywords and/or operators. +* `Engine::register_custom_operator` to define a custom operator. Version 0.16.1 diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 39c9fb68..6c483cde 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -95,18 +95,19 @@ The Rhai Scripting Language 8. [Maximum Call Stack Depth](safety/max-call-stack.md) 9. [Maximum Statement Depth](safety/max-stmt-depth.md) 8. [Advanced Topics](advanced.md) - 1. [Disable Keywords and/or Operators](engine/disable.md) - 2. [Object-Oriented Programming (OOP)](language/oop.md) - 3. [Serialization/Deserialization of `Dynamic` with `serde`](rust/serde.md) - 4. [Script Optimization](engine/optimize/index.md) + 1. [Object-Oriented Programming (OOP)](language/oop.md) + 2. [Serialization/Deserialization of `Dynamic` with `serde`](rust/serde.md) + 3. [Script Optimization](engine/optimize/index.md) 1. [Optimization Levels](engine/optimize/optimize-levels.md) 2. [Re-Optimize an AST](engine/optimize/reoptimize.md) 3. [Eager Function Evaluation](engine/optimize/eager.md) 4. [Side-Effect Considerations](engine/optimize/side-effects.md) 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) - 5. [Eval Statement](language/eval.md) -9. [Appendix](appendix/index.md) - 6. [Keywords](appendix/keywords.md) - 7. [Operators](appendix/operators.md) - 8. [Literals](appendix/literals.md) + 4. [Disable Keywords and/or Operators](engine/disable.md) + 5. [Custom Operators](engine/custom-op.md) + 6. [Eval Statement](language/eval.md) +9. [Appendix](appendix/index.md) + 1. [Keywords](appendix/keywords.md) + 2. [Operators](appendix/operators.md) + 3. [Literals](appendix/literals.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 28f20aef..5a5739cb 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -66,3 +66,5 @@ Flexible * Supports [most build targets](targets.md) including `no-std` and [WASM]. * Surgically [disable keywords and operators] to restrict the language. + +* [Custom operators]. diff --git a/doc/src/appendix/operators.md b/doc/src/appendix/operators.md index 56782b95..bc0459b8 100644 --- a/doc/src/appendix/operators.md +++ b/doc/src/appendix/operators.md @@ -3,28 +3,28 @@ Operators {{#include ../links.md}} -| Operator | Description | Binary? | -| :---------------: | ------------------------------ | :-----: | -| `+` | Add | Yes | -| `-` | Subtract, Minus | Yes/No | -| `*` | Multiply | Yes | -| `/` | Divide | Yes | -| `%` | Modulo | Yes | -| `~` | Power | Yes | -| `>>` | Right bit-shift | Yes | -| `<<` | Left bit-shift | Yes | -| `&` | Bit-wise _And_, Boolean _And_ | Yes | -| \| | Bit-wise _Or_, Boolean _Or_ | Yes | -| `^` | Bit-wise _Xor_ | Yes | -| `==` | Equals to | Yes | -| `~=` | Not equals to | Yes | -| `>` | Greater than | Yes | -| `>=` | Greater than or equals to | Yes | -| `<` | Less than | Yes | -| `<=` | Less than or equals to | Yes | -| `>=` | Greater than or equals to | Yes | -| `&&` | Boolean _And_ (short-circuits) | Yes | -| \|\| | Boolean _Or_ (short-circuits) | Yes | -| `!` | Boolean _Not_ | No | -| `[` .. `]` | Indexing | Yes | -| `.` | Property access, Method call | Yes | +| Operator | Description | Binary? | Binding direction | +| :---------------: | ------------------------------ | :-----: | :---------------: | +| `+` | Add | Yes | Left | +| `-` | Subtract, Minus | Yes/No | Left | +| `*` | Multiply | Yes | Left | +| `/` | Divide | Yes | Left | +| `%` | Modulo | Yes | Left | +| `~` | Power | Yes | Left | +| `>>` | Right bit-shift | Yes | Left | +| `<<` | Left bit-shift | Yes | Left | +| `&` | Bit-wise _And_, Boolean _And_ | Yes | Left | +| \| | Bit-wise _Or_, Boolean _Or_ | Yes | Left | +| `^` | Bit-wise _Xor_ | Yes | Left | +| `==` | Equals to | Yes | Left | +| `~=` | Not equals to | Yes | Left | +| `>` | Greater than | Yes | Left | +| `>=` | Greater than or equals to | Yes | Left | +| `<` | Less than | Yes | Left | +| `<=` | Less than or equals to | Yes | Left | +| `>=` | Greater than or equals to | Yes | Left | +| `&&` | Boolean _And_ (short-circuits) | Yes | Left | +| \|\| | Boolean _Or_ (short-circuits) | Yes | Left | +| `!` | Boolean _Not_ | No | Left | +| `[` .. `]` | Indexing | Yes | Right | +| `.` | Property access, Method call | Yes | Right | diff --git a/doc/src/engine/custom-op.md b/doc/src/engine/custom-op.md new file mode 100644 index 00000000..1c12263e --- /dev/null +++ b/doc/src/engine/custom-op.md @@ -0,0 +1,105 @@ +Custom Operators +================ + +{{#include ../links.md}} + +For use as a DSL (Domain-Specific Languages), it is sometimes more convenient to augment Rhai with +customized operators performing specific logic. + +`Engine::register_custom_operator` registers a keyword as a custom operator. + + +Example +------- + +```rust +use rhai::{Engine, RegisterFn}; + +let mut engine = Engine::new(); + +// Register a custom operator called 'foo' and give it +// a precedence of 140 (i.e. between +|- and *|/) +engine.register_custom_operator("foo", 140).unwrap(); + +// Register the implementation of the customer operator as a function +engine.register_fn("foo", |x: i64, y: i64| (x * y) - (x + y)); + +// The custom operator can be used in expressions +let result = engine.eval_expression::("1 + 2 * 3 foo 4 - 5 / 6")?; +// ^ custom operator + +// The above is equivalent to: 1 + ((2 * 3) foo 4) - (5 / 6) +result == 15; +``` + + +Alternatives to a Custom Operator +-------------------------------- + +Custom operators are merely _syntactic sugar_. They map directly to registered functions. + +Therefore, the following are equivalent (assuming `foo` has been registered as a custom operator): + +```rust +1 + 2 * 3 foo 4 - 5 / 6 // use custom operator + +1 + foo(2 * 3, 4) - 5 / 6 // use function call +``` + +A script using custom operators can always be pre-processed, via a pre-processor application, +into a syntax that uses the corresponding function calls. + +Using `Engine::register_custom_operator` merely enables a convenient short-cut. + + +Must Follow Variable Naming +-------------------------- + +All custom operators must be _identifiers_ that follow the same naming rules as [variables]. + +```rust +engine.register_custom_operator("foo", 20); // 'foo' is a valid custom operator + +engine.register_custom_operator("=>", 30); // <- error: '=>' is not a valid custom operator +``` + + +Binary Operators Only +--------------------- + +All custom operators must be _binary_ (i.e. they take two operands). +_Unary_ custom operators are not supported. + +```rust +engine.register_custom_operator("foo", 140).unwrap(); + +engine.register_fn("foo", |x: i64| x * x); + +engine.eval::("1 + 2 * 3 foo 4 - 5 / 6")?; // error: function 'foo (i64, i64)' not found +``` + + +Operator Precedence +------------------- + +All operators in Rhai has a _precedence_ indicating how tightly they bind. + +The following _precedence table_ show the built-in precedence of standard Rhai operators: + +| Category | Operators | Precedence (0-255) | +| ------------------- | :-------------------------------------------------------------------------------------: | :----------------: | +| Assignments | `=`, `+=`, `-=`, `*=`, `/=`, `~=`, `%=`,
`<<=`, `>>=`, `&=`, \|=, `^=` | 0 | +| Logic and bit masks | \|\|, \|, `^` | 30 | +| Logic and bit masks | `&`, `&&` | 60 | +| Comparisons | `==`, `!=`, `>`, `>=`, `<`, `<=` | 90 | +| | `in` | 110 | +| Arithmetic | `+`, `-` | 130 | +| Arithmetic | `*`, `/`, `~` | 160 | +| Bit-shifts | `<<`, `>>` | 190 | +| Arithmetic | `%` | 210 | +| Object | `.` _(binds to right)_ | 240 | +| _Others_ | | 0 | + +A higher precedence binds more tightly than a lower precedence, so `*` and `/` binds before `+` and `-` etc. + +When registering a custom operator, the operator's precedence must also be provided. diff --git a/doc/src/links.md b/doc/src/links.md index 7ef02a10..69a01198 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -104,3 +104,5 @@ [`OptimizationLevel::None`]: {{rootUrl}}/engine/optimize/optimize-levels.md [disable keywords and operators]: {{rootUrl}}/engine/disable.md +[custom operator]: {{rootUrl}}/engine/custom-op.md +[custom operators]: {{rootUrl}}/engine/custom-op.md diff --git a/src/api.rs b/src/api.rs index 5b6acdb5..6f49ad73 100644 --- a/src/api.rs +++ b/src/api.rs @@ -553,7 +553,7 @@ impl Engine { scripts: &[&str], optimization_level: OptimizationLevel, ) -> Result { - let stream = lex(scripts, self.max_string_size, self.disable_tokens.as_ref()); + let stream = lex(scripts, self); self.parse(&mut stream.peekable(), scope, optimization_level) } @@ -678,7 +678,7 @@ impl Engine { // Trims the JSON string and add a '#' in front let scripts = ["#", json.trim()]; - let stream = lex(&scripts, self.max_string_size, self.disable_tokens.as_ref()); + let stream = lex(&scripts, self); let ast = self.parse_global_expr(&mut stream.peekable(), &scope, OptimizationLevel::None)?; @@ -759,7 +759,7 @@ impl Engine { script: &str, ) -> Result { let scripts = [script]; - let stream = lex(&scripts, self.max_string_size, self.disable_tokens.as_ref()); + let stream = lex(&scripts, self); { let mut peekable = stream.peekable(); self.parse_global_expr(&mut peekable, scope, self.optimization_level) @@ -914,7 +914,7 @@ impl Engine { script: &str, ) -> Result> { let scripts = [script]; - let stream = lex(&scripts, self.max_string_size, self.disable_tokens.as_ref()); + let stream = lex(&scripts, self); // No need to optimize a lone expression let ast = self.parse_global_expr(&mut stream.peekable(), scope, OptimizationLevel::None)?; @@ -1047,7 +1047,7 @@ impl Engine { script: &str, ) -> Result<(), Box> { let scripts = [script]; - let stream = lex(&scripts, self.max_string_size, self.disable_tokens.as_ref()); + let stream = lex(&scripts, self); let ast = self.parse(&mut stream.peekable(), scope, self.optimization_level)?; self.consume_ast_with_scope(scope, &ast) } diff --git a/src/engine.rs b/src/engine.rs index 9dff48e4..e0a52882 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -268,8 +268,10 @@ pub struct Engine { /// A hashmap mapping type names to pretty-print names. pub(crate) type_names: Option>, - /// A hash-set containing tokens to disable. - pub(crate) disable_tokens: Option>, + /// A hashset containing symbols to disable. + pub(crate) disabled_symbols: Option>, + /// A hashset containing custom keywords and precedence to recognize. + pub(crate) custom_keywords: Option>, /// Callback closure for implementing the `print` command. pub(crate) print: Callback, @@ -317,7 +319,8 @@ impl Default for Engine { module_resolver: None, type_names: None, - disable_tokens: None, + disabled_symbols: None, + custom_keywords: None, // default print/debug implementations print: Box::new(default_print), @@ -497,7 +500,8 @@ impl Engine { module_resolver: None, type_names: None, - disable_tokens: None, + disabled_symbols: None, + custom_keywords: None, print: Box::new(|_| {}), debug: Box::new(|_| {}), diff --git a/src/parser.rs b/src/parser.rs index 797e28f4..4db75d94 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -332,36 +332,26 @@ pub enum ReturnType { Exception, } -#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] -struct ParseState { +#[derive(Clone)] +struct ParseState<'e> { + /// Reference to the scripting `Engine`. + engine: &'e Engine, /// Encapsulates a local stack with variable names to simulate an actual runtime scope. - pub stack: Vec<(String, ScopeEntryType)>, + stack: Vec<(String, ScopeEntryType)>, /// Encapsulates a local stack with variable names to simulate an actual runtime scope. - pub modules: Vec, + modules: Vec, /// Maximum levels of expression nesting. - pub max_expr_depth: usize, - /// Maximum length of a string. - pub max_string_size: usize, - /// Maximum length of an array. - pub max_array_size: usize, - /// Maximum number of properties in a map. - pub max_map_size: usize, + max_expr_depth: usize, } -impl ParseState { +impl<'e> ParseState<'e> { /// Create a new `ParseState`. - pub fn new( - max_expr_depth: usize, - max_string_size: usize, - max_array_size: usize, - max_map_size: usize, - ) -> Self { + pub fn new(engine: &'e Engine, max_expr_depth: usize) -> Self { Self { + engine, max_expr_depth, - max_string_size, - max_array_size, - max_map_size, - ..Default::default() + stack: Default::default(), + modules: Default::default(), } } /// Find a variable by name in the `ParseState`, searching in reverse. @@ -1206,10 +1196,10 @@ fn parse_array_literal( let mut arr = StaticVec::new(); while !input.peek().unwrap().0.is_eof() { - if state.max_array_size > 0 && arr.len() >= state.max_array_size { + if state.engine.max_array_size > 0 && arr.len() >= state.engine.max_array_size { return Err(PERR::LiteralTooLarge( "Size of array literal".to_string(), - state.max_array_size, + state.engine.max_array_size, ) .into_err(input.peek().unwrap().1)); } @@ -1306,10 +1296,10 @@ fn parse_map_literal( } }; - if state.max_map_size > 0 && map.len() >= state.max_map_size { + if state.engine.max_map_size > 0 && map.len() >= state.engine.max_map_size { return Err(PERR::LiteralTooLarge( "Number of properties in object map literal".to_string(), - state.max_map_size, + state.engine.max_map_size, ) .into_err(input.peek().unwrap().1)); } @@ -1866,7 +1856,8 @@ fn parse_binary_op( loop { let (current_op, _) = input.peek().unwrap(); - let precedence = current_op.precedence(); + let custom = state.engine.custom_keywords.as_ref(); + let precedence = current_op.precedence(custom); let bind_right = current_op.is_bind_right(); // Bind left to the parent lhs expression if precedence is higher @@ -1879,7 +1870,7 @@ fn parse_binary_op( let rhs = parse_unary(input, state, settings)?; - let next_precedence = input.peek().unwrap().0.precedence(); + let next_precedence = input.peek().unwrap().0.precedence(custom); // Bind to right if the next operator has higher precedence // If same precedence, then check if the operator binds right @@ -1949,6 +1940,19 @@ fn parse_binary_op( make_dot_expr(current_lhs, rhs, pos)? } + Token::Custom(s) + if state + .engine + .custom_keywords + .as_ref() + .map(|c| c.contains_key(&s)) + .unwrap_or(false) => + { + // Accept non-native functions for custom operators + let op = (op.0, false, op.2); + Expr::FnCall(Box::new((op, None, hash, args, None))) + } + op_token => return Err(PERR::UnknownOperator(op_token.into()).into_err(pos)), }; } @@ -2467,7 +2471,7 @@ fn parse_fn( settings.ensure_level_within_max_limit(state.max_expr_depth)?; let name = match input.next().unwrap() { - (Token::Identifier(s), _) => s, + (Token::Identifier(s), _) | (Token::Custom(s), _) => s, (_, pos) => return Err(PERR::FnMissingName.into_err(pos)), }; @@ -2555,12 +2559,7 @@ impl Engine { scope: &Scope, optimization_level: OptimizationLevel, ) -> Result { - let mut state = ParseState::new( - self.max_expr_depth, - self.max_string_size, - self.max_array_size, - self.max_map_size, - ); + let mut state = ParseState::new(self, self.max_expr_depth); let settings = ParseSettings { allow_if_expr: false, allow_stmt_expr: false, @@ -2596,12 +2595,7 @@ impl Engine { ) -> Result<(Vec, Vec), ParseError> { let mut statements = Vec::::new(); let mut functions = HashMap::::with_hasher(StraightHasherBuilder); - let mut state = ParseState::new( - self.max_expr_depth, - self.max_string_size, - self.max_array_size, - self.max_map_size, - ); + let mut state = ParseState::new(self, self.max_expr_depth); while !input.peek().unwrap().0.is_eof() { // Collect all the function definitions @@ -2615,12 +2609,7 @@ impl Engine { match input.peek().unwrap() { (Token::Fn, pos) => { - let mut state = ParseState::new( - self.max_function_expr_depth, - self.max_string_size, - self.max_array_size, - self.max_map_size, - ); + let mut state = ParseState::new(self, self.max_function_expr_depth); let settings = ParseSettings { allow_if_expr: true, allow_stmt_expr: true, diff --git a/src/settings.rs b/src/settings.rs index a4379d79..daa714bd 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,6 +2,7 @@ use crate::engine::Engine; use crate::module::ModuleResolver; use crate::optimize::OptimizationLevel; use crate::packages::PackageLibrary; +use crate::token::is_valid_identifier; impl Engine { /// Load a new package into the `Engine`. @@ -194,10 +195,60 @@ impl Engine { /// # } /// ``` pub fn disable_symbol(&mut self, symbol: &str) { - if self.disable_tokens.is_none() { - self.disable_tokens = Some(Default::default()); + if self.disabled_symbols.is_none() { + self.disabled_symbols = Some(Default::default()); } - self.disable_tokens.as_mut().unwrap().insert(symbol.into()); + self.disabled_symbols + .as_mut() + .unwrap() + .insert(symbol.into()); + } + + /// Register a custom operator into the language. + /// + /// The operator must be a valid identifier (i.e. it cannot be a symbol). + /// + /// # Examples + /// + /// ```rust + /// # fn main() -> Result<(), Box> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register a custom operator called 'foo' and give it + /// // a precedence of 140 (i.e. between +|- and *|/). + /// engine.register_custom_operator("foo", 140).unwrap(); + /// + /// // Register a binary function named 'foo' + /// engine.register_fn("foo", |x: i64, y: i64| (x * y) - (x + y)); + /// + /// assert_eq!( + /// engine.eval_expression::("1 + 2 * 3 foo 4 - 5 / 6")?, + /// 15 + /// ); + /// # Ok(()) + /// # } + /// ``` + pub fn register_custom_operator( + &mut self, + keyword: &str, + precedence: u8, + ) -> Result<(), String> { + if !is_valid_identifier(keyword.chars()) { + return Err(format!("not a valid identifier: '{}'", keyword).into()); + } + + if self.custom_keywords.is_none() { + self.custom_keywords = Some(Default::default()); + } + + self.custom_keywords + .as_mut() + .unwrap() + .insert(keyword.into(), precedence); + + Ok(()) } } diff --git a/src/token.rs b/src/token.rs index fc527917..056f9714 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,5 +1,6 @@ //! Main module defining the lexer and parser. +use crate::engine::Engine; use crate::error::LexError; use crate::parser::INT; use crate::utils::StaticVec; @@ -11,7 +12,7 @@ use crate::stdlib::{ borrow::Cow, boxed::Box, char, - collections::HashSet, + collections::{HashMap, HashSet}, fmt, iter::Peekable, str::{Chars, FromStr}, @@ -212,6 +213,7 @@ pub enum Token { As, LexError(Box), Comment(String), + Custom(String), EOF, } @@ -224,12 +226,13 @@ impl Token { IntegerConstant(i) => i.to_string().into(), #[cfg(not(feature = "no_float"))] FloatConstant(f) => f.to_string().into(), - Identifier(s) => s.clone().into(), + StringConstant(_) => "string".into(), CharConstant(c) => c.to_string().into(), + Identifier(s) => s.clone().into(), + Custom(s) => s.clone().into(), LexError(err) => err.to_string().into(), token => match token { - StringConstant(_) => "string", LeftBrace => "{", RightBrace => "}", LeftParen => "(", @@ -324,9 +327,9 @@ impl Token { match self { LexError(_) | - LeftBrace | // (+expr) - is unary + LeftBrace | // {+expr} - is unary // RightBrace | {expr} - expr not unary & is closing - LeftParen | // {-expr} - is unary + LeftParen | // (-expr) - is unary // RightParen | (expr) - expr not unary & is closing LeftBracket | // [-expr] - is unary // RightBracket | [expr] - expr not unary & is closing @@ -371,14 +374,14 @@ impl Token { Throw | PowerOf | In | - PowerOfAssign => true, + PowerOfAssign => true, _ => false, } } /// Get the precedence number of the token. - pub fn precedence(&self) -> u8 { + pub fn precedence(&self, custom: Option<&HashMap>) -> u8 { use Token::*; match self { @@ -387,24 +390,27 @@ impl Token { | RightShiftAssign | AndAssign | OrAssign | XOrAssign | ModuloAssign | PowerOfAssign => 0, - Or | XOr | Pipe => 40, + Or | XOr | Pipe => 30, - And | Ampersand => 50, + And | Ampersand => 60, LessThan | LessThanEqualsTo | GreaterThan | GreaterThanEqualsTo | EqualsTo - | NotEqualsTo => 60, + | NotEqualsTo => 90, - In => 70, + In => 110, - Plus | Minus => 80, + Plus | Minus => 130, - Divide | Multiply | PowerOf => 90, + Divide | Multiply | PowerOf => 160, - LeftShift | RightShift => 100, + LeftShift | RightShift => 190, - Modulo => 110, + Modulo => 210, - Period => 120, + Period => 240, + + // Custom operators + Custom(s) => custom.map_or(0, |c| *c.get(s).unwrap()), _ => 0, } @@ -1211,9 +1217,9 @@ impl InputStream for MultiInputsStream<'_> { } /// An iterator on a `Token` stream. -pub struct TokenIterator<'a, 't> { - /// Disable certain tokens. - pub disable_tokens: Option<&'t HashSet>, +pub struct TokenIterator<'a, 'e> { + /// Reference to the scripting `Engine`. + engine: &'e Engine, /// Current state. state: TokenizeState, /// Current position. @@ -1226,15 +1232,15 @@ impl<'a> Iterator for TokenIterator<'a, '_> { type Item = (Token, Position); fn next(&mut self) -> Option { - match get_next_token(&mut self.stream, &mut self.state, &mut self.pos) { - None => None, - r @ Some(_) if self.disable_tokens.is_none() => r, - Some((token, pos)) - if token.is_operator() - && self - .disable_tokens - .unwrap() - .contains(token.syntax().as_ref()) => + match ( + get_next_token(&mut self.stream, &mut self.state, &mut self.pos), + self.engine.disabled_symbols.as_ref(), + self.engine.custom_keywords.as_ref(), + ) { + (None, _, _) => None, + (r @ Some(_), None, None) => r, + (Some((token, pos)), Some(disabled), _) + if token.is_operator() && disabled.contains(token.syntax().as_ref()) => { // Convert disallowed operators into lex errors Some(( @@ -1242,31 +1248,27 @@ impl<'a> Iterator for TokenIterator<'a, '_> { pos, )) } - Some((token, pos)) - if token.is_keyword() - && self - .disable_tokens - .unwrap() - .contains(token.syntax().as_ref()) => + (Some((token, pos)), Some(disabled), _) + if token.is_keyword() && disabled.contains(token.syntax().as_ref()) => { // Convert disallowed keywords into identifiers Some((Token::Identifier(token.syntax().into()), pos)) } - r => r, + (Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => { + // Convert custom keywords + Some((Token::Custom(s), pos)) + } + (r, _, _) => r, } } } /// Tokenize an input text stream. -pub fn lex<'a, 't>( - input: &'a [&'a str], - max_string_size: usize, - disable_tokens: Option<&'t HashSet>, -) -> TokenIterator<'a, 't> { +pub fn lex<'a, 'e>(input: &'a [&'a str], engine: &'e Engine) -> TokenIterator<'a, 'e> { TokenIterator { - disable_tokens, + engine, state: TokenizeState { - max_string_size, + max_string_size: engine.max_string_size, non_unary: false, comment_level: 0, end_with_none: false, diff --git a/tests/tokens.rs b/tests/tokens.rs index 5fa0b9b6..b86f6a0e 100644 --- a/tests/tokens.rs +++ b/tests/tokens.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, ParseErrorType}; +use rhai::{Engine, EvalAltResult, ParseErrorType, RegisterFn, INT}; #[test] fn test_tokens_disabled() { @@ -18,3 +18,32 @@ fn test_tokens_disabled() { ParseErrorType::BadInput(ref s) if s == "Unexpected '+='" )); } + +#[test] +fn test_tokens_custom_operator() -> Result<(), Box> { + let mut engine = Engine::new(); + + // Register a custom operator called `foo` and give it + // a precedence of 140 (i.e. between +|- and *|/). + engine.register_custom_operator("foo", 140).unwrap(); + + // Register a binary function named `foo` + engine.register_fn("foo", |x: INT, y: INT| (x * y) - (x + y)); + + assert_eq!( + engine.eval_expression::("1 + 2 * 3 foo 4 - 5 / 6")?, + 15 + ); + + assert_eq!( + engine.eval::( + r" + fn foo(x, y) { y - x } + 1 + 2 * 3 foo 4 - 5 / 6 + " + )?, + -1 + ); + + Ok(()) +} From 4052ad3df1b32e8f2ee7da8c0a9edeee12a93b5f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 5 Jul 2020 23:07:02 +0800 Subject: [PATCH 19/46] Assignments return () and no compound assignments. --- src/engine.rs | 28 +++++++++++++--------------- src/optimize.rs | 18 +----------------- 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index e0a52882..b3eb7d7a 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1663,14 +1663,20 @@ impl Engine { Expr::Variable(_) => unreachable!(), // idx_lhs[idx_expr] op= rhs #[cfg(not(feature = "no_index"))] - Expr::Index(_) => self.eval_dot_index_chain( - scope, mods, state, lib, this_ptr, lhs_expr, level, new_val, - ), + Expr::Index(_) => { + self.eval_dot_index_chain( + scope, mods, state, lib, this_ptr, lhs_expr, level, new_val, + )?; + Ok(Default::default()) + } // dot_lhs.dot_rhs op= rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(_) => self.eval_dot_index_chain( - scope, mods, state, lib, this_ptr, lhs_expr, level, new_val, - ), + Expr::Dot(_) => { + self.eval_dot_index_chain( + scope, mods, state, lib, this_ptr, lhs_expr, level, new_val, + )?; + Ok(Default::default()) + } // Error assignment to constant expr if expr.is_constant() => { Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( @@ -1965,15 +1971,7 @@ impl Engine { Stmt::Noop(_) => Ok(Default::default()), // Expression as statement - Stmt::Expr(expr) => { - let result = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; - - Ok(match expr.as_ref() { - // If it is a simple assignment, erase the result at the root - Expr::Assignment(_) => Default::default(), - _ => result, - }) - } + Stmt::Expr(expr) => self.eval_expr(scope, mods, state, lib, this_ptr, expr, level), // Block scope Stmt::Block(x) => { diff --git a/src/optimize.rs b/src/optimize.rs index 5676bad4..f3f09919 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -382,23 +382,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { stmt => Expr::Stmt(Box::new((stmt, x.1))), }, // id op= expr - Expr::Assignment(x) => match x.2 { - //id = id2 op= rhs - Expr::Assignment(x2) if x.1.is_empty() => match (x.0, &x2.0) { - // var = var op= expr2 -> var op= expr2 - (Expr::Variable(a), Expr::Variable(b)) - if a.1.is_none() && b.1.is_none() && a.0 == b.0 && a.3 == b.3 => - { - // Assignment to the same variable - fold - state.set_dirty(); - Expr::Assignment(Box::new((Expr::Variable(a), x2.1, optimize_expr(x2.2, state), x.3))) - } - // expr1 = expr2 op= rhs - (expr1, _) => Expr::Assignment(Box::new((expr1, x.1, optimize_expr(Expr::Assignment(x2), state), x.3))), - }, - // expr = rhs - expr => Expr::Assignment(Box::new((x.0, x.1, optimize_expr(expr, state), x.3))), - }, + Expr::Assignment(x) => Expr::Assignment(Box::new((x.0, x.1, optimize_expr(x.2, state), x.3))), // lhs.rhs #[cfg(not(feature = "no_object"))] From a27f89b524f9fbfb0e19ef30cfbcc6fababc0a22 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 5 Jul 2020 23:08:44 +0800 Subject: [PATCH 20/46] Add new register_fn_raw API. --- RELEASES.md | 2 ++ doc/src/engine/call-fn.md | 29 +++++++++++++++-- src/api.rs | 68 +++++++++++++++++++++++++++++++++------ src/token.rs | 2 +- tests/call_fn.rs | 48 ++++++++++++++++++++++++++- 5 files changed, 135 insertions(+), 14 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 7b9d8230..72bbd0be 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -14,6 +14,7 @@ Breaking changes ---------------- * `EvalAltResult::ErrorMismatchOutputType` has an extra argument containing the name of the requested type. +* `Engine::call_fn_dynamic` take an extra argument, allowing a `Dynamic` value to be bound to the `this` pointer. New features ------------ @@ -22,6 +23,7 @@ New features This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back. * `Engine::disable_symbol` to surgically disable keywords and/or operators. * `Engine::register_custom_operator` to define a custom operator. +* New low-level API `Engine::register_raw_fn`. Version 0.16.1 diff --git a/doc/src/engine/call-fn.md b/doc/src/engine/call-fn.md index d881d7bf..fd9255d0 100644 --- a/doc/src/engine/call-fn.md +++ b/doc/src/engine/call-fn.md @@ -55,10 +55,35 @@ let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )?; let result: () = engine.call_fn(&mut scope, &ast, "hidden", ())?; ``` + +`Engine::call_fn_dynamic` +------------------------ + For more control, construct all arguments as `Dynamic` values and use `Engine::call_fn_dynamic`, passing it anything that implements `IntoIterator` (such as a simple `Vec`): ```rust -let result: Dynamic = engine.call_fn_dynamic(&mut scope, &ast, "hello", - vec![ String::from("abc").into(), 123_i64.into() ])?; +let result: Dynamic = engine.call_fn_dynamic( + &mut scope, // scope to use + &ast, // AST to use + "hello", // function entry-point + None, // 'this' pointer, if any + [ String::from("abc").into(), 123_i64.into() ])?; // arguments +``` + + +Binding the `this` Pointer +------------------------- + +`Engine::call_fn_dynamic` can also bind a value to the `this` pointer of a script-defined function. + +```rust +let ast = engine.compile("fn action(x) { this += x; }")?; + +let mut value: Dynamic = 1_i64.into(); + +let result = engine.call_fn_dynamic(&mut scope, &ast, "action", Some(&mut value), [ 41_i64.into() ])?; +// ^^^^^^^^^^^^^^^^ binding the 'this' pointer + +assert_eq!(value.as_int().unwrap(), 42); ``` diff --git a/src/api.rs b/src/api.rs index 6f49ad73..74b78b84 100644 --- a/src/api.rs +++ b/src/api.rs @@ -32,6 +32,38 @@ use crate::stdlib::{fs::File, io::prelude::*, path::PathBuf}; /// Engine public API impl Engine { + /// Register a function with the `Engine`. + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. It takes a list of `TypeId`'s indicating the actual types of the parameters. + /// + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`. + /// The arguments are guaranteed to be of the correct types matching the `TypeId`'s. + /// + /// To get access to a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// + /// To get access to a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// Notice that this will _consume_ the argument, replacing it with `()`. + /// + /// To get access to the first mutable parameter, use `args.get_mut(0).unwrap()` + #[deprecated(note = "this function is volatile and may change")] + pub fn register_raw_fn( + &mut self, + name: &str, + args: &[TypeId], + + #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &mut [&mut Dynamic]) -> Result> + + 'static, + + #[cfg(feature = "sync")] func: impl Fn(&Engine, &mut [&mut Dynamic]) -> Result> + + Send + + Sync + + 'static, + ) { + self.global_module.set_fn_var_args(name, args, func); + } + /// Register a custom type for use with the `Engine`. /// The type must implement `Clone`. /// @@ -1127,7 +1159,7 @@ impl Engine { args: A, ) -> Result> { let mut arg_values = args.into_vec(); - let result = self.call_fn_dynamic_raw(scope, ast, name, arg_values.as_mut())?; + let result = self.call_fn_dynamic_raw(scope, ast, name, &mut None, arg_values.as_mut())?; let typ = self.map_type_name(result.type_name()); @@ -1140,7 +1172,15 @@ impl Engine { }); } - /// Call a script function defined in an [`AST`] with multiple `Dynamic` arguments. + /// Call a script function defined in an [`AST`] with multiple `Dynamic` arguments + /// and optionally a value for binding to the 'this' pointer. + /// + /// ## WARNING + /// + /// All the arguments are _consumed_, meaning that they're replaced by `()`. + /// This is to avoid unnecessarily cloning the arguments. + /// Do not use the arguments after this call. If they are needed afterwards, + /// clone them _before_ calling this function. /// /// # Example /// @@ -1148,7 +1188,7 @@ impl Engine { /// # fn main() -> Result<(), Box> { /// # #[cfg(not(feature = "no_function"))] /// # { - /// use rhai::{Engine, Scope}; + /// use rhai::{Engine, Scope, Dynamic}; /// /// let engine = Engine::new(); /// @@ -1156,20 +1196,27 @@ impl Engine { /// fn add(x, y) { len(x) + y + foo } /// fn add1(x) { len(x) + 1 + foo } /// fn bar() { foo/2 } + /// fn action(x) { this += x; } // function using 'this' pointer /// ")?; /// /// let mut scope = Scope::new(); /// scope.push("foo", 42_i64); /// /// // Call the script-defined function - /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add", vec![ String::from("abc").into(), 123_i64.into() ])?; + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add", None, [ String::from("abc").into(), 123_i64.into() ])?; + /// // ^^^^ no 'this' pointer /// assert_eq!(result.cast::(), 168); /// - /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add1", vec![ String::from("abc").into() ])?; + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add1", None, [ String::from("abc").into() ])?; /// assert_eq!(result.cast::(), 46); /// - /// let result= engine.call_fn_dynamic(&mut scope, &ast, "bar", vec![])?; + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "bar", None, [])?; /// assert_eq!(result.cast::(), 21); + /// + /// let mut value: Dynamic = 1_i64.into(); + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "action", Some(&mut value), [ 41_i64.into() ])?; + /// // ^^^^^^^^^^^^^^^^ binding the 'this' pointer + /// assert_eq!(value.as_int().unwrap(), 42); /// # } /// # Ok(()) /// # } @@ -1180,10 +1227,10 @@ impl Engine { scope: &mut Scope, ast: &AST, name: &str, - arg_values: impl IntoIterator, + mut this_ptr: Option<&mut Dynamic>, + mut arg_values: impl AsMut<[Dynamic]>, ) -> Result> { - let mut arg_values: StaticVec<_> = arg_values.into_iter().collect(); - self.call_fn_dynamic_raw(scope, ast, name, arg_values.as_mut()) + self.call_fn_dynamic_raw(scope, ast, name, &mut this_ptr, arg_values.as_mut()) } /// Call a script function defined in an [`AST`] with multiple `Dynamic` arguments. @@ -1200,6 +1247,7 @@ impl Engine { scope: &mut Scope, ast: &AST, name: &str, + this_ptr: &mut Option<&mut Dynamic>, arg_values: &mut [Dynamic], ) -> Result> { let mut args: StaticVec<_> = arg_values.iter_mut().collect(); @@ -1220,7 +1268,7 @@ impl Engine { &mut mods, &mut state, ast.lib(), - &mut None, + this_ptr, name, fn_def, args, diff --git a/src/token.rs b/src/token.rs index 056f9714..673eb11f 100644 --- a/src/token.rs +++ b/src/token.rs @@ -12,7 +12,7 @@ use crate::stdlib::{ borrow::Cow, boxed::Box, char, - collections::{HashMap, HashSet}, + collections::HashMap, fmt, iter::Peekable, str::{Chars, FromStr}, diff --git a/tests/call_fn.rs b/tests/call_fn.rs index dd46450d..f217073e 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -1,5 +1,7 @@ #![cfg(not(feature = "no_function"))] -use rhai::{Engine, EvalAltResult, Func, ParseError, ParseErrorType, Scope, INT}; +use rhai::{ + Dynamic, Engine, EvalAltResult, Func, ImmutableString, ParseError, ParseErrorType, Scope, INT, +}; #[test] fn test_fn() -> Result<(), Box> { @@ -110,3 +112,47 @@ fn test_anonymous_fn() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_fn_ptr() -> Result<(), Box> { + let mut engine = Engine::new(); + + let ast = engine.compile( + r#" + fn foo(x) { this += x; } + + let x = 41; + x.bar("foo", 1); + x + "#, + )?; + let ast_clone = ast.clone(); + + engine.register_raw_fn( + "bar", + &[ + std::any::TypeId::of::(), + std::any::TypeId::of::(), + std::any::TypeId::of::(), + ], + move |engine: &Engine, args: &mut [&mut Dynamic]| { + let callback = args[1].clone().cast::(); + let value = args[2].clone(); + let this_ptr = args.get_mut(0).unwrap(); + + engine.call_fn_dynamic( + &mut Scope::new(), + &ast_clone, + &callback, + Some(this_ptr), + [value], + )?; + + Ok(().into()) + }, + ); + + assert_eq!(engine.eval_ast::(&ast)?, 42); + + Ok(()) +} From 495d202af48fd7d8d32d2cbc441646b0eb77a475 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Jul 2020 10:02:54 +0800 Subject: [PATCH 21/46] Add new AST API. --- RELEASES.md | 1 + src/parser.rs | 63 ++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 72bbd0be..0e2d8677 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -24,6 +24,7 @@ New features * `Engine::disable_symbol` to surgically disable keywords and/or operators. * `Engine::register_custom_operator` to define a custom operator. * New low-level API `Engine::register_raw_fn`. +* `AST::clone_functions_only`, `AST::clone_functions_only_filtered` and `AST::clone_statements_only` to clone only part of an `AST`. Version 0.16.1 diff --git a/src/parser.rs b/src/parser.rs index 4db75d94..7b919b8d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -49,7 +49,7 @@ pub use crate::utils::ImmutableString; /// Compiled AST (abstract syntax tree) of a Rhai script. /// -/// Currently, [`AST`] is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. +/// Currently, `AST` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. #[derive(Debug, Clone, Default)] pub struct AST( /// Global statements. @@ -59,7 +59,7 @@ pub struct AST( ); impl AST { - /// Create a new [`AST`]. + /// Create a new `AST`. pub fn new(statements: Vec, lib: Module) -> Self { Self(statements, lib) } @@ -95,16 +95,43 @@ impl AST { &self.1 } - /// Merge two [`AST`] into one. Both [`AST`]'s are untouched and a new, merged, version + /// Clone the `AST`'s functions into a new `AST`. + /// No statements are cloned. + /// + /// This operation is cheap because functions are shared. + pub fn clone_functions_only(&self) -> Self { + self.clone_functions_only_filtered(|_, _, _| true) + } + + /// Clone the `AST`'s functions into a new `AST` based on a filter predicate. + /// No statements are cloned. + /// + /// This operation is cheap because functions are shared. + pub fn clone_functions_only_filtered( + &self, + filter: impl Fn(FnAccess, &str, usize) -> bool, + ) -> Self { + let mut functions: Module = Default::default(); + functions.merge_filtered(&self.1, filter); + Self(Default::default(), functions) + } + + /// Clone the `AST`'s script statements into a new `AST`. + /// No functions are cloned. + pub fn clone_statements_only(&self) -> Self { + Self(self.0.clone(), Default::default()) + } + + /// Merge two `AST` into one. Both `AST`'s are untouched and a new, merged, version /// is returned. /// - /// The second [`AST`] is simply appended to the end of the first _without any processing_. - /// Thus, the return value of the first [`AST`] (if using expression-statement syntax) is buried. - /// Of course, if the first [`AST`] uses a `return` statement at the end, then - /// the second [`AST`] will essentially be dead code. + /// The second `AST` is simply appended to the end of the first _without any processing_. + /// Thus, the return value of the first `AST` (if using expression-statement syntax) is buried. + /// Of course, if the first `AST` uses a `return` statement at the end, then + /// the second `AST` will essentially be dead code. /// - /// All script-defined functions in the second [`AST`] overwrite similarly-named functions - /// in the first [`AST`] with the same number of parameters. + /// All script-defined functions in the second `AST` overwrite similarly-named functions + /// in the first `AST` with the same number of parameters. /// /// # Example /// @@ -148,16 +175,16 @@ impl AST { self.merge_filtered(other, |_, _, _| true) } - /// Merge two [`AST`] into one. Both [`AST`]'s are untouched and a new, merged, version + /// Merge two `AST` into one. Both `AST`'s are untouched and a new, merged, version /// is returned. /// - /// The second [`AST`] is simply appended to the end of the first _without any processing_. - /// Thus, the return value of the first [`AST`] (if using expression-statement syntax) is buried. - /// Of course, if the first [`AST`] uses a `return` statement at the end, then - /// the second [`AST`] will essentially be dead code. + /// The second `AST` is simply appended to the end of the first _without any processing_. + /// Thus, the return value of the first `AST` (if using expression-statement syntax) is buried. + /// Of course, if the first `AST` uses a `return` statement at the end, then + /// the second `AST` will essentially be dead code. /// - /// All script-defined functions in the second [`AST`] are first selected based on a filter - /// predicate, then overwrite similarly-named functions in the first [`AST`] with the + /// All script-defined functions in the second `AST` are first selected based on a filter + /// predicate, then overwrite similarly-named functions in the first `AST` with the /// same number of parameters. /// /// # Example @@ -251,13 +278,13 @@ impl AST { self.1.retain_functions(filter); } - /// Clear all function definitions in the [`AST`]. + /// Clear all function definitions in the `AST`. #[cfg(not(feature = "no_function"))] pub fn clear_functions(&mut self) { self.1 = Default::default(); } - /// Clear all statements in the [`AST`], leaving only function definitions. + /// Clear all statements in the `AST`, leaving only function definitions. pub fn clear_statements(&mut self) { self.0 = vec![]; } From ea86888638c930963332723869a7782da4c7c9d4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Jul 2020 12:04:02 +0800 Subject: [PATCH 22/46] Add new &Module parameter to native functions. --- doc/src/engine/call-fn.md | 28 +++++++------ src/api.rs | 78 +++++++++++++++++-------------------- src/engine.rs | 9 ++--- src/fn_native.rs | 5 ++- src/fn_register.rs | 3 +- src/module.rs | 28 ++++++------- src/packages/array_basic.rs | 8 +++- src/packages/string_more.rs | 4 +- src/parser.rs | 6 +++ tests/call_fn.rs | 37 ++++++++---------- 10 files changed, 107 insertions(+), 99 deletions(-) diff --git a/doc/src/engine/call-fn.md b/doc/src/engine/call-fn.md index fd9255d0..7a49d6e4 100644 --- a/doc/src/engine/call-fn.md +++ b/doc/src/engine/call-fn.md @@ -56,19 +56,20 @@ let result: () = engine.call_fn(&mut scope, &ast, "hidden", ())?; ``` -`Engine::call_fn_dynamic` ------------------------- +Low-Level API - `Engine::call_fn_dynamic` +---------------------------------------- For more control, construct all arguments as `Dynamic` values and use `Engine::call_fn_dynamic`, passing it -anything that implements `IntoIterator` (such as a simple `Vec`): +anything that implements `AsMut` (such as a simple array or a `Vec`): ```rust -let result: Dynamic = engine.call_fn_dynamic( - &mut scope, // scope to use - &ast, // AST to use - "hello", // function entry-point - None, // 'this' pointer, if any - [ String::from("abc").into(), 123_i64.into() ])?; // arguments +let result = engine.call_fn_dynamic( + &mut scope, // scope to use + ast.into(), // get 'Module' from 'AST' + "hello", // function entry-point + None, // 'this' pointer, if any + [ String::from("abc").into(), 123_i64.into() ] // arguments + )?; ``` @@ -82,8 +83,13 @@ let ast = engine.compile("fn action(x) { this += x; }")?; let mut value: Dynamic = 1_i64.into(); -let result = engine.call_fn_dynamic(&mut scope, &ast, "action", Some(&mut value), [ 41_i64.into() ])?; -// ^^^^^^^^^^^^^^^^ binding the 'this' pointer +let result = engine.call_fn_dynamic( + &mut scope, + ast.into(), + "action", + Some(&mut value), // binding the 'this' pointer + [ 41_i64.into() ] + )?; assert_eq!(value.as_int().unwrap(), 42); ``` diff --git a/src/api.rs b/src/api.rs index 74b78b84..343caaed 100644 --- a/src/api.rs +++ b/src/api.rs @@ -6,6 +6,7 @@ use crate::error::ParseError; use crate::fn_call::FuncArgs; use crate::fn_native::{IteratorFn, SendSync}; use crate::fn_register::RegisterFn; +use crate::module::Module; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::parser::AST; use crate::result::EvalAltResult; @@ -53,10 +54,10 @@ impl Engine { name: &str, args: &[TypeId], - #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &mut [&mut Dynamic]) -> Result> + #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + 'static, - #[cfg(feature = "sync")] func: impl Fn(&Engine, &mut [&mut Dynamic]) -> Result> + #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + Send + Sync + 'static, @@ -459,7 +460,7 @@ impl Engine { self.register_indexer_set(setter); } - /// Compile a string into an [`AST`], which can be used later for evaluation. + /// Compile a string into an `AST`, which can be used later for evaluation. /// /// # Example /// @@ -482,7 +483,7 @@ impl Engine { self.compile_with_scope(&Scope::new(), script) } - /// Compile a string into an [`AST`] using own scope, which can be used later for evaluation. + /// Compile a string 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`. @@ -525,7 +526,7 @@ impl Engine { } /// When passed a list of strings, first join the strings into one large script, - /// and then compile them into an [`AST`] using own scope, which can be used later for evaluation. + /// and then compile them 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`. @@ -578,7 +579,7 @@ impl Engine { self.compile_with_scope_and_optimization_level(scope, scripts, self.optimization_level) } - /// Join a list of strings and compile into an [`AST`] using own scope at a specific optimization level. + /// Join a list of strings and compile into an `AST` using own scope at a specific optimization level. pub(crate) fn compile_with_scope_and_optimization_level( &self, scope: &Scope, @@ -614,7 +615,7 @@ impl Engine { Ok(contents) } - /// Compile a script file into an [`AST`], which can be used later for evaluation. + /// Compile a script file into an `AST`, which can be used later for evaluation. /// /// # Example /// @@ -640,7 +641,7 @@ impl Engine { self.compile_file_with_scope(&Scope::new(), path) } - /// Compile a script file into an [`AST`] using own scope, which can be used later for evaluation. + /// Compile a script file 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`. @@ -722,7 +723,7 @@ impl Engine { self.eval_ast_with_scope(&mut scope, &ast) } - /// Compile a string containing an expression into an [`AST`], + /// Compile a string containing an expression into an `AST`, /// which can be used later for evaluation. /// /// # Example @@ -746,7 +747,7 @@ impl Engine { self.compile_expression_with_scope(&Scope::new(), script) } - /// Compile a string containing an expression into an [`AST`] using own scope, + /// 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 @@ -954,7 +955,7 @@ impl Engine { self.eval_ast_with_scope(scope, &ast) } - /// Evaluate an [`AST`]. + /// Evaluate an `AST`. /// /// # Example /// @@ -976,7 +977,7 @@ impl Engine { self.eval_ast_with_scope(&mut Scope::new(), ast) } - /// Evaluate an [`AST`] with own scope. + /// Evaluate an `AST` with own scope. /// /// # Example /// @@ -1024,7 +1025,7 @@ impl Engine { }); } - /// Evaluate an [`AST`] with own scope. + /// Evaluate an `AST` with own scope. pub(crate) fn eval_ast_with_scope_raw<'a>( &self, scope: &mut Scope, @@ -1090,7 +1091,7 @@ impl Engine { self.consume_ast_with_scope(&mut Scope::new(), ast) } - /// Evaluate an [`AST`] with own scope, but throw away the result and only return error (if any). + /// 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. pub fn consume_ast_with_scope( &self, @@ -1114,7 +1115,7 @@ impl Engine { ) } - /// Call a script function defined in an [`AST`] with multiple arguments. + /// Call a script function defined in an `AST` with multiple arguments. /// Arguments are passed as a tuple. /// /// # Example @@ -1159,7 +1160,8 @@ impl Engine { args: A, ) -> Result> { let mut arg_values = args.into_vec(); - let result = self.call_fn_dynamic_raw(scope, ast, name, &mut None, arg_values.as_mut())?; + let result = + self.call_fn_dynamic_raw(scope, ast.lib(), name, &mut None, arg_values.as_mut())?; let typ = self.map_type_name(result.type_name()); @@ -1172,7 +1174,7 @@ impl Engine { }); } - /// Call a script function defined in an [`AST`] with multiple `Dynamic` arguments + /// Call a script function defined in an `AST` with multiple `Dynamic` arguments /// and optionally a value for binding to the 'this' pointer. /// /// ## WARNING @@ -1203,19 +1205,19 @@ impl Engine { /// scope.push("foo", 42_i64); /// /// // Call the script-defined function - /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add", None, [ String::from("abc").into(), 123_i64.into() ])?; - /// // ^^^^ no 'this' pointer + /// let result = engine.call_fn_dynamic(&mut scope, ast.into(), "add", None, [ String::from("abc").into(), 123_i64.into() ])?; + /// // ^^^^ no 'this' pointer /// assert_eq!(result.cast::(), 168); /// - /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add1", None, [ String::from("abc").into() ])?; + /// let result = engine.call_fn_dynamic(&mut scope, ast.into(), "add1", None, [ String::from("abc").into() ])?; /// assert_eq!(result.cast::(), 46); /// - /// let result = engine.call_fn_dynamic(&mut scope, &ast, "bar", None, [])?; + /// let result = engine.call_fn_dynamic(&mut scope, ast.into(), "bar", None, [])?; /// assert_eq!(result.cast::(), 21); /// /// let mut value: Dynamic = 1_i64.into(); - /// let result = engine.call_fn_dynamic(&mut scope, &ast, "action", Some(&mut value), [ 41_i64.into() ])?; - /// // ^^^^^^^^^^^^^^^^ binding the 'this' pointer + /// let result = engine.call_fn_dynamic(&mut scope, ast.into(), "action", Some(&mut value), [ 41_i64.into() ])?; + /// // ^^^^^^^^^^^^^^^^ binding the 'this' pointer /// assert_eq!(value.as_int().unwrap(), 42); /// # } /// # Ok(()) @@ -1225,15 +1227,15 @@ impl Engine { pub fn call_fn_dynamic( &self, scope: &mut Scope, - ast: &AST, + lib: &Module, name: &str, mut this_ptr: Option<&mut Dynamic>, mut arg_values: impl AsMut<[Dynamic]>, ) -> Result> { - self.call_fn_dynamic_raw(scope, ast, name, &mut this_ptr, arg_values.as_mut()) + self.call_fn_dynamic_raw(scope, lib, name, &mut this_ptr, arg_values.as_mut()) } - /// Call a script function defined in an [`AST`] with multiple `Dynamic` arguments. + /// Call a script function defined in an `AST` with multiple `Dynamic` arguments. /// /// ## WARNING /// @@ -1245,14 +1247,14 @@ impl Engine { pub(crate) fn call_fn_dynamic_raw( &self, scope: &mut Scope, - ast: &AST, + lib: &Module, name: &str, this_ptr: &mut Option<&mut Dynamic>, arg_values: &mut [Dynamic], ) -> Result> { let mut args: StaticVec<_> = arg_values.iter_mut().collect(); - let fn_def = get_script_function_by_signature(ast.lib(), name, args.len(), true) - .ok_or_else(|| { + let fn_def = + get_script_function_by_signature(lib, name, args.len(), true).ok_or_else(|| { Box::new(EvalAltResult::ErrorFunctionNotFound( name.into(), Position::none(), @@ -1264,27 +1266,19 @@ impl Engine { let args = args.as_mut(); self.call_script_fn( - scope, - &mut mods, - &mut state, - ast.lib(), - this_ptr, - name, - fn_def, - args, - 0, + scope, &mut mods, &mut state, lib, this_ptr, name, fn_def, args, 0, ) } - /// Optimize the [`AST`] with constants defined in an external Scope. - /// An optimized copy of the [`AST`] is returned while the original [`AST`] is consumed. + /// Optimize the `AST` with constants defined in an external Scope. + /// An optimized copy of the `AST` is returned while the original `AST` is consumed. /// /// Although optimization is performed by default during compilation, sometimes it is necessary to /// _re_-optimize an AST. For example, when working with constants that are passed in via an - /// external scope, it will be more efficient to optimize the [`AST`] once again to take advantage + /// external scope, it will be more efficient to optimize the `AST` once again to take advantage /// of the new constants. /// - /// With this method, it is no longer necessary to recompile a large script. The script [`AST`] can be + /// With this method, it is no longer necessary to recompile a large script. The script `AST` can be /// compiled just once. Before evaluation, constants are passed into the `Engine` via an external scope /// (i.e. with `scope.push_constant(...)`). Then, the `AST is cloned and the copy re-optimized before running. #[cfg(not(feature = "no_optimize"))] diff --git a/src/engine.rs b/src/engine.rs index b3eb7d7a..93f850b7 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -661,7 +661,7 @@ impl Engine { } // Run external function - let result = func.get_native_fn()(self, args)?; + let result = func.get_native_fn()(self, lib, args)?; // Restore the original reference restore_first_arg(old_this_ptr, args); @@ -1616,7 +1616,7 @@ impl Engine { .or_else(|| self.packages.get_fn(hash_fn)) { // Overriding exact implementation - func(self, &mut [lhs_ptr, &mut rhs_val])?; + func(self, lib, &mut [lhs_ptr, &mut rhs_val])?; } else if run_builtin_op_assignment(op, lhs_ptr, &rhs_val)?.is_none() { // Not built in, map to `var = var op rhs` let op = &op[..op.len() - 1]; // extract operator without = @@ -1885,9 +1885,8 @@ impl Engine { ) .map_err(|err| err.new_position(*pos)) } - Ok(f) => { - f.get_native_fn()(self, args.as_mut()).map_err(|err| err.new_position(*pos)) - } + Ok(f) => f.get_native_fn()(self, lib, args.as_mut()) + .map_err(|err| err.new_position(*pos)), Err(err) => match *err { EvalAltResult::ErrorFunctionNotFound(_, _) if def_val.is_some() => { Ok(def_val.clone().unwrap()) diff --git a/src/fn_native.rs b/src/fn_native.rs index 1279e430..49cb4570 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,5 +1,6 @@ use crate::any::Dynamic; use crate::engine::Engine; +use crate::module::Module; use crate::parser::ScriptFnDef; use crate::result::EvalAltResult; use crate::utils::ImmutableString; @@ -79,11 +80,11 @@ impl> From for FnPtr { /// A general function trail object. #[cfg(not(feature = "sync"))] -pub type FnAny = dyn Fn(&Engine, &mut FnCallArgs) -> Result>; +pub type FnAny = dyn Fn(&Engine, &Module, &mut FnCallArgs) -> Result>; /// A general function trail object. #[cfg(feature = "sync")] pub type FnAny = - dyn Fn(&Engine, &mut FnCallArgs) -> Result> + Send + Sync; + dyn Fn(&Engine, &Module, &mut FnCallArgs) -> Result> + Send + Sync; /// A standard function that gets an iterator from a type. pub type IteratorFn = fn(Dynamic) -> Box>; diff --git a/src/fn_register.rs b/src/fn_register.rs index 1b07cbe8..01a3255f 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -4,6 +4,7 @@ use crate::any::{Dynamic, Variant}; use crate::engine::Engine; use crate::fn_native::{CallableFunction, FnAny, FnCallArgs, SendSync}; +use crate::module::Module; use crate::parser::FnAccess; use crate::result::EvalAltResult; use crate::utils::ImmutableString; @@ -119,7 +120,7 @@ macro_rules! make_func { // ^ function parameter generic type name (A, B, C etc.) // ^ dereferencing function - Box::new(move |_: &Engine, args: &mut FnCallArgs| { + Box::new(move |_: &Engine, _: &Module, args: &mut FnCallArgs| { // The arguments are assumed to be of the correct number and types! #[allow(unused_variables, unused_mut)] diff --git a/src/module.rs b/src/module.rs index 7ae8d355..065bffab 100644 --- a/src/module.rs +++ b/src/module.rs @@ -373,9 +373,11 @@ impl Module { &mut self, name: impl Into, args: &[TypeId], - func: impl Fn(&Engine, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, + func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |engine: &Engine, args: &mut FnCallArgs| func(engine, args).map(Dynamic::from); + let f = move |engine: &Engine, lib: &Module, args: &mut FnCallArgs| { + func(engine, lib, args).map(Dynamic::from) + }; self.set_fn( name, Public, @@ -402,7 +404,7 @@ impl Module { name: impl Into, func: impl Fn() -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &Engine, _: &mut FnCallArgs| func().map(Dynamic::from); + let f = move |_: &Engine, _: &Module, _: &mut FnCallArgs| func().map(Dynamic::from); let args = []; self.set_fn( name, @@ -430,7 +432,7 @@ impl Module { name: impl Into, func: impl Fn(A) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &Engine, args: &mut FnCallArgs| { + let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { func(mem::take(args[0]).cast::()).map(Dynamic::from) }; let args = [TypeId::of::()]; @@ -460,7 +462,7 @@ impl Module { name: impl Into, func: impl Fn(&mut A) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &Engine, args: &mut FnCallArgs| { + let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { func(args[0].downcast_mut::().unwrap()).map(Dynamic::from) }; let args = [TypeId::of::()]; @@ -514,7 +516,7 @@ impl Module { name: impl Into, func: impl Fn(A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &Engine, args: &mut FnCallArgs| { + let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); let b = mem::take(args[1]).cast::(); @@ -550,7 +552,7 @@ impl Module { name: impl Into, func: impl Fn(&mut A, B) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &Engine, args: &mut FnCallArgs| { + let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let a = args[0].downcast_mut::().unwrap(); @@ -641,7 +643,7 @@ impl Module { name: impl Into, func: impl Fn(A, B, C) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &Engine, args: &mut FnCallArgs| { + let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); @@ -683,7 +685,7 @@ impl Module { name: impl Into, func: impl Fn(&mut A, B, C) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &Engine, args: &mut FnCallArgs| { + let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); let a = args[0].downcast_mut::().unwrap(); @@ -720,7 +722,7 @@ impl Module { &mut self, func: impl Fn(&mut A, B, A) -> FuncReturn<()> + SendSync + 'static, ) -> u64 { - let f = move |_: &Engine, args: &mut FnCallArgs| { + let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); let a = args[0].downcast_mut::().unwrap(); @@ -762,7 +764,7 @@ impl Module { name: impl Into, func: impl Fn(A, B, C, D) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &Engine, args: &mut FnCallArgs| { + let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { let a = mem::take(args[0]).cast::(); let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); @@ -811,7 +813,7 @@ impl Module { name: impl Into, func: impl Fn(&mut A, B, C, D) -> FuncReturn + SendSync + 'static, ) -> u64 { - let f = move |_: &Engine, args: &mut FnCallArgs| { + let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { let b = mem::take(args[1]).cast::(); let c = mem::take(args[2]).cast::(); let d = mem::take(args[3]).cast::(); @@ -942,7 +944,7 @@ impl Module { .map(|f| f.get_shared_fn_def()) } - /// Create a new `Module` by evaluating an [`AST`]. + /// Create a new `Module` by evaluating an `AST`. /// /// # Examples /// diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 7c7b5681..acaa1132 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -3,7 +3,7 @@ use crate::any::{Dynamic, Variant}; use crate::def_package; use crate::engine::{Array, Engine}; -use crate::module::FuncReturn; +use crate::module::{FuncReturn, Module}; use crate::parser::{ImmutableString, INT}; use crate::result::EvalAltResult; use crate::token::Position; @@ -25,7 +25,11 @@ fn ins(list: &mut Array, position: INT, item: T) -> FuncRetu } Ok(()) } -fn pad(engine: &Engine, args: &mut [&mut Dynamic]) -> FuncReturn<()> { +fn pad( + engine: &Engine, + _: &Module, + args: &mut [&mut Dynamic], +) -> FuncReturn<()> { let len = *args[1].downcast_ref::().unwrap(); // Check if array will be over max size limit diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index e6acbbb9..5ae39db6 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -1,7 +1,7 @@ use crate::any::Dynamic; use crate::def_package; use crate::engine::Engine; -use crate::module::FuncReturn; +use crate::module::{FuncReturn, Module}; use crate::parser::{ImmutableString, INT}; use crate::result::EvalAltResult; use crate::token::Position; @@ -226,7 +226,7 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str lib.set_fn_var_args( "pad", &[TypeId::of::(), TypeId::of::(), TypeId::of::()], - |engine: &Engine, args: &mut [&mut Dynamic]| { + |engine: &Engine, _: &Module, args: &mut [&mut Dynamic]| { let len = *args[1].downcast_ref::< INT>().unwrap(); // Check if string will be over max size limit diff --git a/src/parser.rs b/src/parser.rs index 7b919b8d..02cf1571 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -298,6 +298,12 @@ impl Add for &AST { } } +impl<'a> From<&'a AST> for &'a Module { + fn from(ast: &'a AST) -> Self { + ast.lib() + } +} + /// A type representing the access mode of a scripted function. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub enum FnAccess { diff --git a/tests/call_fn.rs b/tests/call_fn.rs index f217073e..690577ce 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -1,6 +1,7 @@ #![cfg(not(feature = "no_function"))] use rhai::{ - Dynamic, Engine, EvalAltResult, Func, ImmutableString, ParseError, ParseErrorType, Scope, INT, + Dynamic, Engine, EvalAltResult, Func, ImmutableString, Module, ParseError, ParseErrorType, + Scope, INT, }; #[test] @@ -117,17 +118,6 @@ fn test_anonymous_fn() -> Result<(), Box> { fn test_fn_ptr() -> Result<(), Box> { let mut engine = Engine::new(); - let ast = engine.compile( - r#" - fn foo(x) { this += x; } - - let x = 41; - x.bar("foo", 1); - x - "#, - )?; - let ast_clone = ast.clone(); - engine.register_raw_fn( "bar", &[ @@ -135,24 +125,29 @@ fn test_fn_ptr() -> Result<(), Box> { std::any::TypeId::of::(), std::any::TypeId::of::(), ], - move |engine: &Engine, args: &mut [&mut Dynamic]| { + move |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { let callback = args[1].clone().cast::(); let value = args[2].clone(); let this_ptr = args.get_mut(0).unwrap(); - engine.call_fn_dynamic( - &mut Scope::new(), - &ast_clone, - &callback, - Some(this_ptr), - [value], - )?; + engine.call_fn_dynamic(&mut Scope::new(), lib, &callback, Some(this_ptr), [value])?; Ok(().into()) }, ); - assert_eq!(engine.eval_ast::(&ast)?, 42); + assert_eq!( + engine.eval::( + r#" + fn foo(x) { this += x; } + + let x = 41; + x.bar("foo", 1); + x + "# + )?, + 42 + ); Ok(()) } From 05a4b466d1f8a7f8cf4615f01f24b2d99f9a9c8f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Jul 2020 12:06:57 +0800 Subject: [PATCH 23/46] New register_raw_fn_n shortcuts. --- doc/src/rust/register-raw.md | 127 ++++++++++++++++++++++++++ src/api.rs | 171 ++++++++++++++++++++++++++++++++++- 2 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 doc/src/rust/register-raw.md diff --git a/doc/src/rust/register-raw.md b/doc/src/rust/register-raw.md new file mode 100644 index 00000000..429dae4a --- /dev/null +++ b/doc/src/rust/register-raw.md @@ -0,0 +1,127 @@ +Use the Low-Level API to Register a Rust Function +================================================ + +{{#include ../links.md}} + +When a native Rust function is registered with an `Engine` using the `Engine::register_XXX` API, +Rhai transparently converts all function arguments from [`Dynamic`] into the correct types before +calling the function. + +For more power and flexibility, there is a _low-level_ API to work directly with [`Dynamic`] values +without the conversions. + + +Raw Function Registration +------------------------- + +The `Engine::register_raw_fn` method is marked _volatile_, meaning that it may be changed without warning. + +If this is acceptable, then using this method to register a Rust function opens up more opportunities. + +In particular, a reference to the current `Engine` instance is passed as an argument so the Rust function +can also use `Engine` facilities (like evaluating a script). + +```rust +engine.register_raw_fn( + "increment_by", // function name + &[ // a slice containing parameter types + std::any::TypeId::of::(), // type of first parameter + std::any::TypeId::of::() // type of second parameter + ], + |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { // fixed function signature + // Arguments are guaranteed to be correct in number and of the correct types. + + // But remember this is Rust, so you can keep only one mutable reference at any one time! + // Therefore, get a '&mut' reference to the first argument _last_. + // Alternatively, use `args.split_at_mut(1)` etc. to split the slice first. + + let y: i64 = *args[1].downcast_ref::() // get a reference to the second argument + .unwrap(); // then copying it because it is a primary type + + let y: i64 = std::mem::take(args[1]).cast::(); // alternatively, directly 'consume' it + + let x: &mut i64 = args[0].downcast_mut::() // get a '&mut' reference to the + .unwrap(); // first argument + + *x += y; // perform the action + + Ok(().into()) // must be 'Result>' + } +); + +// The above is the same as (in fact, internally they are equivalent): + +engine.register_fn("increment_by", |x: &mut i64, y: i64| x += y); +``` + + +Shortcuts +--------- + +As usual with Rhai, there are shortcuts. For functions of zero to four parameters, which should be +the majority, use one of the `Engine::register_raw_fn_n` (where `n = 0..4`) methods: + +```rust +// Specify parameter types as generics +engine.register_raw_fn_2::( + "increment_by", + |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { ... } +); +``` + + +Closure Signature +----------------- + +The closure passed to `Engine::register_raw_fn` takes the following form: + +`Fn(engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]) -> Result> + 'static` + +where: + +* `engine` - a reference to the current [`Engine`], with all configurations and settings. + +* `lib` - a reference to the current collection of script-defined functions, as a [`Module`]. + +* `args` - a reference to a slice containing `&mut` references to [`Dynamic`] values. + The slice is guaranteed to contain enough arguments _of the correct types_. + +Remember, in Rhai, all arguments _except_ the _first_ one are always passed by _value_ (i.e. cloned). +Therefore, it is unnecessary to ever mutate any argument except the first one, as all mutations +will be on the cloned copy. + + +Extract Arguments +----------------- + +To extract an argument from the `args` parameter (`&mut [&mut Dynamic]`), use the following: + +| Argument type | Access (`n` = argument position) | Result | +| ------------------------------ | -------------------------------------- | ---------------------------------------------------------- | +| [Primary type][standard types] | `args[n].clone().cast::()` | Copy of value. | +| Custom type | `args[n].downcast_ref::().unwrap()` | Immutable reference to value. | +| Custom type (consumed) | `mem::take(args[n]).cast::()` | The _consumed_ value.
The original value becomes `()`. | +| `this` object | `args[0].downcast_mut::().unwrap()` | Mutable reference to value. | + +When there is a mutable reference to the `this` object (i.e. the first argument), +there can be no other immutable references to `args`, otherwise the Rust borrow checker will complain. + + +Hold Multiple References +------------------------ + +In order to access a value argument that is expensive to clone _while_ holding a mutable reference +to the first argument, either _consume_ that argument via `mem::take` as above, or use `args.split_at` +to partition the slice: + +```rust +// Partition the slice +let (first, rest) = args.split_at_mut(1); + +// Mutable reference to the first parameter +let this_ptr = first[0].downcast_mut::
().unwrap(); + +// Immutable reference to the second value parameter +// This can be mutable but there is no point because the parameter is passed by value +let value = rest[0].downcast_ref::().unwrap(); +``` diff --git a/src/api.rs b/src/api.rs index 343caaed..c17f7afb 100644 --- a/src/api.rs +++ b/src/api.rs @@ -33,7 +33,7 @@ use crate::stdlib::{fs::File, io::prelude::*, path::PathBuf}; /// Engine public API impl Engine { - /// Register a function with the `Engine`. + /// Register a function of the `Engine`. /// /// ## WARNING - Low Level API /// @@ -52,7 +52,7 @@ impl Engine { pub fn register_raw_fn( &mut self, name: &str, - args: &[TypeId], + arg_types: &[TypeId], #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + 'static, @@ -62,7 +62,172 @@ impl Engine { + Sync + 'static, ) { - self.global_module.set_fn_var_args(name, args, func); + self.global_module.set_fn_var_args(name, arg_types, func); + } + + /// Register a function of no parameters with the `Engine`. + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. + #[deprecated(note = "this function is volatile and may change")] + pub fn register_raw_fn_0( + &mut self, + name: &str, + + #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + + 'static, + + #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + + Send + + Sync + + 'static, + ) { + self.global_module.set_fn_var_args(name, &[], func); + } + + /// Register a function of one parameter with the `Engine`. + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. + /// + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`. + /// The argument is guaranteed to be of the correct type. + /// + /// To get access to a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// + /// To get access to a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// Notice that this will _consume_ the argument, replacing it with `()`. + /// + /// To get access to the first mutable parameter, use `args.get_mut(0).unwrap()` + #[deprecated(note = "this function is volatile and may change")] + pub fn register_raw_fn_1( + &mut self, + name: &str, + + #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + + 'static, + + #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + + Send + + Sync + + 'static, + ) { + self.global_module + .set_fn_var_args(name, &[TypeId::of::()], func); + } + + /// Register a function of two parameters with the `Engine`. + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. + /// + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`. + /// The arguments are guaranteed to be of the correct types. + /// + /// To get access to a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// + /// To get access to a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// Notice that this will _consume_ the argument, replacing it with `()`. + /// + /// To get access to the first mutable parameter, use `args.get_mut(0).unwrap()` + #[deprecated(note = "this function is volatile and may change")] + pub fn register_raw_fn_2( + &mut self, + name: &str, + + #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + + 'static, + + #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + + Send + + Sync + + 'static, + ) { + self.global_module + .set_fn_var_args(name, &[TypeId::of::(), TypeId::of::()], func); + } + + /// Register a function of three parameters with the `Engine`. + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. + /// + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`. + /// The arguments are guaranteed to be of the correct types. + /// + /// To get access to a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// + /// To get access to a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// Notice that this will _consume_ the argument, replacing it with `()`. + /// + /// To get access to the first mutable parameter, use `args.get_mut(0).unwrap()` + #[deprecated(note = "this function is volatile and may change")] + pub fn register_raw_fn_3( + &mut self, + name: &str, + + #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + + 'static, + + #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + + Send + + Sync + + 'static, + ) { + self.global_module.set_fn_var_args( + name, + &[TypeId::of::(), TypeId::of::(), TypeId::of::()], + func, + ); + } + + /// Register a function of four parameters with the `Engine`. + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. + /// + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`. + /// The arguments are guaranteed to be of the correct types. + /// + /// To get access to a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// + /// To get access to a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// Notice that this will _consume_ the argument, replacing it with `()`. + /// + /// To get access to the first mutable parameter, use `args.get_mut(0).unwrap()` + #[deprecated(note = "this function is volatile and may change")] + pub fn register_raw_fn_4< + A: Variant + Clone, + B: Variant + Clone, + C: Variant + Clone, + D: Variant + Clone, + >( + &mut self, + name: &str, + + #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + + 'static, + + #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> + + Send + + Sync + + 'static, + ) { + self.global_module.set_fn_var_args( + name, + &[ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ], + func, + ); } /// Register a custom type for use with the `Engine`. From d9fe6a1980611450fdee0cd491a3b017c5239964 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Jul 2020 12:24:28 +0800 Subject: [PATCH 24/46] Add boolean xor operator and readjust % precedence. --- RELEASES.md | 2 ++ src/engine.rs | 1 + src/token.rs | 13 ++++++------- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 0e2d8677..1818519e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -15,6 +15,7 @@ Breaking changes * `EvalAltResult::ErrorMismatchOutputType` has an extra argument containing the name of the requested type. * `Engine::call_fn_dynamic` take an extra argument, allowing a `Dynamic` value to be bound to the `this` pointer. +* Precedence of the `%` (modulo) operator is lowered to below `<<` ad `>>`. This is to handle the case of `x << 3 % 10`. New features ------------ @@ -25,6 +26,7 @@ New features * `Engine::register_custom_operator` to define a custom operator. * New low-level API `Engine::register_raw_fn`. * `AST::clone_functions_only`, `AST::clone_functions_only_filtered` and `AST::clone_statements_only` to clone only part of an `AST`. +* The boolean `^` (XOR) operator is added. Version 0.16.1 diff --git a/src/engine.rs b/src/engine.rs index 93f850b7..e8a47960 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2444,6 +2444,7 @@ fn run_builtin_binary_op( match op { "&" => return Ok(Some((x && y).into())), "|" => return Ok(Some((x || y).into())), + "^" => return Ok(Some((x ^ y).into())), "==" => return Ok(Some((x == y).into())), "!=" => return Ok(Some((x != y).into())), _ => (), diff --git a/src/token.rs b/src/token.rs index 673eb11f..7eb7100d 100644 --- a/src/token.rs +++ b/src/token.rs @@ -394,18 +394,17 @@ impl Token { And | Ampersand => 60, - LessThan | LessThanEqualsTo | GreaterThan | GreaterThanEqualsTo | EqualsTo - | NotEqualsTo => 90, + EqualsTo | NotEqualsTo => 90, - In => 110, + LessThan | LessThanEqualsTo | GreaterThan | GreaterThanEqualsTo => 110, - Plus | Minus => 130, + In => 130, - Divide | Multiply | PowerOf => 160, + Plus | Minus => 150, - LeftShift | RightShift => 190, + Divide | Multiply | PowerOf | Modulo => 180, - Modulo => 210, + LeftShift | RightShift => 210, Period => 240, From 3e45d5d9a5c91c719748ed15a0054437095731aa Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Jul 2020 13:01:57 +0800 Subject: [PATCH 25/46] Refine docs and API. --- doc/src/SUMMARY.md | 7 ++++--- doc/src/appendix/operators.md | 2 +- doc/src/engine/custom-op.md | 20 ++++++++++---------- doc/src/links.md | 1 + doc/src/rust/fallible.md | 2 +- src/api.rs | 20 ++++++++++---------- src/fn_func.rs | 4 ++-- src/lib.rs | 2 +- src/module.rs | 6 ++++++ src/parser.rs | 16 +++++++++++----- src/settings.rs | 4 ++-- tests/tokens.rs | 4 ++-- 12 files changed, 51 insertions(+), 37 deletions(-) diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 6c483cde..6d90df0c 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -104,9 +104,10 @@ The Rhai Scripting Language 4. [Side-Effect Considerations](engine/optimize/side-effects.md) 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) - 4. [Disable Keywords and/or Operators](engine/disable.md) - 5. [Custom Operators](engine/custom-op.md) - 6. [Eval Statement](language/eval.md) + 4. [Low-Level API](rust/register-raw.md) + 5. [Disable Keywords and/or Operators](engine/disable.md) + 6. [Custom Operators](engine/custom-op.md) + 7. [Eval Statement](language/eval.md) 9. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) 2. [Operators](appendix/operators.md) diff --git a/doc/src/appendix/operators.md b/doc/src/appendix/operators.md index bc0459b8..074205e2 100644 --- a/doc/src/appendix/operators.md +++ b/doc/src/appendix/operators.md @@ -15,7 +15,7 @@ Operators | `<<` | Left bit-shift | Yes | Left | | `&` | Bit-wise _And_, Boolean _And_ | Yes | Left | | \| | Bit-wise _Or_, Boolean _Or_ | Yes | Left | -| `^` | Bit-wise _Xor_ | Yes | Left | +| `^` | Bit-wise _Xor_, Boolean _Xor_ | Yes | Left | | `==` | Equals to | Yes | Left | | `~=` | Not equals to | Yes | Left | | `>` | Greater than | Yes | Left | diff --git a/doc/src/engine/custom-op.md b/doc/src/engine/custom-op.md index 1c12263e..e3c1d099 100644 --- a/doc/src/engine/custom-op.md +++ b/doc/src/engine/custom-op.md @@ -18,8 +18,8 @@ use rhai::{Engine, RegisterFn}; let mut engine = Engine::new(); // Register a custom operator called 'foo' and give it -// a precedence of 140 (i.e. between +|- and *|/) -engine.register_custom_operator("foo", 140).unwrap(); +// a precedence of 160 (i.e. between +|- and *|/) +engine.register_custom_operator("foo", 160).unwrap(); // Register the implementation of the customer operator as a function engine.register_fn("foo", |x: i64, y: i64| (x * y) - (x + y)); @@ -49,7 +49,7 @@ Therefore, the following are equivalent (assuming `foo` has been registered as a A script using custom operators can always be pre-processed, via a pre-processor application, into a syntax that uses the corresponding function calls. -Using `Engine::register_custom_operator` merely enables a convenient short-cut. +Using `Engine::register_custom_operator` merely enables a convenient shortcut. Must Follow Variable Naming @@ -71,7 +71,7 @@ All custom operators must be _binary_ (i.e. they take two operands). _Unary_ custom operators are not supported. ```rust -engine.register_custom_operator("foo", 140).unwrap(); +engine.register_custom_operator("foo", 160).unwrap(); engine.register_fn("foo", |x: i64| x * x); @@ -91,12 +91,12 @@ The following _precedence table_ show the built-in precedence of standard Rhai o | Assignments | `=`, `+=`, `-=`, `*=`, `/=`, `~=`, `%=`,
`<<=`, `>>=`, `&=`, \|=, `^=` | 0 | | Logic and bit masks | \|\|, \|, `^` | 30 | | Logic and bit masks | `&`, `&&` | 60 | -| Comparisons | `==`, `!=`, `>`, `>=`, `<`, `<=` | 90 | -| | `in` | 110 | -| Arithmetic | `+`, `-` | 130 | -| Arithmetic | `*`, `/`, `~` | 160 | -| Bit-shifts | `<<`, `>>` | 190 | -| Arithmetic | `%` | 210 | +| Comparisons | `==`, `!=` | 90 | +| Comparisons | `>`, `>=`, `<`, `<=` | 110 | +| | `in` | 130 | +| Arithmetic | `+`, `-` | 150 | +| Arithmetic | `*`, `/`, `~`, `%` | 180 | +| Bit-shifts | `<<`, `>>` | 210 | | Object | `.` _(binds to right)_ | 240 | | _Others_ | | 0 | diff --git a/doc/src/links.md b/doc/src/links.md index 69a01198..a81bfd0c 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -20,6 +20,7 @@ [`Engine`]: {{rootUrl}}/engine/hello-world.md [traits]: {{rootUrl}}/rust/traits.md [`private`]: {{rootUrl}}/engine/call-fn.md +[`call_fn`]: {{rootUrl}}/engine/call-fn.md [`Func`]: {{rootUrl}}/engine/func.md [`AST`]: {{rootUrl}}/engine/compile.md [`eval_expression`]: {{rootUrl}}/engine/expressions.md diff --git a/doc/src/rust/fallible.md b/doc/src/rust/fallible.md index 16c16826..86d15576 100644 --- a/doc/src/rust/fallible.md +++ b/doc/src/rust/fallible.md @@ -16,7 +16,7 @@ use rhai::RegisterResultFn; // use 'RegisterResultFn' trait fn safe_divide(x: i64, y: i64) -> Result> { if y == 0 { // Return an error if y is zero - Err("Division by zero!".into()) // short-cut to create Box + Err("Division by zero!".into()) // shortcut to create Box } else { Ok((x / y).into()) // convert result into 'Dynamic' } diff --git a/src/api.rs b/src/api.rs index c17f7afb..42805fa3 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1325,8 +1325,7 @@ impl Engine { args: A, ) -> Result> { let mut arg_values = args.into_vec(); - let result = - self.call_fn_dynamic_raw(scope, ast.lib(), name, &mut None, arg_values.as_mut())?; + let result = self.call_fn_dynamic_raw(scope, ast, name, &mut None, arg_values.as_mut())?; let typ = self.map_type_name(result.type_name()); @@ -1370,19 +1369,19 @@ impl Engine { /// scope.push("foo", 42_i64); /// /// // Call the script-defined function - /// let result = engine.call_fn_dynamic(&mut scope, ast.into(), "add", None, [ String::from("abc").into(), 123_i64.into() ])?; - /// // ^^^^ no 'this' pointer + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add", None, [ String::from("abc").into(), 123_i64.into() ])?; + /// // ^^^^ no 'this' pointer /// assert_eq!(result.cast::(), 168); /// - /// let result = engine.call_fn_dynamic(&mut scope, ast.into(), "add1", None, [ String::from("abc").into() ])?; + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add1", None, [ String::from("abc").into() ])?; /// assert_eq!(result.cast::(), 46); /// - /// let result = engine.call_fn_dynamic(&mut scope, ast.into(), "bar", None, [])?; + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "bar", None, [])?; /// assert_eq!(result.cast::(), 21); /// /// let mut value: Dynamic = 1_i64.into(); - /// let result = engine.call_fn_dynamic(&mut scope, ast.into(), "action", Some(&mut value), [ 41_i64.into() ])?; - /// // ^^^^^^^^^^^^^^^^ binding the 'this' pointer + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "action", Some(&mut value), [ 41_i64.into() ])?; + /// // ^^^^^^^^^^^^^^^^ binding the 'this' pointer /// assert_eq!(value.as_int().unwrap(), 42); /// # } /// # Ok(()) @@ -1392,7 +1391,7 @@ impl Engine { pub fn call_fn_dynamic( &self, scope: &mut Scope, - lib: &Module, + lib: impl AsRef, name: &str, mut this_ptr: Option<&mut Dynamic>, mut arg_values: impl AsMut<[Dynamic]>, @@ -1412,11 +1411,12 @@ impl Engine { pub(crate) fn call_fn_dynamic_raw( &self, scope: &mut Scope, - lib: &Module, + lib: impl AsRef, name: &str, this_ptr: &mut Option<&mut Dynamic>, arg_values: &mut [Dynamic], ) -> Result> { + let lib = lib.as_ref(); let mut args: StaticVec<_> = arg_values.iter_mut().collect(); let fn_def = get_script_function_by_signature(lib, name, args.len(), true).ok_or_else(|| { diff --git a/src/fn_func.rs b/src/fn_func.rs index ccb861a6..bbbab4aa 100644 --- a/src/fn_func.rs +++ b/src/fn_func.rs @@ -15,8 +15,8 @@ use crate::stdlib::{boxed::Box, string::ToString}; pub trait Func { type Output; - /// Create a Rust anonymous function from an [`AST`]. - /// The `Engine` and [`AST`] are consumed and basically embedded into the closure. + /// Create a Rust anonymous function from an `AST`. + /// The `Engine` and `AST` are consumed and basically embedded into the closure. /// /// # Examples /// diff --git a/src/lib.rs b/src/lib.rs index c3563e38..13221e2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ //! | `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | //! | `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | //! | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | -//! | `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, `Engine`, `Scope` and [`AST`] are all `Send + Sync`. | +//! | `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, `Engine`, `Scope` and `AST` are all `Send + Sync`. | //! | `serde` | Enable serialization/deserialization via `serde`. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | //! | `internals` | Expose internal data structures (beware they may be volatile from version to version). | //! diff --git a/src/module.rs b/src/module.rs index 065bffab..cd08e5bc 100644 --- a/src/module.rs +++ b/src/module.rs @@ -102,6 +102,12 @@ impl Clone for Module { } } +impl AsRef for Module { + fn as_ref(&self) -> &Module { + self + } +} + impl Module { /// Create a new module. /// diff --git a/src/parser.rs b/src/parser.rs index 02cf1571..c774f0d0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -66,14 +66,14 @@ impl AST { /// Get the statements. #[cfg(not(feature = "internals"))] - pub(crate) fn statements(&self) -> &Vec { + pub(crate) fn statements(&self) -> &[Stmt] { &self.0 } /// Get the statements. #[cfg(feature = "internals")] #[deprecated(note = "this method is volatile and may change")] - pub fn statements(&self) -> &Vec { + pub fn statements(&self) -> &[Stmt] { &self.0 } @@ -298,9 +298,15 @@ impl Add for &AST { } } -impl<'a> From<&'a AST> for &'a Module { - fn from(ast: &'a AST) -> Self { - ast.lib() +impl AsRef<[Stmt]> for AST { + fn as_ref(&self) -> &[Stmt] { + self.statements() + } +} + +impl AsRef for AST { + fn as_ref(&self) -> &Module { + self.lib() } } diff --git a/src/settings.rs b/src/settings.rs index daa714bd..07b87198 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -218,8 +218,8 @@ impl Engine { /// let mut engine = Engine::new(); /// /// // Register a custom operator called 'foo' and give it - /// // a precedence of 140 (i.e. between +|- and *|/). - /// engine.register_custom_operator("foo", 140).unwrap(); + /// // a precedence of 160 (i.e. between +|- and *|/). + /// engine.register_custom_operator("foo", 160).unwrap(); /// /// // Register a binary function named 'foo' /// engine.register_fn("foo", |x: i64, y: i64| (x * y) - (x + y)); diff --git a/tests/tokens.rs b/tests/tokens.rs index b86f6a0e..bb033619 100644 --- a/tests/tokens.rs +++ b/tests/tokens.rs @@ -24,8 +24,8 @@ fn test_tokens_custom_operator() -> Result<(), Box> { let mut engine = Engine::new(); // Register a custom operator called `foo` and give it - // a precedence of 140 (i.e. between +|- and *|/). - engine.register_custom_operator("foo", 140).unwrap(); + // a precedence of 160 (i.e. between +|- and *|/). + engine.register_custom_operator("foo", 160).unwrap(); // Register a binary function named `foo` engine.register_fn("foo", |x: INT, y: INT| (x * y) - (x + y)); From 8bc1b25edd111df3553a39469f8bc52b6289393f Mon Sep 17 00:00:00 2001 From: Alvin Wong Date: Mon, 6 Jul 2020 15:30:33 +0800 Subject: [PATCH 26/46] Fix `only_i32` build --- src/serde/ser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serde/ser.rs b/src/serde/ser.rs index 1477da2d..79b1b733 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -162,7 +162,7 @@ impl Serializer for &mut DynamicSerializer { #[cfg(not(feature = "only_i32"))] return self.serialize_i64(i64::from(v)); #[cfg(feature = "only_i32")] - if v > i32::MAX as u64 { + if v > i32::MAX as u32 { return Ok(Dynamic::from(v)); } else { return self.serialize_i32(v as i32); From 8f53ce50d47c9cc965ba03a6be853d063eb88e05 Mon Sep 17 00:00:00 2001 From: Alvin Wong Date: Sun, 5 Jul 2020 11:35:50 +0800 Subject: [PATCH 27/46] Ensure rhai::INT can be deserialized into any integer types --- src/serde/de.rs | 98 +++++++++++++++++++++++++++++++++++++------------ tests/serde.rs | 46 +++++++++++++++++++++++ 2 files changed, 120 insertions(+), 24 deletions(-) diff --git a/src/serde/de.rs b/src/serde/de.rs index 82910073..f91c051d 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -49,6 +49,20 @@ impl<'de> DynamicDeserializer<'de> { Position::none(), ))) } + fn deserialize_int>( + &mut self, + v: crate::INT, + visitor: V, + ) -> Result> { + #[cfg(not(feature = "only_i32"))] + { + visitor.visit_i64(v) + } + #[cfg(feature = "only_i32")] + { + visitor.visit_i32(v) + } + } } /// Deserialize a `Dynamic` value into a Rust type that implements `serde::Deserialize`. @@ -159,51 +173,87 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { } fn deserialize_i8>(self, visitor: V) -> Result> { - self.value - .downcast_ref::() - .map_or_else(|| self.type_error(), |&x| visitor.visit_i8(x)) + if let Ok(v) = self.value.as_int() { + self.deserialize_int(v, visitor) + } else { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error(), |&x| visitor.visit_i8(x)) + } } fn deserialize_i16>(self, visitor: V) -> Result> { - self.value - .downcast_ref::() - .map_or_else(|| self.type_error(), |&x| visitor.visit_i16(x)) + if let Ok(v) = self.value.as_int() { + self.deserialize_int(v, visitor) + } else { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error(), |&x| visitor.visit_i16(x)) + } } fn deserialize_i32>(self, visitor: V) -> Result> { - self.value - .downcast_ref::() - .map_or_else(|| self.type_error(), |&x| visitor.visit_i32(x)) + if let Ok(v) = self.value.as_int() { + self.deserialize_int(v, visitor) + } else if cfg!(feature = "only_i32") { + self.type_error() + } else { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error(), |&x| visitor.visit_i32(x)) + } } fn deserialize_i64>(self, visitor: V) -> Result> { - self.value - .downcast_ref::() - .map_or_else(|| self.type_error(), |&x| visitor.visit_i64(x)) + if let Ok(v) = self.value.as_int() { + self.deserialize_int(v, visitor) + } else if cfg!(not(feature = "only_i32")) { + self.type_error() + } else { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error(), |&x| visitor.visit_i64(x)) + } } fn deserialize_u8>(self, visitor: V) -> Result> { - self.value - .downcast_ref::() - .map_or_else(|| self.type_error(), |&x| visitor.visit_u8(x)) + if let Ok(v) = self.value.as_int() { + self.deserialize_int(v, visitor) + } else { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error(), |&x| visitor.visit_u8(x)) + } } fn deserialize_u16>(self, visitor: V) -> Result> { - self.value - .downcast_ref::() - .map_or_else(|| self.type_error(), |&x| visitor.visit_u16(x)) + if let Ok(v) = self.value.as_int() { + self.deserialize_int(v, visitor) + } else { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error(), |&x| visitor.visit_u16(x)) + } } fn deserialize_u32>(self, visitor: V) -> Result> { - self.value - .downcast_ref::() - .map_or_else(|| self.type_error(), |&x| visitor.visit_u32(x)) + if let Ok(v) = self.value.as_int() { + self.deserialize_int(v, visitor) + } else { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error(), |&x| visitor.visit_u32(x)) + } } fn deserialize_u64>(self, visitor: V) -> Result> { - self.value - .downcast_ref::() - .map_or_else(|| self.type_error(), |&x| visitor.visit_u64(x)) + if let Ok(v) = self.value.as_int() { + self.deserialize_int(v, visitor) + } else { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error(), |&x| visitor.visit_u64(x)) + } } fn deserialize_f32>(self, visitor: V) -> Result> { diff --git a/tests/serde.rs b/tests/serde.rs index 82ec330b..95a3c24e 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -33,6 +33,38 @@ fn test_serde_ser_primary_types() -> Result<(), Box> { Ok(()) } +#[test] +fn test_serde_ser_integer_types() -> Result<(), Box> { + assert_eq!(to_dynamic(42_i8)?.type_name(), std::any::type_name::()); + assert_eq!( + to_dynamic(42_i16)?.type_name(), + std::any::type_name::() + ); + assert_eq!( + to_dynamic(42_i32)?.type_name(), + std::any::type_name::() + ); + assert_eq!( + to_dynamic(42_i64)?.type_name(), + std::any::type_name::() + ); + assert_eq!(to_dynamic(42_u8)?.type_name(), std::any::type_name::()); + assert_eq!( + to_dynamic(42_u16)?.type_name(), + std::any::type_name::() + ); + assert_eq!( + to_dynamic(42_u32)?.type_name(), + std::any::type_name::() + ); + assert_eq!( + to_dynamic(42_u64)?.type_name(), + std::any::type_name::() + ); + + Ok(()) +} + #[test] #[cfg(not(feature = "no_index"))] fn test_serde_ser_array() -> Result<(), Box> { @@ -106,6 +138,20 @@ fn test_serde_de_primary_types() -> Result<(), Box> { Ok(()) } +#[test] +fn test_serde_de_integer_types() -> Result<(), Box> { + assert_eq!(42_i8, from_dynamic(&Dynamic::from(42 as INT))?); + assert_eq!(42_i16, from_dynamic(&Dynamic::from(42 as INT))?); + assert_eq!(42_i32, from_dynamic(&Dynamic::from(42 as INT))?); + assert_eq!(42_i64, from_dynamic(&Dynamic::from(42 as INT))?); + assert_eq!(42_u8, from_dynamic(&Dynamic::from(42 as INT))?); + assert_eq!(42_u16, from_dynamic(&Dynamic::from(42 as INT))?); + assert_eq!(42_u32, from_dynamic(&Dynamic::from(42 as INT))?); + assert_eq!(42_u64, from_dynamic(&Dynamic::from(42 as INT))?); + + Ok(()) +} + #[test] #[cfg(not(feature = "no_index"))] fn test_serde_de_array() -> Result<(), Box> { From 46cdec12803e331358622b971e907fc00c7e5431 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Jul 2020 16:20:03 +0800 Subject: [PATCH 28/46] Refine docs and tests. --- doc/src/language/loop.md | 2 +- doc/src/language/while.md | 2 +- src/api.rs | 6 +++--- tests/call_fn.rs | 2 +- tests/looping.rs | 2 +- tests/var_scope.rs | 2 +- tests/while_loop.rs | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/src/language/loop.md b/doc/src/language/loop.md index b92c3f7e..e625082a 100644 --- a/doc/src/language/loop.md +++ b/doc/src/language/loop.md @@ -12,7 +12,7 @@ Like C, `continue` can be used to skip to the next iteration, by-passing all fol let x = 10; loop { - x = x - 1; + x -= 1; if x > 5 { continue; } // skip to the next iteration diff --git a/doc/src/language/while.md b/doc/src/language/while.md index e912175e..5b7a5ac8 100644 --- a/doc/src/language/while.md +++ b/doc/src/language/while.md @@ -12,7 +12,7 @@ Like C, `continue` can be used to skip to the next iteration, by-passing all fol let x = 10; while x > 0 { - x = x - 1; + x -= 1; if x < 6 { continue; } // skip to the next iteration print(x); if x == 5 { break; } // break out of while loop diff --git a/src/api.rs b/src/api.rs index 42805fa3..5df12aba 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1046,8 +1046,8 @@ impl Engine { /// let mut scope = Scope::new(); /// scope.push("x", 40_i64); /// - /// assert_eq!(engine.eval_with_scope::(&mut scope, "x = x + 2; x")?, 42); - /// assert_eq!(engine.eval_with_scope::(&mut scope, "x = x + 2; x")?, 44); + /// assert_eq!(engine.eval_with_scope::(&mut scope, "x += 2; x")?, 42); + /// assert_eq!(engine.eval_with_scope::(&mut scope, "x += 2; x")?, 44); /// /// // The variable in the scope is modified /// assert_eq!(scope.get_value::("x").expect("variable x should exist"), 44); @@ -1160,7 +1160,7 @@ impl Engine { /// scope.push("x", 40_i64); /// /// // Compile a script to an AST and store it for later evaluation - /// let ast = engine.compile("x = x + 2; x")?; + /// let ast = engine.compile("x += 2; x")?; /// /// // Evaluate it /// assert_eq!(engine.eval_ast_with_scope::(&mut scope, &ast)?, 42); diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 690577ce..715fbc87 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -32,7 +32,7 @@ fn test_call_fn() -> Result<(), Box> { x + y } fn hello(x) { - x = x * foo; + x *= foo; foo = 1; x } diff --git a/tests/looping.rs b/tests/looping.rs index 3a4804ce..951f8cd7 100644 --- a/tests/looping.rs +++ b/tests/looping.rs @@ -14,7 +14,7 @@ fn test_loop() -> Result<(), Box> { if i < 10 { i += 1; if x > 20 { continue; } - x = x + i; + x += i; } else { break; } diff --git a/tests/var_scope.rs b/tests/var_scope.rs index fde83a05..0b3cc1b9 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -7,7 +7,7 @@ fn test_var_scope() -> Result<(), Box> { engine.eval_with_scope::<()>(&mut scope, "let x = 4 + 5")?; assert_eq!(engine.eval_with_scope::(&mut scope, "x")?, 9); - engine.eval_with_scope::<()>(&mut scope, "x = x + 1; x = x + 2;")?; + engine.eval_with_scope::<()>(&mut scope, "x += 1; x += 2;")?; assert_eq!(engine.eval_with_scope::(&mut scope, "x")?, 12); scope.set_value("x", 42 as INT); diff --git a/tests/while_loop.rs b/tests/while_loop.rs index 8916cd7c..bbcd091b 100644 --- a/tests/while_loop.rs +++ b/tests/while_loop.rs @@ -10,10 +10,10 @@ fn test_while() -> Result<(), Box> { let x = 0; while x < 10 { - x = x + 1; + x += 1; if x > 5 { break; } if x > 3 { continue; } - x = x + 3; + x += 3; } x From 0644c67252da8f76fa4b2add7b0e9d5e95ac43c8 Mon Sep 17 00:00:00 2001 From: Alvin Wong Date: Sun, 5 Jul 2020 11:43:48 +0800 Subject: [PATCH 29/46] Impl. deserializing `enum` representations --- src/serde/de.rs | 84 +++++++++++++++- tests/serde.rs | 258 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+), 3 deletions(-) diff --git a/src/serde/de.rs b/src/serde/de.rs index f91c051d..88533718 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -7,7 +7,10 @@ use crate::result::EvalAltResult; use crate::token::Position; use crate::utils::ImmutableString; -use serde::de::{DeserializeSeed, Deserializer, Error, MapAccess, SeqAccess, Visitor}; +use serde::de::{ + DeserializeSeed, Deserializer, EnumAccess, Error, IntoDeserializer, MapAccess, SeqAccess, + VariantAccess, Visitor, +}; use serde::Deserialize; #[cfg(not(feature = "no_index"))] @@ -384,9 +387,30 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { self, _name: &'static str, _variants: &'static [&'static str], - _: V, + visitor: V, ) -> Result> { - self.type_error() + if let Ok(s) = self.value.as_str() { + visitor.visit_enum(s.into_deserializer()) + } else { + #[cfg(not(feature = "no_object"))] + if let Some(map) = self.value.downcast_ref::() { + let mut iter = map.iter(); + let first = iter.next(); + let second = iter.next(); + if let (Some((key, value)), None) = (first, second) { + visitor.visit_enum(EnumDeserializer { + tag: &key, + content: DynamicDeserializer::from_dynamic(value), + }) + } else { + self.type_error() + } + } else { + self.type_error() + } + #[cfg(feature = "no_object")] + return self.type_error(); + } } fn deserialize_identifier>( @@ -494,3 +518,57 @@ where )) } } + +#[cfg(not(feature = "no_object"))] +struct EnumDeserializer<'t, 'de: 't> { + tag: &'t str, + content: DynamicDeserializer<'de>, +} + +#[cfg(not(feature = "no_object"))] +impl<'t, 'de> EnumAccess<'de> for EnumDeserializer<'t, 'de> { + type Error = Box; + type Variant = Self; + + fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> + where + V: DeserializeSeed<'de>, + { + seed.deserialize(self.tag.into_deserializer()) + .map(|v| (v, self)) + } +} + +#[cfg(not(feature = "no_object"))] +impl<'t, 'de> VariantAccess<'de> for EnumDeserializer<'t, 'de> { + type Error = Box; + + fn unit_variant(mut self) -> Result<(), Self::Error> { + Deserialize::deserialize(&mut self.content) + } + + fn newtype_variant_seed(mut self, seed: T) -> Result + where + T: DeserializeSeed<'de>, + { + seed.deserialize(&mut self.content) + } + + fn tuple_variant(mut self, len: usize, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.content.deserialize_tuple(len, visitor) + } + + fn struct_variant( + mut self, + fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.content.deserialize_struct("", fields, visitor) + } +} diff --git a/tests/serde.rs b/tests/serde.rs index 95a3c24e..1d509eea 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -236,3 +236,261 @@ fn test_serde_de_script() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_serde_de_unit_enum() -> Result<(), Box> { + #[derive(Debug, PartialEq, Deserialize)] + enum MyEnum { + VariantFoo, + VariantBar, + } + + { + let d = Dynamic::from("VariantFoo".to_string()); + assert_eq!(MyEnum::VariantFoo, from_dynamic(&d)?); + } + + { + let d = Dynamic::from("VariantBar".to_string()); + assert_eq!(MyEnum::VariantBar, from_dynamic(&d)?); + } + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_serde_de_externally_tagged_enum() -> Result<(), Box> { + #[derive(Debug, PartialEq, Deserialize)] + #[serde(deny_unknown_fields)] + enum MyEnum { + VariantUnit, + #[cfg(not(feature = "no_index"))] + VariantUnitTuple(), + VariantNewtype(i32), + #[cfg(not(feature = "no_index"))] + VariantTuple(i32, i32), + VariantEmptyStruct {}, + VariantStruct { + a: i32, + }, + } + + { + let d = Dynamic::from("VariantUnit".to_string()); + assert_eq!(MyEnum::VariantUnit, from_dynamic(&d).unwrap()); + } + + #[cfg(not(feature = "no_index"))] + { + let array: Array = vec![]; + let mut map_outer = Map::new(); + map_outer.insert("VariantUnitTuple".into(), array.into()); + assert_eq!( + MyEnum::VariantUnitTuple(), + from_dynamic(&map_outer.into()).unwrap() + ); + } + + { + let mut map_outer = Map::new(); + map_outer.insert("VariantNewtype".into(), (123 as INT).into()); + assert_eq!( + MyEnum::VariantNewtype(123), + from_dynamic(&map_outer.into()).unwrap() + ); + } + + #[cfg(not(feature = "no_index"))] + { + let array: Array = vec![(123 as INT).into(), (456 as INT).into()]; + let mut map_outer = Map::new(); + map_outer.insert("VariantTuple".into(), array.into()); + assert_eq!( + MyEnum::VariantTuple(123, 456), + from_dynamic(&map_outer.into()).unwrap() + ); + } + + { + let map_inner = Map::new(); + let mut map_outer = Map::new(); + map_outer.insert("VariantEmptyStruct".into(), map_inner.into()); + assert_eq!( + MyEnum::VariantEmptyStruct {}, + from_dynamic(&map_outer.into()).unwrap() + ); + } + + { + let mut map_inner = Map::new(); + map_inner.insert("a".into(), (123 as INT).into()); + let mut map_outer = Map::new(); + map_outer.insert("VariantStruct".into(), map_inner.into()); + assert_eq!( + MyEnum::VariantStruct { a: 123 }, + from_dynamic(&map_outer.into()).unwrap() + ); + } + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_serde_de_internally_tagged_enum() -> Result<(), Box> { + #[derive(Debug, PartialEq, Deserialize)] + #[serde(tag = "tag", deny_unknown_fields)] + enum MyEnum { + VariantEmptyStruct {}, + VariantStruct { a: i32 }, + } + + { + let mut map = Map::new(); + map.insert("tag".into(), "VariantStruct".into()); + map.insert("a".into(), (123 as INT).into()); + assert_eq!( + MyEnum::VariantStruct { a: 123 }, + from_dynamic(&map.into()).unwrap() + ); + } + + { + let mut map = Map::new(); + map.insert("tag".into(), "VariantEmptyStruct".into()); + assert_eq!( + MyEnum::VariantEmptyStruct {}, + from_dynamic(&map.into()).unwrap() + ); + } + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_serde_de_adjacently_tagged_enum() -> Result<(), Box> { + #[derive(Debug, PartialEq, Deserialize)] + #[serde(tag = "tag", content = "content", deny_unknown_fields)] + enum MyEnum { + VariantUnit, + #[cfg(not(feature = "no_index"))] + VariantUnitTuple(), + VariantNewtype(i32), + #[cfg(not(feature = "no_index"))] + VariantTuple(i32, i32), + VariantEmptyStruct {}, + VariantStruct { + a: i32, + }, + } + + { + let mut map_outer = Map::new(); + map_outer.insert("tag".into(), "VariantUnit".into()); + assert_eq!( + MyEnum::VariantUnit, + from_dynamic(&map_outer.into()).unwrap() + ); + } + + #[cfg(not(feature = "no_index"))] + { + let array: Array = vec![]; + let mut map_outer = Map::new(); + map_outer.insert("tag".into(), "VariantUnitTuple".into()); + map_outer.insert("content".into(), array.into()); + assert_eq!( + MyEnum::VariantUnitTuple(), + from_dynamic(&map_outer.into()).unwrap() + ); + } + + { + let mut map_outer = Map::new(); + map_outer.insert("tag".into(), "VariantNewtype".into()); + map_outer.insert("content".into(), (123 as INT).into()); + assert_eq!( + MyEnum::VariantNewtype(123), + from_dynamic(&map_outer.into()).unwrap() + ); + } + + #[cfg(not(feature = "no_index"))] + { + let array: Array = vec![(123 as INT).into(), (456 as INT).into()]; + let mut map_outer = Map::new(); + map_outer.insert("tag".into(), "VariantTuple".into()); + map_outer.insert("content".into(), array.into()); + assert_eq!( + MyEnum::VariantTuple(123, 456), + from_dynamic(&map_outer.into()).unwrap() + ); + } + + { + let map_inner = Map::new(); + let mut map_outer = Map::new(); + map_outer.insert("tag".into(), "VariantEmptyStruct".into()); + map_outer.insert("content".into(), map_inner.into()); + assert_eq!( + MyEnum::VariantEmptyStruct {}, + from_dynamic(&map_outer.into()).unwrap() + ); + } + + { + let mut map_inner = Map::new(); + map_inner.insert("a".into(), (123 as INT).into()); + let mut map_outer = Map::new(); + map_outer.insert("tag".into(), "VariantStruct".into()); + map_outer.insert("content".into(), map_inner.into()); + assert_eq!( + MyEnum::VariantStruct { a: 123 }, + from_dynamic(&map_outer.into()).unwrap() + ); + } + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_serde_de_untagged_enum() -> Result<(), Box> { + #[derive(Debug, PartialEq, Deserialize)] + #[serde(untagged, deny_unknown_fields)] + enum MyEnum { + VariantEmptyStruct {}, + VariantStruct1 { a: i32 }, + VariantStruct2 { b: i32 }, + } + + { + let map = Map::new(); + assert_eq!( + MyEnum::VariantEmptyStruct {}, + from_dynamic(&map.into()).unwrap() + ); + } + + { + let mut map = Map::new(); + map.insert("a".into(), (123 as INT).into()); + assert_eq!( + MyEnum::VariantStruct1 { a: 123 }, + from_dynamic(&map.into()).unwrap() + ); + } + + { + let mut map = Map::new(); + map.insert("b".into(), (123 as INT).into()); + assert_eq!( + MyEnum::VariantStruct2 { b: 123 }, + from_dynamic(&map.into()).unwrap() + ); + } + + Ok(()) +} From 4b6afc9c727295ebcd06b12f29a56c61eb3cc9f6 Mon Sep 17 00:00:00 2001 From: Alvin Wong Date: Mon, 6 Jul 2020 16:11:54 +0800 Subject: [PATCH 30/46] Add `enum` serialization tests --- tests/serde.rs | 230 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git a/tests/serde.rs b/tests/serde.rs index 1d509eea..cf86dcf2 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -117,6 +117,236 @@ fn test_serde_ser_struct() -> Result<(), Box> { Ok(()) } +#[test] +fn test_serde_ser_unit_enum() -> Result<(), Box> { + #[derive(Serialize)] + enum MyEnum { + VariantFoo, + VariantBar, + } + + let d = to_dynamic(MyEnum::VariantFoo)?; + assert_eq!("VariantFoo", d.as_str().unwrap()); + + let d = to_dynamic(MyEnum::VariantBar)?; + assert_eq!("VariantBar", d.as_str().unwrap()); + + Ok(()) +} + +#[test] +#[ignore = "failing test"] +#[cfg(not(feature = "no_object"))] +fn test_serde_ser_externally_tagged_enum() -> Result<(), Box> { + #[derive(Serialize)] + enum MyEnum { + VariantUnit, + #[cfg(not(feature = "no_index"))] + VariantUnitTuple(), + VariantNewtype(i32), + #[cfg(not(feature = "no_index"))] + VariantTuple(i32, i32), + VariantEmptyStruct {}, + VariantStruct { + a: i32, + }, + } + + { + let d = to_dynamic(MyEnum::VariantUnit)?; + assert_eq!("VariantUnit", d.as_str().unwrap()); + } + + #[cfg(not(feature = "no_index"))] + { + let d = to_dynamic(MyEnum::VariantUnitTuple())?; + let mut map = d.cast::(); + let content = map.remove("VariantUnitTuple").unwrap().cast::(); + assert!(map.is_empty()); + assert!(content.is_empty()); + } + + { + let d = to_dynamic(MyEnum::VariantNewtype(123))?; + let mut map = d.cast::(); + let content = map.remove("VariantNewtype").unwrap(); + assert!(map.is_empty()); + assert_eq!(Ok(123), content.as_int()); + } + + #[cfg(not(feature = "no_index"))] + { + let d = to_dynamic(MyEnum::VariantTuple(123, 456))?; + let mut map = d.cast::(); + let content = map.remove("VariantTuple").unwrap().cast::(); + assert!(map.is_empty()); + assert_eq!(2, content.len()); + assert_eq!(Ok(123), content[0].as_int()); + assert_eq!(Ok(456), content[1].as_int()); + } + + { + let d = to_dynamic(MyEnum::VariantEmptyStruct {})?; + let mut map = d.cast::(); + let map_inner = map.remove("VariantEmptyStruct").unwrap().cast::(); + assert!(map.is_empty()); + assert!(map_inner.is_empty()); + } + + { + let d = to_dynamic(MyEnum::VariantStruct { a: 123 })?; + let mut map = d.cast::(); + let mut map_inner = map.remove("VariantStruct").unwrap().cast::(); + assert!(map.is_empty()); + assert_eq!(Ok(123), map_inner.remove("a").unwrap().as_int()); + assert!(map_inner.is_empty()); + } + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_serde_ser_internally_tagged_enum() -> Result<(), Box> { + #[derive(Serialize)] + #[serde(tag = "tag")] + enum MyEnum { + VariantEmptyStruct {}, + VariantStruct { a: i32 }, + } + + let d = to_dynamic(MyEnum::VariantEmptyStruct {})?; + let mut map = d.cast::(); + assert_eq!( + Ok("VariantEmptyStruct"), + map.remove("tag").unwrap().as_str() + ); + assert!(map.is_empty()); + + let d = to_dynamic(MyEnum::VariantStruct { a: 123 })?; + let mut map = d.cast::(); + assert_eq!(Ok("VariantStruct"), map.remove("tag").unwrap().as_str()); + assert_eq!(Ok(123), map.remove("a").unwrap().as_int()); + assert!(map.is_empty()); + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_serde_ser_adjacently_tagged_enum() -> Result<(), Box> { + #[derive(Serialize)] + #[serde(tag = "tag", content = "content")] + enum MyEnum { + VariantUnit, + #[cfg(not(feature = "no_index"))] + VariantUnitTuple(), + VariantNewtype(i32), + #[cfg(not(feature = "no_index"))] + VariantTuple(i32, i32), + VariantEmptyStruct {}, + VariantStruct { + a: i32, + }, + } + + { + let d = to_dynamic(MyEnum::VariantUnit)?; + let mut map = d.cast::(); + assert_eq!(Ok("VariantUnit"), map.remove("tag").unwrap().as_str()); + assert!(map.is_empty()); + } + + #[cfg(not(feature = "no_index"))] + { + let d = to_dynamic(MyEnum::VariantUnitTuple())?; + let mut map = d.cast::(); + assert_eq!(Ok("VariantUnitTuple"), map.remove("tag").unwrap().as_str()); + let content = map.remove("content").unwrap().cast::(); + assert!(map.is_empty()); + assert!(content.is_empty()); + } + + { + let d = to_dynamic(MyEnum::VariantNewtype(123))?; + let mut map = d.cast::(); + assert_eq!(Ok("VariantNewtype"), map.remove("tag").unwrap().as_str()); + let content = map.remove("content").unwrap(); + assert!(map.is_empty()); + assert_eq!(Ok(123), content.as_int()); + } + + #[cfg(not(feature = "no_index"))] + { + let d = to_dynamic(MyEnum::VariantTuple(123, 456))?; + let mut map = d.cast::(); + assert_eq!(Ok("VariantTuple"), map.remove("tag").unwrap().as_str()); + let content = map.remove("content").unwrap().cast::(); + assert!(map.is_empty()); + assert_eq!(2, content.len()); + assert_eq!(Ok(123), content[0].as_int()); + assert_eq!(Ok(456), content[1].as_int()); + } + + { + let d = to_dynamic(MyEnum::VariantEmptyStruct {})?; + let mut map = d.cast::(); + assert_eq!( + Ok("VariantEmptyStruct"), + map.remove("tag").unwrap().as_str() + ); + let map_inner = map.remove("content").unwrap().cast::(); + assert!(map.is_empty()); + assert!(map_inner.is_empty()); + } + + { + let d = to_dynamic(MyEnum::VariantStruct { a: 123 })?; + let mut map = d.cast::(); + assert_eq!(Ok("VariantStruct"), map.remove("tag").unwrap().as_str()); + let mut map_inner = map.remove("content").unwrap().cast::(); + assert!(map.is_empty()); + assert_eq!(Ok(123), map_inner.remove("a").unwrap().as_int()); + assert!(map_inner.is_empty()); + } + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_serde_ser_untagged_enum() -> Result<(), Box> { + #[derive(Serialize)] + #[serde(untagged)] + enum MyEnum { + VariantEmptyStruct {}, + VariantStruct1 { a: i32 }, + VariantStruct2 { b: i32 }, + } + + { + let d = to_dynamic(MyEnum::VariantEmptyStruct {})?; + let map = d.cast::(); + assert!(map.is_empty()); + } + + { + let d = to_dynamic(MyEnum::VariantStruct1 { a: 123 })?; + let mut map = d.cast::(); + assert_eq!(Ok(123), map.remove("a").unwrap().as_int()); + assert!(map.is_empty()); + } + + { + let d = to_dynamic(MyEnum::VariantStruct2 { b: 123 })?; + let mut map = d.cast::(); + assert_eq!(Ok(123), map.remove("b").unwrap().as_int()); + assert!(map.is_empty()); + } + + Ok(()) +} + #[test] fn test_serde_de_primary_types() -> Result<(), Box> { assert_eq!(42_u16, from_dynamic(&Dynamic::from(42_u16))?); From 4a3a32dc3aee55009b39954037ea453c8a256d0d Mon Sep 17 00:00:00 2001 From: Alvin Wong Date: Mon, 6 Jul 2020 18:31:23 +0800 Subject: [PATCH 31/46] Fix serializing externally-tagged `enum` representations --- src/serde/ser.rs | 143 +++++++++++++++++++++++++++++++---------------- tests/serde.rs | 1 - 2 files changed, 94 insertions(+), 50 deletions(-) diff --git a/src/serde/ser.rs b/src/serde/ser.rs index 79b1b733..181573b0 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -103,10 +103,16 @@ impl Serializer for &mut DynamicSerializer { type SerializeSeq = DynamicSerializer; type SerializeTuple = DynamicSerializer; type SerializeTupleStruct = DynamicSerializer; - type SerializeTupleVariant = DynamicSerializer; + #[cfg(not(any(feature = "no_object", feature = "no_index")))] + type SerializeTupleVariant = TupleVariantSerializer; + #[cfg(any(feature = "no_object", feature = "no_index"))] + type SerializeTupleVariant = serde::ser::Impossible>; type SerializeMap = DynamicSerializer; type SerializeStruct = DynamicSerializer; - type SerializeStructVariant = DynamicSerializer; + #[cfg(not(feature = "no_object"))] + type SerializeStructVariant = StructVariantSerializer; + #[cfg(feature = "no_object")] + type SerializeStructVariant = serde::ser::Impossible>; fn serialize_bool(self, v: bool) -> Result> { Ok(v.into()) @@ -244,10 +250,20 @@ impl Serializer for &mut DynamicSerializer { self, _name: &'static str, _variant_index: u32, - _variant: &'static str, + variant: &'static str, value: &T, ) -> Result> { - value.serialize(&mut *self) + #[cfg(not(feature = "no_object"))] + { + let content = to_dynamic(value)?; + make_variant(variant, content) + } + #[cfg(feature = "no_object")] + return Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + "Dynamic".into(), + "map".into(), + Position::none(), + ))); } fn serialize_seq(self, _len: Option) -> Result> { @@ -277,10 +293,26 @@ impl Serializer for &mut DynamicSerializer { self, _name: &'static str, _variant_index: u32, - _variant: &'static str, + variant: &'static str, len: usize, ) -> Result> { - self.serialize_seq(Some(len)) + #[cfg(not(any(feature = "no_object", feature = "no_index")))] + return Ok(TupleVariantSerializer { + variant, + array: Array::with_capacity(len), + }); + #[cfg(any(feature = "no_object", feature = "no_index"))] + { + #[cfg(feature = "no_object")] + let err_type = "map"; + #[cfg(not(feature = "no_object"))] + let err_type = "array"; + Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + "Dynamic".into(), + err_type.into(), + Position::none(), + ))) + } } fn serialize_map(self, _len: Option) -> Result> { @@ -306,10 +338,20 @@ impl Serializer for &mut DynamicSerializer { self, _name: &'static str, _variant_index: u32, - _variant: &'static str, + variant: &'static str, len: usize, ) -> Result> { - self.serialize_map(Some(len)) + #[cfg(not(feature = "no_object"))] + return Ok(StructVariantSerializer { + variant, + map: Map::with_capacity(len), + }); + #[cfg(feature = "no_object")] + return Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + "Dynamic".into(), + "map".into(), + Position::none(), + ))); } } @@ -395,33 +437,6 @@ impl SerializeTupleStruct for DynamicSerializer { } } -impl SerializeTupleVariant for DynamicSerializer { - type Ok = Dynamic; - type Error = Box; - - fn serialize_field( - &mut self, - value: &T, - ) -> Result<(), Box> { - #[cfg(not(feature = "no_index"))] - { - let value = value.serialize(&mut *self)?; - let arr = self.value.downcast_mut::().unwrap(); - arr.push(value); - Ok(()) - } - #[cfg(feature = "no_index")] - unreachable!() - } - - fn end(self) -> Result> { - #[cfg(not(feature = "no_index"))] - return Ok(self.value); - #[cfg(feature = "no_index")] - unreachable!() - } -} - impl SerializeMap for DynamicSerializer { type Ok = Dynamic; type Error = Box; @@ -520,7 +535,39 @@ impl SerializeStruct for DynamicSerializer { } } -impl SerializeStructVariant for DynamicSerializer { +#[cfg(not(any(feature = "no_object", feature = "no_index")))] +pub struct TupleVariantSerializer { + variant: &'static str, + array: Array, +} + +#[cfg(not(any(feature = "no_object", feature = "no_index")))] +impl SerializeTupleVariant for TupleVariantSerializer { + type Ok = Dynamic; + type Error = Box; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Box> { + let value = to_dynamic(value)?; + self.array.push(value); + Ok(()) + } + + fn end(self) -> Result> { + make_variant(self.variant, self.array.into()) + } +} + +#[cfg(not(feature = "no_object"))] +pub struct StructVariantSerializer { + variant: &'static str, + map: Map, +} + +#[cfg(not(feature = "no_object"))] +impl SerializeStructVariant for StructVariantSerializer { type Ok = Dynamic; type Error = Box; @@ -529,21 +576,19 @@ impl SerializeStructVariant for DynamicSerializer { key: &'static str, value: &T, ) -> Result<(), Box> { - #[cfg(not(feature = "no_object"))] - { - let value = value.serialize(&mut *self)?; - let map = self.value.downcast_mut::().unwrap(); - map.insert(key.into(), value); - Ok(()) - } - #[cfg(feature = "no_object")] - unreachable!() + let value = to_dynamic(value)?; + self.map.insert(key.into(), value); + Ok(()) } fn end(self) -> Result> { - #[cfg(not(feature = "no_object"))] - return Ok(self.value); - #[cfg(feature = "no_object")] - unreachable!() + make_variant(self.variant, self.map.into()) } } + +#[cfg(not(feature = "no_object"))] +fn make_variant(variant: &'static str, value: Dynamic) -> Result> { + let mut map = Map::with_capacity(1); + map.insert(variant.into(), value); + Ok(map.into()) +} diff --git a/tests/serde.rs b/tests/serde.rs index cf86dcf2..7b0a4c5f 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -135,7 +135,6 @@ fn test_serde_ser_unit_enum() -> Result<(), Box> { } #[test] -#[ignore = "failing test"] #[cfg(not(feature = "no_object"))] fn test_serde_ser_externally_tagged_enum() -> Result<(), Box> { #[derive(Serialize)] From d0711394f000d5254d9a0ec215f73c95961884ff Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Jul 2020 21:30:35 +0800 Subject: [PATCH 32/46] Expose FnPtr and move name checking into --- .gitignore | 4 +++- src/engine.rs | 18 ++++++------------ src/fn_native.rs | 40 ++++++++++++++++++++++++++++++++++++---- src/lib.rs | 2 +- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 140811c2..e884ce0f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ target/ Cargo.lock .vscode/ .cargo/ -doc/book/ \ No newline at end of file +doc/book/ +before +after diff --git a/src/engine.rs b/src/engine.rs index e8a47960..f999235b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -11,7 +11,7 @@ use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, St use crate::r#unsafe::unsafe_cast_var_name_to_lifetime; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::token::{is_valid_identifier, Position}; +use crate::token::Position; use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] @@ -22,6 +22,7 @@ use crate::stdlib::{ borrow::Cow, boxed::Box, collections::{HashMap, HashSet}, + convert::TryFrom, format, iter::{empty, once}, mem, @@ -1735,6 +1736,7 @@ impl Engine { let expr = args_expr.get(0); let arg_value = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; + return arg_value .take_immutable_string() .map_err(|typ| { @@ -1744,17 +1746,9 @@ impl Engine { expr.position(), )) }) - .and_then(|s| { - if is_valid_identifier(s.chars()) { - Ok(s) - } else { - Err(Box::new(EvalAltResult::ErrorFunctionNotFound( - s.to_string(), - expr.position(), - ))) - } - }) - .map(|s| FnPtr::from(s).into()); + .and_then(|s| FnPtr::try_from(s)) + .map(Into::::into) + .map_err(|err| err.new_position(*pos)); } } diff --git a/src/fn_native.rs b/src/fn_native.rs index 49cb4570..be4d6593 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -3,9 +3,10 @@ use crate::engine::Engine; use crate::module::Module; use crate::parser::ScriptFnDef; use crate::result::EvalAltResult; +use crate::token::{is_valid_identifier, Position}; use crate::utils::ImmutableString; -use crate::stdlib::{boxed::Box, fmt, rc::Rc, sync::Arc}; +use crate::stdlib::{boxed::Box, convert::TryFrom, fmt, rc::Rc, sync::Arc}; /// Trait that maps to `Send + Sync` only under the `sync` feature. #[cfg(feature = "sync")] @@ -52,6 +53,10 @@ pub type FnCallArgs<'a> = [&'a mut Dynamic]; pub struct FnPtr(ImmutableString); impl FnPtr { + /// Create a new function pointer. + pub(crate) fn new>(name: S) -> Self { + Self(name.into()) + } /// Get the name of the function. pub fn fn_name(&self) -> &str { self.get_fn_name().as_ref() @@ -72,9 +77,36 @@ impl fmt::Display for FnPtr { } } -impl> From for FnPtr { - fn from(value: S) -> Self { - Self(value.into()) +impl TryFrom for FnPtr { + type Error = Box; + + fn try_from(value: ImmutableString) -> Result { + if is_valid_identifier(value.chars()) { + Ok(Self(value)) + } else { + Err(Box::new(EvalAltResult::ErrorFunctionNotFound( + value.to_string(), + Position::none(), + ))) + } + } +} + +impl TryFrom for FnPtr { + type Error = Box; + + fn try_from(value: String) -> Result { + let s: ImmutableString = value.into(); + Self::try_from(s) + } +} + +impl TryFrom<&str> for FnPtr { + type Error = Box; + + fn try_from(value: &str) -> Result { + let s: ImmutableString = value.into(); + Self::try_from(s) } } diff --git a/src/lib.rs b/src/lib.rs index 13221e2c..768ad25a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,7 +98,7 @@ mod utils; pub use any::Dynamic; pub use engine::Engine; pub use error::{ParseError, ParseErrorType}; -pub use fn_native::IteratorFn; +pub use fn_native::{FnPtr, IteratorFn}; pub use fn_register::{RegisterFn, RegisterResultFn}; pub use module::Module; pub use parser::{ImmutableString, AST, INT}; From a2c0554f3c5556a2ca4f8f004c4fb46929b2a7d3 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Jul 2020 21:52:07 +0800 Subject: [PATCH 33/46] Make consistent style. --- tests/serde.rs | 125 +++++++++++++++++-------------------------------- 1 file changed, 42 insertions(+), 83 deletions(-) diff --git a/tests/serde.rs b/tests/serde.rs index 7b0a4c5f..005e4e8c 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -10,57 +10,33 @@ use rhai::Map; #[test] fn test_serde_ser_primary_types() -> Result<(), Box> { - assert_eq!( - to_dynamic(42_u64)?.type_name(), - std::any::type_name::() - ); - assert_eq!(to_dynamic(u64::MAX)?.type_name(), "u64"); - assert_eq!( - to_dynamic(42 as INT)?.type_name(), - std::any::type_name::() - ); - assert_eq!(to_dynamic(true)?.type_name(), "bool"); - assert_eq!(to_dynamic(())?.type_name(), "()"); + assert!(to_dynamic(42_u64)?.is::()); + assert!(to_dynamic(u64::MAX)?.is::()); + assert!(to_dynamic(42 as INT)?.is::()); + assert!(to_dynamic(true)?.is::()); + assert!(to_dynamic(())?.is::<()>()); #[cfg(not(feature = "no_float"))] { - assert_eq!(to_dynamic(123.456_f64)?.type_name(), "f64"); - assert_eq!(to_dynamic(123.456_f32)?.type_name(), "f32"); + assert!(to_dynamic(123.456_f64)?.is::()); + assert!(to_dynamic(123.456_f32)?.is::()); } - assert_eq!(to_dynamic("hello".to_string())?.type_name(), "string"); + assert!(to_dynamic("hello".to_string())?.is::()); Ok(()) } #[test] fn test_serde_ser_integer_types() -> Result<(), Box> { - assert_eq!(to_dynamic(42_i8)?.type_name(), std::any::type_name::()); - assert_eq!( - to_dynamic(42_i16)?.type_name(), - std::any::type_name::() - ); - assert_eq!( - to_dynamic(42_i32)?.type_name(), - std::any::type_name::() - ); - assert_eq!( - to_dynamic(42_i64)?.type_name(), - std::any::type_name::() - ); - assert_eq!(to_dynamic(42_u8)?.type_name(), std::any::type_name::()); - assert_eq!( - to_dynamic(42_u16)?.type_name(), - std::any::type_name::() - ); - assert_eq!( - to_dynamic(42_u32)?.type_name(), - std::any::type_name::() - ); - assert_eq!( - to_dynamic(42_u64)?.type_name(), - std::any::type_name::() - ); + assert!(to_dynamic(42_i8)?.is::()); + assert!(to_dynamic(42_i16)?.is::()); + assert!(to_dynamic(42_i32)?.is::()); + assert!(to_dynamic(42_i64)?.is::()); + assert!(to_dynamic(42_u8)?.is::()); + assert!(to_dynamic(42_u16)?.is::()); + assert!(to_dynamic(42_u32)?.is::()); + assert!(to_dynamic(42_u64)?.is::()); Ok(()) } @@ -72,7 +48,7 @@ fn test_serde_ser_array() -> Result<(), Box> { let d = to_dynamic(arr)?; assert!(d.is::()); - assert_eq!(d.cast::().len(), 4); + assert_eq!(4, d.cast::().len()); Ok(()) } @@ -105,14 +81,14 @@ fn test_serde_ser_struct() -> Result<(), Box> { assert!(d.is::()); let mut map = d.cast::(); - let mut obj = map.remove("obj").unwrap().cast::(); - let mut seq = map.remove("seq").unwrap().cast::(); + let obj = map.remove("obj").unwrap().cast::(); + let seq = map.remove("seq").unwrap().cast::(); - assert_eq!(obj.remove("a").unwrap().cast::(), 123); - assert!(obj.remove("b").unwrap().cast::()); - assert_eq!(map.remove("int").unwrap().cast::(), 42); + assert_eq!(Ok(123), obj["a"].as_int()); + assert!(obj["b"].as_bool().unwrap()); + assert_eq!(Ok(42), map["int"].as_int()); assert_eq!(seq.len(), 3); - assert_eq!(seq.remove(1).cast::(), "kitty"); + assert_eq!(Ok("kitty"), seq[1].as_str()); Ok(()) } @@ -126,10 +102,10 @@ fn test_serde_ser_unit_enum() -> Result<(), Box> { } let d = to_dynamic(MyEnum::VariantFoo)?; - assert_eq!("VariantFoo", d.as_str().unwrap()); + assert_eq!(Ok("VariantFoo"), d.as_str()); let d = to_dynamic(MyEnum::VariantBar)?; - assert_eq!("VariantBar", d.as_str().unwrap()); + assert_eq!(Ok("VariantBar"), d.as_str()); Ok(()) } @@ -152,22 +128,19 @@ fn test_serde_ser_externally_tagged_enum() -> Result<(), Box> { } { - let d = to_dynamic(MyEnum::VariantUnit)?; - assert_eq!("VariantUnit", d.as_str().unwrap()); + assert_eq!(Ok("VariantUnit"), to_dynamic(MyEnum::VariantUnit)?.as_str()); } #[cfg(not(feature = "no_index"))] { - let d = to_dynamic(MyEnum::VariantUnitTuple())?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantUnitTuple())?.cast::(); let content = map.remove("VariantUnitTuple").unwrap().cast::(); assert!(map.is_empty()); assert!(content.is_empty()); } { - let d = to_dynamic(MyEnum::VariantNewtype(123))?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantNewtype(123))?.cast::(); let content = map.remove("VariantNewtype").unwrap(); assert!(map.is_empty()); assert_eq!(Ok(123), content.as_int()); @@ -175,8 +148,7 @@ fn test_serde_ser_externally_tagged_enum() -> Result<(), Box> { #[cfg(not(feature = "no_index"))] { - let d = to_dynamic(MyEnum::VariantTuple(123, 456))?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantTuple(123, 456))?.cast::(); let content = map.remove("VariantTuple").unwrap().cast::(); assert!(map.is_empty()); assert_eq!(2, content.len()); @@ -185,16 +157,14 @@ fn test_serde_ser_externally_tagged_enum() -> Result<(), Box> { } { - let d = to_dynamic(MyEnum::VariantEmptyStruct {})?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantEmptyStruct {})?.cast::(); let map_inner = map.remove("VariantEmptyStruct").unwrap().cast::(); assert!(map.is_empty()); assert!(map_inner.is_empty()); } { - let d = to_dynamic(MyEnum::VariantStruct { a: 123 })?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantStruct { a: 123 })?.cast::(); let mut map_inner = map.remove("VariantStruct").unwrap().cast::(); assert!(map.is_empty()); assert_eq!(Ok(123), map_inner.remove("a").unwrap().as_int()); @@ -214,16 +184,14 @@ fn test_serde_ser_internally_tagged_enum() -> Result<(), Box> { VariantStruct { a: i32 }, } - let d = to_dynamic(MyEnum::VariantEmptyStruct {})?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantEmptyStruct {})?.cast::(); assert_eq!( Ok("VariantEmptyStruct"), map.remove("tag").unwrap().as_str() ); assert!(map.is_empty()); - let d = to_dynamic(MyEnum::VariantStruct { a: 123 })?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantStruct { a: 123 })?.cast::(); assert_eq!(Ok("VariantStruct"), map.remove("tag").unwrap().as_str()); assert_eq!(Ok(123), map.remove("a").unwrap().as_int()); assert!(map.is_empty()); @@ -250,16 +218,14 @@ fn test_serde_ser_adjacently_tagged_enum() -> Result<(), Box> { } { - let d = to_dynamic(MyEnum::VariantUnit)?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantUnit)?.cast::(); assert_eq!(Ok("VariantUnit"), map.remove("tag").unwrap().as_str()); assert!(map.is_empty()); } #[cfg(not(feature = "no_index"))] { - let d = to_dynamic(MyEnum::VariantUnitTuple())?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantUnitTuple())?.cast::(); assert_eq!(Ok("VariantUnitTuple"), map.remove("tag").unwrap().as_str()); let content = map.remove("content").unwrap().cast::(); assert!(map.is_empty()); @@ -267,8 +233,7 @@ fn test_serde_ser_adjacently_tagged_enum() -> Result<(), Box> { } { - let d = to_dynamic(MyEnum::VariantNewtype(123))?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantNewtype(123))?.cast::(); assert_eq!(Ok("VariantNewtype"), map.remove("tag").unwrap().as_str()); let content = map.remove("content").unwrap(); assert!(map.is_empty()); @@ -277,8 +242,7 @@ fn test_serde_ser_adjacently_tagged_enum() -> Result<(), Box> { #[cfg(not(feature = "no_index"))] { - let d = to_dynamic(MyEnum::VariantTuple(123, 456))?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantTuple(123, 456))?.cast::(); assert_eq!(Ok("VariantTuple"), map.remove("tag").unwrap().as_str()); let content = map.remove("content").unwrap().cast::(); assert!(map.is_empty()); @@ -288,8 +252,7 @@ fn test_serde_ser_adjacently_tagged_enum() -> Result<(), Box> { } { - let d = to_dynamic(MyEnum::VariantEmptyStruct {})?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantEmptyStruct {})?.cast::(); assert_eq!( Ok("VariantEmptyStruct"), map.remove("tag").unwrap().as_str() @@ -300,8 +263,7 @@ fn test_serde_ser_adjacently_tagged_enum() -> Result<(), Box> { } { - let d = to_dynamic(MyEnum::VariantStruct { a: 123 })?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantStruct { a: 123 })?.cast::(); assert_eq!(Ok("VariantStruct"), map.remove("tag").unwrap().as_str()); let mut map_inner = map.remove("content").unwrap().cast::(); assert!(map.is_empty()); @@ -324,21 +286,18 @@ fn test_serde_ser_untagged_enum() -> Result<(), Box> { } { - let d = to_dynamic(MyEnum::VariantEmptyStruct {})?; - let map = d.cast::(); + let map = to_dynamic(MyEnum::VariantEmptyStruct {})?.cast::(); assert!(map.is_empty()); } { - let d = to_dynamic(MyEnum::VariantStruct1 { a: 123 })?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantStruct1 { a: 123 })?.cast::(); assert_eq!(Ok(123), map.remove("a").unwrap().as_int()); assert!(map.is_empty()); } { - let d = to_dynamic(MyEnum::VariantStruct2 { b: 123 })?; - let mut map = d.cast::(); + let mut map = to_dynamic(MyEnum::VariantStruct2 { b: 123 })?.cast::(); assert_eq!(Ok(123), map.remove("b").unwrap().as_int()); assert!(map.is_empty()); } From 2b3f10f2c5f839cb869b3188443ff31f7690b412 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 6 Jul 2020 22:57:50 +0800 Subject: [PATCH 34/46] Define functions anywhere during parsing. --- src/fn_native.rs | 2 +- src/parser.rs | 263 ++++++++++++++++++++++++++++------------------- 2 files changed, 156 insertions(+), 109 deletions(-) diff --git a/src/fn_native.rs b/src/fn_native.rs index be4d6593..70594208 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -54,7 +54,7 @@ pub struct FnPtr(ImmutableString); impl FnPtr { /// Create a new function pointer. - pub(crate) fn new>(name: S) -> Self { + pub(crate) fn new_unchecked>(name: S) -> Self { Self(name.into()) } /// Get the name of the function. diff --git a/src/parser.rs b/src/parser.rs index c774f0d0..b9320132 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -47,6 +47,8 @@ type PERR = ParseErrorType; pub use crate::utils::ImmutableString; +type FunctionsLib = HashMap; + /// Compiled AST (abstract syntax tree) of a Rhai script. /// /// Currently, `AST` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. @@ -381,14 +383,17 @@ struct ParseState<'e> { modules: Vec, /// Maximum levels of expression nesting. max_expr_depth: usize, + /// Maximum levels of expression nesting in functions. + max_function_expr_depth: usize, } impl<'e> ParseState<'e> { /// Create a new `ParseState`. - pub fn new(engine: &'e Engine, max_expr_depth: usize) -> Self { + pub fn new(engine: &'e Engine, max_expr_depth: usize, max_function_expr_depth: usize) -> Self { Self { engine, max_expr_depth, + max_function_expr_depth, stack: Default::default(), modules: Default::default(), } @@ -428,6 +433,8 @@ struct ParseSettings { is_global: bool, /// Is the current position inside a loop? is_breakable: bool, + /// Is anonymous function allowed? + allow_anonymous_fn: bool, /// Is if-expression allowed? allow_if_expr: bool, /// Is statement-expression allowed? @@ -901,6 +908,7 @@ fn match_token(input: &mut TokenStream, token: Token) -> Result Result { settings.ensure_level_within_max_limit(state.max_expr_depth)?; @@ -909,7 +917,7 @@ fn parse_paren_expr( return Ok(Expr::Unit(settings.pos)); } - let expr = parse_expr(input, state, settings.level_up())?; + let expr = parse_expr(input, state, lib, settings.level_up())?; match input.next().unwrap() { // ( xxx ) @@ -929,6 +937,7 @@ fn parse_paren_expr( fn parse_call_expr( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, id: String, mut modules: Option>, settings: ParseSettings, @@ -987,7 +996,7 @@ fn parse_call_expr( match input.peek().unwrap() { // id(...args, ) - handle trailing comma (Token::RightParen, _) => (), - _ => args.push(parse_expr(input, state, settings)?), + _ => args.push(parse_expr(input, state, lib, settings)?), } match input.peek().unwrap() { @@ -1050,12 +1059,13 @@ fn parse_call_expr( fn parse_index_chain( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, lhs: Expr, mut settings: ParseSettings, ) -> Result { settings.ensure_level_within_max_limit(state.max_expr_depth)?; - let idx_expr = parse_expr(input, state, settings.level_up())?; + let idx_expr = parse_expr(input, state, lib, settings.level_up())?; // Check type of indexing - must be integer or string match &idx_expr { @@ -1197,7 +1207,8 @@ fn parse_index_chain( let prev_pos = settings.pos; settings.pos = eat_token(input, Token::LeftBracket); // Recursively parse the indexing chain, right-binding each - let idx_expr = parse_index_chain(input, state, idx_expr, settings.level_up())?; + let idx_expr = + parse_index_chain(input, state, lib, idx_expr, settings.level_up())?; // Indexing binds to right Ok(Expr::Index(Box::new((lhs, idx_expr, prev_pos)))) } @@ -1228,6 +1239,7 @@ fn parse_index_chain( fn parse_array_literal( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, settings: ParseSettings, ) -> Result { settings.ensure_level_within_max_limit(state.max_expr_depth)?; @@ -1249,7 +1261,7 @@ fn parse_array_literal( break; } _ => { - let expr = parse_expr(input, state, settings.level_up())?; + let expr = parse_expr(input, state, lib, settings.level_up())?; arr.push(expr); } } @@ -1284,6 +1296,7 @@ fn parse_array_literal( fn parse_map_literal( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, settings: ParseSettings, ) -> Result { settings.ensure_level_within_max_limit(state.max_expr_depth)?; @@ -1343,7 +1356,7 @@ fn parse_map_literal( .into_err(input.peek().unwrap().1)); } - let expr = parse_expr(input, state, settings.level_up())?; + let expr = parse_expr(input, state, lib, settings.level_up())?; map.push(((Into::::into(name), pos), expr)); } } @@ -1388,6 +1401,7 @@ fn parse_map_literal( fn parse_primary( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { let (token, token_pos) = input.peek().unwrap(); @@ -1397,7 +1411,7 @@ fn parse_primary( let (token, _) = match token { // { - block statement as expression Token::LeftBrace if settings.allow_stmt_expr => { - return parse_block(input, state, settings.level_up()) + return parse_block(input, state, lib, settings.level_up()) .map(|block| Expr::Stmt(Box::new((block, settings.pos)))) } Token::EOF => return Err(PERR::UnexpectedEOF.into_err(settings.pos)), @@ -1414,11 +1428,11 @@ fn parse_primary( let index = state.find_var(&s); Expr::Variable(Box::new(((s, settings.pos), None, 0, index))) } - Token::LeftParen => parse_paren_expr(input, state, settings.level_up())?, + Token::LeftParen => parse_paren_expr(input, state, lib, settings.level_up())?, #[cfg(not(feature = "no_index"))] - Token::LeftBracket => parse_array_literal(input, state, settings.level_up())?, + Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?, #[cfg(not(feature = "no_object"))] - Token::MapStart => parse_map_literal(input, state, settings.level_up())?, + Token::MapStart => parse_map_literal(input, state, lib, settings.level_up())?, Token::True => Expr::True(settings.pos), Token::False => Expr::False(settings.pos), Token::LexError(err) => return Err(err.into_err(settings.pos)), @@ -1445,7 +1459,7 @@ fn parse_primary( (Expr::Variable(x), Token::LeftParen) => { let ((name, pos), modules, _, _) = *x; settings.pos = pos; - parse_call_expr(input, state, name, modules, settings.level_up())? + parse_call_expr(input, state, lib, name, modules, settings.level_up())? } (Expr::Property(_), _) => unreachable!(), // module access @@ -1468,7 +1482,7 @@ fn parse_primary( // Indexing #[cfg(not(feature = "no_index"))] (expr, Token::LeftBracket) => { - parse_index_chain(input, state, expr, settings.level_up())? + parse_index_chain(input, state, lib, expr, settings.level_up())? } // Unknown postfix operator (expr, token) => unreachable!( @@ -1499,6 +1513,7 @@ fn parse_primary( fn parse_unary( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { let (token, token_pos) = input.peek().unwrap(); @@ -1508,14 +1523,14 @@ fn parse_unary( match token { // If statement is allowed to act as expressions Token::If if settings.allow_if_expr => Ok(Expr::Stmt(Box::new(( - parse_if(input, state, settings.level_up())?, + parse_if(input, state, lib, settings.level_up())?, settings.pos, )))), // -expr Token::UnaryMinus => { let pos = eat_token(input, Token::UnaryMinus); - match parse_unary(input, state, settings.level_up())? { + match parse_unary(input, state, lib, settings.level_up())? { // Negative integer Expr::IntegerConstant(x) => { let (num, pos) = *x; @@ -1555,13 +1570,13 @@ fn parse_unary( // +expr Token::UnaryPlus => { eat_token(input, Token::UnaryPlus); - parse_unary(input, state, settings.level_up()) + parse_unary(input, state, lib, settings.level_up()) } // !expr Token::Bang => { let pos = eat_token(input, Token::Bang); let mut args = StaticVec::new(); - let expr = parse_primary(input, state, settings.level_up())?; + let expr = parse_primary(input, state, lib, settings.level_up())?; args.push(expr); let op = "!"; @@ -1578,7 +1593,7 @@ fn parse_unary( // Token::EOF => Err(PERR::UnexpectedEOF.into_err(settings.pos)), // All other tokens - _ => parse_primary(input, state, settings.level_up()), + _ => parse_primary(input, state, lib, settings.level_up()), } } @@ -1646,6 +1661,7 @@ fn make_assignment_stmt<'a>( fn parse_op_assignment_stmt( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, lhs: Expr, mut settings: ParseSettings, ) -> Result { @@ -1672,7 +1688,7 @@ fn parse_op_assignment_stmt( }; let (_, pos) = input.next().unwrap(); - let rhs = parse_expr(input, state, settings.level_up())?; + let rhs = parse_expr(input, state, lib, settings.level_up())?; make_assignment_stmt(op, state, lhs, rhs, pos) } @@ -1884,6 +1900,7 @@ fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result Result { settings.pos = input.peek().unwrap().1; settings.ensure_level_within_max_limit(state.max_expr_depth)?; - let lhs = parse_unary(input, state, settings.level_up())?; - parse_binary_op(input, state, 1, lhs, settings.level_up()) + let lhs = parse_unary(input, state, lib, settings.level_up())?; + parse_binary_op(input, state, lib, 1, lhs, settings.level_up()) } /// Make sure that the expression is not a statement expression (i.e. wrapped in `{}`). @@ -2053,6 +2071,7 @@ fn ensure_not_assignment(input: &mut TokenStream) -> Result<(), ParseError> { fn parse_if( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { // if ... @@ -2061,18 +2080,18 @@ fn parse_if( // if guard { if_body } ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, settings.level_up())?; + let guard = parse_expr(input, state, lib, settings.level_up())?; ensure_not_assignment(input)?; - let if_body = parse_block(input, state, settings.level_up())?; + let if_body = parse_block(input, state, lib, settings.level_up())?; // if guard { if_body } else ... let else_body = if match_token(input, Token::Else).unwrap_or(false) { Some(if let (Token::If, _) = input.peek().unwrap() { // if guard { if_body } else if ... - parse_if(input, state, settings.level_up())? + parse_if(input, state, lib, settings.level_up())? } else { // if guard { if_body } else { else-body } - parse_block(input, state, settings.level_up())? + parse_block(input, state, lib, settings.level_up())? }) } else { None @@ -2085,6 +2104,7 @@ fn parse_if( fn parse_while( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { // while ... @@ -2093,11 +2113,11 @@ fn parse_while( // while guard { body } ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, settings.level_up())?; + let guard = parse_expr(input, state, lib, settings.level_up())?; ensure_not_assignment(input)?; settings.is_breakable = true; - let body = parse_block(input, state, settings.level_up())?; + let body = parse_block(input, state, lib, settings.level_up())?; Ok(Stmt::While(Box::new((guard, body)))) } @@ -2106,6 +2126,7 @@ fn parse_while( fn parse_loop( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { // loop ... @@ -2114,7 +2135,7 @@ fn parse_loop( // loop { body } settings.is_breakable = true; - let body = parse_block(input, state, settings.level_up())?; + let body = parse_block(input, state, lib, settings.level_up())?; Ok(Stmt::Loop(Box::new(body))) } @@ -2123,6 +2144,7 @@ fn parse_loop( fn parse_for( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { // for ... @@ -2155,13 +2177,13 @@ fn parse_for( // for name in expr { body } ensure_not_statement_expr(input, "a boolean")?; - let expr = parse_expr(input, state, settings.level_up())?; + let expr = parse_expr(input, state, lib, settings.level_up())?; let prev_stack_len = state.stack.len(); state.stack.push((name.clone(), ScopeEntryType::Normal)); settings.is_breakable = true; - let body = parse_block(input, state, settings.level_up())?; + let body = parse_block(input, state, lib, settings.level_up())?; state.stack.truncate(prev_stack_len); @@ -2172,6 +2194,7 @@ fn parse_for( fn parse_let( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, var_type: ScopeEntryType, mut settings: ParseSettings, ) -> Result { @@ -2199,7 +2222,7 @@ fn parse_let( // let name = ... if match_token(input, Token::Equals)? { // let name = expr - let init_value = parse_expr(input, state, settings.level_up())?; + let init_value = parse_expr(input, state, lib, settings.level_up())?; match var_type { // let name = expr @@ -2237,6 +2260,7 @@ fn parse_let( fn parse_import( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { // import ... @@ -2244,7 +2268,7 @@ fn parse_import( settings.ensure_level_within_max_limit(state.max_expr_depth)?; // import expr ... - let expr = parse_expr(input, state, settings.level_up())?; + let expr = parse_expr(input, state, lib, settings.level_up())?; // import expr as ... match input.next().unwrap() { @@ -2273,6 +2297,7 @@ fn parse_import( fn parse_export( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { settings.pos = eat_token(input, Token::Export); @@ -2333,6 +2358,7 @@ fn parse_export( fn parse_block( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { // Must start with { @@ -2357,7 +2383,12 @@ fn parse_block( while !match_token(input, Token::RightBrace)? { // Parse statements inside the block settings.is_global = false; - let stmt = parse_stmt(input, state, settings.level_up())?; + + let stmt = if let Some(s) = parse_stmt(input, state, lib, settings.level_up())? { + s + } else { + continue; + }; // See if it needs a terminating semicolon let need_semicolon = !stmt.is_self_terminated(); @@ -2402,13 +2433,14 @@ fn parse_block( fn parse_expr_stmt( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { settings.pos = input.peek().unwrap().1; settings.ensure_level_within_max_limit(state.max_expr_depth)?; - let expr = parse_expr(input, state, settings.level_up())?; - let expr = parse_op_assignment_stmt(input, state, expr, settings.level_up())?; + let expr = parse_expr(input, state, lib, settings.level_up())?; + let expr = parse_op_assignment_stmt(input, state, lib, expr, settings.level_up())?; Ok(Stmt::Expr(Box::new(expr))) } @@ -2416,12 +2448,13 @@ fn parse_expr_stmt( fn parse_stmt( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, mut settings: ParseSettings, -) -> Result { +) -> Result, ParseError> { use ScopeEntryType::{Constant, Normal}; let (token, token_pos) = match input.peek().unwrap() { - (Token::EOF, pos) => return Ok(Stmt::Noop(*pos)), + (Token::EOF, pos) => return Ok(Some(Stmt::Noop(*pos))), x => x, }; settings.pos = *token_pos; @@ -2429,28 +2462,71 @@ fn parse_stmt( match token { // Semicolon - empty statement - Token::SemiColon => Ok(Stmt::Noop(settings.pos)), + Token::SemiColon => Ok(Some(Stmt::Noop(settings.pos))), - Token::LeftBrace => parse_block(input, state, settings.level_up()), + Token::LeftBrace => parse_block(input, state, lib, settings.level_up()).map(Some), // fn ... #[cfg(not(feature = "no_function"))] Token::Fn if !settings.is_global => Err(PERR::WrongFnDefinition.into_err(settings.pos)), - #[cfg(not(feature = "no_function"))] - Token::Fn => unreachable!(), - Token::If => parse_if(input, state, settings.level_up()), - Token::While => parse_while(input, state, settings.level_up()), - Token::Loop => parse_loop(input, state, settings.level_up()), - Token::For => parse_for(input, state, settings.level_up()), + #[cfg(not(feature = "no_function"))] + Token::Fn | Token::Private => { + let access = if matches!(token, Token::Private) { + eat_token(input, Token::Private); + FnAccess::Private + } else { + FnAccess::Public + }; + + match input.next().unwrap() { + (Token::Fn, pos) => { + let mut state = ParseState::new( + state.engine, + state.max_function_expr_depth, + state.max_function_expr_depth, + ); + + let settings = ParseSettings { + allow_if_expr: true, + allow_stmt_expr: true, + allow_anonymous_fn: true, + is_global: false, + is_breakable: false, + level: 0, + pos: pos, + }; + + let func = parse_fn(input, &mut state, lib, access, settings)?; + + // Qualifiers (none) + function name + number of arguments. + let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); + + lib.insert(hash, func); + + Ok(None) + } + + (_, pos) => Err(PERR::MissingToken( + Token::Fn.into(), + format!("following '{}'", Token::Private.syntax()), + ) + .into_err(pos)), + } + } + + Token::If => parse_if(input, state, lib, settings.level_up()).map(Some), + Token::While => parse_while(input, state, lib, settings.level_up()).map(Some), + Token::Loop => parse_loop(input, state, lib, settings.level_up()).map(Some), + Token::For => parse_for(input, state, lib, settings.level_up()).map(Some), Token::Continue if settings.is_breakable => { let pos = eat_token(input, Token::Continue); - Ok(Stmt::Continue(pos)) + Ok(Some(Stmt::Continue(pos))) } Token::Break if settings.is_breakable => { let pos = eat_token(input, Token::Break); - Ok(Stmt::Break(pos)) + Ok(Some(Stmt::Break(pos))) } Token::Continue | Token::Break => Err(PERR::LoopBreak.into_err(settings.pos)), @@ -2463,38 +2539,41 @@ fn parse_stmt( match input.peek().unwrap() { // `return`/`throw` at - (Token::EOF, pos) => Ok(Stmt::ReturnWithVal(Box::new(((return_type, *pos), None)))), + (Token::EOF, pos) => Ok(Some(Stmt::ReturnWithVal(Box::new(( + (return_type, *pos), + None, + ))))), // `return;` or `throw;` - (Token::SemiColon, _) => Ok(Stmt::ReturnWithVal(Box::new(( + (Token::SemiColon, _) => Ok(Some(Stmt::ReturnWithVal(Box::new(( (return_type, settings.pos), None, - )))), + ))))), // `return` or `throw` with expression (_, _) => { - let expr = parse_expr(input, state, settings.level_up())?; + let expr = parse_expr(input, state, lib, settings.level_up())?; let pos = expr.position(); - Ok(Stmt::ReturnWithVal(Box::new(( + Ok(Some(Stmt::ReturnWithVal(Box::new(( (return_type, pos), Some(expr), - )))) + ))))) } } } - Token::Let => parse_let(input, state, Normal, settings.level_up()), - Token::Const => parse_let(input, state, Constant, settings.level_up()), + Token::Let => parse_let(input, state, lib, Normal, settings.level_up()).map(Some), + Token::Const => parse_let(input, state, lib, Constant, settings.level_up()).map(Some), #[cfg(not(feature = "no_module"))] - Token::Import => parse_import(input, state, settings.level_up()), + Token::Import => parse_import(input, state, lib, settings.level_up()).map(Some), #[cfg(not(feature = "no_module"))] Token::Export if !settings.is_global => Err(PERR::WrongExport.into_err(settings.pos)), #[cfg(not(feature = "no_module"))] - Token::Export => parse_export(input, state, settings.level_up()), + Token::Export => parse_export(input, state, lib, settings.level_up()).map(Some), - _ => parse_expr_stmt(input, state, settings.level_up()), + _ => parse_expr_stmt(input, state, lib, settings.level_up()).map(Some), } } @@ -2503,10 +2582,10 @@ fn parse_stmt( fn parse_fn( input: &mut TokenStream, state: &mut ParseState, + lib: &mut FunctionsLib, access: FnAccess, mut settings: ParseSettings, ) -> Result { - settings.pos = eat_token(input, Token::Fn); settings.ensure_level_within_max_limit(state.max_expr_depth)?; let name = match input.next().unwrap() { @@ -2575,7 +2654,7 @@ fn parse_fn( let body = match input.peek().unwrap() { (Token::LeftBrace, _) => { settings.is_breakable = false; - parse_block(input, state, settings.level_up())? + parse_block(input, state, lib, settings.level_up())? } (_, pos) => return Err(PERR::FnMissingBody(name).into_err(*pos)), }; @@ -2598,16 +2677,18 @@ impl Engine { scope: &Scope, optimization_level: OptimizationLevel, ) -> Result { - let mut state = ParseState::new(self, self.max_expr_depth); + let mut functions = Default::default(); + let mut state = ParseState::new(self, self.max_expr_depth, self.max_function_expr_depth); let settings = ParseSettings { allow_if_expr: false, allow_stmt_expr: false, + allow_anonymous_fn: false, is_global: true, is_breakable: false, level: 0, pos: Position::none(), }; - let expr = parse_expr(input, &mut state, settings)?; + let expr = parse_expr(input, &mut state, &mut functions, settings)?; match input.peek().unwrap() { (Token::EOF, _) => (), @@ -2632,60 +2713,26 @@ impl Engine { &self, input: &mut TokenStream, ) -> Result<(Vec, Vec), ParseError> { - let mut statements = Vec::::new(); - let mut functions = HashMap::::with_hasher(StraightHasherBuilder); - let mut state = ParseState::new(self, self.max_expr_depth); + let mut statements: Vec = Default::default(); + let mut functions = Default::default(); + let mut state = ParseState::new(self, self.max_expr_depth, self.max_function_expr_depth); while !input.peek().unwrap().0.is_eof() { - // Collect all the function definitions - #[cfg(not(feature = "no_function"))] - { - let (access, must_be_fn) = if match_token(input, Token::Private)? { - (FnAccess::Private, true) - } else { - (FnAccess::Public, false) - }; - - match input.peek().unwrap() { - (Token::Fn, pos) => { - let mut state = ParseState::new(self, self.max_function_expr_depth); - let settings = ParseSettings { - allow_if_expr: true, - allow_stmt_expr: true, - is_global: false, - is_breakable: false, - level: 0, - pos: *pos, - }; - let func = parse_fn(input, &mut state, access, settings)?; - - // Qualifiers (none) + function name + number of arguments. - let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); - - functions.insert(hash, func); - continue; - } - (_, pos) if must_be_fn => { - return Err(PERR::MissingToken( - Token::Fn.into(), - format!("following '{}'", Token::Private.syntax()), - ) - .into_err(*pos)) - } - _ => (), - } - } - - // Actual statement let settings = ParseSettings { allow_if_expr: true, allow_stmt_expr: true, + allow_anonymous_fn: true, is_global: true, is_breakable: false, level: 0, pos: Position::none(), }; - let stmt = parse_stmt(input, &mut state, settings)?; + + let stmt = if let Some(s) = parse_stmt(input, &mut state, &mut functions, settings)? { + s + } else { + continue; + }; let need_semicolon = !stmt.is_self_terminated(); From ff6d205c1dd198c701e8582d2ee3ab0924b9f1c7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 7 Jul 2020 22:59:23 +0800 Subject: [PATCH 35/46] Make Modules::set_raw_fn public. --- RELEASES.md | 4 +- src/api.rs | 136 ++++++++++---------------- src/module.rs | 188 +++++++++++++++++------------------- src/packages/array_basic.rs | 2 +- src/packages/string_more.rs | 2 +- tests/call_fn.rs | 20 ++-- 6 files changed, 158 insertions(+), 194 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 1818519e..8a17c106 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -24,9 +24,11 @@ New features This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back. * `Engine::disable_symbol` to surgically disable keywords and/or operators. * `Engine::register_custom_operator` to define a custom operator. -* New low-level API `Engine::register_raw_fn`. +* New low-level API `Engine::register_raw_fn` and `Engine::register_raw_fn_XXX`. +* New low-level API `Module::set_raw_fn` mirroring `Engine::register_raw_fn`. * `AST::clone_functions_only`, `AST::clone_functions_only_filtered` and `AST::clone_statements_only` to clone only part of an `AST`. * The boolean `^` (XOR) operator is added. +* `FnPtr` is exposed as the function pointer type. Version 0.16.1 diff --git a/src/api.rs b/src/api.rs index 5df12aba..a6500443 100644 --- a/src/api.rs +++ b/src/api.rs @@ -6,7 +6,7 @@ use crate::error::ParseError; use crate::fn_call::FuncArgs; use crate::fn_native::{IteratorFn, SendSync}; use crate::fn_register::RegisterFn; -use crate::module::Module; +use crate::module::{FuncReturn, Module}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::parser::AST; use crate::result::EvalAltResult; @@ -39,30 +39,23 @@ impl Engine { /// /// This function is very low level. It takes a list of `TypeId`'s indicating the actual types of the parameters. /// - /// Arguments are simply passed in as a mutable array of `&mut Dynamic`. + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`, /// The arguments are guaranteed to be of the correct types matching the `TypeId`'s. /// - /// To get access to a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// To access a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` /// - /// To get access to a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// To access a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. /// Notice that this will _consume_ the argument, replacing it with `()`. /// - /// To get access to the first mutable parameter, use `args.get_mut(0).unwrap()` + /// To access the first mutable parameter, use `args.get_mut(0).unwrap()` #[deprecated(note = "this function is volatile and may change")] - pub fn register_raw_fn( + pub fn register_raw_fn( &mut self, name: &str, arg_types: &[TypeId], - - #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + 'static, - - #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + Send - + Sync - + 'static, + func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, ) { - self.global_module.set_fn_var_args(name, arg_types, func); + self.global_module.set_raw_fn(name, arg_types, func); } /// Register a function of no parameters with the `Engine`. @@ -71,19 +64,12 @@ impl Engine { /// /// This function is very low level. #[deprecated(note = "this function is volatile and may change")] - pub fn register_raw_fn_0( + pub fn register_raw_fn_0( &mut self, name: &str, - - #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + 'static, - - #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + Send - + Sync - + 'static, + func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, ) { - self.global_module.set_fn_var_args(name, &[], func); + self.global_module.set_raw_fn(name, &[], func); } /// Register a function of one parameter with the `Engine`. @@ -92,30 +78,23 @@ impl Engine { /// /// This function is very low level. /// - /// Arguments are simply passed in as a mutable array of `&mut Dynamic`. - /// The argument is guaranteed to be of the correct type. + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`, + /// which is guaranteed to contain enough arguments of the correct types. /// - /// To get access to a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// To access a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` /// - /// To get access to a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// To access a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. /// Notice that this will _consume_ the argument, replacing it with `()`. /// - /// To get access to the first mutable parameter, use `args.get_mut(0).unwrap()` + /// To access the first mutable parameter, use `args.get_mut(0).unwrap()` #[deprecated(note = "this function is volatile and may change")] - pub fn register_raw_fn_1( + pub fn register_raw_fn_1( &mut self, name: &str, - - #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + 'static, - - #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + Send - + Sync - + 'static, + func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, ) { self.global_module - .set_fn_var_args(name, &[TypeId::of::
()], func); + .set_raw_fn(name, &[TypeId::of::()], func); } /// Register a function of two parameters with the `Engine`. @@ -124,30 +103,23 @@ impl Engine { /// /// This function is very low level. /// - /// Arguments are simply passed in as a mutable array of `&mut Dynamic`. - /// The arguments are guaranteed to be of the correct types. + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`, + /// which is guaranteed to contain enough arguments of the correct types. /// - /// To get access to a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// To access a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` /// - /// To get access to a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// To access a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. /// Notice that this will _consume_ the argument, replacing it with `()`. /// - /// To get access to the first mutable parameter, use `args.get_mut(0).unwrap()` + /// To access the first mutable parameter, use `args.get_mut(0).unwrap()` #[deprecated(note = "this function is volatile and may change")] - pub fn register_raw_fn_2( + pub fn register_raw_fn_2( &mut self, name: &str, - - #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + 'static, - - #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + Send - + Sync - + 'static, + func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, ) { self.global_module - .set_fn_var_args(name, &[TypeId::of::(), TypeId::of::()], func); + .set_raw_fn(name, &[TypeId::of::(), TypeId::of::()], func); } /// Register a function of three parameters with the `Engine`. @@ -156,29 +128,27 @@ impl Engine { /// /// This function is very low level. /// - /// Arguments are simply passed in as a mutable array of `&mut Dynamic`. - /// The arguments are guaranteed to be of the correct types. + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`, + /// which is guaranteed to contain enough arguments of the correct types. /// - /// To get access to a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// To access a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` /// - /// To get access to a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// To access a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. /// Notice that this will _consume_ the argument, replacing it with `()`. /// - /// To get access to the first mutable parameter, use `args.get_mut(0).unwrap()` + /// To access the first mutable parameter, use `args.get_mut(0).unwrap()` #[deprecated(note = "this function is volatile and may change")] - pub fn register_raw_fn_3( + pub fn register_raw_fn_3< + A: Variant + Clone, + B: Variant + Clone, + C: Variant + Clone, + T: Variant + Clone, + >( &mut self, name: &str, - - #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + 'static, - - #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + Send - + Sync - + 'static, + func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, ) { - self.global_module.set_fn_var_args( + self.global_module.set_raw_fn( name, &[TypeId::of::(), TypeId::of::(), TypeId::of::()], func, @@ -191,34 +161,28 @@ impl Engine { /// /// This function is very low level. /// - /// Arguments are simply passed in as a mutable array of `&mut Dynamic`. - /// The arguments are guaranteed to be of the correct types. + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`, + /// which is guaranteed to contain enough arguments of the correct types. /// - /// To get access to a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// To access a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` /// - /// To get access to a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// To access a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. /// Notice that this will _consume_ the argument, replacing it with `()`. /// - /// To get access to the first mutable parameter, use `args.get_mut(0).unwrap()` + /// To access the first mutable parameter, use `args.get_mut(0).unwrap()` #[deprecated(note = "this function is volatile and may change")] pub fn register_raw_fn_4< A: Variant + Clone, B: Variant + Clone, C: Variant + Clone, D: Variant + Clone, + T: Variant + Clone, >( &mut self, name: &str, - - #[cfg(not(feature = "sync"))] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + 'static, - - #[cfg(feature = "sync")] func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> Result> - + Send - + Sync - + 'static, + func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, ) { - self.global_module.set_fn_var_args( + self.global_module.set_raw_fn( name, &[ TypeId::of::(), @@ -1395,7 +1359,7 @@ impl Engine { name: &str, mut this_ptr: Option<&mut Dynamic>, mut arg_values: impl AsMut<[Dynamic]>, - ) -> Result> { + ) -> FuncReturn { self.call_fn_dynamic_raw(scope, lib, name, &mut this_ptr, arg_values.as_mut()) } @@ -1415,7 +1379,7 @@ impl Engine { name: &str, this_ptr: &mut Option<&mut Dynamic>, arg_values: &mut [Dynamic], - ) -> Result> { + ) -> FuncReturn { let lib = lib.as_ref(); let mut args: StaticVec<_> = arg_values.iter_mut().collect(); let fn_def = diff --git a/src/module.rs b/src/module.rs index cd08e5bc..9703552b 100644 --- a/src/module.rs +++ b/src/module.rs @@ -3,7 +3,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; use crate::engine::{make_getter, make_setter, Engine, Imports, FN_IDX_GET, FN_IDX_SET}; -use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync, Shared}; +use crate::fn_native::{CallableFunction as Func, FnCallArgs, IteratorFn, SendSync, Shared}; use crate::parser::{ FnAccess, FnAccess::{Private, Public}, @@ -49,18 +49,14 @@ pub struct Module { all_variables: HashMap, /// External Rust functions. - functions: HashMap< - u64, - (String, FnAccess, StaticVec, CallableFunction), - StraightHasherBuilder, - >, + functions: HashMap, Func), StraightHasherBuilder>, /// Iterator functions, keyed by the type producing the iterator. type_iterators: HashMap, /// Flattened collection of all external Rust functions, native or scripted, /// including those in sub-modules. - all_functions: HashMap, + all_functions: HashMap, /// Is the module indexed? indexed: bool, @@ -346,18 +342,18 @@ impl Module { /// Set a Rust function into the module, returning a hash key. /// /// If there is an existing Rust function of the same hash, it is replaced. - pub fn set_fn( + pub(crate) fn set_fn( &mut self, name: impl Into, access: FnAccess, - params: &[TypeId], - func: CallableFunction, + arg_types: &[TypeId], + func: Func, ) -> u64 { let name = name.into(); - let hash_fn = calc_fn_hash(empty(), &name, params.len(), params.iter().cloned()); + let hash_fn = calc_fn_hash(empty(), &name, arg_types.len(), arg_types.iter().cloned()); - let params = params.into_iter().cloned().collect(); + let params = arg_types.into_iter().cloned().collect(); self.functions .insert(hash_fn, (name, access, params, func.into())); @@ -367,29 +363,72 @@ impl Module { hash_fn } - /// Set a Rust function taking a reference to the scripting `Engine`, plus a list of - /// mutable `Dynamic` references into the module, returning a hash key. - /// A list of `TypeId`'s is taken as the argument types. + /// Set a Rust function taking a reference to the scripting `Engine`, the current set of functions, + /// plus a list of mutable `Dynamic` references into the module, returning a hash key. /// /// Use this to register a built-in function which must reference settings on the scripting - /// `Engine` (e.g. to prevent growing an array beyond the allowed maximum size). + /// `Engine` (e.g. to prevent growing an array beyond the allowed maximum size), or to call a + /// script-defined function in the current evaluation context. /// /// If there is a similar existing Rust function, it is replaced. - pub(crate) fn set_fn_var_args( + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. + /// + /// A list of `TypeId`'s is taken as the argument types. + /// + /// Arguments are simply passed in as a mutable array of `&mut Dynamic`, + /// which is guaranteed to contain enough arguments of the correct types. + /// + /// To access a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` + /// + /// To access a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. + /// Notice that this will _consume_ the argument, replacing it with `()`. + /// + /// To access the first mutable parameter, use `args.get_mut(0).unwrap()` + /// + /// # Examples + /// + /// ``` + /// use rhai::Module; + /// + /// let mut module = Module::new(); + /// let hash = module.set_raw_fn("double_or_not", + /// // Pass parameter types via a slice with TypeId's + /// &[std::any::TypeId::of::(), std::any::TypeId::of::() ], + /// // Fixed closure signature + /// |engine, lib, args| { + /// // 'args' is guaranteed to be the right length and of the correct types + /// + /// // Get the second parameter by 'consuming' it + /// let double = std::mem::take(args[1]).cast::(); + /// // Since it is a primary type, it can also be cheaply copied + /// let double = args[1].clone().cast::(); + /// // Get a mutable reference to the first argument. + /// let x = args[0].downcast_mut::().unwrap(); + /// + /// let orig = *x; + /// + /// if double { + /// *x *= 2; // the first argument can be mutated + /// } + /// + /// Ok(orig) // return Result> + /// }); + /// + /// assert!(module.contains_fn(hash)); + /// ``` + pub fn set_raw_fn( &mut self, name: impl Into, - args: &[TypeId], + arg_types: &[TypeId], func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |engine: &Engine, lib: &Module, args: &mut FnCallArgs| { func(engine, lib, args).map(Dynamic::from) }; - self.set_fn( - name, - Public, - args, - CallableFunction::from_method(Box::new(f)), - ) + self.set_fn(name, Public, arg_types, Func::from_method(Box::new(f))) } /// Set a Rust function taking no parameters into the module, returning a hash key. @@ -411,13 +450,8 @@ impl Module { func: impl Fn() -> FuncReturn + SendSync + 'static, ) -> u64 { let f = move |_: &Engine, _: &Module, _: &mut FnCallArgs| func().map(Dynamic::from); - let args = []; - self.set_fn( - name, - Public, - &args, - CallableFunction::from_pure(Box::new(f)), - ) + let arg_types = []; + self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f))) } /// Set a Rust function taking one parameter into the module, returning a hash key. @@ -441,13 +475,8 @@ impl Module { let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { func(mem::take(args[0]).cast::()).map(Dynamic::from) }; - let args = [TypeId::of::()]; - self.set_fn( - name, - Public, - &args, - CallableFunction::from_pure(Box::new(f)), - ) + let arg_types = [TypeId::of::()]; + self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f))) } /// Set a Rust function taking one mutable parameter into the module, returning a hash key. @@ -471,13 +500,8 @@ impl Module { let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| { func(args[0].downcast_mut::().unwrap()).map(Dynamic::from) }; - let args = [TypeId::of::()]; - self.set_fn( - name, - Public, - &args, - CallableFunction::from_method(Box::new(f)), - ) + let arg_types = [TypeId::of::()]; + self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f))) } /// Set a Rust getter function taking one mutable parameter, returning a hash key. @@ -528,13 +552,8 @@ impl Module { func(a, b).map(Dynamic::from) }; - let args = [TypeId::of::(), TypeId::of::()]; - self.set_fn( - name, - Public, - &args, - CallableFunction::from_pure(Box::new(f)), - ) + let arg_types = [TypeId::of::(), TypeId::of::()]; + self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f))) } /// Set a Rust function taking two parameters (the first one mutable) into the module, @@ -564,13 +583,8 @@ impl Module { func(a, b).map(Dynamic::from) }; - let args = [TypeId::of::(), TypeId::of::()]; - self.set_fn( - name, - Public, - &args, - CallableFunction::from_method(Box::new(f)), - ) + let arg_types = [TypeId::of::(), TypeId::of::()]; + self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f))) } /// Set a Rust setter function taking two parameters (the first one mutable) into the module, @@ -656,13 +670,8 @@ impl Module { func(a, b, c).map(Dynamic::from) }; - let args = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; - self.set_fn( - name, - Public, - &args, - CallableFunction::from_pure(Box::new(f)), - ) + let arg_types = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; + self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f))) } /// Set a Rust function taking three parameters (the first one mutable) into the module, @@ -698,13 +707,8 @@ impl Module { func(a, b, c).map(Dynamic::from) }; - let args = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; - self.set_fn( - name, - Public, - &args, - CallableFunction::from_method(Box::new(f)), - ) + let arg_types = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; + self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f))) } /// Set a Rust index setter taking three parameters (the first one mutable) into the module, @@ -735,12 +739,12 @@ impl Module { func(a, b, c).map(Dynamic::from) }; - let args = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; + let arg_types = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; self.set_fn( FN_IDX_SET, Public, - &args, - CallableFunction::from_method(Box::new(f)), + &arg_types, + Func::from_method(Box::new(f)), ) } @@ -778,18 +782,13 @@ impl Module { func(a, b, c, d).map(Dynamic::from) }; - let args = [ + let arg_types = [ TypeId::of::(), TypeId::of::(), TypeId::of::(), TypeId::of::(), ]; - self.set_fn( - name, - Public, - &args, - CallableFunction::from_pure(Box::new(f)), - ) + self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f))) } /// Set a Rust function taking four parameters (the first one mutable) into the module, @@ -827,25 +826,20 @@ impl Module { func(a, b, c, d).map(Dynamic::from) }; - let args = [ + let arg_types = [ TypeId::of::(), TypeId::of::(), TypeId::of::(), TypeId::of::(), ]; - self.set_fn( - name, - Public, - &args, - CallableFunction::from_method(Box::new(f)), - ) + self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f))) } /// Get a Rust function. /// /// The `u64` hash is calculated by the function `crate::calc_fn_hash`. /// It is also returned by the `set_fn_XXX` calls. - pub(crate) fn get_fn(&self, hash_fn: u64) -> Option<&CallableFunction> { + pub(crate) fn get_fn(&self, hash_fn: u64) -> Option<&Func> { self.functions.get(&hash_fn).map(|(_, _, _, v)| v) } @@ -857,7 +851,7 @@ impl Module { pub(crate) fn get_qualified_fn( &mut self, hash_qualified_fn: u64, - ) -> Result<&CallableFunction, Box> { + ) -> Result<&Func, Box> { self.all_functions.get(&hash_qualified_fn).ok_or_else(|| { Box::new(EvalAltResult::ErrorFunctionNotFound( String::new(), @@ -886,9 +880,7 @@ impl Module { .iter() .filter(|(_, (_, _, _, v))| match v { #[cfg(not(feature = "no_function"))] - CallableFunction::Script(ref f) => { - filter(f.access, f.name.as_str(), f.params.len()) - } + Func::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()), _ => true, }) .map(|(&k, v)| (k, v.clone())), @@ -906,7 +898,7 @@ impl Module { #[cfg(not(feature = "no_function"))] pub(crate) fn retain_functions(&mut self, filter: impl Fn(FnAccess, &str, usize) -> bool) { self.functions.retain(|_, (_, _, _, v)| match v { - CallableFunction::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()), + Func::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()), _ => true, }); @@ -936,7 +928,7 @@ impl Module { /// Get an iterator to the functions in the module. pub(crate) fn iter_fn( &self, - ) -> impl Iterator, CallableFunction)> { + ) -> impl Iterator, Func)> { self.functions.values() } @@ -1003,7 +995,7 @@ impl Module { module: &'a Module, qualifiers: &mut Vec<&'a str>, variables: &mut Vec<(u64, Dynamic)>, - functions: &mut Vec<(u64, CallableFunction)>, + functions: &mut Vec<(u64, Func)>, ) { for (name, m) in &module.modules { // Index all the sub-modules first. diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index acaa1132..0a580933 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -67,7 +67,7 @@ macro_rules! reg_tri { macro_rules! reg_pad { ($lib:expr, $op:expr, $func:ident, $($par:ty),*) => { $({ - $lib.set_fn_var_args($op, + $lib.set_raw_fn($op, &[TypeId::of::(), TypeId::of::(), TypeId::of::<$par>()], $func::<$par> ); diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 5ae39db6..248b0e93 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -223,7 +223,7 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str Ok(()) }, ); - lib.set_fn_var_args( + lib.set_raw_fn( "pad", &[TypeId::of::(), TypeId::of::(), TypeId::of::()], |engine: &Engine, _: &Module, args: &mut [&mut Dynamic]| { diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 715fbc87..62291830 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -1,7 +1,7 @@ #![cfg(not(feature = "no_function"))] use rhai::{ - Dynamic, Engine, EvalAltResult, Func, ImmutableString, Module, ParseError, ParseErrorType, - Scope, INT, + Dynamic, Engine, EvalAltResult, FnPtr, Func, ImmutableString, Module, ParseError, + ParseErrorType, Scope, INT, }; #[test] @@ -122,17 +122,23 @@ fn test_fn_ptr() -> Result<(), Box> { "bar", &[ std::any::TypeId::of::(), - std::any::TypeId::of::(), + std::any::TypeId::of::(), std::any::TypeId::of::(), ], move |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { - let callback = args[1].clone().cast::(); + let fp = std::mem::take(args[1]).cast::(); let value = args[2].clone(); let this_ptr = args.get_mut(0).unwrap(); - engine.call_fn_dynamic(&mut Scope::new(), lib, &callback, Some(this_ptr), [value])?; + engine.call_fn_dynamic( + &mut Scope::new(), + lib, + fp.fn_name(), + Some(this_ptr), + [value], + )?; - Ok(().into()) + Ok(()) }, ); @@ -142,7 +148,7 @@ fn test_fn_ptr() -> Result<(), Box> { fn foo(x) { this += x; } let x = 41; - x.bar("foo", 1); + x.bar(Fn("foo"), 1); x "# )?, From 236ba40784ea39f8d46d57db9c10ec4c4d35e88e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 7 Jul 2020 23:44:23 +0800 Subject: [PATCH 36/46] Add ModuleResolversCollection. --- src/module.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/src/module.rs b/src/module.rs index 9703552b..9fa9f9a7 100644 --- a/src/module.rs +++ b/src/module.rs @@ -1145,6 +1145,7 @@ pub trait ModuleResolver: SendSync { /// Re-export module resolvers. #[cfg(not(feature = "no_module"))] pub mod resolvers { + pub use super::collection::ModuleResolversCollection; #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] pub use super::file::FileModuleResolver; @@ -1340,9 +1341,6 @@ mod stat { /// Module resolution service that serves modules added into it. /// - /// `StaticModuleResolver` is a smart pointer to a `HashMap`. - /// It can simply be treated as `&HashMap`. - /// /// # Examples /// /// ``` @@ -1436,3 +1434,86 @@ mod stat { } } } + +/// Module resolver collection. +#[cfg(not(feature = "no_module"))] +mod collection { + use super::*; + + /// Module resolution service that holds a collection of module resolves, + /// to be searched in sequential order. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Engine, Module}; + /// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection}; + /// + /// let mut collection = ModuleResolversCollection::new(); + /// + /// let resolver = StaticModuleResolver::new(); + /// collection.push(resolver); + /// + /// let mut engine = Engine::new(); + /// engine.set_module_resolver(Some(collection)); + /// ``` + #[derive(Default)] + pub struct ModuleResolversCollection(Vec>); + + impl ModuleResolversCollection { + /// Create a new `ModuleResolversCollection`. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Engine, Module}; + /// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection}; + /// + /// let mut collection = ModuleResolversCollection::new(); + /// + /// let resolver = StaticModuleResolver::new(); + /// collection.push(resolver); + /// + /// let mut engine = Engine::new(); + /// engine.set_module_resolver(Some(collection)); + /// ``` + pub fn new() -> Self { + Default::default() + } + } + + impl ModuleResolversCollection { + /// Add a module keyed by its path. + pub fn push(&mut self, resolver: impl ModuleResolver + 'static) { + self.0.push(Box::new(resolver)); + } + /// Get an iterator of all the module resolvers. + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|v| v.as_ref()) + } + /// Remove all module resolvers. + pub fn clear(&mut self) { + self.0.clear(); + } + } + + impl ModuleResolver for ModuleResolversCollection { + fn resolve( + &self, + engine: &Engine, + path: &str, + pos: Position, + ) -> Result> { + for resolver in self.0.iter() { + if let Ok(module) = resolver.resolve(engine, path, pos) { + return Ok(module); + } + } + + Err(Box::new(EvalAltResult::ErrorModuleNotFound( + path.into(), + pos, + ))) + } + } +} From 150f02d8b7ffcc990e47bb1faef0f4c323a0ae3b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 8 Jul 2020 09:48:25 +0800 Subject: [PATCH 37/46] Update docs regarding modules. --- RELEASES.md | 2 + doc/src/SUMMARY.md | 6 +-- doc/src/language/eval.md | 8 +-- doc/src/language/functions.md | 10 ++-- doc/src/language/modules/export.md | 4 +- doc/src/language/modules/import.md | 1 + doc/src/language/modules/rust.md | 30 ----------- doc/src/language/values-and-types.md | 2 +- doc/src/links.md | 2 +- doc/src/rust/custom.md | 4 +- .../modules/imp-resolver.md | 0 doc/src/rust/modules/index.md | 45 ++++++++++++++++ .../{language => rust}/modules/resolvers.md | 17 ++++-- doc/src/rust/operators.md | 8 +-- doc/src/rust/register-raw.md | 54 +++++++++++++++++-- 15 files changed, 134 insertions(+), 59 deletions(-) delete mode 100644 doc/src/language/modules/rust.md rename doc/src/{language => rust}/modules/imp-resolver.md (100%) create mode 100644 doc/src/rust/modules/index.md rename doc/src/{language => rust}/modules/resolvers.md (55%) diff --git a/RELEASES.md b/RELEASES.md index 8a17c106..4afe903b 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,7 @@ This version adds: * [`serde`](https://crates.io/crates/serde) support for working with `Dynamic` values (particularly _object maps_). * Ability to surgically disable keywords and/or operators in the language. * Ability to define custom operators (which must be valid identifiers). +* Low-level API to register functions. Breaking changes ---------------- @@ -29,6 +30,7 @@ New features * `AST::clone_functions_only`, `AST::clone_functions_only_filtered` and `AST::clone_statements_only` to clone only part of an `AST`. * The boolean `^` (XOR) operator is added. * `FnPtr` is exposed as the function pointer type. +* `rhai::module_resolvers::ModuleResolversCollection` added to try a list of module resolvers. Version 0.16.1 diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 6d90df0c..3274e8cb 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -79,10 +79,10 @@ The Rhai Scripting Language 16. [Modules](language/modules/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) 2. [Import Modules](language/modules/import.md) - 3. [Create from Rust](language/modules/rust.md) + 3. [Create from Rust](rust/modules/index.md) 4. [Create from AST](language/modules/ast.md) - 5. [Module Resolvers](language/modules/resolvers.md) - 1. [Implement a Custom Module Resolver](language/modules/imp-resolver.md) + 5. [Module Resolvers](rust/modules/resolvers.md) + 1. [Custom Implementation](rust/modules/imp-resolver.md) 7. [Safety and Protection](safety/index.md) 1. [Checked Arithmetic](safety/checked.md) 2. [Sand-Boxing](safety/sandbox.md) diff --git a/doc/src/language/eval.md b/doc/src/language/eval.md index a7e5b979..0af40142 100644 --- a/doc/src/language/eval.md +++ b/doc/src/language/eval.md @@ -19,14 +19,14 @@ script += "x + y"; let result = eval(script); // <- look, JavaScript, we can also do this! -print("Answer: " + result); // prints 42 +result == 42; -print("x = " + x); // prints 10: functions call arguments are passed by value -print("y = " + y); // prints 32: variables defined in 'eval' persist! +x == 10; // prints 10: functions call arguments are passed by value +y == 32; // prints 32: variables defined in 'eval' persist! eval("{ let z = y }"); // to keep a variable local, use a statement block -print("z = " + z); // <- error: variable 'z' not found +print(z); // <- error: variable 'z' not found "print(42)".eval(); // <- nope... method-call style doesn't work with 'eval' ``` diff --git a/doc/src/language/functions.md b/doc/src/language/functions.md index 72dac0b4..b6c0a85f 100644 --- a/doc/src/language/functions.md +++ b/doc/src/language/functions.md @@ -14,8 +14,9 @@ fn sub(x, y,) { // trailing comma in parameters list is OK return x - y; } -print(add(2, 3)); // prints 5 -print(sub(2, 3,)); // prints -1 - trailing comma in arguments list is OK +add(2, 3) == 5; + +sub(2, 3,) == -1; // trailing comma in arguments list is OK ``` @@ -35,8 +36,9 @@ fn add2(x) { return x + 2; // explicit return } -print(add(2, 3)); // prints 5 -print(add2(42)); // prints 44 +add(2, 3) == 5; + +add2(42) == 44; ``` diff --git a/doc/src/language/modules/export.md b/doc/src/language/modules/export.md index dbb4ddca..f05e008c 100644 --- a/doc/src/language/modules/export.md +++ b/doc/src/language/modules/export.md @@ -22,6 +22,8 @@ The `export` statement, which can only be at global level, exposes selected vari Variables not exported are _private_ and hidden to the outside. +Everything exported from a module is **constant** (**read-only**). + ```rust // This is a module script. @@ -49,8 +51,6 @@ All functions are automatically exported, _unless_ it is explicitly opt-out with Functions declared [`private`] are hidden to the outside. -Everything exported from a module is **constant** (**read-only**). - ```rust // This is a module script. diff --git a/doc/src/language/modules/import.md b/doc/src/language/modules/import.md index 4ede1f52..dc94b5a8 100644 --- a/doc/src/language/modules/import.md +++ b/doc/src/language/modules/import.md @@ -3,6 +3,7 @@ Import a Module {{#include ../../links.md}} + `import` Statement ----------------- diff --git a/doc/src/language/modules/rust.md b/doc/src/language/modules/rust.md deleted file mode 100644 index 74f90896..00000000 --- a/doc/src/language/modules/rust.md +++ /dev/null @@ -1,30 +0,0 @@ -Create a Module from Rust -======================== - -{{#include ../../links.md}} - -To load a custom module (written in Rust) into an [`Engine`], first create a [`Module`] type, -add variables/functions into it, then finally push it into a custom [`Scope`]. - -This has the equivalent effect of putting an [`import`] statement at the beginning of any script run. - -```rust -use rhai::{Engine, Scope, Module, i64}; - -let mut engine = Engine::new(); -let mut scope = Scope::new(); - -let mut module = Module::new(); // new module -module.set_var("answer", 41_i64); // variable 'answer' under module -module.set_fn_1("inc", |x: i64| Ok(x+1)); // use the 'set_fn_XXX' API to add functions - -// Push the module into the custom scope under the name 'question' -// This is equivalent to 'import "..." as question;' -scope.push_module("question", module); - -// Use module-qualified variables -engine.eval_expression_with_scope::(&scope, "question::answer + 1")? == 42; - -// Call module-qualified functions -engine.eval_expression_with_scope::(&scope, "question::inc(question::answer)")? == 42; -``` diff --git a/doc/src/language/values-and-types.md b/doc/src/language/values-and-types.md index 70c276a9..55b03229 100644 --- a/doc/src/language/values-and-types.md +++ b/doc/src/language/values-and-types.md @@ -15,7 +15,7 @@ The following primitive types are supported natively: | **[`Array`]** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | `"[ ?, ?, ? ]"` | | **[Object map]** (disabled with [`no_object`]) | `rhai::Map` | `"map"` | `"#{ "a": 1, "b": 2 }"` | | **[Timestamp]** (implemented in the [`BasicTimePackage`][packages], disabled with [`no_std`]) | `std::time::Instant` ([`instant::Instant`] if not [WASM] build) | `"timestamp"` | _not supported_ | -| **[Function pointer]** | _None_ | `Fn` | `"Fn(foo)"` | +| **[Function pointer]** | `rhai::FnPtr` | `Fn` | `"Fn(foo)"` | | **[`Dynamic`] value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | | **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | | **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | diff --git a/doc/src/links.md b/doc/src/links.md index a81bfd0c..85561acc 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -82,7 +82,7 @@ [`Module`]: {{rootUrl}}/language/modules/index.md [module]: {{rootUrl}}/language/modules/index.md [modules]: {{rootUrl}}/language/modules/index.md -[module resolver]: {{rootUrl}}/language/modules/imp-resolver.md +[module resolver]: {{rootUrl}}/rust/modules/resolvers.md [`export`]: {{rootUrl}}/language/modules/export.md [`import`]: {{rootUrl}}/language/modules/import.md diff --git a/doc/src/rust/custom.md b/doc/src/rust/custom.md index b87f6409..43837cd5 100644 --- a/doc/src/rust/custom.md +++ b/doc/src/rust/custom.md @@ -136,10 +136,10 @@ with a special "pretty-print" name, [`type_of()`] will return that name instead. engine.register_type::(); engine.register_fn("new_ts", TestStruct::new); let x = new_ts(); -print(x.type_of()); // prints "path::to::module::TestStruct" +x.type_of() == "path::to::module::TestStruct"; engine.register_type_with_name::("Hello"); engine.register_fn("new_ts", TestStruct::new); let x = new_ts(); -print(x.type_of()); // prints "Hello" +x.type_of() == "Hello"; ``` diff --git a/doc/src/language/modules/imp-resolver.md b/doc/src/rust/modules/imp-resolver.md similarity index 100% rename from doc/src/language/modules/imp-resolver.md rename to doc/src/rust/modules/imp-resolver.md diff --git a/doc/src/rust/modules/index.md b/doc/src/rust/modules/index.md new file mode 100644 index 00000000..be6576bb --- /dev/null +++ b/doc/src/rust/modules/index.md @@ -0,0 +1,45 @@ +Create a Module from Rust +======================== + +{{#include ../../links.md}} + +Manually creating a [`Module`] is possible via the `Module` API. + +For the complete `Module` API, refer to the [documentation](https://docs.rs/rhai/{{version}}/rhai/struct.Module.html) online. + + +Make the Module Available to the Engine +-------------------------------------- + +In order to _use_ a custom module, there must be a [module resolver]. + +The easiest way is to use, for example, the [`StaticModuleResolver`][module resolver] to hold such +a custom module. + +```rust +use rhai::{Engine, Scope, Module, i64}; +use rhai::module_resolvers::StaticModuleResolver; + +let mut engine = Engine::new(); +let mut scope = Scope::new(); + +let mut module = Module::new(); // new module +module.set_var("answer", 41_i64); // variable 'answer' under module +module.set_fn_1("inc", |x: i64| Ok(x+1)); // use the 'set_fn_XXX' API to add functions + +// Create the module resolver +let mut resolver = StaticModuleResolver::new(); + +// Add the module into the module resolver under the name 'question' +// They module can then be accessed via: 'import "question" as q;' +resolver.insert("question", module); + +// Set the module resolver into the 'Engine' +engine.set_module_resolver(Some(resolver)); + +// Use module-qualified variables +engine.eval::(&scope, r#"import "question" as q; q::answer + 1"#)? == 42; + +// Call module-qualified functions +engine.eval::(&scope, r#"import "question" as q; q::inc(q::answer)"#)? == 42; +``` diff --git a/doc/src/language/modules/resolvers.md b/doc/src/rust/modules/resolvers.md similarity index 55% rename from doc/src/language/modules/resolvers.md rename to doc/src/rust/modules/resolvers.md index ed2bf54c..4a3b97e7 100644 --- a/doc/src/language/modules/resolvers.md +++ b/doc/src/rust/modules/resolvers.md @@ -7,15 +7,24 @@ When encountering an [`import`] statement, Rhai attempts to _resolve_ the module _Module Resolvers_ are service types that implement the [`ModuleResolver`][traits] trait. + +Built-In Module Resolvers +------------------------ + There are a number of standard resolvers built into Rhai, the default being the `FileModuleResolver` which simply loads a script file based on the path (with `.rhai` extension attached) and execute it to form a module. Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace. -| Module Resolver | Description | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | -| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | +| Module Resolver | Description | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | +| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | +| `ModuleResolversCollection` | A collection of module resolvers. Modules will be resolved from each resolver in sequential order.
This is useful when multiple types of modules are needed simultaneously. | + + +Set into `Engine` +----------------- An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`: diff --git a/doc/src/rust/operators.md b/doc/src/rust/operators.md index 8374bfb4..e4ce9b65 100644 --- a/doc/src/rust/operators.md +++ b/doc/src/rust/operators.md @@ -39,17 +39,19 @@ engine.register_fn("+", strange_add); // overload '+' operator for let result: i64 = engine.eval("1 + 0"); // the overloading version is used -println!("result: {}", result); // prints 42 +result == 42; let result: f64 = engine.eval("1.0 + 0.0"); // '+' operator for two floats not overloaded -println!("result: {}", result); // prints 1.0 +result == 1.0; fn mixed_add(a: i64, b: f64) -> f64 { (a as f64) + b } engine.register_fn("+", mixed_add); // register '+' operator for an integer and a float -let result: i64 = engine.eval("1 + 1.0"); // prints 2.0 (normally an error) +let result: i64 = engine.eval("1 + 1.0"); // <- normally an error... + +result == 2.0; // ... but not now ``` diff --git a/doc/src/rust/register-raw.md b/doc/src/rust/register-raw.md index 429dae4a..6d3f22c5 100644 --- a/doc/src/rust/register-raw.md +++ b/doc/src/rust/register-raw.md @@ -75,15 +75,18 @@ Closure Signature The closure passed to `Engine::register_raw_fn` takes the following form: -`Fn(engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]) -> Result> + 'static` +`Fn(engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]) -> Result> + 'static` where: -* `engine` - a reference to the current [`Engine`], with all configurations and settings. +* `T : Variant + Clone` - return type of the function. -* `lib` - a reference to the current collection of script-defined functions, as a [`Module`]. +* `engine : &Engine` - the current [`Engine`], with all configurations and settings. -* `args` - a reference to a slice containing `&mut` references to [`Dynamic`] values. +* `lib : &Module` - the current global library of script-defined functions, as a [`Module`]. + This is sometimes useful for calling a script-defined function within the same evaluation context using [`Engine::call_fn`][`call_fn`]. + +* `args : &mut [&mut Dynamic]` - a slice containing `&mut` references to [`Dynamic`] values. The slice is guaranteed to contain enough arguments _of the correct types_. Remember, in Rhai, all arguments _except_ the _first_ one are always passed by _value_ (i.e. cloned). @@ -100,13 +103,54 @@ To extract an argument from the `args` parameter (`&mut [&mut Dynamic]`), use th | ------------------------------ | -------------------------------------- | ---------------------------------------------------------- | | [Primary type][standard types] | `args[n].clone().cast::()` | Copy of value. | | Custom type | `args[n].downcast_ref::().unwrap()` | Immutable reference to value. | -| Custom type (consumed) | `mem::take(args[n]).cast::()` | The _consumed_ value.
The original value becomes `()`. | +| Custom type (consumed) | `std::mem::take(args[n]).cast::()` | The _consumed_ value.
The original value becomes `()`. | | `this` object | `args[0].downcast_mut::().unwrap()` | Mutable reference to value. | When there is a mutable reference to the `this` object (i.e. the first argument), there can be no other immutable references to `args`, otherwise the Rust borrow checker will complain. +Example - Passing a Function Pointer to a Rust Function +------------------------------------------------------ + +```rust +use rhai::{Engine, Module, Dynamic, FnPtr}; + +let mut engine = Engine::new(); + +// Register a Rust function +engine.register_raw_fn( + "bar", + &[ + std::any::TypeId::of::(), // parameter types + std::any::TypeId::of::(), + std::any::TypeId::of::(), + ], + move |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { + // 'args' is guaranteed to contain enough arguments of the correct types + + let fp = std::mem::take(args[1]).cast::(); // 2nd argument - function pointer + let value = args[2].clone(); // 3rd argument - function argument + let this_ptr = args.get_mut(0).unwrap(); // 1st argument - this pointer + + // Use 'call_fn_dynamic' to call the function name. + // Pass 'lib' as the current global library of functions. + engine.call_fn_dynamic(&mut Scope::new(), lib, fp.fn_name(), Some(this_ptr), [value])?; + + Ok(()) + }, +); + +let result = engine.eval::(r#" + fn foo(x) { this += x; } // script-defined function 'foo' + + let x = 41; // object + x.bar(Fn("foo"), 1); // pass 'foo' as function pointer + x +"#)?; +``` + + Hold Multiple References ------------------------ From d92a514f48b5f04e2c20ba0c45636146b75855bb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 8 Jul 2020 12:09:18 +0800 Subject: [PATCH 38/46] Add reserved symbols. --- doc/src/SUMMARY.md | 2 +- doc/src/appendix/operators.md | 26 +++++- src/token.rs | 164 +++++++++++++++++++--------------- 3 files changed, 118 insertions(+), 74 deletions(-) diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 3274e8cb..438b4590 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -110,5 +110,5 @@ The Rhai Scripting Language 7. [Eval Statement](language/eval.md) 9. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) - 2. [Operators](appendix/operators.md) + 2. [Operators and Symbols](appendix/operators.md) 3. [Literals](appendix/literals.md) diff --git a/doc/src/appendix/operators.md b/doc/src/appendix/operators.md index 074205e2..3e895da9 100644 --- a/doc/src/appendix/operators.md +++ b/doc/src/appendix/operators.md @@ -1,8 +1,12 @@ -Operators -========= +Operators and Symbols +==================== {{#include ../links.md}} + +Operators +--------- + | Operator | Description | Binary? | Binding direction | | :---------------: | ------------------------------ | :-----: | :---------------: | | `+` | Add | Yes | Left | @@ -28,3 +32,21 @@ Operators | `!` | Boolean _Not_ | No | Left | | `[` .. `]` | Indexing | Yes | Right | | `.` | Property access, Method call | Yes | Right | + + +Symbols +------- + +| Symbol | Description | +| ------------ | ------------------------ | +| `:` | Property value separator | +| `::` | Module path separator | +| `=>` | _Reserved_ | +| `->` | _Reserved_ | +| `<-` | _Reserved_ | +| `===` | _Reserved_ | +| `!==` | _Reserved_ | +| `:=` | _Reserved_ | +| `::<` .. `>` | _Reserved_ | +| `@` | _Reserved_ | +| `(*` .. `*)` | _Reserved_ | diff --git a/src/token.rs b/src/token.rs index 7eb7100d..fb02f14c 100644 --- a/src/token.rs +++ b/src/token.rs @@ -213,6 +213,7 @@ pub enum Token { As, LexError(Box), Comment(String), + Reserved(String), Custom(String), EOF, } @@ -229,6 +230,7 @@ impl Token { StringConstant(_) => "string".into(), CharConstant(c) => c.to_string().into(), Identifier(s) => s.clone().into(), + Reserved(s) => s.clone().into(), Custom(s) => s.clone().into(), LexError(err) => err.to_string().into(), @@ -339,7 +341,6 @@ impl Token { UnaryMinus | Multiply | Divide | - Colon | Comma | Period | Equals | @@ -750,7 +751,9 @@ fn get_next_token_inner( } } // 0x????, 0o????, 0b???? - ch @ 'x' | ch @ 'X' | ch @ 'o' | ch @ 'O' | ch @ 'b' | ch @ 'B' if c == '0' => { + ch @ 'x' | ch @ 'X' | ch @ 'o' | ch @ 'O' | ch @ 'b' | ch @ 'B' + if c == '0' => + { result.push(next_char); eat_next(stream, pos); @@ -889,42 +892,48 @@ fn get_next_token_inner( } // " - string literal - ('"', _) => return parse_string_literal(stream, state, pos, '"') - .map_or_else( - |err| Some((Token::LexError(Box::new(err.0)), err.1)), - |out| Some((Token::StringConstant(out), start_pos)), - ), + ('"', _) => { + return parse_string_literal(stream, state, pos, '"').map_or_else( + |err| Some((Token::LexError(Box::new(err.0)), err.1)), + |out| Some((Token::StringConstant(out), start_pos)), + ) + } // ' - character literal - ('\'', '\'') => return Some(( - Token::LexError(Box::new(LERR::MalformedChar("".to_string()))), - start_pos, - )), - ('\'', _) => return Some( - parse_string_literal(stream, state, pos, '\'') - .map_or_else( - |err| (Token::LexError(Box::new(err.0)), err.1), - |result| { - let mut chars = result.chars(); - let first = chars.next(); + ('\'', '\'') => { + return Some(( + Token::LexError(Box::new(LERR::MalformedChar("".to_string()))), + start_pos, + )) + } + ('\'', _) => { + return Some(parse_string_literal(stream, state, pos, '\'').map_or_else( + |err| (Token::LexError(Box::new(err.0)), err.1), + |result| { + let mut chars = result.chars(); + let first = chars.next(); - if chars.next().is_some() { - ( - Token::LexError(Box::new(LERR::MalformedChar(result))), - start_pos, - ) - } else { - (Token::CharConstant(first.expect("should be Some")), start_pos) - } - }, - ), - ), + if chars.next().is_some() { + ( + Token::LexError(Box::new(LERR::MalformedChar(result))), + start_pos, + ) + } else { + ( + Token::CharConstant(first.expect("should be Some")), + start_pos, + ) + } + }, + )) + } // Braces ('{', _) => return Some((Token::LeftBrace, start_pos)), ('}', _) => return Some((Token::RightBrace, start_pos)), // Parentheses + ('(', '*') => return Some((Token::Reserved("(*".into()), start_pos)), ('(', _) => return Some((Token::LeftParen, start_pos)), (')', _) => return Some((Token::RightParen, start_pos)), @@ -953,15 +962,11 @@ fn get_next_token_inner( eat_next(stream, pos); return Some((Token::MinusAssign, start_pos)); } - ('-', '>') => return Some(( - Token::LexError(Box::new(LERR::ImproperSymbol( - "'->' is not a valid symbol. This is not C or C++!".to_string(), - ))), - start_pos, - )), + ('-', '>') => return Some((Token::Reserved("->".into()), start_pos)), ('-', _) if !state.non_unary => return Some((Token::UnaryMinus, start_pos)), ('-', _) => return Some((Token::Minus, start_pos)), + ('*', ')') => return Some((Token::Reserved("*)".into()), start_pos)), ('*', '=') => { eat_next(stream, pos); return Some((Token::MultiplyAssign, start_pos)); @@ -1026,49 +1031,31 @@ fn get_next_token_inner( // Warn against `===` if stream.peek_next() == Some('=') { - return Some(( - Token::LexError(Box::new(LERR::ImproperSymbol( - "'===' is not a valid operator. This is not JavaScript! Should it be '=='?" - .to_string(), - ))), - start_pos, - )); + return Some((Token::Reserved("===".into()), start_pos)); } return Some((Token::EqualsTo, start_pos)); } - ('=', '>') => return Some(( - Token::LexError(Box::new(LERR::ImproperSymbol( - "'=>' is not a valid symbol. This is not Rust! Should it be '>='?" - .to_string(), - ))), - start_pos, - )), + ('=', '>') => return Some((Token::Reserved("=>".into()), start_pos)), ('=', _) => return Some((Token::Equals, start_pos)), (':', ':') => { eat_next(stream, pos); + + if stream.peek_next() == Some('<') { + return Some((Token::Reserved("::<".into()), start_pos)); + } + return Some((Token::DoubleColon, start_pos)); } - (':', '=') => return Some(( - Token::LexError(Box::new(LERR::ImproperSymbol( - "':=' is not a valid assignment operator. This is not Pascal! Should it be simply '='?" - .to_string(), - ))), - start_pos, - )), + (':', '=') => return Some((Token::Reserved(":=".into()), start_pos)), (':', _) => return Some((Token::Colon, start_pos)), ('<', '=') => { eat_next(stream, pos); return Some((Token::LessThanEqualsTo, start_pos)); } - ('<', '-') => return Some(( - Token::LexError(Box::new(LERR::ImproperSymbol( - "'<-' is not a valid symbol. Should it be '<='?".to_string(), - ))), - start_pos, - )), + ('<', '-') => return Some((Token::Reserved("<-".into()), start_pos)), ('<', '<') => { eat_next(stream, pos); @@ -1106,15 +1093,8 @@ fn get_next_token_inner( ('!', '=') => { eat_next(stream, pos); - // Warn against `!==` if stream.peek_next() == Some('=') { - return Some(( - Token::LexError(Box::new(LERR::ImproperSymbol( - "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?" - .to_string(), - ))), - start_pos, - )); + return Some((Token::Reserved("!==".into()), start_pos)); } return Some((Token::NotEqualsTo, start_pos)); @@ -1159,10 +1139,17 @@ fn get_next_token_inner( } ('~', _) => return Some((Token::PowerOf, start_pos)), + ('@', _) => return Some((Token::Reserved("@".into()), start_pos)), + ('\0', _) => unreachable!(), (ch, _) if ch.is_whitespace() => (), - (ch, _) => return Some((Token::LexError(Box::new(LERR::UnexpectedInput(ch.to_string()))), start_pos)), + (ch, _) => { + return Some(( + Token::LexError(Box::new(LERR::UnexpectedInput(ch.to_string()))), + start_pos, + )) + } } } @@ -1237,6 +1224,41 @@ impl<'a> Iterator for TokenIterator<'a, '_> { self.engine.custom_keywords.as_ref(), ) { (None, _, _) => None, + (Some((Token::Reserved(s), pos)), None, None) => return Some((match s.as_str() { + "===" => Token::LexError(Box::new(LERR::ImproperSymbol( + "'===' is not a valid operator. This is not JavaScript! Should it be '=='?" + .to_string(), + ))), + "!==" => Token::LexError(Box::new(LERR::ImproperSymbol( + "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?" + .to_string(), + ))), + "->" => Token::LexError(Box::new(LERR::ImproperSymbol( + "'->' is not a valid symbol. This is not C or C++!".to_string(), + ))), + "<-" => Token::LexError(Box::new(LERR::ImproperSymbol( + "'<-' is not a valid symbol. This is not Go! Should it be '<='?".to_string(), + ))), + "=>" => Token::LexError(Box::new(LERR::ImproperSymbol( + "'=>' is not a valid symbol. This is not Rust! Should it be '>='?" + .to_string(), + ))), + ":=" => Token::LexError(Box::new(LERR::ImproperSymbol( + "':=' is not a valid assignment operator. This is not Go! Should it be simply '='?" + .to_string(), + ))), + "::<" => Token::LexError(Box::new(LERR::ImproperSymbol( + "'::<>' is not a valid symbol. This is not Rust! Should it be '::'?" + .to_string(), + ))), + "(*" | "*)" => Token::LexError(Box::new(LERR::ImproperSymbol( + "'(* .. *)' is not a valid comment style. This is not Pascal! Should it be '/* .. */'?" + .to_string(), + ))), + token => Token::LexError(Box::new(LERR::ImproperSymbol( + format!("'{}' is not a valid symbol.", token) + ))), + }, pos)), (r @ Some(_), None, None) => r, (Some((token, pos)), Some(disabled), _) if token.is_operator() && disabled.contains(token.syntax().as_ref()) => From 703cc414b871651d7674941a9fd2a171dd35d9cf Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 8 Jul 2020 13:06:00 +0800 Subject: [PATCH 39/46] Allow mutating a module-qualified function's first argument if it is a variable. --- src/engine.rs | 168 ++++++++++++++++++++++++++++++++++++----------- src/module.rs | 2 +- tests/modules.rs | 16 +++++ 3 files changed, 145 insertions(+), 41 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index f999235b..297c3906 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -399,6 +399,39 @@ fn default_print(s: &str) { /// Search for a module within an imports stack. /// Position in `EvalAltResult` is None and must be set afterwards. fn search_imports<'s>( + mods: &'s Imports, + state: &mut State, + modules: &Box, +) -> Result<&'s Module, Box> { + let (root, root_pos) = modules.get(0); + + // Qualified - check if the root module is directly indexed + let index = if state.always_search { + None + } else { + modules.index() + }; + + Ok(if let Some(index) = index { + let offset = mods.len() - index.get(); + &mods.get(offset).unwrap().1 + } else { + mods.iter() + .rev() + .find(|(n, _)| n == root) + .map(|(_, m)| m) + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorModuleNotFound( + root.to_string(), + *root_pos, + )) + })? + }) +} + +/// Search for a module within an imports stack. +/// Position in `EvalAltResult` is None and must be set afterwards. +fn search_imports_mut<'s>( mods: &'s mut Imports, state: &mut State, modules: &Box, @@ -429,15 +462,49 @@ fn search_imports<'s>( }) } -/// Search for a variable within the scope -fn search_scope<'s, 'a>( +/// Search for a variable within the scope and imports +fn search_namespace<'s, 'a>( scope: &'s mut Scope, mods: &'s mut Imports, state: &mut State, this_ptr: &'s mut Option<&mut Dynamic>, expr: &'a Expr, ) -> Result<(&'s mut Dynamic, &'a str, ScopeEntryType, Position), Box> { - let ((name, pos), modules, hash_var, index) = match expr { + match expr { + Expr::Variable(v) => match v.as_ref() { + // Qualified variable + ((name, pos), Some(modules), hash_var, _) => { + let module = search_imports_mut(mods, state, modules)?; + let target = module + .get_qualified_var_mut(*hash_var) + .map_err(|err| match *err { + EvalAltResult::ErrorVariableNotFound(_, _) => { + Box::new(EvalAltResult::ErrorVariableNotFound( + format!("{}{}", modules, name), + *pos, + )) + } + _ => err.new_position(*pos), + })?; + + // Module variables are constant + Ok((target, name, ScopeEntryType::Constant, *pos)) + } + // Normal variable access + _ => search_scope_only(scope, state, this_ptr, expr), + }, + _ => unreachable!(), + } +} + +/// Search for a variable within the scope +fn search_scope_only<'s, 'a>( + scope: &'s mut Scope, + state: &mut State, + this_ptr: &'s mut Option<&mut Dynamic>, + expr: &'a Expr, +) -> Result<(&'s mut Dynamic, &'a str, ScopeEntryType, Position), Box> { + let ((name, pos), _, _, index) = match expr { Expr::Variable(v) => v.as_ref(), _ => unreachable!(), }; @@ -451,37 +518,21 @@ fn search_scope<'s, 'a>( } } - // Check if it is qualified - if let Some(modules) = modules { - let module = search_imports(mods, state, modules)?; - let target = module - .get_qualified_var_mut(*hash_var) - .map_err(|err| match *err { - EvalAltResult::ErrorVariableNotFound(_, _) => Box::new( - EvalAltResult::ErrorVariableNotFound(format!("{}{}", modules, name), *pos), - ), - _ => err.new_position(*pos), - })?; + // Check if it is directly indexed + let index = if state.always_search { None } else { *index }; - // Module variables are constant - Ok((target, name, ScopeEntryType::Constant, *pos)) + let index = if let Some(index) = index { + scope.len() - index.get() } else { - // Unqualified - check if it is directly indexed - let index = if state.always_search { None } else { *index }; + // Find the variable in the scope + scope + .get_index(name) + .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), *pos)))? + .0 + }; - let index = if let Some(index) = index { - scope.len() - index.get() - } else { - // Find the variable in the scope - scope - .get_index(name) - .ok_or_else(|| Box::new(EvalAltResult::ErrorVariableNotFound(name.into(), *pos)))? - .0 - }; - - let (val, typ) = scope.get_mut(index); - Ok((val, name, typ, *pos)) - } + let (val, typ) = scope.get_mut(index); + Ok((val, name, typ, *pos)) } impl Engine { @@ -1276,7 +1327,8 @@ impl Engine { self.inc_operations(state) .map_err(|err| err.new_position(*var_pos))?; - let (target, _, typ, pos) = search_scope(scope, mods, state, this_ptr, dot_lhs)?; + let (target, _, typ, pos) = + search_namespace(scope, mods, state, this_ptr, dot_lhs)?; // Constants cannot be modified match typ { @@ -1573,7 +1625,7 @@ impl Engine { } } Expr::Variable(_) => { - let (val, _, _, _) = search_scope(scope, mods, state, this_ptr, expr)?; + let (val, _, _, _) = search_namespace(scope, mods, state, this_ptr, expr)?; Ok(val.clone()) } Expr::Property(_) => unreachable!(), @@ -1587,7 +1639,7 @@ impl Engine { let mut rhs_val = self.eval_expr(scope, mods, state, lib, this_ptr, rhs_expr, level)?; let (lhs_ptr, name, typ, pos) = - search_scope(scope, mods, state, this_ptr, lhs_expr)?; + search_namespace(scope, mods, state, this_ptr, lhs_expr)?; self.inc_operations(state) .map_err(|err| err.new_position(pos))?; @@ -1800,7 +1852,7 @@ impl Engine { .collect::>()?; let (target, _, _, pos) = - search_scope(scope, mods, state, this_ptr, lhs)?; + search_namespace(scope, mods, state, this_ptr, lhs)?; self.inc_operations(state) .map_err(|err| err.new_position(pos))?; @@ -1836,12 +1888,48 @@ impl Engine { let ((name, _, pos), modules, hash_script, args_expr, def_val) = x.as_ref(); let modules = modules.as_ref().unwrap(); - let mut arg_values = args_expr - .iter() - .map(|expr| self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)) - .collect::, _>>()?; + let mut arg_values: StaticVec; + let mut args: StaticVec<_>; - let mut args: StaticVec<_> = arg_values.iter_mut().collect(); + if args_expr.is_empty() { + // No arguments + args = Default::default(); + } else { + // See if the first argument is a variable (not module-qualified). + // If so, convert to method-call style in order to leverage potential + // &mut first argument and avoid cloning the value + match args_expr.get(0) { + // func(x, ...) -> x.func(...) + Expr::Variable(x) if x.1.is_none() => { + arg_values = args_expr + .iter() + .skip(1) + .map(|expr| { + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) + }) + .collect::>()?; + + let (target, _, _, pos) = + search_scope_only(scope, state, this_ptr, args_expr.get(0))?; + + self.inc_operations(state) + .map_err(|err| err.new_position(pos))?; + + args = once(target).chain(arg_values.iter_mut()).collect(); + } + // func(..., ...) or func(mod::x, ...) + _ => { + arg_values = args_expr + .iter() + .map(|expr| { + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) + }) + .collect::>()?; + + args = arg_values.iter_mut().collect(); + } + } + } let module = search_imports(mods, state, modules)?; diff --git a/src/module.rs b/src/module.rs index 9fa9f9a7..7181e4d7 100644 --- a/src/module.rs +++ b/src/module.rs @@ -849,7 +849,7 @@ impl Module { /// The `u64` hash is calculated by the function `crate::calc_fn_hash` and must match /// the hash calculated by `index_all_sub_modules`. pub(crate) fn get_qualified_fn( - &mut self, + &self, hash_qualified_fn: u64, ) -> Result<&Func, Box> { self.all_functions.get(&hash_qualified_fn).ok_or_else(|| { diff --git a/tests/modules.rs b/tests/modules.rs index 0e1eb3ba..479ec429 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -73,6 +73,10 @@ fn test_module_resolver() -> Result<(), Box> { module.set_fn_4("sum".to_string(), |x: INT, y: INT, z: INT, w: INT| { Ok(x + y + z + w) }); + module.set_fn_1_mut("double".to_string(), |x: &mut INT| { + *x *= 2; + Ok(()) + }); resolver.insert("hello", module); @@ -90,6 +94,18 @@ fn test_module_resolver() -> Result<(), Box> { 42 ); + assert_eq!( + engine.eval::( + r#" + import "hello" as h; + let x = 21; + h::double(x); + x + "# + )?, + 42 + ); + #[cfg(not(feature = "unchecked"))] { engine.set_max_modules(5); From e33760a7d4b25af7064c5d0c70b7fcf1d7edd20c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 9 Jul 2020 13:27:21 +0800 Subject: [PATCH 40/46] Fix bug in StaticVec. --- src/utils.rs | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 77cef0e7..5dd619ba 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -356,10 +356,13 @@ impl StaticVec { panic!("nothing to pop!"); } - let result = if self.is_fixed_storage() { - self.extract_from_list(self.len - 1) + if self.is_fixed_storage() { + let value = self.extract_from_list(self.len - 1); + self.len -= 1; + value } else { let value = self.more.pop().unwrap(); + self.len -= 1; // Move back to the fixed list if self.more.len() == MAX_STATIC_VEC { @@ -370,11 +373,7 @@ impl StaticVec { } value - }; - - self.len -= 1; - - result + } } /// Remove a value from this `StaticVec` at a particular position. /// @@ -386,18 +385,20 @@ impl StaticVec { panic!("index OOB in StaticVec"); } - let result = if self.is_fixed_storage() { + if self.is_fixed_storage() { let value = self.extract_from_list(index); // Move all items one slot to the left - for x in index..self.len - 1 { - let orig_value = self.extract_from_list(x + 1); - self.set_into_list(x, orig_value, false); + for x in index + 1..self.len - 1 { + let orig_value = self.extract_from_list(x); + self.set_into_list(x - 1, orig_value, false); } + self.len -= 1; value } else { let value = self.more.remove(index); + self.len -= 1; // Move back to the fixed list if self.more.len() == MAX_STATIC_VEC { @@ -408,11 +409,7 @@ impl StaticVec { } value - }; - - self.len -= 1; - - result + } } /// Get the number of items in this `StaticVec`. #[inline(always)] From 99164ebceb33c192022db665e1eb71fe3379c070 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 9 Jul 2020 19:54:28 +0800 Subject: [PATCH 41/46] Add custom syntax. --- README.md | 3 +- RELEASES.md | 1 + src/engine.rs | 39 ++++++++++++- src/lib.rs | 11 +++- src/optimize.rs | 10 +++- src/parser.rs | 104 ++++++++++++++++++++++++++++++++++- src/syntax.rs | 132 ++++++++++++++++++++++++++++++++++++++++++++ src/token.rs | 142 ++++++++++++++++++++++++++++++++++-------------- tests/maps.rs | 104 +++++++---------------------------- tests/syntax.rs | 69 +++++++++++++++++++++++ 10 files changed, 483 insertions(+), 132 deletions(-) create mode 100644 src/syntax.rs create mode 100644 tests/syntax.rs diff --git a/README.md b/README.md index e448ba69..285abe90 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ Features * Rugged - protected against malicious attacks (such as [stack-overflow](https://schungx.github.io/rhai/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/rhai/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/rhai/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts. * Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run. * [Function overloading](https://schungx.github.io/rhai/language/overload.html). -* [Operator overloading](https://schungx.github.io/rhai/rust/operators.html) and [custom operators](https://schungx.github.io/rhai/engine/custom-op.html). +* [Operator overloading](https://schungx.github.io/rhai/rust/operators.html). +* Support for use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html) - [disabling keywords/operators](https://schungx.github.io/rhai/engine/disable.html), [custom operators](https://schungx.github.io/rhai/engine/custom-op.html). * Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html). * Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). diff --git a/RELEASES.md b/RELEASES.md index 4afe903b..8a022390 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -31,6 +31,7 @@ New features * The boolean `^` (XOR) operator is added. * `FnPtr` is exposed as the function pointer type. * `rhai::module_resolvers::ModuleResolversCollection` added to try a list of module resolvers. +* It is now possible to mutate the first argument of a module-qualified function call when the argument is a simple variable (but not a module constant). Version 0.16.1 diff --git a/src/engine.rs b/src/engine.rs index 297c3906..ee963eb2 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -11,6 +11,7 @@ use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, St use crate::r#unsafe::unsafe_cast_var_name_to_lifetime; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; +use crate::syntax::CustomSyntax; use crate::token::Position; use crate::utils::StaticVec; @@ -82,8 +83,12 @@ pub const KEYWORD_THIS: &str = "this"; pub const FN_TO_STRING: &str = "to_string"; pub const FN_GET: &str = "get$"; pub const FN_SET: &str = "set$"; -pub const FN_IDX_GET: &str = "$index$get$"; -pub const FN_IDX_SET: &str = "$index$set$"; +pub const FN_IDX_GET: &str = "index$get$"; +pub const FN_IDX_SET: &str = "index$set$"; +pub const MARKER_EXPR: &str = "$expr$"; +pub const MARKER_STMT: &str = "$stmt$"; +pub const MARKER_BLOCK: &str = "$block$"; +pub const MARKER_IDENT: &str = "$ident$"; /// A type specifying the method of chaining. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] @@ -273,6 +278,8 @@ pub struct Engine { pub(crate) disabled_symbols: Option>, /// A hashset containing custom keywords and precedence to recognize. pub(crate) custom_keywords: Option>, + /// Custom syntax. + pub(crate) custom_syntax: Option>, /// Callback closure for implementing the `print` command. pub(crate) print: Callback, @@ -322,6 +329,7 @@ impl Default for Engine { type_names: None, disabled_symbols: None, custom_keywords: None, + custom_syntax: None, // default print/debug implementations print: Box::new(default_print), @@ -554,6 +562,7 @@ impl Engine { type_names: None, disabled_symbols: None, custom_keywords: None, + custom_syntax: None, print: Box::new(|_| {}), debug: Box::new(|_| {}), @@ -1595,6 +1604,26 @@ impl Engine { } } + /// Evaluate an expression inside an AST. + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. It evaluates an expression from an AST. + #[cfg(feature = "internals")] + #[deprecated(note = "this method is volatile and may change")] + pub fn eval_expr_from_ast( + &self, + scope: &mut Scope, + mods: &mut Imports, + state: &mut State, + lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, + expr: &Expr, + level: usize, + ) -> Result> { + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) + } + /// Evaluate an expression fn eval_expr( &self, @@ -2026,6 +2055,12 @@ impl Engine { Expr::False(_) => Ok(false.into()), Expr::Unit(_) => Ok(().into()), + Expr::Custom(x) => { + let func = (x.0).1.as_ref(); + let exprs = (x.0).0.as_ref(); + func(self, scope, mods, state, lib, this_ptr, exprs, level) + } + _ => unreachable!(), }; diff --git a/src/lib.rs b/src/lib.rs index 768ad25a..b6e73b49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,6 +91,7 @@ mod scope; mod serde; mod settings; mod stdlib; +mod syntax; mod token; mod r#unsafe; mod utils; @@ -153,13 +154,21 @@ pub use optimize::OptimizationLevel; // Expose internal data structures. +#[cfg(feature = "internals")] +#[deprecated(note = "this type is volatile and may change")] +pub use error::LexError; + #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] pub use token::{get_next_token, parse_string_literal, InputStream, Token, TokenizeState}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] -pub use parser::{Expr, ReturnType, ScriptFnDef, Stmt}; +pub use parser::{CustomExpr, Expr, ReturnType, ScriptFnDef, Stmt}; + +#[cfg(feature = "internals")] +#[deprecated(note = "this type is volatile and may change")] +pub use engine::{Imports, State as EvalState}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] diff --git a/src/optimize.rs b/src/optimize.rs index f3f09919..0fad8450 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -2,7 +2,7 @@ use crate::any::Dynamic; use crate::calc_fn_hash; use crate::engine::{Engine, Imports, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, KEYWORD_TYPE_OF}; use crate::module::Module; -use crate::parser::{map_dynamic_to_expr, Expr, ReturnType, ScriptFnDef, Stmt, AST}; +use crate::parser::{map_dynamic_to_expr, CustomExpr, Expr, ReturnType, ScriptFnDef, Stmt, AST}; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; use crate::utils::StaticVec; @@ -598,6 +598,14 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { state.find_constant(&name).expect("should find constant in scope!").clone().set_position(pos) } + // Custom syntax + Expr::Custom(x) => Expr::Custom(Box::new(( + CustomExpr( + (x.0).0.into_iter().map(|expr| optimize_expr(expr, state)).collect(), + (x.0).1), + x.1 + ))), + // All other expressions - skip expr => expr, } diff --git a/src/parser.rs b/src/parser.rs index b9320132..c32a919a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,11 +2,16 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, KEYWORD_THIS}; +use crate::engine::{ + make_getter, make_setter, Engine, KEYWORD_THIS, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT, + MARKER_STMT, +}; use crate::error::{LexError, ParseError, ParseErrorType}; +use crate::fn_native::Shared; use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; +use crate::syntax::FnCustomSyntaxEval; use crate::token::{Position, Token, TokenStream}; use crate::utils::{StaticVec, StraightHasherBuilder}; @@ -568,6 +573,15 @@ impl Stmt { } } +#[derive(Clone)] +pub struct CustomExpr(pub StaticVec, pub Shared); + +impl fmt::Debug for CustomExpr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + /// An expression. /// /// Each variant is at most one pointer in size (for speed), @@ -632,6 +646,8 @@ pub enum Expr { False(Position), /// () Unit(Position), + /// Custom syntax + Custom(Box<(CustomExpr, Position)>), } impl Default for Expr { @@ -726,6 +742,8 @@ impl Expr { Self::True(pos) | Self::False(pos) | Self::Unit(pos) => *pos, Self::Dot(x) | Self::Index(x) => x.0.position(), + + Self::Custom(x) => x.1, } } @@ -758,6 +776,7 @@ impl Expr { Self::Assignment(x) => x.3 = new_pos, Self::Dot(x) => x.2 = new_pos, Self::Index(x) => x.2 = new_pos, + Self::Custom(x) => x.1 = new_pos, } self @@ -861,6 +880,8 @@ impl Expr { Token::LeftParen => true, _ => false, }, + + Self::Custom(_) => false, } } @@ -2024,6 +2045,85 @@ fn parse_expr( settings.pos = input.peek().unwrap().1; settings.ensure_level_within_max_limit(state.max_expr_depth)?; + // Check if it is a custom syntax. + if let Some(ref custom) = state.engine.custom_syntax { + let (token, pos) = input.peek().unwrap(); + let token_pos = *pos; + + match token { + Token::Custom(key) if custom.contains_key(key) => { + let custom = custom.get_key_value(key).unwrap(); + let (key, syntax) = custom; + + input.next().unwrap(); + + let mut exprs: StaticVec = Default::default(); + + // Adjust the variables stack + match syntax.scope_delta { + delta if delta > 0 => { + state.stack.push(("".to_string(), ScopeEntryType::Normal)) + } + delta if delta < 0 && state.stack.len() <= delta.abs() as usize => { + state.stack.clear() + } + delta if delta < 0 => state + .stack + .truncate(state.stack.len() - delta.abs() as usize), + _ => (), + } + + for segment in syntax.segments.iter() { + settings.pos = input.peek().unwrap().1; + let settings = settings.level_up(); + + match segment.as_str() { + MARKER_IDENT => match input.next().unwrap() { + (Token::Identifier(s), pos) => { + exprs.push(Expr::Variable(Box::new(((s, pos), None, 0, None)))); + } + (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), + }, + MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?), + MARKER_STMT => { + let stmt = parse_stmt(input, state, lib, settings)? + .unwrap_or_else(|| Stmt::Noop(settings.pos)); + let pos = stmt.position(); + exprs.push(Expr::Stmt(Box::new((stmt, pos)))) + } + MARKER_BLOCK => { + let stmt = parse_block(input, state, lib, settings)?; + let pos = stmt.position(); + exprs.push(Expr::Stmt(Box::new((stmt, pos)))) + } + s => match input.peek().unwrap() { + (Token::Custom(custom), _) if custom == s => { + input.next().unwrap(); + } + (t, _) if t.syntax().as_ref() == s => { + input.next().unwrap(); + } + (_, pos) => { + return Err(PERR::MissingToken( + s.to_string(), + format!("for '{}' expression", key), + ) + .into_err(*pos)) + } + }, + } + } + + return Ok(Expr::Custom(Box::new(( + CustomExpr(exprs, syntax.func.clone()), + token_pos, + )))); + } + _ => (), + } + } + + // Parse expression normally. let lhs = parse_unary(input, state, lib, settings.level_up())?; parse_binary_op(input, state, lib, 1, lhs, settings.level_up()) } @@ -2297,7 +2397,7 @@ fn parse_import( fn parse_export( input: &mut TokenStream, state: &mut ParseState, - lib: &mut FunctionsLib, + _lib: &mut FunctionsLib, mut settings: ParseSettings, ) -> Result { settings.pos = eat_token(input, Token::Export); diff --git a/src/syntax.rs b/src/syntax.rs new file mode 100644 index 00000000..e1a05786 --- /dev/null +++ b/src/syntax.rs @@ -0,0 +1,132 @@ +use crate::any::Dynamic; +use crate::engine::{Engine, Imports, State, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT, MARKER_STMT}; +use crate::error::LexError; +use crate::fn_native::{SendSync, Shared}; +use crate::module::Module; +use crate::parser::Expr; +use crate::result::EvalAltResult; +use crate::scope::Scope; +use crate::token::{is_valid_identifier, Token}; +use crate::utils::StaticVec; + +use crate::stdlib::{ + fmt, + rc::Rc, + string::{String, ToString}, + sync::Arc, +}; + +/// A general function trail object. +#[cfg(not(feature = "sync"))] +pub type FnCustomSyntaxEval = dyn Fn( + &Engine, + &mut Scope, + &mut Imports, + &mut State, + &Module, + &mut Option<&mut Dynamic>, + &[Expr], + usize, +) -> Result>; +/// A general function trail object. +#[cfg(feature = "sync")] +pub type FnCustomSyntaxEval = dyn Fn( + &Engine, + &mut Scope, + &mut Imports, + &mut State, + &Module, + &mut Option<&mut Dynamic>, + &[Expr], + usize, + ) -> Result> + + Send + + Sync; + +#[derive(Clone)] +pub struct CustomSyntax { + pub segments: StaticVec, + pub func: Shared, + pub scope_delta: isize, +} + +impl fmt::Debug for CustomSyntax { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.segments, f) + } +} + +impl Engine { + pub fn add_custom_syntax + ToString>( + &mut self, + value: &[S], + scope_delta: isize, + func: impl Fn( + &Engine, + &mut Scope, + &mut Imports, + &mut State, + &Module, + &mut Option<&mut Dynamic>, + &[Expr], + usize, + ) -> Result> + + SendSync + + 'static, + ) -> Result<(), Box> { + if value.is_empty() { + return Err(Box::new(LexError::ImproperSymbol("".to_string()))); + } + + let mut segments: StaticVec<_> = Default::default(); + + for s in value { + let seg = match s.as_ref() { + // Markers not in first position + MARKER_EXPR | MARKER_STMT | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => { + s.to_string() + } + // Standard symbols not in first position + s if !segments.is_empty() && Token::lookup_from_syntax(s).is_some() => s.into(), + // Custom keyword + s if is_valid_identifier(s.chars()) => { + if self.custom_keywords.is_none() { + self.custom_keywords = Some(Default::default()); + } + + if !self.custom_keywords.as_ref().unwrap().contains_key(s) { + self.custom_keywords.as_mut().unwrap().insert(s.into(), 0); + } + + s.into() + } + // Anything else is an error + _ => return Err(Box::new(LexError::ImproperSymbol(s.to_string()))), + }; + + segments.push(seg); + } + + let key = segments.remove(0); + + let syntax = CustomSyntax { + segments, + #[cfg(not(feature = "sync"))] + func: Rc::new(func), + #[cfg(feature = "sync")] + func: Arc::new(func), + scope_delta, + }; + + if self.custom_syntax.is_none() { + self.custom_syntax = Some(Default::default()); + } + + self.custom_syntax + .as_mut() + .unwrap() + .insert(key, syntax.into()); + + Ok(()) + } +} diff --git a/src/token.rs b/src/token.rs index fb02f14c..484c7b84 100644 --- a/src/token.rs +++ b/src/token.rs @@ -312,6 +312,87 @@ impl Token { } } + /// Reverse lookup a token from a piece of syntax. + pub fn lookup_from_syntax(syntax: &str) -> Option { + use Token::*; + + Some(match syntax { + "{" => LeftBrace, + "}" => RightBrace, + "(" => LeftParen, + ")" => RightParen, + "[" => LeftBracket, + "]" => RightBracket, + "+" => Plus, + "-" => Minus, + "*" => Multiply, + "/" => Divide, + ";" => SemiColon, + ":" => Colon, + "::" => DoubleColon, + "," => Comma, + "." => Period, + "#{" => MapStart, + "=" => Equals, + "true" => True, + "false" => False, + "let" => Let, + "const" => Const, + "if" => If, + "else" => Else, + "while" => While, + "loop" => Loop, + "for" => For, + "in" => In, + "<" => LessThan, + ">" => GreaterThan, + "!" => Bang, + "<=" => LessThanEqualsTo, + ">=" => GreaterThanEqualsTo, + "==" => EqualsTo, + "!=" => NotEqualsTo, + "|" => Pipe, + "||" => Or, + "&" => Ampersand, + "&&" => And, + #[cfg(not(feature = "no_function"))] + "fn" => Fn, + "continue" => Continue, + "break" => Break, + "return" => Return, + "throw" => Throw, + "+=" => PlusAssign, + "-=" => MinusAssign, + "*=" => MultiplyAssign, + "/=" => DivideAssign, + "<<=" => LeftShiftAssign, + ">>=" => RightShiftAssign, + "&=" => AndAssign, + "|=" => OrAssign, + "^=" => XOrAssign, + "<<" => LeftShift, + ">>" => RightShift, + "^" => XOr, + "%" => Modulo, + "%=" => ModuloAssign, + "~" => PowerOf, + "~=" => PowerOfAssign, + #[cfg(not(feature = "no_function"))] + "private" => Private, + #[cfg(not(feature = "no_module"))] + "import" => Import, + #[cfg(not(feature = "no_module"))] + "export" => Export, + #[cfg(not(feature = "no_module"))] + "as" => As, + "===" | "!==" | "->" | "<-" | "=>" | ":=" | "::<" | "(*" | "*)" | "#" => { + Reserved(syntax.into()) + } + + _ => return None, + }) + } + // Is this token EOF? pub fn is_eof(&self) -> bool { use Token::*; @@ -628,9 +709,9 @@ pub fn parse_string_literal( } /// Consume the next character. -fn eat_next(stream: &mut impl InputStream, pos: &mut Position) { - stream.get_next(); +fn eat_next(stream: &mut impl InputStream, pos: &mut Position) -> Option { pos.advance(); + stream.get_next() } /// Scan for a block comment until the end. @@ -858,35 +939,8 @@ fn get_next_token_inner( } return Some(( - match identifier.as_str() { - "true" => Token::True, - "false" => Token::False, - "let" => Token::Let, - "const" => Token::Const, - "if" => Token::If, - "else" => Token::Else, - "while" => Token::While, - "loop" => Token::Loop, - "continue" => Token::Continue, - "break" => Token::Break, - "return" => Token::Return, - "throw" => Token::Throw, - "for" => Token::For, - "in" => Token::In, - #[cfg(not(feature = "no_function"))] - "private" => Token::Private, - #[cfg(not(feature = "no_module"))] - "import" => Token::Import, - #[cfg(not(feature = "no_module"))] - "export" => Token::Export, - #[cfg(not(feature = "no_module"))] - "as" => Token::As, - - #[cfg(not(feature = "no_function"))] - "fn" => Token::Fn, - - _ => Token::Identifier(identifier), - }, + Token::lookup_from_syntax(&identifier) + .unwrap_or_else(|| Token::Identifier(identifier)), start_pos, )); } @@ -947,6 +1001,7 @@ fn get_next_token_inner( eat_next(stream, pos); return Some((Token::MapStart, start_pos)); } + ('#', _) => return Some((Token::Reserved("#".into()), start_pos)), // Operators ('+', '=') => { @@ -1163,40 +1218,42 @@ fn get_next_token_inner( } /// A type that implements the `InputStream` trait. -/// Multiple charaacter streams are jointed together to form one single stream. +/// Multiple character streams are jointed together to form one single stream. pub struct MultiInputsStream<'a> { /// The input character streams. streams: StaticVec>>, + /// The current stream index. + index: usize, } impl InputStream for MultiInputsStream<'_> { /// Get the next character fn get_next(&mut self) -> Option { loop { - if self.streams.is_empty() { + if self.index >= self.streams.len() { // No more streams return None; - } else if let Some(ch) = self.streams[0].next() { + } else if let Some(ch) = self.streams[self.index].next() { // Next character in current stream return Some(ch); } else { // Jump to the next stream - let _ = self.streams.remove(0); + self.index += 1; } } } /// Peek the next character fn peek_next(&mut self) -> Option { loop { - if self.streams.is_empty() { + if self.index >= self.streams.len() { // No more streams return None; - } else if let Some(ch) = self.streams[0].peek() { + } else if let Some(&ch) = self.streams[self.index].peek() { // Next character in current stream - return Some(*ch); + return Some(ch); } else { // Jump to the next stream - let _ = self.streams.remove(0); + self.index += 1; } } } @@ -1252,7 +1309,11 @@ impl<'a> Iterator for TokenIterator<'a, '_> { .to_string(), ))), "(*" | "*)" => Token::LexError(Box::new(LERR::ImproperSymbol( - "'(* .. *)' is not a valid comment style. This is not Pascal! Should it be '/* .. */'?" + "'(* .. *)' is not a valid comment format. This is not Pascal! Should it be '/* .. */'?" + .to_string(), + ))), + "#" => Token::LexError(Box::new(LERR::ImproperSymbol( + "'#' is not a valid symbol. Should it be '#{'?" .to_string(), ))), token => Token::LexError(Box::new(LERR::ImproperSymbol( @@ -1298,6 +1359,7 @@ pub fn lex<'a, 'e>(input: &'a [&'a str], engine: &'e Engine) -> TokenIterator<'a pos: Position::new(1, 0), stream: MultiInputsStream { streams: input.iter().map(|s| s.chars().peekable()).collect(), + index: 0, }, } } diff --git a/tests/maps.rs b/tests/maps.rs index bc2b24a9..c57deaa6 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -21,7 +21,7 @@ fn test_map_indexing() -> Result<(), Box> { r#" let y = #{d: 1, "e": #{a: 42, b: 88, "": "hello"}, " 123 xyz": 9}; y.e[""][4] - "# + "# )?, 'o' ); @@ -47,7 +47,7 @@ fn test_map_indexing() -> Result<(), Box> { let x = #{a: 1, b: 2, c: 3}; let c = x.remove("c"); x.len() + c - "# + "# )?, 5 ); @@ -58,7 +58,7 @@ fn test_map_indexing() -> Result<(), Box> { let y = #{b: 42, d: 9}; x.mixin(y); x.len() + x.b - " + " )?, 46 ); @@ -68,7 +68,7 @@ fn test_map_indexing() -> Result<(), Box> { let x = #{a: 1, b: 2, c: 3}; x += #{b: 42, d: 9}; x.len() + x.b - " + " )?, 46 ); @@ -79,7 +79,7 @@ fn test_map_indexing() -> Result<(), Box> { let x = #{a: 1, b: 2, c: 3}; let y = #{b: 42, d: 9}; x + y - " + " )? .len(), 4 @@ -94,27 +94,9 @@ fn test_map_assign() -> Result<(), Box> { let x = engine.eval::(r#"let x = #{a: 1, b: true, "c$": "hello"}; x"#)?; - assert_eq!( - x.get("a") - .cloned() - .expect("should have property a") - .cast::(), - 1 - ); - assert_eq!( - x.get("b") - .cloned() - .expect("should have property b") - .cast::(), - true - ); - assert_eq!( - x.get("c$") - .cloned() - .expect("should have property c$") - .cast::(), - "hello" - ); + assert_eq!(x["a"].clone().cast::(), 1); + assert_eq!(x["b"].clone().cast::(), true); + assert_eq!(x["c$"].clone().cast::(), "hello"); Ok(()) } @@ -125,27 +107,9 @@ fn test_map_return() -> Result<(), Box> { let x = engine.eval::(r#"#{a: 1, b: true, "c$": "hello"}"#)?; - assert_eq!( - x.get("a") - .cloned() - .expect("should have property a") - .cast::(), - 1 - ); - assert_eq!( - x.get("b") - .cloned() - .expect("should have property b") - .cast::(), - true - ); - assert_eq!( - x.get("c$") - .cloned() - .expect("should have property c$") - .cast::(), - "hello" - ); + assert_eq!(x["a"].clone().cast::(), 1); + assert_eq!(x["b"].clone().cast::(), true); + assert_eq!(x["c$"].clone().cast::(), "hello"); Ok(()) } @@ -167,7 +131,7 @@ fn test_map_for() -> Result<(), Box> { } s - "# + "# )? .len(), 11 @@ -188,41 +152,11 @@ fn test_map_json() -> Result<(), Box> { assert!(!map.contains_key("x")); - assert_eq!( - map.get("a") - .cloned() - .expect("should have property a") - .cast::(), - 1 - ); - assert_eq!( - map.get("b") - .cloned() - .expect("should have property b") - .cast::(), - true - ); - assert_eq!( - map.get("c") - .cloned() - .expect("should have property a") - .cast::(), - 42 - ); - assert_eq!( - map.get("$d e f!") - .cloned() - .expect("should have property $d e f!") - .cast::(), - "hello" - ); - assert_eq!( - map.get("z") - .cloned() - .expect("should have property z") - .cast::<()>(), - () - ); + assert_eq!(map["a"].clone().cast::(), 1); + assert_eq!(map["b"].clone().cast::(), true); + assert_eq!(map["c"].clone().cast::(), 42); + assert_eq!(map["$d e f!"].clone().cast::(), "hello"); + assert_eq!(map["z"].clone().cast::<()>(), ()); #[cfg(not(feature = "no_index"))] { @@ -241,7 +175,7 @@ fn test_map_json() -> Result<(), Box> { } s - "# + "# )? .len(), 11 @@ -265,7 +199,7 @@ fn test_map_oop() -> Result<(), Box> { obj.action(2); obj.data - "#, + "#, )?, 42 ); diff --git a/tests/syntax.rs b/tests/syntax.rs new file mode 100644 index 00000000..f12a0919 --- /dev/null +++ b/tests/syntax.rs @@ -0,0 +1,69 @@ +#![cfg(feature = "internals")] +use rhai::{ + Dynamic, Engine, EvalAltResult, EvalState, Expr, Imports, LexError, Module, Scope, INT, +}; + +#[test] +fn test_custom_syntax() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine + .add_custom_syntax( + &["do", "$ident$", "$block$", "while", "$expr$"], + 1, + |engine: &Engine, + scope: &mut Scope, + mods: &mut Imports, + state: &mut EvalState, + lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, + exprs: &[Expr], + level: usize| { + let var_name = match exprs.get(0).unwrap() { + Expr::Variable(s) => (s.0).0.clone(), + _ => unreachable!(), + }; + let stmt = exprs.get(1).unwrap(); + let expr = exprs.get(2).unwrap(); + + scope.push(var_name, 0 as INT); + + loop { + engine.eval_expr_from_ast(scope, mods, state, lib, this_ptr, stmt, level)?; + + if !engine + .eval_expr_from_ast(scope, mods, state, lib, this_ptr, expr, level)? + .as_bool() + .map_err(|_| { + EvalAltResult::ErrorBooleanArgMismatch( + "do-while".into(), + expr.position(), + ) + })? + { + break; + } + } + + Ok(().into()) + }, + ) + .unwrap(); + + assert!(matches!( + *engine.add_custom_syntax(&["!"], 0, |_, _, _, _, _, _, _, _| Ok(().into())).expect_err("should error"), + LexError::ImproperSymbol(s) if s == "!" + )); + + assert_eq!( + engine.eval::( + r" + do x { x += 1 } while x < 42; + x + " + )?, + 42 + ); + + Ok(()) +} From f36b4a69ae4ad6be03674c25d6297de3bc8058c3 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 9 Jul 2020 22:21:07 +0800 Subject: [PATCH 42/46] FIXED - method calls inside dot chain. --- RELEASES.md | 5 + doc/src/SUMMARY.md | 8 +- doc/src/about/features.md | 2 +- doc/src/appendix/operators.md | 1 + doc/src/engine/custom-syntax.md | 5 + doc/src/engine/dsl.md | 80 +++++++++ doc/src/links.md | 2 + doc/src/start/features.md | 2 +- src/engine.rs | 287 +++++++++++++++++++------------- 9 files changed, 275 insertions(+), 117 deletions(-) create mode 100644 doc/src/engine/custom-syntax.md create mode 100644 doc/src/engine/dsl.md diff --git a/RELEASES.md b/RELEASES.md index 8a022390..69c0186d 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,6 +11,11 @@ This version adds: * Ability to define custom operators (which must be valid identifiers). * Low-level API to register functions. +Bug fixes +--------- + +* Fixed method calls in the middle of a dot chain. + Breaking changes ---------------- diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 438b4590..b436efb9 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -105,9 +105,11 @@ The Rhai Scripting Language 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) 4. [Low-Level API](rust/register-raw.md) - 5. [Disable Keywords and/or Operators](engine/disable.md) - 6. [Custom Operators](engine/custom-op.md) - 7. [Eval Statement](language/eval.md) + 5. [Use as DSL](engine/dsl.md) + 1. [Disable Keywords and/or Operators](engine/disable.md) + 2. [Custom Operators](engine/custom-op.md) + 3. [Custom Syntax](engine/custom-syntax.md) + 6. [Eval Statement](language/eval.md) 9. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) 2. [Operators and Symbols](appendix/operators.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 5a5739cb..88f585f0 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -67,4 +67,4 @@ Flexible * Surgically [disable keywords and operators] to restrict the language. -* [Custom operators]. +* Use as a [DSL] by [disabling keywords/operators][disable keywords and operators], [custom operators] and defining [custom syntax]. diff --git a/doc/src/appendix/operators.md b/doc/src/appendix/operators.md index 3e895da9..b4303d79 100644 --- a/doc/src/appendix/operators.md +++ b/doc/src/appendix/operators.md @@ -41,6 +41,7 @@ Symbols | ------------ | ------------------------ | | `:` | Property value separator | | `::` | Module path separator | +| `#` | _Reserved_ | | `=>` | _Reserved_ | | `->` | _Reserved_ | | `<-` | _Reserved_ | diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md new file mode 100644 index 00000000..fe3f399c --- /dev/null +++ b/doc/src/engine/custom-syntax.md @@ -0,0 +1,5 @@ +Custom Syntax +============= + +{{#include ../links.md}} + diff --git a/doc/src/engine/dsl.md b/doc/src/engine/dsl.md new file mode 100644 index 00000000..adc04e44 --- /dev/null +++ b/doc/src/engine/dsl.md @@ -0,0 +1,80 @@ +Use Rhai as a Domain-Specific Language (DSL) +=========================================== + +{{#include ../links.md}} + +Rhai can be successfully used as a domain-specific language (DSL). + + +Expressions Only +---------------- + +In many DSL scenarios, only evaluation of expressions is needed. + +The `Engine::eval_expression_XXX`[`eval_expression`] API can be used to restrict +a script to expressions only. + + +Disable Keywords and/or Operators +-------------------------------- + +In some DSL scenarios, it is necessary to further restrict the language to exclude certain +language features that are not necessary or dangerous to the application. + +For example, a DSL may disable the `while` loop altogether while keeping all other statement +types intact. + +It is possible, in Rhai, to surgically [disable keywords and operators]. + + +Custom Operators +---------------- + +On the other hand, some DSL scenarios require special operators that make sense only for +that specific environment. In such cases, it is possible to define [custom operators] in Rhai. + +For example: + +```rust +let animal = "rabbit"; +let food = "carrot"; + +animal eats food // custom operator - 'eats' + +eats(animal, food) // <- the above really de-sugars to this +``` + +Although a [custom operator] always de-sugars to a simple function call, +nevertheless it makes the DSL syntax much simpler and expressive. + + +Custom Syntax +------------- + +For advanced DSL scenarios, it is possible to define entire expression [_syntax_][custom syntax] - +essentially custom statement types. + +The [`internals`] feature is needed to be able to define [custom syntax] in Rhai. + +For example: + +```rust +let table = [..., ..., ..., ...]; + +// Syntax = "select" $ident$ $ident$ "from" $expr$ "->" $ident$ ":" $expr$ +let total = select sum price from table -> row : row.weight > 50; +``` + +After registering this custom syntax with Rhai, it can be used anywhere inside a script as +a normal expression. + +For its evaluation, the callback function will receive the following list of parameters: + +`exprs[0] = "sum"` - math operator +`exprs[1] = "price"` - field name +`exprs[2] = Expr(table)` - data source +`exprs[3] = "row"` - loop variable name +`exprs[4] = Expr(row.wright > 50)` - expression + +The other identified, such as `"select"`, `"from"`, as as as symbols `->` and `:` are +parsed in the order defined within the custom syntax. diff --git a/doc/src/links.md b/doc/src/links.md index 85561acc..82ceab3b 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -89,6 +89,7 @@ [`eval`]: {{rootUrl}}/language/eval.md [OOP]: {{rootUrl}}/language/oop.md +[DSL]: {{rootUrl}}/engine/dsl.md [maximum statement depth]: {{rootUrl}}/safety/max-stmt-depth.md [maximum call stack depth]: {{rootUrl}}/safety/max-call-stack.md @@ -107,3 +108,4 @@ [disable keywords and operators]: {{rootUrl}}/engine/disable.md [custom operator]: {{rootUrl}}/engine/custom-op.md [custom operators]: {{rootUrl}}/engine/custom-op.md +[custom syntax]: {{rootUrl}}/engine/custom-syntax.md diff --git a/doc/src/start/features.md b/doc/src/start/features.md index 77a17e10..023834fd 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -25,7 +25,7 @@ more control over what a script can (or cannot) do. | `no_module` | Disable loading external [modules]. | | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | | `serde` | Enable serialization/deserialization via [`serde`]. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | -| `internals` | Expose internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version. | +| `internals` | Expose internal data structures (e.g. [`AST`] nodes) and enable defining [custom syntax]. Beware that Rhai internals are volatile and may change from version to version. | Example diff --git a/src/engine.rs b/src/engine.rs index ee963eb2..1fcae581 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1012,6 +1012,81 @@ impl Engine { return Ok(result); } + /// Call a dot method. + fn call_method( + &self, + state: &mut State, + lib: &Module, + target: &mut Target, + expr: &Expr, + mut idx_val: Dynamic, + level: usize, + ) -> Result<(Dynamic, bool), Box> { + let ((name, native, pos), _, hash, _, def_val) = match expr { + Expr::FnCall(x) => x.as_ref(), + _ => unreachable!(), + }; + + let is_ref = target.is_ref(); + let is_value = target.is_value(); + let def_val = def_val.as_ref(); + + // Get a reference to the mutation target Dynamic + let obj = target.as_mut(); + let idx = idx_val.downcast_mut::>().unwrap(); + let mut fn_name = name.as_ref(); + + // Check if it is a FnPtr call + let (result, updated) = if fn_name == KEYWORD_FN_PTR_CALL && obj.is::() { + // Redirect function name + fn_name = obj.as_str().unwrap(); + // Recalculate hash + let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); + // Arguments are passed as-is + let mut arg_values = idx.iter_mut().collect::>(); + let args = arg_values.as_mut(); + + // Map it to name(args) in function-call style + self.exec_fn_call( + state, lib, fn_name, *native, hash, args, false, false, def_val, level, + ) + } else { + let redirected: Option; + let mut hash = *hash; + + // Check if it is a map method call in OOP style + if let Some(map) = obj.downcast_ref::() { + if let Some(val) = map.get(fn_name) { + if let Some(f) = val.downcast_ref::() { + // Remap the function name + redirected = Some(f.get_fn_name().clone()); + fn_name = redirected.as_ref().unwrap(); + + // Recalculate the hash based on the new function name + hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); + } + } + }; + + // Attached object pointer in front of the arguments + let mut arg_values = once(obj).chain(idx.iter_mut()).collect::>(); + let args = arg_values.as_mut(); + + self.exec_fn_call( + state, lib, fn_name, *native, hash, args, is_ref, true, def_val, level, + ) + } + .map_err(|err| err.new_position(*pos))?; + + // Feed the changed temp value back + if updated && !is_ref && !is_value { + let new_val = target.as_mut().clone(); + target.set_value(new_val)?; + } + + Ok((result, updated)) + } + /// Chain-evaluate a dot/index chain. /// Position in `EvalAltResult` is None and must be set afterwards. fn eval_dot_index_chain_helper( @@ -1031,7 +1106,6 @@ impl Engine { } let is_ref = target.is_ref(); - let is_value = target.is_value(); let next_chain = match rhs { Expr::Index(_) => ChainType::Index, @@ -1040,7 +1114,7 @@ impl Engine { }; // Pop the last index value - let mut idx_val = idx_values.pop(); + let idx_val = idx_values.pop(); match chain_type { #[cfg(not(feature = "no_index"))] @@ -1124,69 +1198,7 @@ impl Engine { match rhs { // xxx.fn_name(arg_expr_list) Expr::FnCall(x) if x.1.is_none() => { - let ((name, native, pos), _, hash, _, def_val) = x.as_ref(); - let def_val = def_val.as_ref(); - - // Get a reference to the mutation target Dynamic - let (result, updated) = { - let obj = target.as_mut(); - let idx = idx_val.downcast_mut::>().unwrap(); - let mut fn_name = name.as_ref(); - - // Check if it is a FnPtr call - if fn_name == KEYWORD_FN_PTR_CALL && obj.is::() { - // Redirect function name - fn_name = obj.as_str().unwrap(); - // Recalculate hash - let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); - // Arguments are passed as-is - let mut arg_values = idx.iter_mut().collect::>(); - let args = arg_values.as_mut(); - - // Map it to name(args) in function-call style - self.exec_fn_call( - state, lib, fn_name, *native, hash, args, false, false, - def_val, level, - ) - } else { - let redirected: Option; - let mut hash = *hash; - - // Check if it is a map method call in OOP style - if let Some(map) = obj.downcast_ref::() { - if let Some(val) = map.get(fn_name) { - if let Some(f) = val.downcast_ref::() { - // Remap the function name - redirected = Some(f.get_fn_name().clone()); - fn_name = redirected.as_ref().unwrap(); - - // Recalculate the hash based on the new function name - hash = - calc_fn_hash(empty(), fn_name, idx.len(), empty()); - } - } - }; - - // Attached object pointer in front of the arguments - let mut arg_values = - once(obj).chain(idx.iter_mut()).collect::>(); - let args = arg_values.as_mut(); - - self.exec_fn_call( - state, lib, fn_name, *native, hash, args, is_ref, true, - def_val, level, - ) - } - .map_err(|err| err.new_position(*pos))? - }; - - // Feed the changed temp value back - if updated && !is_ref && !is_value { - let new_val = target.as_mut().clone(); - target.set_value(new_val)?; - } - - Ok((result, updated)) + self.call_method(state, lib, target, rhs, idx_val, level) } // xxx.module::fn_name(...) - syntax error Expr::FnCall(_) => unreachable!(), @@ -1230,16 +1242,26 @@ impl Engine { .map(|(v, _)| (v, false)) .map_err(|err| err.new_position(*pos)) } - // {xxx:map}.prop[expr] | {xxx:map}.prop.expr + // {xxx:map}.sub_lhs[expr] | {xxx:map}.sub_lhs.expr Expr::Index(x) | Expr::Dot(x) if target.is::() => { - let (prop, expr, pos) = x.as_ref(); + let (sub_lhs, expr, pos) = x.as_ref(); - let mut val = if let Expr::Property(p) = prop { - let ((prop, _, _), _) = p.as_ref(); - let index = prop.clone().into(); - self.get_indexed_mut(state, lib, target, index, *pos, false, level)? - } else { - unreachable!(); + let mut val = match sub_lhs { + Expr::Property(p) => { + let ((prop, _, _), _) = p.as_ref(); + let index = prop.clone().into(); + self.get_indexed_mut(state, lib, target, index, *pos, false, level)? + } + // {xxx:map}.fn_name(arg_expr_list)[expr] | {xxx:map}.fn_name(arg_expr_list).expr + Expr::FnCall(x) if x.1.is_none() => { + let (val, _) = + self.call_method(state, lib, target, sub_lhs, idx_val, level)?; + val.into() + } + // {xxx:map}.module::fn_name(...) - syntax error + Expr::FnCall(_) => unreachable!(), + // Others - syntax error + _ => unreachable!(), }; self.eval_dot_index_chain_helper( @@ -1248,49 +1270,72 @@ impl Engine { ) .map_err(|err| err.new_position(*pos)) } - // xxx.prop[expr] | xxx.prop.expr + // xxx.sub_lhs[expr] | xxx.sub_lhs.expr Expr::Index(x) | Expr::Dot(x) => { - let (prop, expr, pos) = x.as_ref(); - let args = &mut [target.as_mut(), &mut Default::default()]; + let (sub_lhs, expr, pos) = x.as_ref(); - let (mut val, updated) = if let Expr::Property(p) = prop { - let ((_, getter, _), _) = p.as_ref(); - let args = &mut args[..1]; - self.exec_fn_call( - state, lib, getter, true, 0, args, is_ref, true, None, level, - ) - .map_err(|err| err.new_position(*pos))? - } else { - unreachable!(); - }; - let val = &mut val; - let target = &mut val.into(); + match sub_lhs { + // xxx.prop[expr] | xxx.prop.expr + Expr::Property(p) => { + let ((_, getter, setter), _) = p.as_ref(); + let arg_values = &mut [target.as_mut(), &mut Default::default()]; + let args = &mut arg_values[..1]; + let (mut val, updated) = self + .exec_fn_call( + state, lib, getter, true, 0, args, is_ref, true, None, + level, + ) + .map_err(|err| err.new_position(*pos))?; - let (result, may_be_changed) = self - .eval_dot_index_chain_helper( - state, lib, this_ptr, target, expr, idx_values, next_chain, level, - new_val, - ) - .map_err(|err| err.new_position(*pos))?; + let val = &mut val; + let target = &mut val.into(); - // Feed the value back via a setter just in case it has been updated - if updated || may_be_changed { - if let Expr::Property(p) = prop { - let ((_, _, setter), _) = p.as_ref(); - // Re-use args because the first &mut parameter will not be consumed - args[1] = val; - self.exec_fn_call( - state, lib, setter, true, 0, args, is_ref, true, None, level, - ) - .or_else(|err| match *err { - // If there is no setter, no need to feed it back because the property is read-only - EvalAltResult::ErrorDotExpr(_, _) => Ok(Default::default()), - _ => Err(err.new_position(*pos)), - })?; + let (result, may_be_changed) = self + .eval_dot_index_chain_helper( + state, lib, this_ptr, target, expr, idx_values, next_chain, + level, new_val, + ) + .map_err(|err| err.new_position(*pos))?; + + // Feed the value back via a setter just in case it has been updated + if updated || may_be_changed { + // Re-use args because the first &mut parameter will not be consumed + arg_values[1] = val; + self.exec_fn_call( + state, lib, setter, true, 0, arg_values, is_ref, true, + None, level, + ) + .or_else( + |err| match *err { + // If there is no setter, no need to feed it back because the property is read-only + EvalAltResult::ErrorDotExpr(_, _) => { + Ok(Default::default()) + } + _ => Err(err.new_position(*pos)), + }, + )?; + } + + Ok((result, may_be_changed)) } - } + // xxx.fn_name(arg_expr_list)[expr] | xxx.fn_name(arg_expr_list).expr + Expr::FnCall(x) if x.1.is_none() => { + let (mut val, _) = + self.call_method(state, lib, target, sub_lhs, idx_val, level)?; + let val = &mut val; + let target = &mut val.into(); - Ok((result, may_be_changed)) + self.eval_dot_index_chain_helper( + state, lib, this_ptr, target, expr, idx_values, next_chain, + level, new_val, + ) + .map_err(|err| err.new_position(*pos)) + } + // xxx.module::fn_name(...) - syntax error + Expr::FnCall(_) => unreachable!(), + // Others - syntax error + _ => unreachable!(), + } } // Syntax error _ => Err(Box::new(EvalAltResult::ErrorDotExpr( @@ -1325,7 +1370,7 @@ impl Engine { let idx_values = &mut StaticVec::new(); self.eval_indexed_chain( - scope, mods, state, lib, this_ptr, dot_rhs, idx_values, 0, level, + scope, mods, state, lib, this_ptr, dot_rhs, chain_type, idx_values, 0, level, )?; match dot_lhs { @@ -1389,6 +1434,7 @@ impl Engine { lib: &Module, this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, + chain_type: ChainType, idx_values: &mut StaticVec, size: usize, level: usize, @@ -1415,12 +1461,29 @@ impl Engine { // Evaluate in left-to-right order let lhs_val = match lhs { Expr::Property(_) => Default::default(), // Store a placeholder in case of a property + Expr::FnCall(x) if chain_type == ChainType::Dot && x.1.is_none() => { + let arg_values = x + .3 + .iter() + .map(|arg_expr| { + self.eval_expr(scope, mods, state, lib, this_ptr, arg_expr, level) + }) + .collect::, _>>()?; + + Dynamic::from(arg_values) + } + Expr::FnCall(_) => unreachable!(), _ => self.eval_expr(scope, mods, state, lib, this_ptr, lhs, level)?, }; // Push in reverse order + let chain_type = match expr { + Expr::Index(_) => ChainType::Index, + Expr::Dot(_) => ChainType::Dot, + _ => unreachable!(), + }; self.eval_indexed_chain( - scope, mods, state, lib, this_ptr, rhs, idx_values, size, level, + scope, mods, state, lib, this_ptr, rhs, chain_type, idx_values, size, level, )?; idx_values.push(lhs_val); From 7436fc1c0594b509460276b0b35386863eca5635 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 10 Jul 2020 11:41:56 +0800 Subject: [PATCH 43/46] Fix bug in tokenizing reserved symbols. --- src/token.rs | 66 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/src/token.rs b/src/token.rs index 484c7b84..460ddcb5 100644 --- a/src/token.rs +++ b/src/token.rs @@ -531,7 +531,7 @@ impl Token { } } - /// Is this token a keyword? + /// Is this token a standard keyword? pub fn is_keyword(&self) -> bool { use Token::*; @@ -548,6 +548,22 @@ impl Token { _ => false, } } + + /// Is this token a reserved keyword? + pub fn is_reserved(&self) -> bool { + match self { + Self::Reserved(_) => true, + _ => false, + } + } + + /// Is this token a custom keyword? + pub fn is_custom(&self) -> bool { + match self { + Self::Custom(_) => true, + _ => false, + } + } } impl From for String { @@ -987,7 +1003,10 @@ fn get_next_token_inner( ('}', _) => return Some((Token::RightBrace, start_pos)), // Parentheses - ('(', '*') => return Some((Token::Reserved("(*".into()), start_pos)), + ('(', '*') => { + eat_next(stream, pos); + return Some((Token::Reserved("(*".into()), start_pos)); + } ('(', _) => return Some((Token::LeftParen, start_pos)), (')', _) => return Some((Token::RightParen, start_pos)), @@ -1017,11 +1036,17 @@ fn get_next_token_inner( eat_next(stream, pos); return Some((Token::MinusAssign, start_pos)); } - ('-', '>') => return Some((Token::Reserved("->".into()), start_pos)), + ('-', '>') => { + eat_next(stream, pos); + return Some((Token::Reserved("->".into()), start_pos)); + } ('-', _) if !state.non_unary => return Some((Token::UnaryMinus, start_pos)), ('-', _) => return Some((Token::Minus, start_pos)), - ('*', ')') => return Some((Token::Reserved("*)".into()), start_pos)), + ('*', ')') => { + eat_next(stream, pos); + return Some((Token::Reserved("*)".into()), start_pos)); + } ('*', '=') => { eat_next(stream, pos); return Some((Token::MultiplyAssign, start_pos)); @@ -1086,31 +1111,42 @@ fn get_next_token_inner( // Warn against `===` if stream.peek_next() == Some('=') { + eat_next(stream, pos); return Some((Token::Reserved("===".into()), start_pos)); } return Some((Token::EqualsTo, start_pos)); } - ('=', '>') => return Some((Token::Reserved("=>".into()), start_pos)), + ('=', '>') => { + eat_next(stream, pos); + return Some((Token::Reserved("=>".into()), start_pos)); + } ('=', _) => return Some((Token::Equals, start_pos)), (':', ':') => { eat_next(stream, pos); if stream.peek_next() == Some('<') { + eat_next(stream, pos); return Some((Token::Reserved("::<".into()), start_pos)); } return Some((Token::DoubleColon, start_pos)); } - (':', '=') => return Some((Token::Reserved(":=".into()), start_pos)), + (':', '=') => { + eat_next(stream, pos); + return Some((Token::Reserved(":=".into()), start_pos)); + } (':', _) => return Some((Token::Colon, start_pos)), ('<', '=') => { eat_next(stream, pos); return Some((Token::LessThanEqualsTo, start_pos)); } - ('<', '-') => return Some((Token::Reserved("<-".into()), start_pos)), + ('<', '-') => { + eat_next(stream, pos); + return Some((Token::Reserved("<-".into()), start_pos)); + } ('<', '<') => { eat_next(stream, pos); @@ -1149,6 +1185,7 @@ fn get_next_token_inner( eat_next(stream, pos); if stream.peek_next() == Some('=') { + eat_next(stream, pos); return Some((Token::Reserved("!==".into()), start_pos)); } @@ -1321,6 +1358,17 @@ impl<'a> Iterator for TokenIterator<'a, '_> { ))), }, pos)), (r @ Some(_), None, None) => r, + (Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => { + // Convert custom keywords + Some((Token::Custom(s), pos)) + } + (Some((token, pos)), _, Some(custom)) + if (token.is_keyword() || token.is_operator() || token.is_reserved()) + && custom.contains_key(token.syntax().as_ref()) => + { + // Convert into custom keywords + Some((Token::Custom(token.syntax().into()), pos)) + } (Some((token, pos)), Some(disabled), _) if token.is_operator() && disabled.contains(token.syntax().as_ref()) => { @@ -1336,10 +1384,6 @@ impl<'a> Iterator for TokenIterator<'a, '_> { // Convert disallowed keywords into identifiers Some((Token::Identifier(token.syntax().into()), pos)) } - (Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => { - // Convert custom keywords - Some((Token::Custom(s), pos)) - } (r, _, _) => r, } } From ebffbf0f9873c60264a693450b9e6d7005f75a4b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 10 Jul 2020 22:01:47 +0800 Subject: [PATCH 44/46] Refine docs and add custom syntax. --- Cargo.toml | 2 +- README.md | 2 +- doc/src/SUMMARY.md | 2 +- doc/src/about/features.md | 3 +- doc/src/engine/custom-syntax.md | 281 +++++++++++++++++++++++++++++++- doc/src/engine/dsl.md | 10 +- doc/src/language/loop.md | 4 +- src/engine.rs | 19 ++- src/fn_native.rs | 1 + src/lib.rs | 1 + src/optimize.rs | 6 +- src/parser.rs | 38 +++-- src/syntax.rs | 33 +++- tests/syntax.rs | 40 +++-- 14 files changed, 391 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6d48bc8d..99da6522 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] -default = [] +default = ["internals"] plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync diff --git a/README.md b/README.md index 285abe90..fb4acca1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Features * Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run. * [Function overloading](https://schungx.github.io/rhai/language/overload.html). * [Operator overloading](https://schungx.github.io/rhai/rust/operators.html). -* Support for use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html) - [disabling keywords/operators](https://schungx.github.io/rhai/engine/disable.html), [custom operators](https://schungx.github.io/rhai/engine/custom-op.html). +* Support for use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html) - [disabling keywords/operators](https://schungx.github.io/rhai/engine/disable.html), [custom operators](https://schungx.github.io/rhai/engine/custom-op.html) and extending the language with [custom syntax](https://schungx.github.io/rhai/engine/custom-syntax.html). * Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html). * Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index b436efb9..e344c306 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -108,7 +108,7 @@ The Rhai Scripting Language 5. [Use as DSL](engine/dsl.md) 1. [Disable Keywords and/or Operators](engine/disable.md) 2. [Custom Operators](engine/custom-op.md) - 3. [Custom Syntax](engine/custom-syntax.md) + 3. [Extending with Custom Syntax](engine/custom-syntax.md) 6. [Eval Statement](language/eval.md) 9. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 88f585f0..13b2e4c1 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -67,4 +67,5 @@ Flexible * Surgically [disable keywords and operators] to restrict the language. -* Use as a [DSL] by [disabling keywords/operators][disable keywords and operators], [custom operators] and defining [custom syntax]. +* Use as a [DSL] by [disabling keywords/operators][disable keywords and operators], [custom operators] + and extending the language with [custom syntax]. diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md index fe3f399c..542efaa5 100644 --- a/doc/src/engine/custom-syntax.md +++ b/doc/src/engine/custom-syntax.md @@ -1,5 +1,282 @@ -Custom Syntax -============= +Extending Rhai with Custom Syntax +================================ {{#include ../links.md}} + +For the ultimate advantageous, there is a built-in facility to _extend_ the Rhai language +with custom-defined _syntax_. + +But before going off to define the next weird statement type, heed this warning: + + +Don't Do It™ +------------ + +Stick with standard language syntax as much as possible. + +Having to learn Rhai is bad enough, no sane user would ever want to learn _yet_ another +obscure language syntax just to do something. + +Try to use [custom operators] first. Defining a custom syntax should be considered a _last resort_. + + +Where This Might Be Useful +------------------------- + +* Where an operation is used a _LOT_ and a custom syntax saves a lot of typing. + +* Where a custom syntax _significantly_ simplifies the code and _significantly_ enhances understanding of the code's intent. + +* Where certain logic cannot be easily encapsulated inside a function. This is usually the case where _closures_ are required, because Rhai does not have closures. + +* Where you just want to confuse your user and make their lives miserable, because you can. + + +Step One - Start With `internals` +-------------------------------- + +Since a custom syntax taps deeply into the `AST` and evaluation process of the `Engine`, +the [`internals`] feature must be on in order to expose these necessary internal data structures. + +Beware that Rhai internal data structures are _volatile_ and may change without warning. + +Caveat emptor. + + +Step Two - Design The Syntax +--------------------------- + +A custom syntax is simply a list of symbols. + +These symbol types can be used: + +* Standard [keywords]({{rootUrl}}/appendix/keywords.md) + +* Standard [operators]({{rootUrl}}/appendix/operators.md#operators). + +* Reserved [symbols]({{rootUrl}}/appendix/operators.md#symbols). + +* Identifiers following the [variable] naming rules. + +* `$expr$` - any valid expression, statement or statement block. + +* `$block$` - any valid statement block (i.e. must be enclosed by `'{'` .. `'}'`). + +* `$ident$` - any [variable] name. + +### The First Symbol Must be a Keyword + +There is no specific limit on the combination and sequencing of each symbol type, +except the _first_ symbol which must be a [custom keyword]. + +It _cannot_ be a [built-in keyword]({{rootUrl}}/appendix/keywords.md). + +However, it _may_ be a built-in keyword that has been [disabled][disable keywords and operators]. + +### The First Symbol Must be Unique + +Rhai uses the _first_ symbol as a clue to parse custom syntax. + +Therefore, at any one time, there can only be _one_ custom syntax starting with each unique symbol. + +Any new custom syntax definition using the same first symbol simply _overwrites_ the previous one. + +### Example + +```rust +exec $ident$ <- $expr$ : $block$ +``` + +The above syntax is made up of a stream of symbols: + +| Position | Input | Symbol | Description | +| :------: | :---: | :-------: | -------------------------------------------------------------------------------------------------------- | +| 1 | | `exec` | custom keyword | +| 2 | 1 | `$ident$` | a variable name | +| 3 | | `<-` | the left-arrow symbol (which is a [reserved symbol]({{rootUrl}}/appendix/operators.md#symbols) in Rhai). | +| 4 | 2 | `$expr$` | an expression, which may be enclosed with `{` .. `}`, or not. | +| 5 | | `:` | the colon symbol | +| 6 | 3 | `$block$` | a statement block, which must be enclosed with `{` .. `}`. | + +This syntax matches the following sample code and generates three inputs (one for each non-keyword): + +```rust +// Assuming the 'exec' custom syntax implementation declares the variable 'hello': +let x = exec hello <- foo(1, 2) : { + hello += bar(hello); + baz(hello); + }; + +print(x); // variable 'x' has a value returned by the custom syntax + +print(hello); // variable declared by a custom syntax persists! +``` + + +Step Three - Implementation +-------------------------- + +Any custom syntax must include an _implementation_ of it. + +### Function Signature + +The function signature of an implementation is: + +```rust +Fn( + engine: &Engine, + scope: &mut Scope, + mods: &mut Imports, + state: &mut State, + lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, + inputs: &[Expr], + level: usize +) -> Result> +``` + +where: + +* `engine : &Engine` - reference to the current [`Engine`]. +* `scope : &mut Scope` - mutable reference to the current [`Scope`]; variables can be added to it. +* `mods : &mut Imports` - mutable reference to the current collection of imported [`Module`]'s; **do not touch**. +* `state : &mut State` - mutable reference to the current evaluation state; **do not touch**. +* `lib : &Module` - reference to the current collection of script-defined functions. +* `this_ptr : &mut Option<&mut Dynamic>` - mutable reference to the current binding of the `this` pointer; **do not touch**. +* `inputs : &[Expr]` - a list of input expression trees. +* `level : usize` - the current function call level. + +There are a lot of parameters, most of which should not be touched or Bad Things Happen™. +They represent the running _content_ of a script evaluation and should simply be passed +straight-through the the [`Engine`]. + +### Access Arguments + +The most important argument is `inputs` where the matched identifiers (`$ident$`), expressions/statements (`$expr$`) +and statement blocks (`$block$) are provided. + +To access a particular argument, use the following patterns: + +| Argument type | Pattern (`n` = slot in `inputs`) | Result type | Description | +| :-----------: | ---------------------------------------- | :---------: | ------------------ | +| `$ident$` | `inputs[n].get_variable_name().unwrap()` | `&str` | name of a variable | +| `$expr$` | `inputs.get(n).unwrap()` | `Expr` | an expression tree | +| `$block$` | `inputs.get(n).unwrap()` | `Expr` | an expression tree | + +### Evaluate an Expression Tree + +Use the `engine::eval_expression_tree` method to evaluate an expression tree. + +```rust +let expr = inputs.get(0).unwrap(); +let result = engine.eval_expression_tree(scope, mods, state, lib, this_ptr, expr, level)?; +``` + +As can be seem above, most arguments are simply passed straight-through to `engine::eval_expression_tree`. + +### Declare Variables + +New variables maybe declared (usually with a variable name that is passed in via `$ident$). + +It can simply be pushed into the [`scope`]. + +However, beware that all new variables must be declared _prior_ to evaluating any expression tree. +In other words, any `scope.push(...)` calls must come _before_ any `engine::eval_expression_tree(...)` calls. + +```rust +let var_name = inputs[0].get_variable_name().unwrap().to_string(); +let expr = inputs.get(1).unwrap(); + +scope.push(var_name, 0 as INT); // do this BEFORE engine.eval_expression_tree! + +let result = engine.eval_expression_tree(scope, mods, state, lib, this_ptr, expr, level)?; +``` + + +Step Four - Register the Custom Syntax +------------------------------------- + +Use `Engine::register_custom_syntax` to register a custom syntax. + +Again, beware that the _first_ symbol must be unique. If there already exists a custom syntax starting +with that symbol, the previous syntax will be overwritten. + +The syntax is passed simply as a slice of `&str`. + +```rust +// Custom syntax implementation +fn implementation_func( + engine: &Engine, + scope: &mut Scope, + mods: &mut Imports, + state: &mut State, + lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, + inputs: &[Expr], + level: usize +) -> Result> { + let var_name = inputs[0].get_variable_name().unwrap().to_string(); + let stmt = inputs.get(1).unwrap(); + let condition = inputs.get(2).unwrap(); + + // Push one new variable into the 'scope' BEFORE 'eval_expression_tree' + scope.push(var_name, 0 as INT); + + loop { + // Evaluate the statement block + engine.eval_expression_tree(scope, mods, state, lib, this_ptr, stmt, level)?; + + // Evaluate the condition expression + let stop = !engine + .eval_expression_tree(scope, mods, state, lib, this_ptr, condition, level)? + .as_bool() + .map_err(|_| EvalAltResult::ErrorBooleanArgMismatch( + "do-while".into(), expr.position() + ))?; + + if stop { + break; + } + } + + Ok(().into()) +} + +// Register the custom syntax (sample): do |x| -> { x += 1 } while x < 0; +engine.register_custom_syntax( + &[ "do", "|", "$ident$", "|", "->", "$block$", "while", "$expr$" ], // the custom syntax + 1, // the number of new variables declared within this custom syntax + implementation_func +)?; +``` + + +Step Five - Disable Unneeded Statement Types +------------------------------------------- + +When a DSL needs a custom syntax, most likely than not it is extremely specialized. +Therefore, many statement types actually may not make sense under the same usage scenario. + +So, while at it, better [disable][disable keywords and operators] those built-in keywords +and operators that should not be used by the user. The would leave only the bare minimum +language surface exposed, together with the custom syntax that is tailor-designed for +the scenario. + +A keyword or operator that is disabled can still be used in a custom syntax. + +In an extreme case, it is possible to disable _every_ keyword in the language, leaving only +custom syntax (plus possibly expressions). But again, Don't Do It™ - unless you are certain +of what you're doing. + + +Step Six - Document +------------------- + +For custom syntax, documentation is crucial. + +Make sure there are _lots_ of examples for users to follow. + + +Step Seven - Profit! +-------------------- diff --git a/doc/src/engine/dsl.md b/doc/src/engine/dsl.md index adc04e44..5534f243 100644 --- a/doc/src/engine/dsl.md +++ b/doc/src/engine/dsl.md @@ -56,13 +56,17 @@ essentially custom statement types. The [`internals`] feature is needed to be able to define [custom syntax] in Rhai. -For example: +For example, the following is a SQL like syntax for some obscure DSL operation: ```rust let table = [..., ..., ..., ...]; -// Syntax = "select" $ident$ $ident$ "from" $expr$ "->" $ident$ ":" $expr$ -let total = select sum price from table -> row : row.weight > 50; +// Syntax = "calculate" $ident$ $ident$ "from" $expr$ "->" $ident$ ":" $expr$ +let total = calculate sum price from table -> row : row.weight > 50; + +// Note: There is nothing special about the use of symbols; to make it look exactly like SQL: +// Syntax = "SELECT" $ident$ "(" $ident$ ")" "FROM" $expr$ "AS" $ident$ "WHERE" $expr$ +let total = SELECT sum(price) FROM table AS row WHERE row.weight > 50; ``` After registering this custom syntax with Rhai, it can be used anywhere inside a script as diff --git a/doc/src/language/loop.md b/doc/src/language/loop.md index e625082a..3fd9b5fb 100644 --- a/doc/src/language/loop.md +++ b/doc/src/language/loop.md @@ -3,9 +3,9 @@ Infinite `loop` {{#include ../links.md}} -Infinite loops follow C syntax. +Infinite loops follow Rust syntax. -Like C, `continue` can be used to skip to the next iteration, by-passing all following statements; +Like Rust, `continue` can be used to skip to the next iteration, by-passing all following statements; `break` can be used to break out of the loop unconditionally. ```rust diff --git a/src/engine.rs b/src/engine.rs index 1fcae581..dfded485 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -11,13 +11,15 @@ use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, St use crate::r#unsafe::unsafe_cast_var_name_to_lifetime; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::syntax::CustomSyntax; use crate::token::Position; use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; +#[cfg(feature = "internals")] +use crate::syntax::CustomSyntax; + use crate::stdlib::{ any::{type_name, TypeId}, borrow::Cow, @@ -85,9 +87,12 @@ pub const FN_GET: &str = "get$"; pub const FN_SET: &str = "set$"; pub const FN_IDX_GET: &str = "index$get$"; pub const FN_IDX_SET: &str = "index$set$"; + +#[cfg(feature = "internals")] pub const MARKER_EXPR: &str = "$expr$"; -pub const MARKER_STMT: &str = "$stmt$"; +#[cfg(feature = "internals")] pub const MARKER_BLOCK: &str = "$block$"; +#[cfg(feature = "internals")] pub const MARKER_IDENT: &str = "$ident$"; /// A type specifying the method of chaining. @@ -279,6 +284,7 @@ pub struct Engine { /// A hashset containing custom keywords and precedence to recognize. pub(crate) custom_keywords: Option>, /// Custom syntax. + #[cfg(feature = "internals")] pub(crate) custom_syntax: Option>, /// Callback closure for implementing the `print` command. @@ -329,6 +335,8 @@ impl Default for Engine { type_names: None, disabled_symbols: None, custom_keywords: None, + + #[cfg(feature = "internals")] custom_syntax: None, // default print/debug implementations @@ -562,6 +570,8 @@ impl Engine { type_names: None, disabled_symbols: None, custom_keywords: None, + + #[cfg(feature = "internals")] custom_syntax: None, print: Box::new(|_| {}), @@ -1667,14 +1677,14 @@ impl Engine { } } - /// Evaluate an expression inside an AST. + /// Evaluate an expression tree. /// /// ## WARNING - Low Level API /// /// This function is very low level. It evaluates an expression from an AST. #[cfg(feature = "internals")] #[deprecated(note = "this method is volatile and may change")] - pub fn eval_expr_from_ast( + pub fn eval_expression_tree( &self, scope: &mut Scope, mods: &mut Imports, @@ -2118,6 +2128,7 @@ impl Engine { Expr::False(_) => Ok(false.into()), Expr::Unit(_) => Ok(().into()), + #[cfg(feature = "internals")] Expr::Custom(x) => { let func = (x.0).1.as_ref(); let exprs = (x.0).0.as_ref(); diff --git a/src/fn_native.rs b/src/fn_native.rs index 70594208..399d1746 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,3 +1,4 @@ +//! Module containing interfaces with native-Rust functions. use crate::any::Dynamic; use crate::engine::Engine; use crate::module::Module; diff --git a/src/lib.rs b/src/lib.rs index b6e73b49..6e319260 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,6 +91,7 @@ mod scope; mod serde; mod settings; mod stdlib; +#[cfg(feature = "internals")] mod syntax; mod token; mod r#unsafe; diff --git a/src/optimize.rs b/src/optimize.rs index 0fad8450..ed07d1fb 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -2,10 +2,13 @@ use crate::any::Dynamic; use crate::calc_fn_hash; use crate::engine::{Engine, Imports, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, KEYWORD_TYPE_OF}; use crate::module::Module; -use crate::parser::{map_dynamic_to_expr, CustomExpr, Expr, ReturnType, ScriptFnDef, Stmt, AST}; +use crate::parser::{map_dynamic_to_expr, Expr, ReturnType, ScriptFnDef, Stmt, AST}; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; use crate::utils::StaticVec; +#[cfg(feature = "internals")] +use crate::parser::CustomExpr; + use crate::stdlib::{ boxed::Box, iter::empty, @@ -599,6 +602,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { } // Custom syntax + #[cfg(feature = "internals")] Expr::Custom(x) => Expr::Custom(Box::new(( CustomExpr( (x.0).0.into_iter().map(|expr| optimize_expr(expr, state)).collect(), diff --git a/src/parser.rs b/src/parser.rs index c32a919a..4df5b799 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,19 +2,23 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; -use crate::engine::{ - make_getter, make_setter, Engine, KEYWORD_THIS, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT, - MARKER_STMT, -}; +use crate::engine::{make_getter, make_setter, Engine, KEYWORD_THIS}; use crate::error::{LexError, ParseError, ParseErrorType}; -use crate::fn_native::Shared; use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::syntax::FnCustomSyntaxEval; use crate::token::{Position, Token, TokenStream}; use crate::utils::{StaticVec, StraightHasherBuilder}; +#[cfg(feature = "internals")] +use crate::engine::{MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; + +#[cfg(feature = "internals")] +use crate::fn_native::Shared; + +#[cfg(feature = "internals")] +use crate::syntax::FnCustomSyntaxEval; + use crate::stdlib::{ borrow::Cow, boxed::Box, @@ -574,8 +578,10 @@ impl Stmt { } #[derive(Clone)] +#[cfg(feature = "internals")] pub struct CustomExpr(pub StaticVec, pub Shared); +#[cfg(feature = "internals")] impl fmt::Debug for CustomExpr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) @@ -647,6 +653,7 @@ pub enum Expr { /// () Unit(Position), /// Custom syntax + #[cfg(feature = "internals")] Custom(Box<(CustomExpr, Position)>), } @@ -743,6 +750,7 @@ impl Expr { Self::Dot(x) | Self::Index(x) => x.0.position(), + #[cfg(feature = "internals")] Self::Custom(x) => x.1, } } @@ -776,6 +784,8 @@ impl Expr { Self::Assignment(x) => x.3 = new_pos, Self::Dot(x) => x.2 = new_pos, Self::Index(x) => x.2 = new_pos, + + #[cfg(feature = "internals")] Self::Custom(x) => x.1 = new_pos, } @@ -881,6 +891,7 @@ impl Expr { _ => false, }, + #[cfg(feature = "internals")] Self::Custom(_) => false, } } @@ -897,6 +908,14 @@ impl Expr { _ => self, } } + + #[cfg(feature = "internals")] + pub fn get_variable_name(&self) -> Option<&str> { + match self { + Self::Variable(x) => Some((x.0).0.as_str()), + _ => None, + } + } } /// Consume a particular token, checking that it is the expected one. @@ -2046,6 +2065,7 @@ fn parse_expr( settings.ensure_level_within_max_limit(state.max_expr_depth)?; // Check if it is a custom syntax. + #[cfg(feature = "internals")] if let Some(ref custom) = state.engine.custom_syntax { let (token, pos) = input.peek().unwrap(); let token_pos = *pos; @@ -2085,12 +2105,6 @@ fn parse_expr( (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }, MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?), - MARKER_STMT => { - let stmt = parse_stmt(input, state, lib, settings)? - .unwrap_or_else(|| Stmt::Noop(settings.pos)); - let pos = stmt.position(); - exprs.push(Expr::Stmt(Box::new((stmt, pos)))) - } MARKER_BLOCK => { let stmt = parse_block(input, state, lib, settings)?; let pos = stmt.position(); diff --git a/src/syntax.rs b/src/syntax.rs index e1a05786..2fea999e 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -1,5 +1,8 @@ +//! Module containing implementation for custom syntax. +#![cfg(feature = "internals")] + use crate::any::Dynamic; -use crate::engine::{Engine, Imports, State, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT, MARKER_STMT}; +use crate::engine::{Engine, Imports, State, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; use crate::error::LexError; use crate::fn_native::{SendSync, Shared}; use crate::module::Module; @@ -57,7 +60,7 @@ impl fmt::Debug for CustomSyntax { } impl Engine { - pub fn add_custom_syntax + ToString>( + pub fn register_custom_syntax + ToString>( &mut self, value: &[S], scope_delta: isize, @@ -83,12 +86,28 @@ impl Engine { for s in value { let seg = match s.as_ref() { // Markers not in first position - MARKER_EXPR | MARKER_STMT | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => { - s.to_string() - } + MARKER_EXPR | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => s.to_string(), // Standard symbols not in first position - s if !segments.is_empty() && Token::lookup_from_syntax(s).is_some() => s.into(), - // Custom keyword + s if !segments.is_empty() && Token::lookup_from_syntax(s).is_some() => { + if self + .disabled_symbols + .as_ref() + .map(|d| d.contains(s)) + .unwrap_or(false) + { + // If symbol is disabled, make it a custom keyword + if self.custom_keywords.is_none() { + self.custom_keywords = Some(Default::default()); + } + + if !self.custom_keywords.as_ref().unwrap().contains_key(s) { + self.custom_keywords.as_mut().unwrap().insert(s.into(), 0); + } + } + + s.into() + } + // Identifier s if is_valid_identifier(s.chars()) => { if self.custom_keywords.is_none() { self.custom_keywords = Some(Default::default()); diff --git a/tests/syntax.rs b/tests/syntax.rs index f12a0919..501c90b3 100644 --- a/tests/syntax.rs +++ b/tests/syntax.rs @@ -7,9 +7,16 @@ use rhai::{ fn test_custom_syntax() -> Result<(), Box> { let mut engine = Engine::new(); + // Disable 'while' and make sure it still works with custom syntax + engine.disable_symbol("while"); + engine.consume("while false {}").expect_err("should error"); + engine.consume("let while = 0")?; + engine - .add_custom_syntax( - &["do", "$ident$", "$block$", "while", "$expr$"], + .register_custom_syntax( + &[ + "do", "|", "$ident$", "|", "->", "$block$", "while", "$expr$", + ], 1, |engine: &Engine, scope: &mut Scope, @@ -17,22 +24,19 @@ fn test_custom_syntax() -> Result<(), Box> { state: &mut EvalState, lib: &Module, this_ptr: &mut Option<&mut Dynamic>, - exprs: &[Expr], + inputs: &[Expr], level: usize| { - let var_name = match exprs.get(0).unwrap() { - Expr::Variable(s) => (s.0).0.clone(), - _ => unreachable!(), - }; - let stmt = exprs.get(1).unwrap(); - let expr = exprs.get(2).unwrap(); + let var_name = inputs[0].get_variable_name().unwrap().to_string(); + let stmt = inputs.get(1).unwrap(); + let expr = inputs.get(2).unwrap(); scope.push(var_name, 0 as INT); loop { - engine.eval_expr_from_ast(scope, mods, state, lib, this_ptr, stmt, level)?; + engine.eval_expression_tree(scope, mods, state, lib, this_ptr, stmt, level)?; if !engine - .eval_expr_from_ast(scope, mods, state, lib, this_ptr, expr, level)? + .eval_expression_tree(scope, mods, state, lib, this_ptr, expr, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch( @@ -50,20 +54,24 @@ fn test_custom_syntax() -> Result<(), Box> { ) .unwrap(); - assert!(matches!( - *engine.add_custom_syntax(&["!"], 0, |_, _, _, _, _, _, _, _| Ok(().into())).expect_err("should error"), - LexError::ImproperSymbol(s) if s == "!" - )); + // 'while' is now a custom keyword so this it can no longer be a variable + engine.consume("let while = 0").expect_err("should error"); assert_eq!( engine.eval::( r" - do x { x += 1 } while x < 42; + do |x| -> { x += 1 } while x < 42; x " )?, 42 ); + // The first symbol must be an identifier + assert!(matches!( + *engine.register_custom_syntax(&["!"], 0, |_, _, _, _, _, _, _, _| Ok(().into())).expect_err("should error"), + LexError::ImproperSymbol(s) if s == "!" + )); + Ok(()) } From 2a8d63fd5f0e6fb3ca19a0584243c9292abae38a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 11 Jul 2020 15:09:17 +0800 Subject: [PATCH 45/46] Refine custom syntax. --- Cargo.toml | 2 +- doc/src/engine/custom-syntax.md | 20 ++++++++--------- doc/src/engine/dsl.md | 4 ++-- src/engine.rs | 38 +++++++++++++++++++++++++++++---- src/lib.rs | 2 +- src/module.rs | 6 +++++- src/parser.rs | 8 ------- src/syntax.rs | 7 +++--- tests/syntax.rs | 4 ++-- 9 files changed, 58 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 99da6522..6d48bc8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] -default = ["internals"] +default = [] plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md index 542efaa5..e0529841 100644 --- a/doc/src/engine/custom-syntax.md +++ b/doc/src/engine/custom-syntax.md @@ -1,5 +1,5 @@ -Extending Rhai with Custom Syntax -================================ +Extend Rhai with Custom Syntax +============================= {{#include ../links.md}} @@ -131,7 +131,7 @@ Fn( state: &mut State, lib: &Module, this_ptr: &mut Option<&mut Dynamic>, - inputs: &[Expr], + inputs: &[Expression], level: usize ) -> Result> ``` @@ -144,7 +144,7 @@ where: * `state : &mut State` - mutable reference to the current evaluation state; **do not touch**. * `lib : &Module` - reference to the current collection of script-defined functions. * `this_ptr : &mut Option<&mut Dynamic>` - mutable reference to the current binding of the `this` pointer; **do not touch**. -* `inputs : &[Expr]` - a list of input expression trees. +* `inputs : &[Expression]` - a list of input expression trees. * `level : usize` - the current function call level. There are a lot of parameters, most of which should not be touched or Bad Things Happen™. @@ -158,11 +158,11 @@ and statement blocks (`$block$) are provided. To access a particular argument, use the following patterns: -| Argument type | Pattern (`n` = slot in `inputs`) | Result type | Description | -| :-----------: | ---------------------------------------- | :---------: | ------------------ | -| `$ident$` | `inputs[n].get_variable_name().unwrap()` | `&str` | name of a variable | -| `$expr$` | `inputs.get(n).unwrap()` | `Expr` | an expression tree | -| `$block$` | `inputs.get(n).unwrap()` | `Expr` | an expression tree | +| Argument type | Pattern (`n` = slot in `inputs`) | Result type | Description | +| :-----------: | ---------------------------------------- | :----------: | ------------------ | +| `$ident$` | `inputs[n].get_variable_name().unwrap()` | `&str` | name of a variable | +| `$expr$` | `inputs.get(n).unwrap()` | `Expression` | an expression tree | +| `$block$` | `inputs.get(n).unwrap()` | `Expression` | an expression tree | ### Evaluate an Expression Tree @@ -213,7 +213,7 @@ fn implementation_func( state: &mut State, lib: &Module, this_ptr: &mut Option<&mut Dynamic>, - inputs: &[Expr], + inputs: &[Expression], level: usize ) -> Result> { let var_name = inputs[0].get_variable_name().unwrap().to_string(); diff --git a/doc/src/engine/dsl.md b/doc/src/engine/dsl.md index 5534f243..3585936b 100644 --- a/doc/src/engine/dsl.md +++ b/doc/src/engine/dsl.md @@ -76,9 +76,9 @@ For its evaluation, the callback function will receive the following list of par `exprs[0] = "sum"` - math operator `exprs[1] = "price"` - field name -`exprs[2] = Expr(table)` - data source +`exprs[2] = Expression(table)` - data source `exprs[3] = "row"` - loop variable name -`exprs[4] = Expr(row.wright > 50)` - expression +`exprs[4] = Expression(row.wright > 50)` - expression The other identified, such as `"select"`, `"from"`, as as as symbols `->` and `:` are parsed in the order defined within the custom syntax. diff --git a/src/engine.rs b/src/engine.rs index dfded485..0455c6f1 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -95,6 +95,36 @@ pub const MARKER_BLOCK: &str = "$block$"; #[cfg(feature = "internals")] pub const MARKER_IDENT: &str = "$ident$"; +#[cfg(feature = "internals")] +pub struct Expression<'a>(&'a Expr); + +#[cfg(feature = "internals")] +impl<'a> From<&'a Expr> for Expression<'a> { + fn from(expr: &'a Expr) -> Self { + Self(expr) + } +} + +#[cfg(feature = "internals")] +impl Expression<'_> { + /// If this expression is a variable name, return it. Otherwise `None`. + #[cfg(feature = "internals")] + pub fn get_variable_name(&self) -> Option<&str> { + match self.0 { + Expr::Variable(x) => Some((x.0).0.as_str()), + _ => None, + } + } + /// Get the expression. + pub(crate) fn expr(&self) -> &Expr { + &self.0 + } + /// Get the position of this expression. + pub fn position(&self) -> Position { + self.0.position() + } +} + /// A type specifying the method of chaining. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] enum ChainType { @@ -1691,10 +1721,10 @@ impl Engine { state: &mut State, lib: &Module, this_ptr: &mut Option<&mut Dynamic>, - expr: &Expr, + expr: &Expression, level: usize, ) -> Result> { - self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) + self.eval_expr(scope, mods, state, lib, this_ptr, expr.expr(), level) } /// Evaluate an expression @@ -2131,8 +2161,8 @@ impl Engine { #[cfg(feature = "internals")] Expr::Custom(x) => { let func = (x.0).1.as_ref(); - let exprs = (x.0).0.as_ref(); - func(self, scope, mods, state, lib, this_ptr, exprs, level) + let ep: StaticVec<_> = (x.0).0.iter().map(|e| e.into()).collect(); + func(self, scope, mods, state, lib, this_ptr, ep.as_ref(), level) } _ => unreachable!(), diff --git a/src/lib.rs b/src/lib.rs index 6e319260..a3edc8ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,7 +169,7 @@ pub use parser::{CustomExpr, Expr, ReturnType, ScriptFnDef, Stmt}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] -pub use engine::{Imports, State as EvalState}; +pub use engine::{Expression, Imports, State as EvalState}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] diff --git a/src/module.rs b/src/module.rs index 7181e4d7..94bd40a4 100644 --- a/src/module.rs +++ b/src/module.rs @@ -342,7 +342,11 @@ impl Module { /// Set a Rust function into the module, returning a hash key. /// /// If there is an existing Rust function of the same hash, it is replaced. - pub(crate) fn set_fn( + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. + pub fn set_fn( &mut self, name: impl Into, access: FnAccess, diff --git a/src/parser.rs b/src/parser.rs index 4df5b799..55fbdabf 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -908,14 +908,6 @@ impl Expr { _ => self, } } - - #[cfg(feature = "internals")] - pub fn get_variable_name(&self) -> Option<&str> { - match self { - Self::Variable(x) => Some((x.0).0.as_str()), - _ => None, - } - } } /// Consume a particular token, checking that it is the expected one. diff --git a/src/syntax.rs b/src/syntax.rs index 2fea999e..05ab5e0b 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -2,11 +2,10 @@ #![cfg(feature = "internals")] use crate::any::Dynamic; -use crate::engine::{Engine, Imports, State, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; +use crate::engine::{Engine, Expression, Imports, State, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; use crate::error::LexError; use crate::fn_native::{SendSync, Shared}; use crate::module::Module; -use crate::parser::Expr; use crate::result::EvalAltResult; use crate::scope::Scope; use crate::token::{is_valid_identifier, Token}; @@ -28,7 +27,7 @@ pub type FnCustomSyntaxEval = dyn Fn( &mut State, &Module, &mut Option<&mut Dynamic>, - &[Expr], + &[Expression], usize, ) -> Result>; /// A general function trail object. @@ -71,7 +70,7 @@ impl Engine { &mut State, &Module, &mut Option<&mut Dynamic>, - &[Expr], + &[Expression], usize, ) -> Result> + SendSync diff --git a/tests/syntax.rs b/tests/syntax.rs index 501c90b3..0fdf3c07 100644 --- a/tests/syntax.rs +++ b/tests/syntax.rs @@ -1,6 +1,6 @@ #![cfg(feature = "internals")] use rhai::{ - Dynamic, Engine, EvalAltResult, EvalState, Expr, Imports, LexError, Module, Scope, INT, + Dynamic, Engine, EvalAltResult, EvalState, Expression, Imports, LexError, Module, Scope, INT, }; #[test] @@ -24,7 +24,7 @@ fn test_custom_syntax() -> Result<(), Box> { state: &mut EvalState, lib: &Module, this_ptr: &mut Option<&mut Dynamic>, - inputs: &[Expr], + inputs: &[Expression], level: usize| { let var_name = inputs[0].get_variable_name().unwrap().to_string(); let stmt = inputs.get(1).unwrap(); From 8449f8c55e55401e6cf404b234f848c0a8ce0f3b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 12 Jul 2020 11:46:53 +0800 Subject: [PATCH 46/46] Make API chainable. --- RELEASES.md | 1 + doc/src/engine/custom-op.md | 18 ++++---- doc/src/engine/disable.md | 5 ++- doc/src/rust/custom.md | 37 +++++++++-------- doc/src/rust/functions.md | 5 ++- doc/src/rust/generic.md | 7 ++-- doc/src/rust/getters-setters.md | 8 ++-- doc/src/rust/indexers.md | 5 ++- doc/src/rust/options.md | 1 + doc/src/rust/scope.md | 10 ++--- doc/src/rust/strings.md | 7 ++-- src/api.rs | 73 +++++++++++++++++++++------------ src/fn_register.rs | 10 +++-- src/module.rs | 23 +++++++---- src/optimize.rs | 14 ++++--- src/scope.rs | 50 +++++++++++++++------- src/settings.rs | 58 +++++++++++++++----------- src/syntax.rs | 6 +-- tests/method_call.rs | 8 ++-- tests/mismatched_op.rs | 6 ++- tests/modules.rs | 1 + tests/print.rs | 13 +++--- 22 files changed, 223 insertions(+), 143 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 69c0186d..c1bbc632 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -22,6 +22,7 @@ Breaking changes * `EvalAltResult::ErrorMismatchOutputType` has an extra argument containing the name of the requested type. * `Engine::call_fn_dynamic` take an extra argument, allowing a `Dynamic` value to be bound to the `this` pointer. * Precedence of the `%` (modulo) operator is lowered to below `<<` ad `>>`. This is to handle the case of `x << 3 % 10`. +* Many configuration/setting API's now returns `&mut Self` so that the calls can be chained. This should not affect most code. New features ------------ diff --git a/doc/src/engine/custom-op.md b/doc/src/engine/custom-op.md index e3c1d099..0b92c8a9 100644 --- a/doc/src/engine/custom-op.md +++ b/doc/src/engine/custom-op.md @@ -17,12 +17,12 @@ use rhai::{Engine, RegisterFn}; let mut engine = Engine::new(); -// Register a custom operator called 'foo' and give it -// a precedence of 160 (i.e. between +|- and *|/) -engine.register_custom_operator("foo", 160).unwrap(); - -// Register the implementation of the customer operator as a function -engine.register_fn("foo", |x: i64, y: i64| (x * y) - (x + y)); +// Register a custom operator named 'foo' and give it a precedence of 160 +// (i.e. between +|- and *|/) +// Also register the implementation of the customer operator as a function +engine + .register_custom_operator("foo", 160).unwrap() + .register_fn("foo", |x: i64, y: i64| (x * y) - (x + y)); // The custom operator can be used in expressions let result = engine.eval_expression::("1 + 2 * 3 foo 4 - 5 / 6")?; @@ -71,9 +71,9 @@ All custom operators must be _binary_ (i.e. they take two operands). _Unary_ custom operators are not supported. ```rust -engine.register_custom_operator("foo", 160).unwrap(); - -engine.register_fn("foo", |x: i64| x * x); +engine + .register_custom_operator("foo", 160).unwrap() + .register_fn("foo", |x: i64| x * x); engine.eval::("1 + 2 * 3 foo 4 - 5 / 6")?; // error: function 'foo (i64, i64)' not found ``` diff --git a/doc/src/engine/disable.md b/doc/src/engine/disable.md index f34e763c..3810fa5e 100644 --- a/doc/src/engine/disable.md +++ b/doc/src/engine/disable.md @@ -13,8 +13,9 @@ use rhai::Engine; let mut engine = Engine::new(); -engine.disable_symbol("if"); // disable the 'if' keyword -engine.disable_symbol("+="); // disable the '+=' operator +engine + .disable_symbol("if") // disable the 'if' keyword + .disable_symbol("+="); // disable the '+=' operator // The following all return parse errors. diff --git a/doc/src/rust/custom.md b/doc/src/rust/custom.md index 43837cd5..7460f50e 100644 --- a/doc/src/rust/custom.md +++ b/doc/src/rust/custom.md @@ -9,7 +9,7 @@ Support for custom types can be turned off via the [`no_object`] feature. ```rust use rhai::{Engine, EvalAltResult}; -use rhai::RegisterFn; +use rhai::RegisterFn; // remember 'RegisterFn' is needed #[derive(Clone)] struct TestStruct { @@ -28,14 +28,14 @@ impl TestStruct { let mut engine = Engine::new(); -engine.register_type::(); - -engine.register_fn("update", TestStruct::update); -engine.register_fn("new_ts", TestStruct::new); +engine + .register_type::() // most API's can be chained up + .register_fn("update", TestStruct::update) + .register_fn("new_ts", TestStruct::new); let result = engine.eval::("let x = new_ts(); x.update(); x")?; -println!("result: {}", result.field); // prints 42 +println!("result: {}", result.field); // prints 42 ``` Register a Custom Type @@ -52,7 +52,7 @@ struct TestStruct { } impl TestStruct { - fn update(&mut self) { // methods take &mut as first parameter + fn update(&mut self) { // methods take &mut as first parameter self.field += 41; } @@ -75,8 +75,9 @@ using one of the `Engine::register_XXX` API. Below, the `update` and `new` methods are registered using `Engine::register_fn`. ```rust -engine.register_fn("update", TestStruct::update); // registers 'update(&mut TestStruct)' -engine.register_fn("new_ts", TestStruct::new); // registers 'new()' +engine + .register_fn("update", TestStruct::update) // registers 'update(&mut TestStruct)' + .register_fn("new_ts", TestStruct::new); // registers 'new()' ``` ***Note**: Rhai follows the convention that methods of custom types take a `&mut` first parameter @@ -107,13 +108,13 @@ fn foo(ts: &mut TestStruct) -> i64 { ts.field } -engine.register_fn("foo", foo); // register a Rust native function +engine.register_fn("foo", foo); // register a Rust native function let result = engine.eval::( - "let x = new_ts(); x.foo()" // 'foo' can be called like a method on 'x' + "let x = new_ts(); x.foo()" // 'foo' can be called like a method on 'x' )?; -println!("result: {}", result); // prints 1 +println!("result: {}", result); // prints 1 ``` Under [`no_object`], however, the _method_ style of function calls @@ -133,13 +134,17 @@ If `Engine::register_type_with_name` is used to register the custom type with a special "pretty-print" name, [`type_of()`] will return that name instead. ```rust -engine.register_type::(); -engine.register_fn("new_ts", TestStruct::new); +engine + .register_type::() + .register_fn("new_ts", TestStruct::new); + let x = new_ts(); x.type_of() == "path::to::module::TestStruct"; -engine.register_type_with_name::("Hello"); -engine.register_fn("new_ts", TestStruct::new); +engine + .register_type_with_name::("Hello") + .register_fn("new_ts", TestStruct::new); + let x = new_ts(); x.type_of() == "Hello"; ``` diff --git a/doc/src/rust/functions.md b/doc/src/rust/functions.md index aca6fe8e..a929484c 100644 --- a/doc/src/rust/functions.md +++ b/doc/src/rust/functions.md @@ -31,8 +31,9 @@ fn get_any_value() -> Result> { let mut engine = Engine::new(); -engine.register_fn("add", add_len); -engine.register_fn("add_str", add_len_str); +engine + .register_fn("add", add_len) + .register_fn("add_str", add_len_str); let result = engine.eval::(r#"add(40, "xx")"#)?; diff --git a/doc/src/rust/generic.md b/doc/src/rust/generic.md index c68bf562..d2bea789 100644 --- a/doc/src/rust/generic.md +++ b/doc/src/rust/generic.md @@ -19,9 +19,10 @@ fn show_it(x: &mut T) { let mut engine = Engine::new(); -engine.register_fn("print", show_it::); -engine.register_fn("print", show_it::); -engine.register_fn("print", show_it::); +engine + .register_fn("print", show_it::) + .register_fn("print", show_it::) + .register_fn("print", show_it::); ``` The above example shows how to register multiple functions diff --git a/doc/src/rust/getters-setters.md b/doc/src/rust/getters-setters.md index 92c659b9..b6afd6f0 100644 --- a/doc/src/rust/getters-setters.md +++ b/doc/src/rust/getters-setters.md @@ -30,10 +30,10 @@ impl TestStruct { let mut engine = Engine::new(); -engine.register_type::(); - -engine.register_get_set("xyz", TestStruct::get_field, TestStruct::set_field); -engine.register_fn("new_ts", TestStruct::new); + engine + .register_type::() + .register_get_set("xyz", TestStruct::get_field, TestStruct::set_field) + .register_fn("new_ts", TestStruct::new); // Return result can be 'String' - Rhai will automatically convert it from 'ImmutableString' let result = engine.eval::(r#"let a = new_ts(); a.xyz = "42"; a.xyz"#)?; diff --git a/doc/src/rust/indexers.md b/doc/src/rust/indexers.md index 800c43b8..35f50ba2 100644 --- a/doc/src/rust/indexers.md +++ b/doc/src/rust/indexers.md @@ -38,8 +38,9 @@ engine.register_type::(); engine.register_fn("new_ts", TestStruct::new); // Shorthand: engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); -engine.register_indexer_get(TestStruct::get_field); -engine.register_indexer_set(TestStruct::set_field); +engine + .register_indexer_get(TestStruct::get_field) + .register_indexer_set(TestStruct::set_field); let result = engine.eval::("let a = new_ts(); a[2] = 42; a[2]")?; diff --git a/doc/src/rust/options.md b/doc/src/rust/options.md index f0a14b97..a87e1d21 100644 --- a/doc/src/rust/options.md +++ b/doc/src/rust/options.md @@ -15,3 +15,4 @@ A number of other configuration options are available from the `Engine` to fine- | `set_max_string_size` | [`unchecked`] | Set the maximum length (in UTF-8 bytes) for [strings]. See [maximum length of strings]. | | `set_max_array_size` | [`unchecked`], [`no_index`] | Set the maximum size for [arrays]. See [maximum size of arrays]. | | `set_max_map_size` | [`unchecked`], [`no_object`] | Set the maximum number of properties for [object maps]. See [maximum size of object maps]. | +| `disable_symbol` | | Disable a certain keyword or operator. See [disable keywords and operators]. | diff --git a/doc/src/rust/scope.md b/doc/src/rust/scope.md index a33e4b4c..daa08a15 100644 --- a/doc/src/rust/scope.md +++ b/doc/src/rust/scope.md @@ -28,11 +28,11 @@ let mut scope = Scope::new(); // Then push (i.e. add) some initialized variables into the state. // Remember the system number types in Rhai are i64 (i32 if 'only_i32') ond f64. // Better stick to them or it gets hard working with the script. -scope.push("y", 42_i64); -scope.push("z", 999_i64); - -// 'set_value' adds a variable when one doesn't exist -scope.set_value("s", "hello, world!".to_string()); // remember to use 'String', not '&str' +scope + .push("y", 42_i64) + .push("z", 999_i64) + .set_value("s", "hello, world!".to_string()); //'set_value' adds a variable when one doesn't exist + // remember to use 'String', not '&str' // First invocation engine.eval_with_scope::<()>(&mut scope, r" diff --git a/doc/src/rust/strings.md b/doc/src/rust/strings.md index ac03c874..5b72fe6e 100644 --- a/doc/src/rust/strings.md +++ b/doc/src/rust/strings.md @@ -11,9 +11,10 @@ fn get_len1(s: String) -> i64 { s.len() as i64 } // <- Rhai will not fn get_len2(s: &str) -> i64 { s.len() as i64 } // <- Rhai finds this function fine fn get_len3(s: ImmutableString) -> i64 { s.len() as i64 } // <- the above is equivalent to this -engine.register_fn("len1", get_len1); -engine.register_fn("len2", get_len2); -engine.register_fn("len3", get_len3); +engine + .register_fn("len1", get_len1) + .register_fn("len2", get_len2) + .register_fn("len3", get_len3); let len = engine.eval::("x.len1()")?; // error: function 'len1 (&str | ImmutableString)' not found let len = engine.eval::("x.len2()")?; // works fine diff --git a/src/api.rs b/src/api.rs index a6500443..6b275a71 100644 --- a/src/api.rs +++ b/src/api.rs @@ -54,8 +54,9 @@ impl Engine { name: &str, arg_types: &[TypeId], func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, - ) { + ) -> &mut Self { self.global_module.set_raw_fn(name, arg_types, func); + self } /// Register a function of no parameters with the `Engine`. @@ -68,8 +69,9 @@ impl Engine { &mut self, name: &str, func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, - ) { + ) -> &mut Self { self.global_module.set_raw_fn(name, &[], func); + self } /// Register a function of one parameter with the `Engine`. @@ -92,9 +94,10 @@ impl Engine { &mut self, name: &str, func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, - ) { + ) -> &mut Self { self.global_module .set_raw_fn(name, &[TypeId::of::
()], func); + self } /// Register a function of two parameters with the `Engine`. @@ -117,9 +120,10 @@ impl Engine { &mut self, name: &str, func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, - ) { + ) -> &mut Self { self.global_module .set_raw_fn(name, &[TypeId::of::(), TypeId::of::()], func); + self } /// Register a function of three parameters with the `Engine`. @@ -147,12 +151,13 @@ impl Engine { &mut self, name: &str, func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, - ) { + ) -> &mut Self { self.global_module.set_raw_fn( name, &[TypeId::of::(), TypeId::of::(), TypeId::of::()], func, ); + self } /// Register a function of four parameters with the `Engine`. @@ -181,7 +186,7 @@ impl Engine { &mut self, name: &str, func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn + SendSync + 'static, - ) { + ) -> &mut Self { self.global_module.set_raw_fn( name, &[ @@ -192,6 +197,7 @@ impl Engine { ], func, ); + self } /// Register a custom type for use with the `Engine`. @@ -231,8 +237,8 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_object"))] - pub fn register_type(&mut self) { - self.register_type_with_name::(type_name::()); + pub fn register_type(&mut self) -> &mut Self { + self.register_type_with_name::(type_name::()) } /// Register a custom type for use with the `Engine`, with a pretty-print name @@ -279,7 +285,7 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_object"))] - pub fn register_type_with_name(&mut self, name: &str) { + pub fn register_type_with_name(&mut self, name: &str) -> &mut Self { if self.type_names.is_none() { self.type_names = Some(Default::default()); } @@ -288,12 +294,14 @@ impl Engine { .as_mut() .unwrap() .insert(type_name::().to_string(), name.to_string()); + self } /// Register an iterator adapter for a type with the `Engine`. /// This is an advanced feature. - pub fn register_iterator(&mut self, f: IteratorFn) { + pub fn register_iterator(&mut self, f: IteratorFn) -> &mut Self { self.global_module.set_iter(TypeId::of::(), f); + self } /// Register a getter function for a member of a registered type with the `Engine`. @@ -337,11 +345,12 @@ impl Engine { &mut self, name: &str, callback: impl Fn(&mut T) -> U + SendSync + 'static, - ) where + ) -> &mut Self + where T: Variant + Clone, U: Variant + Clone, { - self.register_fn(&make_getter(name), callback); + self.register_fn(&make_getter(name), callback) } /// Register a setter function for a member of a registered type with the `Engine`. @@ -385,11 +394,12 @@ impl Engine { &mut self, name: &str, callback: impl Fn(&mut T, U) + SendSync + 'static, - ) where + ) -> &mut Self + where T: Variant + Clone, U: Variant + Clone, { - self.register_fn(&make_setter(name), callback); + self.register_fn(&make_setter(name), callback) } /// Shorthand for registering both getter and setter functions @@ -436,12 +446,12 @@ impl Engine { name: &str, get_fn: impl Fn(&mut T) -> U + SendSync + 'static, set_fn: impl Fn(&mut T, U) + SendSync + 'static, - ) where + ) -> &mut Self + where T: Variant + Clone, U: Variant + Clone, { - self.register_get(name, get_fn); - self.register_set(name, set_fn); + self.register_get(name, get_fn).register_set(name, set_fn) } /// Register an index getter for a registered type with the `Engine`. @@ -485,12 +495,13 @@ impl Engine { pub fn register_indexer_get( &mut self, callback: impl Fn(&mut T, X) -> U + SendSync + 'static, - ) where + ) -> &mut Self + where T: Variant + Clone, U: Variant + Clone, X: Variant + Clone, { - self.register_fn(FN_IDX_GET, callback); + self.register_fn(FN_IDX_GET, callback) } /// Register an index setter for a registered type with the `Engine`. @@ -533,12 +544,13 @@ impl Engine { pub fn register_indexer_set( &mut self, callback: impl Fn(&mut T, X, U) -> () + SendSync + 'static, - ) where + ) -> &mut Self + where T: Variant + Clone, U: Variant + Clone, X: Variant + Clone, { - self.register_fn(FN_IDX_SET, callback); + self.register_fn(FN_IDX_SET, callback) } /// Shorthand for register both index getter and setter functions for a registered type with the `Engine`. @@ -580,13 +592,14 @@ impl Engine { &mut self, getter: impl Fn(&mut T, X) -> U + SendSync + 'static, setter: impl Fn(&mut T, X, U) -> () + SendSync + 'static, - ) where + ) -> &mut Self + where T: Variant + Clone, U: Variant + Clone, X: Variant + Clone, { - self.register_indexer_get(getter); - self.register_indexer_set(setter); + self.register_indexer_get(getter) + .register_indexer_set(setter) } /// Compile a string into an `AST`, which can be used later for evaluation. @@ -1466,8 +1479,12 @@ impl Engine { /// # Ok(()) /// # } /// ``` - pub fn on_progress(&mut self, callback: impl Fn(&u64) -> bool + SendSync + 'static) { + pub fn on_progress( + &mut self, + callback: impl Fn(&u64) -> bool + SendSync + 'static, + ) -> &mut Self { self.progress = Some(Box::new(callback)); + self } /// Override default action of `print` (print to stdout using `println!`) @@ -1494,8 +1511,9 @@ impl Engine { /// # Ok(()) /// # } /// ``` - pub fn on_print(&mut self, callback: impl Fn(&str) + SendSync + 'static) { + pub fn on_print(&mut self, callback: impl Fn(&str) + SendSync + 'static) -> &mut Self { self.print = Box::new(callback); + self } /// Override default action of `debug` (print to stdout using `println!`) @@ -1522,7 +1540,8 @@ impl Engine { /// # Ok(()) /// # } /// ``` - pub fn on_debug(&mut self, callback: impl Fn(&str) + SendSync + 'static) { + pub fn on_debug(&mut self, callback: impl Fn(&str) + SendSync + 'static) -> &mut Self { self.debug = Box::new(callback); + self } } diff --git a/src/fn_register.rs b/src/fn_register.rs index 01a3255f..7cf7be74 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -40,7 +40,7 @@ pub trait RegisterFn { /// # Ok(()) /// # } /// ``` - fn register_fn(&mut self, name: &str, f: FN); + fn register_fn(&mut self, name: &str, f: FN) -> &mut Self; } /// Trait to register fallible custom functions returning `Result>` with the `Engine`. @@ -70,7 +70,7 @@ pub trait RegisterResultFn { /// engine.eval::("div(42, 0)") /// .expect_err("expecting division by zero error!"); /// ``` - fn register_result_fn(&mut self, name: &str, f: FN); + fn register_result_fn(&mut self, name: &str, f: FN) -> &mut Self; } // These types are used to build a unique _marker_ tuple type for each combination @@ -182,11 +182,12 @@ macro_rules! def_register { RET: Variant + Clone > RegisterFn for Engine { - fn register_fn(&mut self, name: &str, f: FN) { + fn register_fn(&mut self, name: &str, f: FN) -> &mut Self { self.global_module.set_fn(name, FnAccess::Public, &[$(map_type_id::<$par>()),*], CallableFunction::$abi(make_func!(f : map_dynamic ; $($par => $clone),*)) ); + self } } @@ -195,11 +196,12 @@ macro_rules! def_register { FN: Fn($($param),*) -> Result> + SendSync + 'static, > RegisterResultFn for Engine { - fn register_result_fn(&mut self, name: &str, f: FN) { + fn register_result_fn(&mut self, name: &str, f: FN) -> &mut Self { self.global_module.set_fn(name, FnAccess::Public, &[$(map_type_id::<$par>()),*], CallableFunction::$abi(make_func!(f : map_result ; $($par => $clone),*)) ); + self } } diff --git a/src/module.rs b/src/module.rs index 94bd40a4..c64d16e2 100644 --- a/src/module.rs +++ b/src/module.rs @@ -214,9 +214,10 @@ impl Module { /// module.set_var("answer", 42_i64); /// assert_eq!(module.get_var_value::("answer").unwrap(), 42); /// ``` - pub fn set_var(&mut self, name: impl Into, value: impl Variant + Clone) { + pub fn set_var(&mut self, name: impl Into, value: impl Variant + Clone) -> &mut Self { self.variables.insert(name.into(), Dynamic::from(value)); self.indexed = false; + self } /// Get a mutable reference to a modules-qualified variable. @@ -239,7 +240,7 @@ impl Module { /// /// If there is an existing function of the same name and number of arguments, it is replaced. #[cfg(not(feature = "no_function"))] - pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) { + pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) -> &mut Self { // None + function name + number of arguments. let hash_script = calc_fn_hash(empty(), &fn_def.name, fn_def.params.len(), empty()); self.functions.insert( @@ -252,6 +253,7 @@ impl Module { ), ); self.indexed = false; + self } /// Does a sub-module exist in the module? @@ -316,9 +318,10 @@ impl Module { /// module.set_sub_module("question", sub_module); /// assert!(module.get_sub_module("question").is_some()); /// ``` - pub fn set_sub_module(&mut self, name: impl Into, sub_module: Module) { + pub fn set_sub_module(&mut self, name: impl Into, sub_module: Module) -> &mut Self { self.modules.insert(name.into(), sub_module.into()); self.indexed = false; + self } /// Does the particular Rust function exist in the module? @@ -865,7 +868,7 @@ impl Module { } /// Merge another module into this module. - pub fn merge(&mut self, other: &Self) { + pub fn merge(&mut self, other: &Self) -> &mut Self { self.merge_filtered(other, |_, _, _| true) } @@ -874,7 +877,7 @@ impl Module { &mut self, other: &Self, filter: impl Fn(FnAccess, &str, usize) -> bool, - ) { + ) -> &mut Self { self.variables .extend(other.variables.iter().map(|(k, v)| (k.clone(), v.clone()))); @@ -896,11 +899,15 @@ impl Module { self.all_functions.clear(); self.all_variables.clear(); self.indexed = false; + self } /// Filter out the functions, retaining only some based on a filter predicate. #[cfg(not(feature = "no_function"))] - pub(crate) fn retain_functions(&mut self, filter: impl Fn(FnAccess, &str, usize) -> bool) { + pub(crate) fn retain_functions( + &mut self, + filter: impl Fn(FnAccess, &str, usize) -> bool, + ) -> &mut Self { self.functions.retain(|_, (_, _, _, v)| match v { Func::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()), _ => true, @@ -909,6 +916,7 @@ impl Module { self.all_functions.clear(); self.all_variables.clear(); self.indexed = false; + self } /// Get the number of variables in the module. @@ -1071,9 +1079,10 @@ impl Module { } /// Set a type iterator into the module. - pub fn set_iter(&mut self, typ: TypeId, func: IteratorFn) { + pub fn set_iter(&mut self, typ: TypeId, func: IteratorFn) -> &mut Self { self.type_iterators.insert(typ, func); self.indexed = false; + self } /// Get the specified type iterator. diff --git a/src/optimize.rs b/src/optimize.rs index ed07d1fb..3da46ffa 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -732,7 +732,9 @@ pub fn optimize_into_ast( } .into() }) - .for_each(|fn_def| lib2.set_script_fn(fn_def)); + .for_each(|fn_def| { + lib2.set_script_fn(fn_def); + }); functions .into_iter() @@ -761,11 +763,13 @@ pub fn optimize_into_ast( }; fn_def.into() }) - .for_each(|fn_def| module.set_script_fn(fn_def)); + .for_each(|fn_def| { + module.set_script_fn(fn_def); + }); } else { - functions - .into_iter() - .for_each(|fn_def| module.set_script_fn(fn_def)); + functions.into_iter().for_each(|fn_def| { + module.set_script_fn(fn_def); + }); } module diff --git a/src/scope.rs b/src/scope.rs index 56b3a8cc..8342c792 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -97,8 +97,9 @@ impl<'a> Scope<'a> { /// assert_eq!(my_scope.len(), 0); /// assert!(my_scope.is_empty()); /// ``` - pub fn clear(&mut self) { + pub fn clear(&mut self) -> &mut Self { self.0.clear(); + self } /// Get the number of entries inside the Scope. @@ -147,8 +148,12 @@ impl<'a> Scope<'a> { /// my_scope.push("x", 42_i64); /// assert_eq!(my_scope.get_value::("x").unwrap(), 42); /// ``` - pub fn push>, T: Variant + Clone>(&mut self, name: K, value: T) { - self.push_dynamic_value(name, EntryType::Normal, Dynamic::from(value), false); + pub fn push>, T: Variant + Clone>( + &mut self, + name: K, + value: T, + ) -> &mut Self { + self.push_dynamic_value(name, EntryType::Normal, Dynamic::from(value), false) } /// Add (push) a new `Dynamic` entry to the Scope. @@ -163,8 +168,8 @@ impl<'a> Scope<'a> { /// my_scope.push_dynamic("x", Dynamic::from(42_i64)); /// assert_eq!(my_scope.get_value::("x").unwrap(), 42); /// ``` - pub fn push_dynamic>>(&mut self, name: K, value: Dynamic) { - self.push_dynamic_value(name, EntryType::Normal, value, false); + pub fn push_dynamic>>(&mut self, name: K, value: Dynamic) -> &mut Self { + self.push_dynamic_value(name, EntryType::Normal, value, false) } /// Add (push) a new constant to the Scope. @@ -185,8 +190,12 @@ impl<'a> Scope<'a> { /// my_scope.push_constant("x", 42_i64); /// assert_eq!(my_scope.get_value::("x").unwrap(), 42); /// ``` - pub fn push_constant>, T: Variant + Clone>(&mut self, name: K, value: T) { - self.push_dynamic_value(name, EntryType::Constant, Dynamic::from(value), true); + pub fn push_constant>, T: Variant + Clone>( + &mut self, + name: K, + value: T, + ) -> &mut Self { + self.push_dynamic_value(name, EntryType::Constant, Dynamic::from(value), true) } /// Add (push) a new constant with a `Dynamic` value to the Scope. @@ -208,8 +217,12 @@ impl<'a> Scope<'a> { /// my_scope.push_constant_dynamic("x", Dynamic::from(42_i64)); /// assert_eq!(my_scope.get_value::("x").unwrap(), 42); /// ``` - pub fn push_constant_dynamic>>(&mut self, name: K, value: Dynamic) { - self.push_dynamic_value(name, EntryType::Constant, value, true); + pub fn push_constant_dynamic>>( + &mut self, + name: K, + value: Dynamic, + ) -> &mut Self { + self.push_dynamic_value(name, EntryType::Constant, value, true) } /// Add (push) a new entry with a `Dynamic` value to the Scope. @@ -219,7 +232,7 @@ impl<'a> Scope<'a> { entry_type: EntryType, value: Dynamic, map_expr: bool, - ) { + ) -> &mut Self { let expr = if map_expr { map_dynamic_to_expr(value.clone(), Position::none()).map(Box::new) } else { @@ -233,6 +246,8 @@ impl<'a> Scope<'a> { value: value.into(), expr, }); + + self } /// Truncate (rewind) the Scope to a previous size. @@ -261,8 +276,9 @@ impl<'a> Scope<'a> { /// assert_eq!(my_scope.len(), 0); /// assert!(my_scope.is_empty()); /// ``` - pub fn rewind(&mut self, size: usize) { + pub fn rewind(&mut self, size: usize) -> &mut Self { self.0.truncate(size); + self } /// Does the scope contain the entry? @@ -341,14 +357,17 @@ impl<'a> Scope<'a> { /// my_scope.set_value("x", 0_i64); /// assert_eq!(my_scope.get_value::("x").unwrap(), 0); /// ``` - pub fn set_value(&mut self, name: &'a str, value: T) { + pub fn set_value(&mut self, name: &'a str, value: T) -> &mut Self { match self.get_index(name) { - None => self.push(name, value), + None => { + self.push(name, value); + } Some((_, EntryType::Constant)) => panic!("variable {} is constant", name), Some((index, EntryType::Normal)) => { - self.0.get_mut(index).unwrap().value = Dynamic::from(value) + self.0.get_mut(index).unwrap().value = Dynamic::from(value); } } + self } /// Get a mutable reference to an entry in the Scope. @@ -358,9 +377,10 @@ impl<'a> Scope<'a> { } /// Update the access type of an entry in the Scope. - pub(crate) fn set_entry_alias(&mut self, index: usize, alias: String) { + pub(crate) fn set_entry_alias(&mut self, index: usize, alias: String) -> &mut Self { let entry = self.0.get_mut(index).expect("invalid index in Scope"); entry.alias = Some(Box::new(alias)); + self } /// Get an iterator to entries in the Scope. diff --git a/src/settings.rs b/src/settings.rs index 07b87198..e3265c4a 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -9,26 +9,19 @@ impl Engine { /// /// When searching for functions, packages loaded later are preferred. /// In other words, loaded packages are searched in reverse order. - pub fn load_package(&mut self, package: PackageLibrary) { - // Push the package to the top - packages are searched in reverse order - self.packages.push(package); - } - - /// Load a new package into the `Engine`. - /// - /// When searching for functions, packages loaded later are preferred. - /// In other words, loaded packages are searched in reverse order. - pub fn load_packages(&mut self, package: PackageLibrary) { + pub fn load_package(&mut self, package: PackageLibrary) -> &mut Self { // Push the package to the top - packages are searched in reverse order self.packages.push(package); + self } /// Control whether and how the `Engine` will optimize an AST after compilation. /// /// Not available under the `no_optimize` feature. #[cfg(not(feature = "no_optimize"))] - pub fn set_optimization_level(&mut self, optimization_level: OptimizationLevel) { - self.optimization_level = optimization_level + pub fn set_optimization_level(&mut self, optimization_level: OptimizationLevel) -> &mut Self { + self.optimization_level = optimization_level; + self } /// The current optimization level. @@ -43,8 +36,9 @@ impl Engine { /// Set the maximum levels of function calls allowed for a script in order to avoid /// infinite recursion and stack overflows. #[cfg(not(feature = "unchecked"))] - pub fn set_max_call_levels(&mut self, levels: usize) { - self.max_call_stack_depth = levels + pub fn set_max_call_levels(&mut self, levels: usize) -> &mut Self { + self.max_call_stack_depth = levels; + self } /// The maximum levels of function calls allowed for a script. @@ -56,12 +50,13 @@ impl Engine { /// Set the maximum number of operations allowed for a script to run to avoid /// consuming too much resources (0 for unlimited). #[cfg(not(feature = "unchecked"))] - pub fn set_max_operations(&mut self, operations: u64) { + pub fn set_max_operations(&mut self, operations: u64) -> &mut Self { self.max_operations = if operations == u64::MAX { 0 } else { operations }; + self } /// The maximum number of operations allowed for a script to run (0 for unlimited). @@ -72,8 +67,9 @@ impl Engine { /// Set the maximum number of imported modules allowed for a script. #[cfg(not(feature = "unchecked"))] - pub fn set_max_modules(&mut self, modules: usize) { + pub fn set_max_modules(&mut self, modules: usize) -> &mut Self { self.max_modules = modules; + self } /// The maximum number of imported modules allowed for a script. @@ -84,7 +80,11 @@ impl Engine { /// Set the depth limits for expressions (0 for unlimited). #[cfg(not(feature = "unchecked"))] - pub fn set_max_expr_depths(&mut self, max_expr_depth: usize, max_function_expr_depth: usize) { + pub fn set_max_expr_depths( + &mut self, + max_expr_depth: usize, + max_function_expr_depth: usize, + ) -> &mut Self { self.max_expr_depth = if max_expr_depth == usize::MAX { 0 } else { @@ -95,6 +95,7 @@ impl Engine { } else { max_function_expr_depth }; + self } /// The depth limit for expressions (0 for unlimited). @@ -111,8 +112,9 @@ impl Engine { /// Set the maximum length of strings (0 for unlimited). #[cfg(not(feature = "unchecked"))] - pub fn set_max_string_size(&mut self, max_size: usize) { + pub fn set_max_string_size(&mut self, max_size: usize) -> &mut Self { self.max_string_size = if max_size == usize::MAX { 0 } else { max_size }; + self } /// The maximum length of strings (0 for unlimited). @@ -124,8 +126,9 @@ impl Engine { /// Set the maximum length of arrays (0 for unlimited). #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "no_index"))] - pub fn set_max_array_size(&mut self, max_size: usize) { + pub fn set_max_array_size(&mut self, max_size: usize) -> &mut Self { self.max_array_size = if max_size == usize::MAX { 0 } else { max_size }; + self } /// The maximum length of arrays (0 for unlimited). @@ -138,8 +141,9 @@ impl Engine { /// Set the maximum length of object maps (0 for unlimited). #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "no_object"))] - pub fn set_max_map_size(&mut self, max_size: usize) { + pub fn set_max_map_size(&mut self, max_size: usize) -> &mut Self { self.max_map_size = if max_size == usize::MAX { 0 } else { max_size }; + self } /// The maximum length of object maps (0 for unlimited). @@ -153,8 +157,12 @@ impl Engine { /// /// Not available under the `no_module` feature. #[cfg(not(feature = "no_module"))] - pub fn set_module_resolver(&mut self, resolver: Option) { + pub fn set_module_resolver( + &mut self, + resolver: Option, + ) -> &mut Self { self.module_resolver = resolver.map(|f| Box::new(f) as Box); + self } /// Disable a particular keyword or operator in the language. @@ -194,7 +202,7 @@ impl Engine { /// # Ok(()) /// # } /// ``` - pub fn disable_symbol(&mut self, symbol: &str) { + pub fn disable_symbol(&mut self, symbol: &str) -> &mut Self { if self.disabled_symbols.is_none() { self.disabled_symbols = Some(Default::default()); } @@ -203,6 +211,8 @@ impl Engine { .as_mut() .unwrap() .insert(symbol.into()); + + self } /// Register a custom operator into the language. @@ -235,7 +245,7 @@ impl Engine { &mut self, keyword: &str, precedence: u8, - ) -> Result<(), String> { + ) -> Result<&mut Self, String> { if !is_valid_identifier(keyword.chars()) { return Err(format!("not a valid identifier: '{}'", keyword).into()); } @@ -249,6 +259,6 @@ impl Engine { .unwrap() .insert(keyword.into(), precedence); - Ok(()) + Ok(self) } } diff --git a/src/syntax.rs b/src/syntax.rs index 05ab5e0b..62aa20bd 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -39,7 +39,7 @@ pub type FnCustomSyntaxEval = dyn Fn( &mut State, &Module, &mut Option<&mut Dynamic>, - &[Expr], + &[Expression], usize, ) -> Result> + Send @@ -75,7 +75,7 @@ impl Engine { ) -> Result> + SendSync + 'static, - ) -> Result<(), Box> { + ) -> Result> { if value.is_empty() { return Err(Box::new(LexError::ImproperSymbol("".to_string()))); } @@ -145,6 +145,6 @@ impl Engine { .unwrap() .insert(key, syntax.into()); - Ok(()) + Ok(self) } } diff --git a/tests/method_call.rs b/tests/method_call.rs index 02c7a950..20612146 100644 --- a/tests/method_call.rs +++ b/tests/method_call.rs @@ -21,10 +21,10 @@ fn test_method_call() -> Result<(), Box> { let mut engine = Engine::new(); - engine.register_type::(); - - engine.register_fn("update", TestStruct::update); - engine.register_fn("new_ts", TestStruct::new); + engine + .register_type::() + .register_fn("update", TestStruct::update) + .register_fn("new_ts", TestStruct::new); assert_eq!( engine.eval::("let x = new_ts(); x.update(1000); x")?, diff --git a/tests/mismatched_op.rs b/tests/mismatched_op.rs index 1199bf94..01373df2 100644 --- a/tests/mismatched_op.rs +++ b/tests/mismatched_op.rs @@ -25,8 +25,10 @@ fn test_mismatched_op_custom_type() { } let mut engine = Engine::new(); - engine.register_type_with_name::("TestStruct"); - engine.register_fn("new_ts", TestStruct::new); + + engine + .register_type_with_name::("TestStruct") + .register_fn("new_ts", TestStruct::new); assert!(matches!( *engine.eval::("60 + new_ts()").expect_err("should error"), diff --git a/tests/modules.rs b/tests/modules.rs index 479ec429..3d0fc48d 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -69,6 +69,7 @@ fn test_module_resolver() -> Result<(), Box> { let mut resolver = StaticModuleResolver::new(); let mut module = Module::new(); + module.set_var("answer", 42 as INT); module.set_fn_4("sum".to_string(), |x: INT, y: INT, z: INT, w: INT| { Ok(x + y + z + w) diff --git a/tests/print.rs b/tests/print.rs index ec707e8b..8c580f42 100644 --- a/tests/print.rs +++ b/tests/print.rs @@ -3,16 +3,17 @@ use std::sync::{Arc, RwLock}; #[test] fn test_print() -> Result<(), Box> { - let mut engine = Engine::new(); - let logbook = Arc::new(RwLock::new(Vec::::new())); // Redirect print/debug output to 'log' - let log = logbook.clone(); - engine.on_print(move |s| log.write().unwrap().push(format!("entry: {}", s))); + let log1 = logbook.clone(); + let log2 = logbook.clone(); - let log = logbook.clone(); - engine.on_debug(move |s| log.write().unwrap().push(format!("DEBUG: {}", s))); + let mut engine = Engine::new(); + + engine + .on_print(move |s| log1.write().unwrap().push(format!("entry: {}", s))) + .on_debug(move |s| log2.write().unwrap().push(format!("DEBUG: {}", s))); // Evaluate script engine.eval::<()>("print(40 + 2)")?;