From a82ebf853ff75286dad7dd82c25cb114469638ef Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 7 May 2020 00:09:35 +0800 Subject: [PATCH 01/21] Delete benchmark.yml --- .github/workflows/benchmark.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 .github/workflows/benchmark.yml diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml deleted file mode 100644 index df310705..00000000 --- a/.github/workflows/benchmark.yml +++ /dev/null @@ -1,29 +0,0 @@ -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 6be9301a26fa6598808b09ea61c11ea4e002a8aa Mon Sep 17 00:00:00 2001 From: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com> Date: Thu, 25 Jun 2020 23:24:35 +0800 Subject: [PATCH 02/21] re-add core crates packages on no_std which is not imported --- src/error.rs | 3 +++ src/packages/array_basic.rs | 3 +++ src/packages/string_more.rs | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/error.rs b/src/error.rs index ce8615ed..9702a7e5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,6 +5,9 @@ use crate::token::Position; use crate::stdlib::{boxed::Box, char, error::Error, fmt, string::String}; +#[cfg(feature = "no_std")] +use crate::alloc::string::ToString; + /// Error when tokenizing the script text. #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum LexError { diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 59358063..59b297a4 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -10,6 +10,9 @@ use crate::token::Position; use crate::stdlib::{any::TypeId, boxed::Box}; +#[cfg(feature = "no_std")] +use crate::alloc::string::ToString; + // Register array utility functions fn push(list: &mut Array, item: T) -> FuncReturn<()> { list.push(Dynamic::from(item)); diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 46805750..73ae6e2d 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -10,6 +10,9 @@ use crate::utils::StaticVec; #[cfg(not(feature = "no_index"))] use crate::engine::Array; +#[cfg(feature = "no_std")] +use crate::alloc::boxed::Box; + use crate::stdlib::{ any::TypeId, fmt::Display, From 0bd636a66040a51f94d0ee263f197c0071fe5afe Mon Sep 17 00:00:00 2001 From: Steve Fan <19037626d@connect.polyu.hk> Date: Fri, 26 Jun 2020 00:40:01 +0800 Subject: [PATCH 03/21] speclatively use stdlib to have automatic no_std compatibility --- src/error.rs | 5 +---- src/packages/array_basic.rs | 5 +---- src/packages/string_more.rs | 4 +--- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/error.rs b/src/error.rs index 9702a7e5..16ea36bd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,10 +3,7 @@ use crate::result::EvalAltResult; use crate::token::Position; -use crate::stdlib::{boxed::Box, char, error::Error, fmt, string::String}; - -#[cfg(feature = "no_std")] -use crate::alloc::string::ToString; +use crate::stdlib::{boxed::Box, char, error::Error, fmt, string::{String, ToString}}; /// Error when tokenizing the script text. #[derive(Debug, Eq, PartialEq, Clone, Hash)] diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 59b297a4..d492f52f 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -8,10 +8,7 @@ use crate::parser::{ImmutableString, INT}; use crate::result::EvalAltResult; use crate::token::Position; -use crate::stdlib::{any::TypeId, boxed::Box}; - -#[cfg(feature = "no_std")] -use crate::alloc::string::ToString; +use crate::stdlib::{any::TypeId, boxed::Box, string::ToString}; // Register array utility functions fn push(list: &mut Array, item: T) -> FuncReturn<()> { diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 73ae6e2d..a472c2dd 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -10,11 +10,9 @@ use crate::utils::StaticVec; #[cfg(not(feature = "no_index"))] use crate::engine::Array; -#[cfg(feature = "no_std")] -use crate::alloc::boxed::Box; - use crate::stdlib::{ any::TypeId, + boxed::Box, fmt::Display, format, string::{String, ToString}, From 760c13d36e5fb08946ca36996e6fe646cefb139c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 1 Jul 2020 22:21:43 +0800 Subject: [PATCH 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] 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(()) +}