diff --git a/CHANGELOG.md b/CHANGELOG.md index 4942004e..17c0f8fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Version 1.5.0 This version adds a debugging interface, which can be used to integrate a debugger. +Based on popular demand, an option is added to throw exceptions when invalid properties are accessed +on object maps (default is to return `()`). + +Also based on popular demand, the `REPL` tool now uses +[`rustyline`](https://crates.io/crates/rustyline) for line editing and history. + Bug fixes --------- @@ -15,11 +21,14 @@ Bug fixes * Globally-defined constants are now encapsulated correctly inside a loaded module and no longer spill across call boundaries. * Type names display is fixed. * Exceptions thrown inside function calls now unwrap correctly when `catch`-ed. +* Error messages for certain invalid property accesses are fixed. Script-breaking changes ----------------------- * For consistency with the `import` statement, the `export` statement no longer exports multiple variables. +* Appending a BLOB to a string (via `+`, `+=`, `append` or string interpolation) now treats the BLOB as a UTF-8 encoded string. +* Appending a string/character to a BLOB (via `+=` or `append`) now adds the string/character as a UTF-8 encoded byte stream. New features ------------ @@ -27,8 +36,10 @@ New features * A debugging interface is added. * A new bin tool, `rhai-dbg` (aka _The Rhai Debugger_), is added to showcase the debugging interface. * A new package, `DebuggingPackage`, is added which contains the `back_trace` function to get the current call stack anywhere in a script. +* `Engine::set_fail_on_invalid_map_property` is added to control whether to raise an error (new `EvalAltResult::ErrorPropertyNotFound`) when invalid properties are accessed on object maps. * `Engine::set_allow_shadowing` is added to allow/disallow variables _shadowing_, with new errors `EvalAltResult::ErrorVariableExists` and `ParseErrorType::VariableExists`. -* `Engine::on_def_var` allows registering a closure which can decide whether a variable definition is allow to continue, or should fail with an error. +* `Engine::on_def_var` allows registering a closure which can decide whether a variable definition is allow to continue, during compilation or runtime, or should fail with an error (`ParseErrorType::ForbiddenVariable` or `EvalAltResult::ErrorForbiddenVariable`). +* A new syntax for defining custom packages is introduced that removes the need to specify the Rhai crate name (internally uses the `$crate` meta variable). Enhancements ------------ @@ -42,6 +53,9 @@ Enhancements * `Expr::start_position` is added to give the beginning of the expression (not the operator's position). * `StmtBlock` and `Stmt::Block` now keep the position of the closing `}` as well. * `EvalAltResult::unwrap_inner` is added to access the base error inside multiple layers of wrappings (e.g. `EvalAltResult::ErrorInFunction`). +* Yet another new syntax is introduced for `def_package!` that further simplifies the old syntax. +* A new method `to_blob` is added to convert a string into a BLOB as UTF-8 encoded bytes. +* A new method `to_array` is added to convert a BLOB into array of integers. REPL tool changes ----------------- diff --git a/Cargo.toml b/Cargo.toml index 4fb2e519..7aa8be11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ unicode-xid = { version = "0.2", default-features = false, optional = true } rust_decimal = { version = "1.16", default-features = false, features = ["maths"], optional = true } # notice that a custom modified version of `rustyline` is used which supports bracketed paste on Windows # this can be moved to the official version when bracketed paste is added -rustyline = { version = "9", optional = true, git = "https://github.com/schungx/rustyline", branch = "bracketed_paste" } +rustyline = { version = "9", optional = true, git = "https://github.com/schungx/rustyline" } [dev-dependencies] serde_bytes = "0.11" diff --git a/examples/README.md b/examples/README.md index a3468bcb..182b46a5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,12 +1,40 @@ Sample Applications =================== -Sample applications that use the Rhai scripting engine. +Standard Examples +----------------- + +| Example | Description | +| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| [`arrays_and_structs`](arrays_and_structs.rs) | shows how to register a Rust type and using it with arrays | +| [`callback`](callback.rs) | shows how to store a Rhai closure and call it later within Rust | +| [`custom_types_and_methods`](custom_types_and_methods.rs) | shows how to register a Rust type and methods/getters/setters for it | +| [`hello`](hello.rs) | simple example that evaluates an expression and prints the result | +| [`reuse_scope`](reuse_scope.rs) | evaluates two pieces of code in separate runs, but using a common `Scope` | +| [`serde`](serde.rs) | example to serialize and deserialize Rust types with [`serde`](https://crates.io/crates/serde) (requires the `serde` feature) | +| [`simple_fn`](simple_fn.rs) | shows how to register a simple Rust function | +| [`strings`](strings.rs) | shows different ways to register Rust functions taking string arguments | +| [`threading`](threading.rs) | shows how to communicate with an `Engine` running in a separate thread via an MPSC channel | -How to Run ----------- +Scriptable Event Handler With State Examples +------------------------------------------- -```bash -cargo run --example sample_app_to_run +Because of its popularity, included are sample implementations for the pattern +[_Scriptable Event Handler With State_](https://rhai.rs/book/patterns/events.html) in different styles. + +| Example | Handler Script | Description | +| ------------------------------------------ | ------------------------------------------------------------------ | :---------------------------------------------------------: | +| [`event_handler_main`](event_handler_main) | [`event_handler_main/script.rhai`](event_handler_main/script.rhai) | [_Main Style_](https://rhai.rs/book/patterns/events-1.html) | +| [`event_handler_js`](event_handler_js) | [`event_handler_js/script.rhai`](event_handler_js/script.rhai) | [_JS Style_](https://rhai.rs/book/patterns/events-2.html) | +| [`event_handler_map`](event_handler_map) | [`event_handler_map/script.rhai`](event_handler_map/script.rhai) | [_Map Style_](https://rhai.rs/book/patterns/events-3.html) | + + +Running Examples +---------------- + +Examples can be run with the following command: + +```sh +cargo run --example {example_name} ``` diff --git a/examples/arrays_and_structs.rs b/examples/arrays_and_structs.rs index dab0880b..1cfeac47 100644 --- a/examples/arrays_and_structs.rs +++ b/examples/arrays_and_structs.rs @@ -1,29 +1,43 @@ +//! An example showing how to register a Rust type and use it with arrays. + use rhai::{Engine, EvalAltResult}; -#[derive(Debug, Clone)] -struct TestStruct { - x: i64, -} - -impl TestStruct { - pub fn update(&mut self) { - self.x += 1000; - } - pub fn new() -> Self { - Self { x: 1 } - } -} - #[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_object"))] fn main() -> Result<(), Box> { + #[derive(Debug, Clone)] + struct TestStruct { + x: i64, + } + + impl TestStruct { + pub fn new() -> Self { + Self { x: 1 } + } + pub fn update(&mut self) { + self.x += 1000; + } + } + let mut engine = Engine::new(); engine - .register_type::() + .register_type_with_name::("TestStruct") .register_fn("new_ts", TestStruct::new) .register_fn("update", TestStruct::update); + #[cfg(feature = "metadata")] + { + println!("Functions registered:"); + + engine + .gen_fn_signatures(false) + .into_iter() + .for_each(|func| println!("{}", func)); + + println!(); + } + let result = engine.eval::( " let x = new_ts(); diff --git a/examples/callback.rs b/examples/callback.rs new file mode 100644 index 00000000..40791bcd --- /dev/null +++ b/examples/callback.rs @@ -0,0 +1,34 @@ +//! This example stores a Rhai closure for later use as a callback. + +use rhai::{Engine, EvalAltResult, FnPtr}; + +fn main() -> Result<(), Box> { + // This script creates a closure which may capture variables. + let script = " + let x = 20; + + // The following closure captures 'x' + return |a, b| (x + a) * b; + "; + + // To call a Rhai closure at a later time, you'd need three things: + // 1) an `Engine` (with all needed functions registered), + // 2) a compiled `AST`, + // 3) the closure (of type `FnPtr`). + let engine = Engine::new(); + + let ast = engine.compile(script)?; + + let closure = engine.eval_ast::(&ast)?; + + // Create a closure that we can call any time, encapsulating the + // `Engine`, `AST` and `FnPtr`. + let func = move |x: i64, y: i64| -> Result { closure.call(&engine, &ast, (x, y)) }; + + // Now we can call `func` anywhere just like a normal function! + let result = func(1, 2)?; + + println!("The Answer: {}", result); // prints 42 + + Ok(()) +} diff --git a/examples/custom_types_and_methods.rs b/examples/custom_types_and_methods.rs index b52d0a27..e8261a73 100644 --- a/examples/custom_types_and_methods.rs +++ b/examples/custom_types_and_methods.rs @@ -1,38 +1,63 @@ +//! An example showing how to register a Rust type and methods/getters/setters for it. + use rhai::{Engine, EvalAltResult}; -#[derive(Debug, Clone)] -struct TestStruct { - x: i64, -} - -impl TestStruct { - pub fn update(&mut self) { - self.x += 1000; - } - - pub fn new() -> Self { - Self { x: 1 } - } -} - #[cfg(not(feature = "no_object"))] fn main() -> Result<(), Box> { + #[derive(Debug, Clone)] + struct TestStruct { + x: i64, + } + + impl TestStruct { + pub fn new() -> Self { + Self { x: 1 } + } + pub fn update(&mut self) { + self.x += 1000; + } + pub fn calculate(&mut self, data: i64) -> i64 { + self.x * data + } + pub fn get_x(&mut self) -> i64 { + self.x + } + pub fn set_x(&mut self, value: i64) { + self.x = value; + } + } + let mut engine = Engine::new(); engine - .register_type::() + .register_type_with_name::("TestStruct") .register_fn("new_ts", TestStruct::new) - .register_fn("update", TestStruct::update); + .register_fn("update", TestStruct::update) + .register_fn("calc", TestStruct::calculate) + .register_get_set("x", TestStruct::get_x, TestStruct::set_x); - let result = engine.eval::( + #[cfg(feature = "metadata")] + { + println!("Functions registered:"); + + engine + .gen_fn_signatures(false) + .into_iter() + .for_each(|func| println!("{}", func)); + + println!(); + } + + let result = engine.eval::( " let x = new_ts(); + x.x = 42; x.update(); - x + x.calc(x.x) ", )?; - println!("result: {}", result.x); // prints 1001 + println!("result: {}", result); // prints 1085764 Ok(()) } diff --git a/examples/hello.rs b/examples/hello.rs index 580315dd..b85b06da 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,3 +1,5 @@ +//! A simple example that evaluates an expression and prints the result. + use rhai::{Engine, EvalAltResult}; fn main() -> Result<(), Box> { @@ -7,7 +9,7 @@ fn main() -> Result<(), Box> { let result = engine.eval::("40 + 2")?; - println!("Answer: {}", result); // prints 42 + println!("The Answer: {}", result); // prints 42 Ok(()) } diff --git a/examples/reuse_scope.rs b/examples/reuse_scope.rs index 240d48c4..3bfa6bf3 100644 --- a/examples/reuse_scope.rs +++ b/examples/reuse_scope.rs @@ -1,3 +1,5 @@ +//! An example that evaluates two pieces of code in separate runs, but using a common `Scope`. + use rhai::{Engine, EvalAltResult, Scope}; fn main() -> Result<(), Box> { diff --git a/examples/serde.rs b/examples/serde.rs index f5554889..625bb5f6 100644 --- a/examples/serde.rs +++ b/examples/serde.rs @@ -1,3 +1,5 @@ +//! An example to serialize and deserialize Rust types. + #[cfg(any(not(feature = "serde"), feature = "no_object"))] fn main() { println!("This example requires the 'serde' feature to run."); diff --git a/examples/simple_fn.rs b/examples/simple_fn.rs index 814e9ccf..06b28c53 100644 --- a/examples/simple_fn.rs +++ b/examples/simple_fn.rs @@ -1,3 +1,5 @@ +//! An example showing how to register a simple Rust function. + use rhai::{Engine, EvalAltResult}; fn add(x: i64, y: i64) -> i64 { diff --git a/examples/strings.rs b/examples/strings.rs index a0997da2..e3fac382 100644 --- a/examples/strings.rs +++ b/examples/strings.rs @@ -1,5 +1,6 @@ -///! This example registers a variety of functions that operate on strings. -///! Remember to use `ImmutableString` or `&str` instead of `String` as parameters. +//! An example that registers a variety of functions that operate on strings. +//! Remember to use `ImmutableString` or `&str` instead of `String` as parameters. + use rhai::{Engine, EvalAltResult, ImmutableString, Scope}; use std::io::{stdin, stdout, Write}; @@ -12,6 +13,7 @@ fn trim_string(s: &mut ImmutableString) { /// Notice this is different from the built-in Rhai 'len' function for strings /// which counts the actual number of Unicode _characters_ in a string. +/// /// This version simply counts the number of _bytes_ in the UTF-8 representation. /// /// This version uses `&str`. diff --git a/examples/threading.rs b/examples/threading.rs index d28f62d7..64cd04dc 100644 --- a/examples/threading.rs +++ b/examples/threading.rs @@ -1,3 +1,6 @@ +//! An advanced example showing how to communicate with an `Engine` running in a separate thread via +//! an MPSC channel. + use rhai::Engine; #[cfg(feature = "sync")] diff --git a/src/api/events.rs b/src/api/events.rs index 0a7b5eaf..0f0061d4 100644 --- a/src/api/events.rs +++ b/src/api/events.rs @@ -5,6 +5,19 @@ use crate::{Dynamic, Engine, EvalContext, Position, RhaiResultOf}; #[cfg(feature = "no_std")] use std::prelude::v1::*; +/// Information on a variable definition. +#[non_exhaustive] +pub struct VarDefInfo<'a> { + /// Name of the variable to be defined. + pub name: &'a str, + /// `true` if the statement is `const`, otherwise it is `let`. + pub is_const: bool, + /// The current nesting level, with zero being the global level. + pub nesting_level: usize, + /// Will the variable _shadow_ an existing variable? + pub will_shadow: bool, +} + impl Engine { /// Provide a callback that will be invoked before each variable access. /// @@ -65,23 +78,25 @@ impl Engine { } /// Provide a callback that will be invoked before the definition of each variable . /// + /// # WARNING - Unstable API + /// + /// This API is volatile and may change in the future. + /// /// # Callback Function Signature /// /// The callback function signature takes the following form: /// - /// > `Fn(name: &str, is_const: bool, block_level: usize, will_shadow: bool, context: &EvalContext) -> Result>` + /// > `Fn(is_runtime: bool, info: VarInfo, context: &EvalContext) -> Result>` /// /// where: - /// * `name`: name of the variable to be defined. - /// * `is_const`: `true` if the statement is `const`, otherwise it is `let`. - /// * `block_level`: the current nesting level of statement blocks, with zero being the global level - /// * `will_shadow`: will the variable _shadow_ an existing variable? + /// * `is_runtime`: `true` if the variable definition event happens during runtime, `false` if during compilation. + /// * `info`: information on the variable. /// * `context`: the current [evaluation context][`EvalContext`]. /// /// ## Return value /// /// * `Ok(true)`: continue with normal variable definition. - /// * `Ok(false)`: deny the variable definition with an [runtime error][EvalAltResult::ErrorRuntime]. + /// * `Ok(false)`: deny the variable definition with an [runtime error][crate::EvalAltResult::ErrorRuntime]. /// /// ## Raising errors /// @@ -96,9 +111,9 @@ impl Engine { /// let mut engine = Engine::new(); /// /// // Register a variable definition filter. - /// engine.on_def_var(|name, is_const, _, _, _| { + /// engine.on_def_var(|_, info, _| { /// // Disallow defining MYSTIC_NUMBER as a constant - /// if name == "MYSTIC_NUMBER" && is_const { + /// if info.name == "MYSTIC_NUMBER" && info.is_const { /// Ok(false) /// } else { /// Ok(true) @@ -114,12 +129,11 @@ impl Engine { /// # Ok(()) /// # } /// ``` + #[deprecated = "This API is volatile and may change in the future."] #[inline(always)] pub fn on_def_var( &mut self, - callback: impl Fn(&str, bool, usize, bool, &EvalContext) -> RhaiResultOf - + SendSync - + 'static, + callback: impl Fn(bool, VarDefInfo, &EvalContext) -> RhaiResultOf + SendSync + 'static, ) -> &mut Self { self.def_var_filter = Some(Box::new(callback)); self @@ -321,8 +335,13 @@ impl Engine { self.debug = Some(Box::new(callback)); self } - /// _(debugging)_ Register callbacks for debugging. + /// _(debugging)_ Register a callback for debugging. /// Exported under the `debugging` feature only. + /// + /// # WARNING - Unstable API + /// + /// This API is volatile and may change in the future. + #[deprecated = "This API is volatile and may change in the future."] #[cfg(feature = "debugging")] #[inline(always)] pub fn register_debugger( diff --git a/src/api/options.rs b/src/api/options.rs index 4fe6a4d7..230df25c 100644 --- a/src/api/options.rs +++ b/src/api/options.rs @@ -18,10 +18,14 @@ pub struct LanguageOptions { pub allow_anonymous_fn: bool, /// Is looping allowed? pub allow_looping: bool, - /// Strict variables mode? - pub strict_var: bool, /// Is variables shadowing allowed? pub allow_shadowing: bool, + /// Strict variables mode? + pub strict_var: bool, + /// Raise error if an object map property does not exist? + /// Returns `()` if `false`. + #[cfg(not(feature = "no_object"))] + pub fail_on_invalid_map_property: bool, } impl LanguageOptions { @@ -37,6 +41,8 @@ impl LanguageOptions { allow_looping: true, strict_var: false, allow_shadowing: true, + #[cfg(not(feature = "no_object"))] + fail_on_invalid_map_property: false, } } } @@ -49,6 +55,7 @@ impl Default for LanguageOptions { impl Engine { /// Is `if`-expression allowed? + /// Default is `true`. #[inline(always)] pub fn allow_if_expression(&self) -> bool { self.options.allow_if_expr @@ -59,6 +66,7 @@ impl Engine { self.options.allow_if_expr = enable; } /// Is `switch` expression allowed? + /// Default is `true`. #[inline(always)] pub fn allow_switch_expression(&self) -> bool { self.options.allow_switch_expr @@ -69,6 +77,7 @@ impl Engine { self.options.allow_switch_expr = enable; } /// Is statement-expression allowed? + /// Default is `true`. #[inline(always)] pub fn allow_statement_expression(&self) -> bool { self.options.allow_stmt_expr @@ -79,6 +88,7 @@ impl Engine { self.options.allow_stmt_expr = enable; } /// Is anonymous function allowed? + /// Default is `true`. /// /// Not available under `no_function`. #[cfg(not(feature = "no_function"))] @@ -95,6 +105,7 @@ impl Engine { self.options.allow_anonymous_fn = enable; } /// Is looping allowed? + /// Default is `true`. #[inline(always)] pub fn allow_looping(&self) -> bool { self.options.allow_looping @@ -104,17 +115,8 @@ impl Engine { pub fn set_allow_looping(&mut self, enable: bool) { self.options.allow_looping = enable; } - /// Is strict variables mode enabled? - #[inline(always)] - pub fn strict_variables(&self) -> bool { - self.options.strict_var - } - /// Set whether strict variables mode is enabled. - #[inline(always)] - pub fn set_strict_variables(&mut self, enable: bool) { - self.options.strict_var = enable; - } /// Is variables shadowing allowed? + /// Default is `true`. #[inline(always)] pub fn allow_shadowing(&self) -> bool { self.options.allow_shadowing @@ -124,4 +126,32 @@ impl Engine { pub fn set_allow_shadowing(&mut self, enable: bool) { self.options.allow_shadowing = enable; } + /// Is strict variables mode enabled? + /// Default is `false`. + #[inline(always)] + pub fn strict_variables(&self) -> bool { + self.options.strict_var + } + /// Set whether strict variables mode is enabled. + #[inline(always)] + pub fn set_strict_variables(&mut self, enable: bool) { + self.options.strict_var = enable; + } + /// Raise error if an object map property does not exist? + /// Default is `false`. + /// + /// Not available under `no_object`. + #[cfg(not(feature = "no_object"))] + #[inline(always)] + pub fn fail_on_invalid_map_property(&self) -> bool { + self.options.fail_on_invalid_map_property + } + /// Set whether to raise error if an object map property does not exist. + /// + /// Not available under `no_object`. + #[cfg(not(feature = "no_object"))] + #[inline(always)] + pub fn set_fail_on_invalid_map_property(&mut self, enable: bool) { + self.options.fail_on_invalid_map_property = enable; + } } diff --git a/src/ast/ast.rs b/src/ast/ast.rs index 0ccad835..06fe65ec 100644 --- a/src/ast/ast.rs +++ b/src/ast/ast.rs @@ -879,7 +879,7 @@ impl Eq for ASTNode<'_> {} impl ASTNode<'_> { /// Get the [`Position`] of this [`ASTNode`]. - pub const fn position(&self) -> Position { + pub fn position(&self) -> Position { match self { ASTNode::Stmt(stmt) => stmt.position(), ASTNode::Expr(expr) => expr.position(), diff --git a/src/ast/expr.rs b/src/ast/expr.rs index 94e5b482..af1fd390 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -440,7 +440,7 @@ impl Default for Expr { impl fmt::Debug for Expr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut display_pos = self.start_position(); + let mut display_pos = format!(" @ {:?}", self.start_position()); match self { Self::DynamicConstant(value, ..) => write!(f, "{:?}", value), @@ -470,24 +470,34 @@ impl fmt::Debug for Expr { f.write_str("Variable(")?; #[cfg(not(feature = "no_module"))] - if let Some((.., ref namespace)) = x.1 { - write!(f, "{}{}", namespace, Token::DoubleColon.literal_syntax())? + if let Some((ref namespace, ..)) = x.1 { + write!(f, "{}{}", namespace, Token::DoubleColon.literal_syntax())?; + let pos = namespace.position(); + if !pos.is_none() { + display_pos = format!(" @ {:?}", pos); + } } f.write_str(&x.2)?; if let Some(n) = i.map_or_else(|| x.0, |n| NonZeroUsize::new(n.get() as usize)) { - write!(f, " #{}", n)? + write!(f, " #{}", n)?; } f.write_str(")") } Self::Property(x, ..) => write!(f, "Property({})", x.2), - Self::Stack(x, ..) => write!(f, "ConstantArg#{}", x), + Self::Stack(x, ..) => write!(f, "ConstantArg[{}]", x), Self::Stmt(x) => { + let pos = x.span(); + if !pos.is_none() { + display_pos = format!(" @ {:?}", pos); + } f.write_str("ExprStmtBlock")?; f.debug_list().entries(x.iter()).finish() } Self::FnCall(x, ..) => fmt::Debug::fmt(x, f), Self::Index(x, term, pos) => { - display_pos = *pos; + if !pos.is_none() { + display_pos = format!(" @ {:?}", pos); + } f.debug_struct("Index") .field("lhs", &x.lhs) @@ -506,7 +516,9 @@ impl fmt::Debug for Expr { ), }; - display_pos = *pos; + if !pos.is_none() { + display_pos = format!(" @ {:?}", pos); + } f.debug_struct(op_name) .field("lhs", &x.lhs) @@ -516,7 +528,7 @@ impl fmt::Debug for Expr { Self::Custom(x, ..) => f.debug_tuple("Custom").field(x).finish(), }?; - display_pos.debug_print(f) + f.write_str(&display_pos) } } @@ -708,8 +720,16 @@ impl Expr { /// For a binary expression, this will be the left-most LHS instead of the operator. #[inline] #[must_use] - pub const fn start_position(&self) -> Position { + pub fn start_position(&self) -> Position { match self { + #[cfg(not(feature = "no_module"))] + Self::Variable(.., x) => { + if let Some((ref namespace, ..)) = x.1 { + namespace.position() + } else { + self.position() + } + } Self::And(x, ..) | Self::Or(x, ..) | Self::Index(x, ..) | Self::Dot(x, ..) => { x.lhs.start_position() } diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index 6c980e1d..27962878 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -2,7 +2,7 @@ use super::{ASTNode, BinaryExpr, Expr, FnCallExpr, Ident, OptionFlags, AST_OPTION_FLAGS::*}; use crate::engine::KEYWORD_EVAL; -use crate::tokenizer::Token; +use crate::tokenizer::{Span, Token}; use crate::{calc_fn_hash, Position, StaticVec, INT}; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -134,7 +134,7 @@ pub struct TryCatchBlock { /// _(internals)_ A scoped block of statements. /// Exported under the `internals` feature only. #[derive(Clone, Hash, Default)] -pub struct StmtBlock(StaticVec, (Position, Position)); +pub struct StmtBlock(StaticVec, Span); impl StmtBlock { /// A [`StmtBlock`] that does not exist. @@ -149,13 +149,13 @@ impl StmtBlock { ) -> Self { let mut statements: StaticVec<_> = statements.into_iter().collect(); statements.shrink_to_fit(); - Self(statements, (start_pos, end_pos)) + Self(statements, Span::new(start_pos, end_pos)) } /// Create an empty [`StmtBlock`]. #[inline(always)] #[must_use] pub const fn empty(pos: Position) -> Self { - Self(StaticVec::new_const(), (pos, pos)) + Self(StaticVec::new_const(), Span::new(pos, pos)) } /// Is this statements block empty? #[inline(always)] @@ -191,38 +191,34 @@ impl StmtBlock { #[inline(always)] #[must_use] pub const fn position(&self) -> Position { - (self.1).0 + (self.1).start() } /// Get the end position (location of the ending `}`) of this statements block. #[inline(always)] #[must_use] pub const fn end_position(&self) -> Position { - (self.1).1 + (self.1).end() } /// Get the positions (locations of the beginning `{` and ending `}`) of this statements block. #[inline(always)] #[must_use] - pub const fn positions(&self) -> (Position, Position) { + pub const fn span(&self) -> Span { self.1 } /// Get the positions (locations of the beginning `{` and ending `}`) of this statements block /// or a default. #[inline(always)] #[must_use] - pub const fn positions_or_else( - &self, - def_start_pos: Position, - def_end_pos: Position, - ) -> (Position, Position) { - ( - (self.1).0.or_else(def_start_pos), - (self.1).1.or_else(def_end_pos), + pub const fn span_or_else(&self, def_start_pos: Position, def_end_pos: Position) -> Span { + Span::new( + (self.1).start().or_else(def_start_pos), + (self.1).end().or_else(def_end_pos), ) } /// Set the positions of this statements block. #[inline(always)] pub fn set_position(&mut self, start_pos: Position, end_pos: Position) { - self.1 = (start_pos, end_pos); + self.1 = Span::new(start_pos, end_pos); } } @@ -260,10 +256,8 @@ impl fmt::Debug for StmtBlock { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("Block")?; fmt::Debug::fmt(&self.0, f)?; - (self.1).0.debug_print(f)?; - #[cfg(not(feature = "no_position"))] - if !(self.1).1.is_none() { - write!(f, "-{:?}", (self.1).1)?; + if !self.1.is_none() { + write!(f, " @ {:?}", self.1)?; } Ok(()) } @@ -273,11 +267,11 @@ impl From for StmtBlock { #[inline] fn from(stmt: Stmt) -> Self { match stmt { - Stmt::Block(mut block, pos) => Self(block.iter_mut().map(mem::take).collect(), pos), - Stmt::Noop(pos) => Self(StaticVec::new_const(), (pos, pos)), + Stmt::Block(mut block, span) => Self(block.iter_mut().map(mem::take).collect(), span), + Stmt::Noop(pos) => Self(StaticVec::new_const(), Span::new(pos, pos)), _ => { let pos = stmt.position(); - Self(vec![stmt].into(), (pos, Position::NONE)) + Self(vec![stmt].into(), Span::new(pos, Position::NONE)) } } } @@ -344,7 +338,7 @@ pub enum Stmt { /// function call forming one statement. FnCall(Box, Position), /// `{` stmt`;` ... `}` - Block(Box<[Stmt]>, (Position, Position)), + Block(Box<[Stmt]>, Span), /// `try` `{` stmt; ... `}` `catch` `(` var `)` `{` stmt; ... `}` TryCatch(Box, Position), /// [expression][Expr] @@ -382,7 +376,7 @@ pub enum Stmt { /// This variant does not map to any language structure. It is currently only used only to /// convert a normal variable into a shared variable when the variable is _captured_ by a closure. #[cfg(not(feature = "no_closure"))] - Share(crate::Identifier), + Share(crate::Identifier, Position), } impl Default for Stmt { @@ -408,11 +402,10 @@ impl Stmt { } /// Get the [position][Position] of this statement. #[must_use] - pub const fn position(&self) -> Position { + pub fn position(&self) -> Position { match self { Self::Noop(pos) | Self::BreakLoop(.., pos) - | Self::Block(.., (pos, ..)) | Self::Assignment(.., pos) | Self::FnCall(.., pos) | Self::If(.., pos) @@ -424,6 +417,8 @@ impl Stmt { | Self::Var(.., pos) | Self::TryCatch(.., pos) => *pos, + Self::Block(.., span) => span.start(), + Self::Expr(x) => x.start_position(), #[cfg(not(feature = "no_module"))] @@ -432,7 +427,7 @@ impl Stmt { Self::Export(.., pos) => *pos, #[cfg(not(feature = "no_closure"))] - Self::Share(..) => Position::NONE, + Self::Share(.., pos) => *pos, } } /// Override the [position][Position] of this statement. @@ -440,7 +435,6 @@ impl Stmt { match self { Self::Noop(pos) | Self::BreakLoop(.., pos) - | Self::Block(.., (pos, ..)) | Self::Assignment(.., pos) | Self::FnCall(.., pos) | Self::If(.., pos) @@ -452,6 +446,8 @@ impl Stmt { | Self::Var(.., pos) | Self::TryCatch(.., pos) => *pos = new_pos, + Self::Block(.., span) => *span = Span::new(new_pos, span.end()), + Self::Expr(x) => { x.set_position(new_pos); } @@ -462,7 +458,7 @@ impl Stmt { Self::Export(.., pos) => *pos = new_pos, #[cfg(not(feature = "no_closure"))] - Self::Share(..) => (), + Self::Share(.., pos) => *pos = new_pos, } self diff --git a/src/bin/rhai-dbg.rs b/src/bin/rhai-dbg.rs index 4b289985..c4ee88ca 100644 --- a/src/bin/rhai-dbg.rs +++ b/src/bin/rhai-dbg.rs @@ -10,28 +10,45 @@ use std::{ }; /// Pretty-print source line. -fn print_source(lines: &[String], pos: Position, offset: usize) { - let line_no = if lines.len() > 1 { - if pos.is_none() { - "".to_string() - } else { - format!("{}: ", pos.line().unwrap()) - } - } else { - "".to_string() - }; - - // Print error position +fn print_source(lines: &[String], pos: Position, offset: usize, window: (usize, usize)) { if pos.is_none() { // No position println!(); - } else { - // Specific position - print line text - println!("{}{}", line_no, lines[pos.line().unwrap() - 1]); + return; + } - // Display position marker + let line = pos.line().unwrap() - 1; + let start = if line >= window.0 { line - window.0 } else { 0 }; + let end = usize::min(line + window.1, lines.len() - 1); + let line_no_len = format!("{}", end).len(); + + // Print error position + if start >= end { + println!("{}: {}", start + 1, lines[start]); if let Some(pos) = pos.position() { - println!("{0:>1$}", "^", line_no.len() + pos + offset); + println!("{0:>1$}", "^", pos + offset + line_no_len + 2); + } + } else { + for n in start..=end { + let marker = if n == line { "> " } else { " " }; + + println!( + "{0}{1}{2:>3$}{5}│ {0}{4}{5}", + if n == line { "\x1b[33m" } else { "" }, + marker, + n + 1, + line_no_len, + lines[n], + if n == line { "\x1b[39m" } else { "" }, + ); + + if n == line { + if let Some(pos) = pos.position() { + let shift = offset + line_no_len + marker.len() + 2; + + println!("{0:>1$}{2:>3$}", "│ ", shift, "\x1b[36m^\x1b[39m", pos + 10); + } + } } } } @@ -40,7 +57,8 @@ fn print_current_source( context: &mut rhai::EvalContext, source: Option<&str>, pos: Position, - lines: &Vec, + lines: &[String], + window: (usize, usize), ) { let current_source = &mut *context .global_runtime_state_mut() @@ -58,7 +76,7 @@ fn print_current_source( println!("{} @ {:?}", src, pos); } else { // Print the current source line - print_source(lines, pos, 0); + print_source(lines, pos, 0, window); } } @@ -101,11 +119,13 @@ fn print_debug_help() { println!("quit, q, exit, kill => quit"); println!("scope => print the scope"); println!("print, p => print all variables de-duplicated"); + println!("print/p this => print the 'this' pointer"); println!("print/p => print the current value of a variable"); #[cfg(not(feature = "no_module"))] println!("imports => print all imported modules"); println!("node => print the current AST node"); println!("list, l => print the current source line"); + println!("list/l => print a source line"); println!("backtrace, bt => print the current call-stack"); println!("info break, i b => print all break-points"); println!("enable/en => enable a break-point"); @@ -122,17 +142,19 @@ fn print_debug_help() { println!( "break/b <#args> => set a new break-point for a function call with #args arguments" ); - println!("throw [message] => throw an exception (message optional)"); + println!("throw => throw a runtime exception"); + println!("throw => throw an exception with string data"); + println!("throw <#> => throw an exception with numeric data"); println!("run, r => restart the script evaluation from beginning"); println!("step, s => go to the next expression, diving into functions"); - println!("over => go to the next expression, skipping oer functions"); + println!("over, o => go to the next expression, skipping oer functions"); println!("next, n, => go to the next statement, skipping over functions"); println!("finish, f => continue until the end of the current function call"); println!("continue, c => continue normal execution"); println!(); } -/// Display the scope. +/// Display the current scope. fn print_scope(scope: &Scope, dedup: bool) { let flattened_clone; let scope = if dedup { @@ -167,8 +189,434 @@ fn print_scope(scope: &Scope, dedup: bool) { ); } } +} - println!(); +// Load script to debug. +fn load_script(engine: &Engine) -> (rhai::AST, String) { + if let Some(filename) = env::args().skip(1).next() { + let mut contents = String::new(); + + let filename = match Path::new(&filename).canonicalize() { + Err(err) => { + eprintln!( + "\x1b[31mError script file path: {}\n{}\x1b[39m", + filename, err + ); + exit(1); + } + Ok(f) => { + match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap()) { + Ok(f) => f.into(), + _ => f, + } + } + }; + + let mut f = match File::open(&filename) { + Err(err) => { + eprintln!( + "\x1b[31mError reading script file: {}\n{}\x1b[39m", + filename.to_string_lossy(), + err + ); + exit(1); + } + Ok(f) => f, + }; + + if let Err(err) = f.read_to_string(&mut contents) { + println!( + "Error reading script file: {}\n{}", + filename.to_string_lossy(), + err + ); + exit(1); + } + + let script = if contents.starts_with("#!") { + // Skip shebang + &contents[contents.find('\n').unwrap_or(0)..] + } else { + &contents[..] + }; + + let ast = match engine + .compile(script) + .map_err(Into::>::into) + { + Err(err) => { + print_error(script, *err); + exit(1); + } + Ok(ast) => ast, + }; + + println!("Script '{}' loaded.", filename.to_string_lossy()); + + (ast, contents) + } else { + eprintln!("\x1b[31mNo script file specified.\x1b[39m"); + exit(1); + } +} + +// Main callback for debugging. +fn debug_callback( + context: &mut rhai::EvalContext, + event: DebuggerEvent, + node: rhai::ASTNode, + source: Option<&str>, + pos: Position, + lines: &[String], +) -> Result> { + // Check event + match event { + DebuggerEvent::Step => (), + DebuggerEvent::BreakPoint(n) => { + match context.global_runtime_state().debugger.break_points()[n] { + #[cfg(not(feature = "no_position"))] + BreakPoint::AtPosition { .. } => (), + BreakPoint::AtFunctionName { ref name, .. } + | BreakPoint::AtFunctionCall { ref name, .. } => { + println!("! Call to function {}.", name) + } + #[cfg(not(feature = "no_object"))] + BreakPoint::AtProperty { ref name, .. } => { + println!("! Property {} accessed.", name) + } + } + } + DebuggerEvent::FunctionExitWithValue(r) => { + println!( + "! Return from function call '{}' => {:?}", + context + .global_runtime_state() + .debugger + .call_stack() + .last() + .unwrap() + .fn_name, + r + ) + } + DebuggerEvent::FunctionExitWithError(err) => { + println!( + "! Return from function call '{}' with error: {}", + context + .global_runtime_state() + .debugger + .call_stack() + .last() + .unwrap() + .fn_name, + err + ) + } + } + + // Print current source line + print_current_source(context, source, pos, lines, (0, 0)); + + // Read stdin for commands + let mut input = String::new(); + + loop { + print!("rhai-dbg> "); + + stdout().flush().expect("couldn't flush stdout"); + + input.clear(); + + match stdin().read_line(&mut input) { + Ok(0) => break Ok(DebuggerCommand::Continue), + Ok(_) => match input + .trim() + .split_whitespace() + .collect::>() + .as_slice() + { + ["help" | "h"] => print_debug_help(), + ["exit" | "quit" | "q" | "kill", ..] => { + println!("Script terminated. Bye!"); + exit(0); + } + ["node"] => { + if pos.is_none() { + println!("{:?}", node); + } else if let Some(source) = source { + println!("{:?} {} @ {:?}", node, source, pos); + } else { + println!("{:?} @ {:?}", node, pos); + } + println!(); + } + ["list" | "l"] => print_current_source(context, source, pos, &lines, (3, 6)), + ["list" | "l", n] if n.parse::().is_ok() => { + let num = n.parse::().unwrap(); + if num <= 0 || num > lines.len() { + eprintln!("\x1b[31mInvalid line: {}\x1b[39m", num); + } else { + let pos = Position::new(num as u16, 0); + print_current_source(context, source, pos, &lines, (3, 6)); + } + } + ["continue" | "c"] => break Ok(DebuggerCommand::Continue), + ["finish" | "f"] => break Ok(DebuggerCommand::FunctionExit), + [] | ["step" | "s"] => break Ok(DebuggerCommand::StepInto), + ["over" | "o"] => break Ok(DebuggerCommand::StepOver), + ["next" | "n"] => break Ok(DebuggerCommand::Next), + ["scope"] => print_scope(context.scope(), false), + ["print" | "p", "this"] => { + if let Some(value) = context.this_ptr() { + println!("=> {:?}", value); + } else { + println!("'this' pointer is unbound."); + } + } + ["print" | "p", var_name] => { + if let Some(value) = context.scope().get_value::(var_name) { + println!("=> {:?}", value); + } else { + eprintln!("Variable not found: {}", var_name); + } + } + ["print" | "p"] => { + print_scope(context.scope(), true); + if let Some(value) = context.this_ptr() { + println!("this = {:?}", value); + } + } + #[cfg(not(feature = "no_module"))] + ["imports"] => { + for (i, (name, module)) in context + .global_runtime_state() + .scan_imports_raw() + .enumerate() + { + println!( + "[{}] {} = {}", + i + 1, + name, + module.id().unwrap_or("") + ); + } + + println!(); + } + #[cfg(not(feature = "no_function"))] + ["backtrace" | "bt"] => { + for frame in context + .global_runtime_state() + .debugger + .call_stack() + .iter() + .rev() + { + println!("{}", frame) + } + } + ["info" | "i", "break" | "b"] => Iterator::for_each( + context + .global_runtime_state() + .debugger + .break_points() + .iter() + .enumerate(), + |(i, bp)| match bp { + #[cfg(not(feature = "no_position"))] + rhai::debugger::BreakPoint::AtPosition { pos, .. } => { + let line_num = format!("[{}] line ", i + 1); + print!("{}", line_num); + print_source(&lines, *pos, line_num.len(), (0, 0)); + } + _ => println!("[{}] {}", i + 1, bp), + }, + ), + ["enable" | "en", n] => { + if let Ok(n) = n.parse::() { + let range = 1..=context + .global_runtime_state_mut() + .debugger + .break_points() + .len(); + if range.contains(&n) { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .get_mut(n - 1) + .unwrap() + .enable(true); + println!("Break-point #{} enabled.", n) + } else { + eprintln!("\x1b[31mInvalid break-point: {}\x1b[39m", n); + } + } else { + eprintln!("\x1b[31mInvalid break-point: '{}'\x1b[39m", n); + } + } + ["disable" | "dis", n] => { + if let Ok(n) = n.parse::() { + let range = 1..=context + .global_runtime_state_mut() + .debugger + .break_points() + .len(); + if range.contains(&n) { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .get_mut(n - 1) + .unwrap() + .enable(false); + println!("Break-point #{} disabled.", n) + } else { + eprintln!("\x1b[31mInvalid break-point: {}\x1b[39m", n); + } + } else { + eprintln!("\x1b[31mInvalid break-point: '{}'\x1b[39m", n); + } + } + ["delete" | "d", n] => { + if let Ok(n) = n.parse::() { + let range = 1..=context + .global_runtime_state_mut() + .debugger + .break_points() + .len(); + if range.contains(&n) { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .remove(n - 1); + println!("Break-point #{} deleted.", n) + } else { + eprintln!("\x1b[31mInvalid break-point: {}\x1b[39m", n); + } + } else { + eprintln!("\x1b[31mInvalid break-point: '{}'\x1b[39m", n); + } + } + ["delete" | "d"] => { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .clear(); + println!("All break-points deleted."); + } + ["break" | "b", fn_name, args] => { + if let Ok(args) = args.parse::() { + let bp = rhai::debugger::BreakPoint::AtFunctionCall { + name: fn_name.trim().into(), + args, + enabled: true, + }; + println!("Break-point added for {}", bp); + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(bp); + } else { + eprintln!("\x1b[31mInvalid number of arguments: '{}'\x1b[39m", args); + } + } + // Property name + #[cfg(not(feature = "no_object"))] + ["break" | "b", param] if param.starts_with('.') && param.len() > 1 => { + let bp = rhai::debugger::BreakPoint::AtProperty { + name: param[1..].into(), + enabled: true, + }; + println!("Break-point added for {}", bp); + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(bp); + } + // Numeric parameter + #[cfg(not(feature = "no_position"))] + ["break" | "b", param] if param.parse::().is_ok() => { + let n = param.parse::().unwrap(); + let range = if source.is_none() { + 1..=lines.len() + } else { + 1..=(u16::MAX as usize) + }; + + if range.contains(&n) { + let bp = rhai::debugger::BreakPoint::AtPosition { + source: source.unwrap_or("").into(), + pos: Position::new(n as u16, 0), + enabled: true, + }; + println!("Break-point added {}", bp); + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(bp); + } else { + eprintln!("\x1b[31mInvalid line number: '{}'\x1b[39m", n); + } + } + // Function name parameter + ["break" | "b", param] => { + let bp = rhai::debugger::BreakPoint::AtFunctionName { + name: param.trim().into(), + enabled: true, + }; + println!("Break-point added for {}", bp); + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(bp); + } + #[cfg(not(feature = "no_position"))] + ["break" | "b"] => { + let bp = rhai::debugger::BreakPoint::AtPosition { + source: source.unwrap_or("").into(), + pos, + enabled: true, + }; + println!("Break-point added {}", bp); + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(bp); + } + ["throw"] => break Err(EvalAltResult::ErrorRuntime(Dynamic::UNIT, pos).into()), + ["throw", num] if num.trim().parse::().is_ok() => { + let value = num.trim().parse::().unwrap().into(); + break Err(EvalAltResult::ErrorRuntime(value, pos).into()); + } + #[cfg(not(feature = "no_float"))] + ["throw", num] if num.trim().parse::().is_ok() => { + let value = num.trim().parse::().unwrap().into(); + break Err(EvalAltResult::ErrorRuntime(value, pos).into()); + } + ["throw", ..] => { + let msg = input.trim().splitn(2, ' ').skip(1).next().unwrap_or(""); + break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into()); + } + ["run" | "r"] => { + println!("Restarting script..."); + break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into()); + } + _ => eprintln!( + "\x1b[31mInvalid debugger command: '{}'\x1b[39m", + input.trim() + ), + }, + Err(err) => panic!("input error: {}", err), + } + } } fn main() { @@ -179,71 +627,10 @@ fn main() { // Initialize scripting engine let mut engine = Engine::new(); - let mut script = String::new(); - let main_ast; + #[cfg(not(feature = "no_optimize"))] + engine.set_optimization_level(rhai::OptimizationLevel::None); - { - // Load init scripts - if let Some(filename) = env::args().skip(1).next() { - let filename = match Path::new(&filename).canonicalize() { - Err(err) => { - eprintln!("Error script file path: {}\n{}", filename, err); - exit(1); - } - Ok(f) => { - match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap()) { - Ok(f) => f.into(), - _ => f, - } - } - }; - - let mut f = match File::open(&filename) { - Err(err) => { - eprintln!( - "Error reading script file: {}\n{}", - filename.to_string_lossy(), - err - ); - exit(1); - } - Ok(f) => f, - }; - - if let Err(err) = f.read_to_string(&mut script) { - println!( - "Error reading script file: {}\n{}", - filename.to_string_lossy(), - err - ); - exit(1); - } - - let script = if script.starts_with("#!") { - // Skip shebang - &script[script.find('\n').unwrap_or(0)..] - } else { - &script[..] - }; - - main_ast = match engine - .compile(&script) - .map_err(Into::>::into) - { - Err(err) => { - print_error(&script, *err); - exit(1); - } - Ok(ast) => ast, - }; - - println!("Script '{}' loaded.", filename.to_string_lossy()); - println!(); - } else { - eprintln!("No script file specified."); - exit(1); - } - } + let (ast, script) = load_script(&engine); // Hook up debugger let lines: Vec<_> = script.trim().split('\n').map(|s| s.to_string()).collect(); @@ -253,328 +640,7 @@ fn main() { || "".into(), // Main debugging interface move |context, event, node, source, pos| { - match event { - DebuggerEvent::Step => (), - DebuggerEvent::BreakPoint(n) => { - match context.global_runtime_state().debugger.break_points()[n] { - #[cfg(not(feature = "no_position"))] - BreakPoint::AtPosition { .. } => (), - BreakPoint::AtFunctionName { ref name, .. } - | BreakPoint::AtFunctionCall { ref name, .. } => { - println!("! Call to function {}.", name) - } - #[cfg(not(feature = "no_object"))] - BreakPoint::AtProperty { ref name, .. } => { - println!("! Property {} accessed.", name) - } - } - } - DebuggerEvent::FunctionExitWithValue(r) => { - println!( - "! Return from function call '{}' => {:?}", - context - .global_runtime_state() - .debugger - .call_stack() - .last() - .unwrap() - .fn_name, - r - ) - } - DebuggerEvent::FunctionExitWithError(err) => { - println!( - "! Return from function call '{}' with error: {}", - context - .global_runtime_state() - .debugger - .call_stack() - .last() - .unwrap() - .fn_name, - err - ) - } - } - - // Print current source line - print_current_source(context, source, pos, &lines); - - // Read stdin for commands - let mut input = String::new(); - - loop { - print!("rhai-dbg> "); - stdout().flush().expect("couldn't flush stdout"); - - input.clear(); - - match stdin().read_line(&mut input) { - Ok(0) => break Ok(DebuggerCommand::Continue), - Ok(_) => match input - .trim() - .split_whitespace() - .collect::>() - .as_slice() - { - ["help" | "h", ..] => print_debug_help(), - ["exit" | "quit" | "q" | "kill", ..] => { - println!("Script terminated. Bye!"); - exit(0); - } - ["node", ..] => { - println!("{:?} {}@{:?}", node, source.unwrap_or_default(), pos); - println!(); - } - ["list" | "l", ..] => print_current_source(context, source, pos, &lines), - ["continue" | "c", ..] => break Ok(DebuggerCommand::Continue), - ["finish" | "f", ..] => break Ok(DebuggerCommand::FunctionExit), - [] | ["step" | "s", ..] => break Ok(DebuggerCommand::StepInto), - ["over", ..] => break Ok(DebuggerCommand::StepOver), - ["next" | "n", ..] => break Ok(DebuggerCommand::Next), - ["scope", ..] => print_scope(context.scope(), false), - ["print" | "p", var_name, ..] => { - if let Some(value) = context.scope().get_value::(var_name) { - if value.is::<()>() { - println!("=> ()"); - } else { - println!("=> {}", value); - } - } else { - eprintln!("Variable not found: {}", var_name); - } - } - ["print" | "p"] => print_scope(context.scope(), true), - #[cfg(not(feature = "no_module"))] - ["imports", ..] => { - for (i, (name, module)) in context - .global_runtime_state() - .scan_imports_raw() - .enumerate() - { - println!( - "[{}] {} = {}", - i + 1, - name, - module.id().unwrap_or("") - ); - } - - println!(); - } - #[cfg(not(feature = "no_function"))] - ["backtrace" | "bt", ..] => { - for frame in context - .global_runtime_state() - .debugger - .call_stack() - .iter() - .rev() - { - println!("{}", frame) - } - } - ["info", "break", ..] | ["i", "b", ..] => Iterator::for_each( - context - .global_runtime_state() - .debugger - .break_points() - .iter() - .enumerate(), - |(i, bp)| match bp { - #[cfg(not(feature = "no_position"))] - rhai::debugger::BreakPoint::AtPosition { pos, .. } => { - let line_num = format!("[{}] line ", i + 1); - print!("{}", line_num); - print_source(&lines, *pos, line_num.len()); - } - _ => println!("[{}] {}", i + 1, bp), - }, - ), - ["enable" | "en", n, ..] => { - if let Ok(n) = n.parse::() { - let range = 1..=context - .global_runtime_state_mut() - .debugger - .break_points() - .len(); - if range.contains(&n) { - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .get_mut(n - 1) - .unwrap() - .enable(true); - println!("Break-point #{} enabled.", n) - } else { - eprintln!("Invalid break-point: {}", n); - } - } else { - eprintln!("Invalid break-point: '{}'", n); - } - } - ["disable" | "dis", n, ..] => { - if let Ok(n) = n.parse::() { - let range = 1..=context - .global_runtime_state_mut() - .debugger - .break_points() - .len(); - if range.contains(&n) { - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .get_mut(n - 1) - .unwrap() - .enable(false); - println!("Break-point #{} disabled.", n) - } else { - eprintln!("Invalid break-point: {}", n); - } - } else { - eprintln!("Invalid break-point: '{}'", n); - } - } - ["delete" | "d", n, ..] => { - if let Ok(n) = n.parse::() { - let range = 1..=context - .global_runtime_state_mut() - .debugger - .break_points() - .len(); - if range.contains(&n) { - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .remove(n - 1); - println!("Break-point #{} deleted.", n) - } else { - eprintln!("Invalid break-point: {}", n); - } - } else { - eprintln!("Invalid break-point: '{}'", n); - } - } - ["delete" | "d", ..] => { - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .clear(); - println!("All break-points deleted."); - } - ["break" | "b", fn_name, args, ..] => { - if let Ok(args) = args.parse::() { - let bp = rhai::debugger::BreakPoint::AtFunctionCall { - name: fn_name.trim().into(), - args, - enabled: true, - }; - println!("Break-point added for {}", bp); - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .push(bp); - } else { - eprintln!("Invalid number of arguments: '{}'", args); - } - } - // Property name - #[cfg(not(feature = "no_object"))] - ["break" | "b", param] if param.starts_with('.') && param.len() > 1 => { - let bp = rhai::debugger::BreakPoint::AtProperty { - name: param[1..].into(), - enabled: true, - }; - println!("Break-point added for {}", bp); - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .push(bp); - } - // Numeric parameter - #[cfg(not(feature = "no_position"))] - ["break" | "b", param] if param.parse::().is_ok() => { - let n = param.parse::().unwrap(); - let range = if source.is_none() { - 1..=lines.len() - } else { - 1..=(u16::MAX as usize) - }; - - if range.contains(&n) { - let bp = rhai::debugger::BreakPoint::AtPosition { - source: source.unwrap_or("").into(), - pos: Position::new(n as u16, 0), - enabled: true, - }; - println!("Break-point added {}", bp); - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .push(bp); - } else { - eprintln!("Invalid line number: {}", n); - } - } - // Function name parameter - ["break" | "b", param] => { - let bp = rhai::debugger::BreakPoint::AtFunctionName { - name: param.trim().into(), - enabled: true, - }; - println!("Break-point added for {}", bp); - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .push(bp); - } - #[cfg(not(feature = "no_position"))] - ["break" | "b"] => { - let bp = rhai::debugger::BreakPoint::AtPosition { - source: source.unwrap_or("").into(), - pos, - enabled: true, - }; - println!("Break-point added {}", bp); - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .push(bp); - } - ["throw"] => { - break Err(EvalAltResult::ErrorRuntime(Dynamic::UNIT, pos).into()) - } - ["throw", num] if num.trim().parse::().is_ok() => { - let value = num.trim().parse::().unwrap().into(); - break Err(EvalAltResult::ErrorRuntime(value, pos).into()); - } - #[cfg(not(feature = "no_float"))] - ["throw", num] if num.trim().parse::().is_ok() => { - let value = num.trim().parse::().unwrap().into(); - break Err(EvalAltResult::ErrorRuntime(value, pos).into()); - } - ["throw", ..] => { - let msg = input.trim().splitn(2, ' ').skip(1).next().unwrap_or(""); - break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into()); - } - ["run" | "r", ..] => { - println!("Restarting script..."); - break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into()); - } - [cmd, ..] => eprintln!("Invalid debugger command: '{}'", cmd), - }, - Err(err) => panic!("input error: {}", err), - } - } + debug_callback(context, event, node, source, pos, &lines) }, ); @@ -587,10 +653,11 @@ fn main() { engine.set_module_resolver(resolver); } - print_debug_help(); + println!("Type 'help' for commands list."); + println!(); // Evaluate - while let Err(err) = engine.run_ast_with_scope(&mut Scope::new(), &main_ast) { + while let Err(err) = engine.run_ast_with_scope(&mut Scope::new(), &ast) { match *err { // Loop back to restart EvalAltResult::ErrorTerminated(..) => (), diff --git a/src/bin/rhai-repl.rs b/src/bin/rhai-repl.rs index 5d574860..a9c775e6 100644 --- a/src/bin/rhai-repl.rs +++ b/src/bin/rhai-repl.rs @@ -10,7 +10,7 @@ const HISTORY_FILE: &str = ".rhai-repl-history"; /// Pretty-print error. fn print_error(input: &str, mut err: EvalAltResult) { - let lines: Vec<_> = input.trim().split('\n').collect(); + let lines: Vec<_> = input.split('\n').collect(); let pos = err.take_position(); let line_no = if lines.len() > 1 { @@ -60,6 +60,7 @@ fn print_help() { #[cfg(feature = "metadata")] println!("json => output all functions in JSON format"); println!("ast => print the last AST (optimized)"); + #[cfg(not(feature = "no_optimize"))] println!("astu => print the last raw, un-optimized AST"); println!(); println!("press Ctrl-Enter or end a line with `\\`"); @@ -84,6 +85,7 @@ fn print_keys() { println!("Ctrl-R => reverse search history"); println!(" (Ctrl-S forward, Ctrl-G cancel)"); println!("Ctrl-L => clear screen"); + #[cfg(target_family = "windows")] println!("Escape => clear all input"); println!("Ctrl-C => exit"); println!("Ctrl-D => EOF (when line empty)"); @@ -94,7 +96,10 @@ fn print_keys() { println!("Ctrl-T => transpose characters"); println!("Ctrl-V => insert special character"); println!("Ctrl-Y => paste yank"); - println!("Ctrl-Z => suspend (Unix), undo (Windows)"); + #[cfg(target_family = "unix")] + println!("Ctrl-Z => suspend"); + #[cfg(target_family = "windows")] + println!("Ctrl-Z => undo"); println!("Ctrl-_ => undo"); println!("Enter => run code"); println!("Shift-Ctrl-Enter => continue to next line"); @@ -311,6 +316,7 @@ fn main() { let mut history_offset = 1; let mut main_ast = AST::empty(); + #[cfg(not(feature = "no_optimize"))] let mut ast_u = AST::empty(); let mut ast = AST::empty(); @@ -344,13 +350,13 @@ fn main() { // Line continuation Ok(mut line) if line.ends_with("\\") => { line.pop(); - input += line.trim_end(); + input += &line; input.push('\n'); } Ok(line) => { - input += line.trim_end(); - if !input.is_empty() && !input.starts_with('!') && input.trim() != "history" - { + input += &line; + let cmd = input.trim(); + if !cmd.is_empty() && !cmd.starts_with('!') && cmd.trim() != "history" { if rl.add_history_entry(input.clone()) { history_offset += 1; } @@ -368,14 +374,14 @@ fn main() { } } - let script = input.trim(); + let cmd = input.trim(); - if script.is_empty() { + if cmd.is_empty() { continue; } // Implement standard commands - match script { + match cmd { "help" => { print_help(); continue; @@ -429,6 +435,7 @@ fn main() { print_scope(&scope); continue; } + #[cfg(not(feature = "no_optimize"))] "astu" => { // print the last un-optimized AST println!("{:#?}\n", ast_u); @@ -473,8 +480,8 @@ fn main() { } continue; } - _ if script.starts_with("!?") => { - let text = script[2..].trim(); + _ if cmd.starts_with("!?") => { + let text = cmd[2..].trim(); if let Some((n, line)) = rl .history() .iter() @@ -489,8 +496,8 @@ fn main() { } continue; } - _ if script.starts_with('!') => { - if let Ok(num) = script[1..].parse::() { + _ if cmd.starts_with('!') => { + if let Ok(num) = cmd[1..].parse::() { if num >= history_offset { if let Some(line) = rl.history().get(num - history_offset) { replacement = Some(line.clone()); @@ -499,7 +506,7 @@ fn main() { } } } else { - let prefix = script[1..].trim(); + let prefix = cmd[1..].trim(); if let Some((n, line)) = rl .history() .iter() @@ -512,20 +519,20 @@ fn main() { continue; } } - eprintln!("History line not found: {}", &script[1..]); + eprintln!("History line not found: {}", &cmd[1..]); continue; } _ => (), } match engine - .compile_with_scope(&scope, &script) + .compile_with_scope(&scope, &input) .map_err(Into::into) .and_then(|r| { - ast_u = r.clone(); - #[cfg(not(feature = "no_optimize"))] { + ast_u = r.clone(); + ast = engine.optimize_ast(&scope, r, optimize_level); } diff --git a/src/eval/chaining.rs b/src/eval/chaining.rs index 898ab201..f80cf737 100644 --- a/src/eval/chaining.rs +++ b/src/eval/chaining.rs @@ -4,9 +4,7 @@ use super::{EvalState, GlobalRuntimeState, Target}; use crate::ast::{Expr, OpAssignment}; use crate::types::dynamic::Union; -use crate::{ - Dynamic, Engine, Module, Position, RhaiError, RhaiResult, RhaiResultOf, Scope, StaticVec, ERR, -}; +use crate::{Dynamic, Engine, Module, Position, RhaiResult, RhaiResultOf, Scope, StaticVec, ERR}; use std::hash::Hash; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -149,7 +147,6 @@ impl Engine { #[cfg(not(feature = "no_index"))] ChainType::Indexing => { let pos = rhs.start_position(); - let root_pos = idx_val.position(); let idx_val = idx_val.into_index_value().expect("`ChainType::Index`"); match rhs { @@ -185,20 +182,15 @@ impl Engine { if let Some(mut new_val) = try_setter { // Try to call index setter if value is changed - let hash_set = - crate::ast::FnCallHashes::from_native(global.hash_idx_set()); - let args = &mut [target, &mut idx_val_for_setter, &mut new_val]; - let fn_name = crate::engine::FN_IDX_SET; - - if let Err(err) = self.exec_fn_call( - None, global, state, lib, fn_name, hash_set, args, is_ref_mut, - true, root_pos, level, - ) { - // Just ignore if there is no index setter - if !matches!(*err, ERR::ErrorFunctionNotFound(..)) { - return Err(err); - } - } + let idx = &mut idx_val_for_setter; + let new_val = &mut new_val; + self.call_indexer_set( + global, state, lib, target, idx, new_val, is_ref_mut, level, + ) + .or_else(|idx_err| match *idx_err { + ERR::ErrorIndexingType(..) => Ok((Dynamic::UNIT, false)), + _ => Err(idx_err), + })?; } Ok(result) @@ -234,14 +226,10 @@ impl Engine { if let Some(mut new_val) = try_setter { // Try to call index setter - let hash_set = - crate::ast::FnCallHashes::from_native(global.hash_idx_set()); - let args = &mut [target, &mut idx_val_for_setter, &mut new_val]; - let fn_name = crate::engine::FN_IDX_SET; - - self.exec_fn_call( - None, global, state, lib, fn_name, hash_set, args, is_ref_mut, - true, root_pos, level, + let idx = &mut idx_val_for_setter; + let new_val = &mut new_val; + self.call_indexer_set( + global, state, lib, target, idx, new_val, is_ref_mut, level, )?; } @@ -341,12 +329,10 @@ impl Engine { .or_else(|err| match *err { // Try an indexer if property does not exist ERR::ErrorDotExpr(..) => { - let prop = name.into(); - self.get_indexed_mut( - global, state, lib, target, prop, *pos, false, true, - level, + let mut prop = name.into(); + self.call_indexer_get( + global, state, lib, target, &mut prop, level, ) - .map(|v| (v.take_or_clone(), false)) .map_err( |idx_err| match *idx_err { ERR::ErrorIndexingType(..) => err, @@ -382,15 +368,10 @@ impl Engine { .or_else(|err| match *err { // Try an indexer if property does not exist ERR::ErrorDotExpr(..) => { - let args = &mut [target, &mut name.into(), &mut new_val]; - let fn_name = crate::engine::FN_IDX_SET; - let hash_set = - crate::ast::FnCallHashes::from_native(global.hash_idx_set()); - let pos = Position::NONE; - - self.exec_fn_call( - None, global, state, lib, fn_name, hash_set, args, is_ref_mut, - true, pos, level, + let idx = &mut name.into(); + let new_val = &mut new_val; + self.call_indexer_set( + global, state, lib, target, idx, new_val, is_ref_mut, level, ) .map_err( |idx_err| match *idx_err { @@ -418,11 +399,10 @@ impl Engine { |err| match *err { // Try an indexer if property does not exist ERR::ErrorDotExpr(..) => { - let prop = name.into(); - self.get_indexed_mut( - global, state, lib, target, prop, *pos, false, true, level, + let mut prop = name.into(); + self.call_indexer_get( + global, state, lib, target, &mut prop, level, ) - .map(|v| (v.take_or_clone(), false)) .map_err(|idx_err| { match *idx_err { ERR::ErrorIndexingType(..) => err, @@ -517,12 +497,10 @@ impl Engine { .or_else(|err| match *err { // Try an indexer if property does not exist ERR::ErrorDotExpr(..) => { - let prop = name.into(); - self.get_indexed_mut( - global, state, lib, target, prop, pos, false, true, - level, + let mut prop = name.into(); + self.call_indexer_get( + global, state, lib, target, &mut prop, level, ) - .map(|v| (v.take_or_clone(), false)) .map_err( |idx_err| match *idx_err { ERR::ErrorIndexingType(..) => err, @@ -566,21 +544,16 @@ impl Engine { |err| match *err { // Try an indexer if property does not exist ERR::ErrorDotExpr(..) => { - let args = - &mut [target.as_mut(), &mut name.into(), val]; - let fn_name = crate::engine::FN_IDX_SET; - let hash_set = - crate::ast::FnCallHashes::from_native( - global.hash_idx_set(), - ); - self.exec_fn_call( - None, global, state, lib, fn_name, hash_set, - args, is_ref_mut, true, pos, level, + let idx = &mut name.into(); + let new_val = val; + self.call_indexer_set( + global, state, lib, target, idx, new_val, + is_ref_mut, level, ) .or_else(|idx_err| match *idx_err { + // If there is no setter, no need to feed it + // back because the property is read-only ERR::ErrorIndexingType(..) => { - // If there is no setter, no need to feed it back because - // the property is read-only Ok((Dynamic::UNIT, false)) } _ => Err(idx_err), @@ -743,7 +716,7 @@ impl Engine { pos = arg_pos; } values.push(value.flatten()); - Ok::<_, RhaiError>((values, pos)) + Ok::<_, crate::RhaiError>((values, pos)) }, )?; @@ -789,7 +762,7 @@ impl Engine { pos = arg_pos } values.push(value.flatten()); - Ok::<_, RhaiError>((values, pos)) + Ok::<_, crate::RhaiError>((values, pos)) }, )?; super::ChainArgument::from_fn_call_args(values, pos) @@ -842,6 +815,52 @@ impl Engine { Ok(()) } + /// Call a get indexer. + /// [`Position`] in [`EvalAltResult`] may be [`NONE`][Position::NONE] and should be set afterwards. + #[inline(always)] + fn call_indexer_get( + &self, + global: &mut GlobalRuntimeState, + state: &mut EvalState, + lib: &[&Module], + target: &mut Dynamic, + idx: &mut Dynamic, + level: usize, + ) -> RhaiResultOf<(Dynamic, bool)> { + let args = &mut [target, idx]; + let hash_get = crate::ast::FnCallHashes::from_native(global.hash_idx_get()); + let fn_name = crate::engine::FN_IDX_GET; + let pos = Position::NONE; + + self.exec_fn_call( + None, global, state, lib, fn_name, hash_get, args, true, true, pos, level, + ) + } + + /// Call a set indexer. + /// [`Position`] in [`EvalAltResult`] may be [`NONE`][Position::NONE] and should be set afterwards. + #[inline(always)] + fn call_indexer_set( + &self, + global: &mut GlobalRuntimeState, + state: &mut EvalState, + lib: &[&Module], + target: &mut Dynamic, + idx: &mut Dynamic, + new_val: &mut Dynamic, + is_ref_mut: bool, + level: usize, + ) -> RhaiResultOf<(Dynamic, bool)> { + let hash_set = crate::ast::FnCallHashes::from_native(global.hash_idx_set()); + let args = &mut [target, idx, new_val]; + let fn_name = crate::engine::FN_IDX_SET; + let pos = Position::NONE; + + self.exec_fn_call( + None, global, state, lib, fn_name, hash_set, args, is_ref_mut, true, pos, level, + ) + } + /// Get the value at the indexed position of a base type. /// [`Position`] in [`EvalAltResult`] may be [`NONE`][Position::NONE] and should be set afterwards. fn get_indexed_mut<'t>( @@ -851,7 +870,7 @@ impl Engine { lib: &[&Module], target: &'t mut Dynamic, idx: Dynamic, - pos: Position, + idx_pos: Position, add_if_not_found: bool, use_indexers: bool, level: usize, @@ -868,10 +887,10 @@ impl Engine { // val_array[idx] let index = idx .as_int() - .map_err(|typ| self.make_type_mismatch_err::(typ, pos))?; + .map_err(|typ| self.make_type_mismatch_err::(typ, idx_pos))?; let len = arr.len(); let arr_idx = super::calc_index(len, index, true, || { - ERR::ErrorArrayBounds(len, index, pos).into() + ERR::ErrorArrayBounds(len, index, idx_pos).into() })?; Ok(arr.get_mut(arr_idx).map(Target::from).unwrap()) @@ -882,10 +901,10 @@ impl Engine { // val_blob[idx] let index = idx .as_int() - .map_err(|typ| self.make_type_mismatch_err::(typ, pos))?; + .map_err(|typ| self.make_type_mismatch_err::(typ, idx_pos))?; let len = arr.len(); let arr_idx = super::calc_index(len, index, true, || { - ERR::ErrorArrayBounds(len, index, pos).into() + ERR::ErrorArrayBounds(len, index, idx_pos).into() })?; let value = arr.get(arr_idx).map(|&v| (v as crate::INT).into()).unwrap(); @@ -901,17 +920,20 @@ impl Engine { Dynamic(Union::Map(map, ..)) => { // val_map[idx] let index = idx.read_lock::().ok_or_else(|| { - self.make_type_mismatch_err::(idx.type_name(), pos) + self.make_type_mismatch_err::(idx.type_name(), idx_pos) })?; if _add_if_not_found && !map.contains_key(index.as_str()) { map.insert(index.clone().into(), Dynamic::UNIT); } - Ok(map - .get_mut(index.as_str()) - .map(Target::from) - .unwrap_or_else(|| Target::from(Dynamic::UNIT))) + if let Some(value) = map.get_mut(index.as_str()) { + Ok(Target::from(value)) + } else if self.fail_on_invalid_map_property() { + Err(ERR::ErrorPropertyNotFound(index.to_string(), idx_pos).into()) + } else { + Ok(Target::from(Dynamic::UNIT)) + } } #[cfg(not(feature = "no_index"))] @@ -926,10 +948,10 @@ impl Engine { let end = range.end; let start = super::calc_index(BITS, start, false, || { - ERR::ErrorBitFieldBounds(BITS, start, pos).into() + ERR::ErrorBitFieldBounds(BITS, start, idx_pos).into() })?; let end = super::calc_index(BITS, end, false, || { - ERR::ErrorBitFieldBounds(BITS, end, pos).into() + ERR::ErrorBitFieldBounds(BITS, end, idx_pos).into() })?; if end <= start { @@ -951,10 +973,10 @@ impl Engine { let end = *range.end(); let start = super::calc_index(BITS, start, false, || { - ERR::ErrorBitFieldBounds(BITS, start, pos).into() + ERR::ErrorBitFieldBounds(BITS, start, idx_pos).into() })?; let end = super::calc_index(BITS, end, false, || { - ERR::ErrorBitFieldBounds(BITS, end, pos).into() + ERR::ErrorBitFieldBounds(BITS, end, idx_pos).into() })?; if end < start { @@ -990,12 +1012,12 @@ impl Engine { // val_int[idx] let index = idx .as_int() - .map_err(|typ| self.make_type_mismatch_err::(typ, pos))?; + .map_err(|typ| self.make_type_mismatch_err::(typ, idx_pos))?; const BITS: usize = std::mem::size_of::() * 8; let bit = super::calc_index(BITS, index, true, || { - ERR::ErrorBitFieldBounds(BITS, index, pos).into() + ERR::ErrorBitFieldBounds(BITS, index, idx_pos).into() })?; let bit_value = (*value & (1 << bit)) != 0; @@ -1012,14 +1034,14 @@ impl Engine { // val_string[idx] let index = idx .as_int() - .map_err(|typ| self.make_type_mismatch_err::(typ, pos))?; + .map_err(|typ| self.make_type_mismatch_err::(typ, idx_pos))?; let (ch, offset) = if index >= 0 { let offset = index as usize; ( s.chars().nth(offset).ok_or_else(|| { let chars_len = s.chars().count(); - ERR::ErrorStringBounds(chars_len, index, pos) + ERR::ErrorStringBounds(chars_len, index, idx_pos) })?, offset, ) @@ -1029,13 +1051,13 @@ impl Engine { // Count from end if negative s.chars().rev().nth(offset - 1).ok_or_else(|| { let chars_len = s.chars().count(); - ERR::ErrorStringBounds(chars_len, index, pos) + ERR::ErrorStringBounds(chars_len, index, idx_pos) })?, offset, ) } else { let chars_len = s.chars().count(); - return Err(ERR::ErrorStringBounds(chars_len, index, pos).into()); + return Err(ERR::ErrorStringBounds(chars_len, index, idx_pos).into()); }; Ok(Target::StringChar { @@ -1045,17 +1067,9 @@ impl Engine { }) } - _ if use_indexers => { - let args = &mut [target, &mut idx]; - let hash_get = crate::ast::FnCallHashes::from_native(global.hash_idx_get()); - let fn_name = crate::engine::FN_IDX_GET; - let pos = Position::NONE; - - self.exec_fn_call( - None, global, state, lib, fn_name, hash_get, args, true, true, pos, level, - ) - .map(|(v, ..)| v.into()) - } + _ if use_indexers => self + .call_indexer_get(global, state, lib, target, &mut idx, level) + .map(|(v, ..)| v.into()), _ => Err(ERR::ErrorIndexingType( format!( diff --git a/src/eval/expr.rs b/src/eval/expr.rs index f0e32227..6a9004e8 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -327,8 +327,7 @@ impl Engine { Expr::Unit(..) => Ok(Dynamic::UNIT), // `... ${...} ...` - Expr::InterpolatedString(x, pos) => { - let mut pos = *pos; + Expr::InterpolatedString(x, _) => { let mut concat: Dynamic = self.const_empty_string().into(); let mut result = Ok(Dynamic::UNIT); @@ -347,7 +346,7 @@ impl Engine { state, lib, Some(OpAssignment::new(OP_CONCAT)), - pos, + expr.start_position(), &mut (&mut concat).into(), ("", Position::NONE), item, @@ -356,8 +355,6 @@ impl Engine { result = Err(err.fill_position(expr.start_position())); break; } - - pos = expr.start_position(); } result.map(|_| concat) diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index a06bb742..294705ac 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -1,6 +1,7 @@ //! Module defining functions for evaluating a statement. use super::{EvalContext, EvalState, GlobalRuntimeState, Target}; +use crate::api::events::VarDefInfo; use crate::ast::{ BinaryExpr, Expr, Ident, OpAssignment, Stmt, SwitchCases, TryCatchBlock, AST_OPTION_FLAGS::*, }; @@ -818,9 +819,15 @@ impl Engine { let export = options.contains(AST_OPTION_EXPORTED); let result = if let Some(ref filter) = self.def_var_filter { - let shadowing = scope.contains(var_name); - let scope_level = state.scope_level; + let will_shadow = scope.contains(var_name); + let nesting_level = state.scope_level; let is_const = entry_type == AccessMode::ReadOnly; + let info = VarDefInfo { + name: var_name, + is_const, + nesting_level, + will_shadow, + }; let context = EvalContext { engine: self, scope, @@ -828,16 +835,16 @@ impl Engine { state, lib, this_ptr, - level: level, + level, }; - match filter(var_name, is_const, scope_level, shadowing, &context) { + match filter(true, info, &context) { Ok(true) => None, - Ok(false) => Some(Err(ERR::ErrorRuntime( - format!("Variable cannot be defined: {}", var_name).into(), - *pos, - ) - .into())), + Ok(false) => { + Some(Err( + ERR::ErrorForbiddenVariable(var_name.to_string(), *pos).into() + )) + } err @ Err(_) => Some(err), } } else { @@ -977,7 +984,7 @@ impl Engine { // Share statement #[cfg(not(feature = "no_closure"))] - Stmt::Share(name) => { + Stmt::Share(name, ..) => { if let Some((index, ..)) = scope.get_index(name) { let val = scope.get_mut_by_index(index); @@ -985,6 +992,8 @@ impl Engine { // Replace the variable with a shared value. *val = std::mem::take(val).into_shared(); } + } else { + unreachable!("variable {} not found for sharing", name); } Ok(Dynamic::UNIT) } diff --git a/src/func/builtin.rs b/src/func/builtin.rs index f2284fbd..1da1d250 100644 --- a/src/func/builtin.rs +++ b/src/func/builtin.rs @@ -682,19 +682,77 @@ pub fn get_builtin_op_assignment_fn(op: &str, x: &Dynamic, y: &Dynamic) -> Optio } } - // blob op= int #[cfg(not(feature = "no_index"))] - if types_pair == (TypeId::of::(), TypeId::of::()) { - use crate::Blob; + { + // string op= blob + if types_pair == (TypeId::of::(), TypeId::of::()) { + return match op { + "+=" => Some(|_, args| { + let buf = { + let x = args[1].read_lock::().expect(BUILTIN); + if x.is_empty() { + return Ok(Dynamic::UNIT); + } + let s = args[0].read_lock::().expect(BUILTIN); + let mut buf = crate::SmartString::from(s.as_str()); + buf.push_str(&String::from_utf8_lossy(&x)); + buf + }; + let mut s = args[0].write_lock::().expect(BUILTIN); + *s = buf.into(); + Ok(Dynamic::UNIT) + }), + _ => None, + }; + } + // blob op= int + if types_pair == (TypeId::of::(), TypeId::of::()) { + use crate::Blob; - return match op { - "+=" => Some(|_, args| { - let x = (args[1].as_int().expect("`INT`") & 0x000000ff) as u8; - let mut blob = args[0].write_lock::().expect(BUILTIN); - Ok(blob.push(x).into()) - }), - _ => None, - }; + return match op { + "+=" => Some(|_, args| { + let x = (args[1].as_int().expect("`INT`") & 0x000000ff) as u8; + let mut blob = args[0].write_lock::().expect(BUILTIN); + Ok(blob.push(x).into()) + }), + _ => None, + }; + } + + // blob op= char + if types_pair == (TypeId::of::(), TypeId::of::()) { + use crate::Blob; + + return match op { + "+=" => Some(|_, args| { + let mut buf = [0_u8; 4]; + let x = args[1].as_char().expect("`char`").encode_utf8(&mut buf); + let mut blob = args[0].write_lock::().expect(BUILTIN); + Ok(blob.extend(x.as_bytes()).into()) + }), + _ => None, + }; + } + + // blob op= string + if types_pair == (TypeId::of::(), TypeId::of::()) { + use crate::Blob; + + return match op { + "+=" => Some(|_, args| { + let s: crate::Blob = { + let s = args[1].read_lock::().expect(BUILTIN); + if s.is_empty() { + return Ok(Dynamic::UNIT); + } + s.as_bytes().into() + }; + let mut blob = args[0].write_lock::().expect(BUILTIN); + Ok(blob.extend(s).into()) + }), + _ => None, + }; + } } // No built-in op-assignments for different types. diff --git a/src/func/native.rs b/src/func/native.rs index 375f6f81..66e793a3 100644 --- a/src/func/native.rs +++ b/src/func/native.rs @@ -1,6 +1,7 @@ //! Module defining interfaces to native-Rust functions. use super::call::FnCallArgs; +use crate::api::events::VarDefInfo; use crate::ast::FnCallHashes; use crate::eval::{EvalState, GlobalRuntimeState}; use crate::plugin::PluginFunction; @@ -447,8 +448,8 @@ pub type OnVarCallback = /// Callback function for variable definition. #[cfg(not(feature = "sync"))] -pub type OnDefVarCallback = dyn Fn(&str, bool, usize, bool, &EvalContext) -> RhaiResultOf; +pub type OnDefVarCallback = dyn Fn(bool, VarDefInfo, &EvalContext) -> RhaiResultOf; /// Callback function for variable definition. #[cfg(feature = "sync")] pub type OnDefVarCallback = - dyn Fn(&str, bool, usize, bool, &EvalContext) -> RhaiResultOf + Send + Sync; + dyn Fn(bool, VarDefInfo, &EvalContext) -> RhaiResultOf + Send + Sync; diff --git a/src/lib.rs b/src/lib.rs index 18061326..07612641 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -147,6 +147,7 @@ type ExclusiveRange = std::ops::Range; type InclusiveRange = std::ops::RangeInclusive; pub use api::custom_syntax::Expression; +pub use api::events::VarDefInfo; pub use ast::{FnAccess, AST}; pub use engine::{Engine, OP_CONTAINS, OP_EQUALS}; pub use eval::EvalContext; @@ -247,7 +248,7 @@ pub use tokenizer::{get_next_token, parse_string_literal}; #[cfg(feature = "internals")] pub use tokenizer::{ - InputStream, MultiInputsStream, Token, TokenIterator, TokenizeState, TokenizerControl, + InputStream, MultiInputsStream, Span, Token, TokenIterator, TokenizeState, TokenizerControl, TokenizerControlBlock, }; diff --git a/src/module/namespace.rs b/src/module/namespace.rs index abfb9901..c2977030 100644 --- a/src/module/namespace.rs +++ b/src/module/namespace.rs @@ -3,7 +3,7 @@ use crate::ast::Ident; use crate::tokenizer::Token; -use crate::StaticVec; +use crate::{Position, StaticVec}; #[cfg(feature = "no_std")] use std::prelude::v1::*; use std::{ @@ -12,7 +12,7 @@ use std::{ ops::{Deref, DerefMut}, }; -/// _(internals)_ A chain of [module][Module] names to namespace-qualify a variable or function call. +/// _(internals)_ A chain of [module][crate::Module] names to namespace-qualify a variable or function call. /// Exported under the `internals` feature only. /// /// Not available under `no_module`. @@ -114,4 +114,12 @@ impl Namespace { pub(crate) fn set_index(&mut self, index: Option) { self.index = index } + /// Get the [position][Position] of this [`NameSpace`]. + /// + /// # Panics + /// + /// Panics if the path is empty. + pub fn position(&self) -> Position { + self.path[0].pos + } } diff --git a/src/optimizer.rs b/src/optimizer.rs index e8ddf4f8..220fae23 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -6,7 +6,7 @@ use crate::engine::{KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_PRINT, use crate::eval::{EvalState, GlobalRuntimeState}; use crate::func::builtin::get_builtin_binary_op_fn; use crate::func::hashing::get_hasher; -use crate::tokenizer::Token; +use crate::tokenizer::{Span, Token}; use crate::types::dynamic::AccessMode; use crate::{ calc_fn_hash, calc_fn_params_hash, combine_hashes, Dynamic, Engine, FnPtr, Position, Scope, @@ -471,7 +471,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // -> { expr, Noop } Stmt::Block( [Stmt::Expr(expr), Stmt::Noop(pos)].into(), - (pos, Position::NONE), + Span::new(pos, Position::NONE), ) } else { // -> expr @@ -490,7 +490,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b match optimize_stmt_block(mem::take(&mut *x.1), state, preserve_result, true, false) { statements if statements.is_empty() => Stmt::Noop(x.1.position()), - statements => Stmt::Block(statements.into_boxed_slice(), x.1.positions()), + statements => Stmt::Block(statements.into_boxed_slice(), x.1.span()), } } // if true { if_block } else { else_block } -> if_block @@ -500,7 +500,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b match optimize_stmt_block(mem::take(&mut *x.0), state, preserve_result, true, false) { statements if statements.is_empty() => Stmt::Noop(x.0.position()), - statements => Stmt::Block(statements.into_boxed_slice(), x.0.positions()), + statements => Stmt::Block(statements.into_boxed_slice(), x.0.span()), } } // if expr { if_block } else { else_block } @@ -534,7 +534,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b mem::take(&mut block.statements), Stmt::Block( def_stmt.into_boxed_slice(), - x.def_case.positions_or_else(*pos, Position::NONE), + x.def_case.span_or_else(*pos, Position::NONE), ) .into(), )), @@ -549,8 +549,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b true, false, ); - *stmt = - Stmt::Block(statements.into_boxed_slice(), block.statements.positions()); + *stmt = Stmt::Block(statements.into_boxed_slice(), block.statements.span()); } state.set_dirty(); @@ -590,7 +589,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b mem::take(&mut block.statements), Stmt::Block( def_stmt.into_boxed_slice(), - x.def_case.positions_or_else(*pos, Position::NONE), + x.def_case.span_or_else(*pos, Position::NONE), ) .into(), )), @@ -601,10 +600,8 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b let statements = mem::take(&mut *block.statements); let statements = optimize_stmt_block(statements, state, true, true, false); - *stmt = Stmt::Block( - statements.into_boxed_slice(), - block.statements.positions(), - ); + *stmt = + Stmt::Block(statements.into_boxed_slice(), block.statements.span()); } state.set_dirty(); @@ -651,7 +648,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b optimize_stmt_block(mem::take(&mut x.def_case), state, true, true, false); *stmt = Stmt::Block( def_stmt.into_boxed_slice(), - x.def_case.positions_or_else(*pos, Position::NONE), + x.def_case.span_or_else(*pos, Position::NONE), ); } // switch @@ -708,8 +705,10 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b if preserve_result { statements.push(Stmt::Noop(pos)) } - *stmt = - Stmt::Block(statements.into_boxed_slice(), (pos, Position::NONE)); + *stmt = Stmt::Block( + statements.into_boxed_slice(), + Span::new(pos, Position::NONE), + ); } else { *stmt = Stmt::Noop(pos); }; @@ -726,7 +725,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b *stmt = Stmt::Block( optimize_stmt_block(mem::take(&mut **body), state, false, true, false) .into_boxed_slice(), - body.positions(), + body.span(), ); } // do { block } while|until expr @@ -747,21 +746,21 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b #[cfg(not(feature = "no_module"))] Stmt::Import(expr, ..) => optimize_expr(expr, state, false), // { block } - Stmt::Block(statements, pos) => { + Stmt::Block(statements, span) => { let statements = mem::take(statements).into_vec().into(); let mut block = optimize_stmt_block(statements, state, preserve_result, true, false); match block.as_mut_slice() { [] => { state.set_dirty(); - *stmt = Stmt::Noop(pos.0); + *stmt = Stmt::Noop(span.start()); } // Only one statement which is not block-dependent - promote [s] if !s.is_block_dependent() => { state.set_dirty(); *stmt = mem::take(s); } - _ => *stmt = Stmt::Block(block.into_boxed_slice(), *pos), + _ => *stmt = Stmt::Block(block.into_boxed_slice(), *span), } } // try { pure try_block } catch ( var ) { catch_block } -> try_block @@ -771,7 +770,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b *stmt = Stmt::Block( optimize_stmt_block(mem::take(&mut *x.try_block), state, false, true, false) .into_boxed_slice(), - x.try_block.positions(), + x.try_block.span(), ); } // try { try_block } catch ( var ) { catch_block } @@ -941,7 +940,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { while n < x.len() - 1 { match (mem::take(&mut x[n]), mem::take(&mut x[n+1])) { (Expr::StringConstant(mut s1, pos), Expr::StringConstant(s2, ..)) => { s1 += s2; x[n] = Expr::StringConstant(s1, pos); x.remove(n+1); state.set_dirty(); } - (expr1, Expr::Unit(..)) => { x[n] = expr1; x.remove(n+1); state.set_dirty(); } + (expr1, Expr::Unit(..)) => { x[n] = expr1; x.remove(n+1); state.set_dirty(); } (Expr::Unit(..), expr2) => { x[n+1] = expr2; x.remove(n); state.set_dirty(); } (expr1, Expr::StringConstant(s, ..)) if s.is_empty() => { x[n] = expr1; x.remove(n+1); state.set_dirty(); } (Expr::StringConstant(s, ..), expr2) if s.is_empty()=> { x[n+1] = expr2; x.remove(n); state.set_dirty(); } diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index 8eae8764..883b29e3 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -190,7 +190,7 @@ macro_rules! reg_functions { def_package! { /// Basic arithmetic package. - crate::ArithmeticPackage => |lib| { + pub ArithmeticPackage(lib) { lib.standard = true; combine_with_exported_module!(lib, "int", int_functions); diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 9d7d67b7..c7556865 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -13,7 +13,7 @@ use std::{any::TypeId, cmp::Ordering, mem}; def_package! { /// Package of basic array utilities. - crate::BasicArrayPackage => |lib| { + pub BasicArrayPackage(lib) { lib.standard = true; combine_with_exported_module!(lib, "array", array_functions); diff --git a/src/packages/bit_field.rs b/src/packages/bit_field.rs index ee4e37f2..55b4ad40 100644 --- a/src/packages/bit_field.rs +++ b/src/packages/bit_field.rs @@ -8,7 +8,7 @@ use std::prelude::v1::*; def_package! { /// Package of basic bit-field utilities. - crate::BitFieldPackage => |lib| { + pub BitFieldPackage(lib) { lib.standard = true; combine_with_exported_module!(lib, "bit_field", bit_field_functions); diff --git a/src/packages/blob_basic.rs b/src/packages/blob_basic.rs index c88dfb16..611d0b83 100644 --- a/src/packages/blob_basic.rs +++ b/src/packages/blob_basic.rs @@ -3,7 +3,7 @@ use crate::eval::{calc_index, calc_offset_len}; use crate::plugin::*; use crate::{ - def_package, Blob, Dynamic, ExclusiveRange, InclusiveRange, NativeCallContext, Position, + def_package, Array, Blob, Dynamic, ExclusiveRange, InclusiveRange, NativeCallContext, Position, RhaiResultOf, INT, }; #[cfg(feature = "no_std")] @@ -20,7 +20,7 @@ const FLOAT_BYTES: usize = mem::size_of::(); def_package! { /// Package of basic BLOB utilities. - crate::BasicBlobPackage => |lib| { + pub BasicBlobPackage(lib) { lib.standard = true; combine_with_exported_module!(lib, "blob", blob_functions); @@ -94,12 +94,36 @@ pub mod blob_functions { blob.resize(len, (value & 0x000000ff) as u8); Ok(blob) } + /// Convert the BLOB into an array of integers. + /// + /// # Example + /// + /// ```rhai + /// let b = blob(5, 0x42); + /// + /// let x = b.to_array(); + /// + /// print(x); // prints "[66, 66, 66, 66, 66]" + /// ``` + #[rhai_fn(pure)] + pub fn to_array(blob: &mut Blob) -> Array { + blob.iter().map(|&ch| (ch as INT).into()).collect() + } /// Return the length of the BLOB. + /// + /// # Example + /// + /// ```rhai + /// let b = blob(10, 0x42); + /// + /// print(b); // prints "[4242424242424242 4242]" + /// + /// print(b.len()); // prints 10 + /// ``` #[rhai_fn(name = "len", get = "len", pure)] pub fn len(blob: &mut Blob) -> INT { blob.len() as INT } - /// Get the byte value at the `index` position in the BLOB. /// /// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last element). @@ -196,15 +220,49 @@ pub mod blob_functions { /// /// print(b1); // prints "[4242424242111111]" /// ``` - pub fn append(blob: &mut Blob, y: Blob) { - if !y.is_empty() { - if blob.is_empty() { - *blob = y; + pub fn append(blob1: &mut Blob, blob2: Blob) { + if !blob2.is_empty() { + if blob1.is_empty() { + *blob1 = blob2; } else { - blob.extend(y); + blob1.extend(blob2); } } } + /// Add a string (as UTF-8 encoded byte-stream) to the end of the BLOB + /// + /// # Example + /// + /// ```rhai + /// let b = blob(5, 0x42); + /// + /// b.append("hello"); + /// + /// print(b); // prints "[424242424268656c 6c6f]" + /// ``` + #[rhai_fn(name = "+=", name = "append")] + pub fn append_str(blob: &mut Blob, string: ImmutableString) { + if !string.is_empty() { + blob.extend(string.as_bytes()); + } + } + /// Add a string (as UTF-8 encoded byte-stream) to the end of the BLOB + /// + /// # Example + /// + /// ```rhai + /// let b = blob(5, 0x42); + /// + /// b.append('!'); + /// + /// print(b); // prints "[424242424221]" + /// ``` + #[rhai_fn(name = "+=", name = "append")] + pub fn append_char(blob: &mut Blob, character: char) { + let mut buf = [0_u8; 4]; + let x = character.encode_utf8(&mut buf); + blob.extend(x.as_bytes()); + } /// Add another BLOB to the end of the BLOB, returning it as a new BLOB. /// /// # Example diff --git a/src/packages/debugging.rs b/src/packages/debugging.rs index ab24bc3a..e48571a8 100644 --- a/src/packages/debugging.rs +++ b/src/packages/debugging.rs @@ -6,19 +6,17 @@ use crate::plugin::*; use std::prelude::v1::*; #[cfg(not(feature = "no_function"))] -use crate::{Dynamic, NativeCallContext}; +#[cfg(not(feature = "no_index"))] +use crate::{Array, Dynamic, NativeCallContext}; #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_index"))] -use crate::Array; - -#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_object"))] use crate::Map; def_package! { /// Package of basic debugging utilities. - crate::DebuggingPackage => |lib| { + pub DebuggingPackage(lib) { lib.standard = true; combine_with_exported_module!(lib, "debugging", debugging_functions); diff --git a/src/packages/fn_basic.rs b/src/packages/fn_basic.rs index 48aeb3d8..505a08b5 100644 --- a/src/packages/fn_basic.rs +++ b/src/packages/fn_basic.rs @@ -5,7 +5,7 @@ use std::prelude::v1::*; def_package! { /// Package of basic function pointer utilities. - crate::BasicFnPackage => |lib| { + pub BasicFnPackage(lib) { lib.standard = true; combine_with_exported_module!(lib, "FnPtr", fn_ptr_functions); diff --git a/src/packages/iter_basic.rs b/src/packages/iter_basic.rs index 78ee4519..bcf50691 100644 --- a/src/packages/iter_basic.rs +++ b/src/packages/iter_basic.rs @@ -299,7 +299,7 @@ macro_rules! reg_range { def_package! { /// Package of basic range iterators - crate::BasicIteratorPackage => |lib| { + pub BasicIteratorPackage(lib) { lib.standard = true; reg_range!(lib | "range" => INT); diff --git a/src/packages/lang_core.rs b/src/packages/lang_core.rs index 5e7c5949..ce28bf37 100644 --- a/src/packages/lang_core.rs +++ b/src/packages/lang_core.rs @@ -7,7 +7,7 @@ use std::prelude::v1::*; def_package! { /// Package of core language features. - crate::LanguageCorePackage => |lib| { + pub LanguageCorePackage(lib) { lib.standard = true; combine_with_exported_module!(lib, "core", core_functions); diff --git a/src/packages/logic.rs b/src/packages/logic.rs index 26821a55..af494f00 100644 --- a/src/packages/logic.rs +++ b/src/packages/logic.rs @@ -37,7 +37,7 @@ macro_rules! reg_functions { def_package! { /// Package of basic logic operators. - crate::LogicPackage => |lib| { + pub LogicPackage(lib) { lib.standard = true; #[cfg(not(feature = "only_i32"))] diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index 8eaf6b17..a3b0931f 100644 --- a/src/packages/map_basic.rs +++ b/src/packages/map_basic.rs @@ -11,7 +11,7 @@ use crate::Array; def_package! { /// Package of basic object map utilities. - crate::BasicMapPackage => |lib| { + pub BasicMapPackage(lib) { lib.standard = true; combine_with_exported_module!(lib, "map", map_functions); diff --git a/src/packages/math_basic.rs b/src/packages/math_basic.rs index 6dd8f630..45fd3790 100644 --- a/src/packages/math_basic.rs +++ b/src/packages/math_basic.rs @@ -53,7 +53,7 @@ macro_rules! reg_functions { def_package! { /// Basic mathematical package. - crate::BasicMathPackage => |lib| { + pub BasicMathPackage(lib) { lib.standard = true; // Integer functions diff --git a/src/packages/mod.rs b/src/packages/mod.rs index f56fafe9..343dd651 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -67,16 +67,52 @@ pub trait Package { /// /// fn add(x: i64, y: i64) -> Result> { Ok(x + y) } /// -/// def_package!(rhai:MyPackage:"My super-duper package", module, -/// { -/// // Load a binary function with all value parameters. -/// module.set_native_fn("my_add", add); -/// }); +/// def_package! { +/// /// My super-duper package. +/// pub MyPackage(module) { +/// // Load a binary function with all value parameters. +/// module.set_native_fn("my_add", add); +/// } +/// } /// ``` #[macro_export] macro_rules! def_package { + ($($(#[$outer:meta])* $mod:vis $package:ident($lib:ident) $block:block)+) => { $( + $(#[$outer])* + $mod struct $package($crate::Shared<$crate::Module>); + + impl $crate::packages::Package for $package { + fn as_shared_module(&self) -> $crate::Shared<$crate::Module> { + self.0.clone() + } + fn init($lib: &mut $crate::Module) { + $block + } + } + + impl Default for $package { + fn default() -> Self { + Self::new() + } + } + + impl $package { + pub fn new() -> Self { + let mut module = $crate::Module::new(); + ::init(&mut module); + module.build_index(); + Self(module.into()) + } + } + )* }; ($($(#[$outer:meta])* $root:ident :: $package:ident => | $lib:ident | $block:block)+) => { $( $(#[$outer])* + /// # Deprecated + /// + /// This old syntax of `def_package!` is deprecated. Use the new syntax instead. + /// + /// This syntax will be removed in the next major version. + #[deprecated(since = "1.5.0", note = "this is an old syntax of `def_package!` and is deprecated; use the new syntax of `def_package!` instead")] pub struct $package($root::Shared<$root::Module>); impl $root::packages::Package for $package { @@ -104,7 +140,6 @@ macro_rules! def_package { } )* }; ($root:ident : $package:ident : $comment:expr , $lib:ident , $block:stmt) => { - #[deprecated(since = "1.4.0", note = "this is an old syntax of `def_package!` and is deprecated; use the new syntax of `def_package!` instead")] #[doc=$comment] /// /// # Deprecated @@ -112,6 +147,7 @@ macro_rules! def_package { /// This old syntax of `def_package!` is deprecated. Use the new syntax instead. /// /// This syntax will be removed in the next major version. + #[deprecated(since = "1.4.0", note = "this is an old syntax of `def_package!` and is deprecated; use the new syntax of `def_package!` instead")] pub struct $package($root::Shared<$root::Module>); impl $root::packages::Package for $package { diff --git a/src/packages/pkg_core.rs b/src/packages/pkg_core.rs index 94d59c7f..1603bcb8 100644 --- a/src/packages/pkg_core.rs +++ b/src/packages/pkg_core.rs @@ -14,7 +14,7 @@ def_package! { /// * [`BasicIteratorPackage`][super::BasicIteratorPackage] /// * [`BasicFnPackage`][super::BasicFnPackage] /// * [`DebuggingPackage`][super::DebuggingPackage] - crate::CorePackage => |lib| { + pub CorePackage(lib) { lib.standard = true; super::LanguageCorePackage::init(lib); diff --git a/src/packages/pkg_std.rs b/src/packages/pkg_std.rs index b388b379..cd8f29db 100644 --- a/src/packages/pkg_std.rs +++ b/src/packages/pkg_std.rs @@ -17,7 +17,7 @@ def_package! { /// * [`BasicMapPackage`][super::BasicMapPackage] /// * [`BasicTimePackage`][super::BasicTimePackage] /// * [`MoreStringPackage`][super::MoreStringPackage] - crate::StandardPackage => |lib| { + pub StandardPackage(lib) { lib.standard = true; super::CorePackage::init(lib); diff --git a/src/packages/string_basic.rs b/src/packages/string_basic.rs index 266ccec3..ae3342ea 100644 --- a/src/packages/string_basic.rs +++ b/src/packages/string_basic.rs @@ -15,7 +15,7 @@ pub const FUNC_TO_DEBUG: &str = "to_debug"; def_package! { /// Package of basic string utilities (e.g. printing) - crate::BasicStringPackage => |lib| { + pub BasicStringPackage(lib) { lib.standard = true; combine_with_exported_module!(lib, "print_debug", print_debug_functions); @@ -64,22 +64,65 @@ mod print_debug_functions { pub fn to_debug_generic(ctx: NativeCallContext, item: &mut Dynamic) -> ImmutableString { ctx.engine().map_type_name(&format!("{:?}", item)).into() } + /// Return the empty string. #[rhai_fn(name = "print", name = "debug")] pub fn print_empty_string(ctx: NativeCallContext) -> ImmutableString { ctx.engine().const_empty_string() } + /// Return the `string`. #[rhai_fn(name = "print", name = "to_string")] pub fn print_string(string: ImmutableString) -> ImmutableString { string } + /// Convert the string into debug format. + #[rhai_fn(name = "debug", name = "to_debug", pure)] + pub fn debug_string(string: &mut ImmutableString) -> ImmutableString { + format!("{:?}", string).into() + } + + /// Return the character into a string. + #[rhai_fn(name = "print", name = "to_string")] + pub fn print_char(character: char) -> ImmutableString { + character.to_string().into() + } + /// Convert the string into debug format. + #[rhai_fn(name = "debug", name = "to_debug")] + pub fn debug_char(character: char) -> ImmutableString { + format!("{:?}", character).into() + } + /// Convert the function pointer into a string in debug format. #[rhai_fn(name = "debug", name = "to_debug", pure)] pub fn debug_fn_ptr(f: &mut FnPtr) -> ImmutableString { f.to_string().into() } + /// Return the boolean value into a string. + #[rhai_fn(name = "print", name = "to_string")] + pub fn print_bool(value: bool) -> ImmutableString { + format!("{}", value).into() + } + /// Convert the boolean value into a string in debug format. + #[rhai_fn(name = "debug", name = "to_debug")] + pub fn debug_bool(value: bool) -> ImmutableString { + format!("{:?}", value).into() + } + + /// Return the empty string. + #[rhai_fn(name = "print", name = "to_string")] + pub fn print_unit(ctx: NativeCallContext, unit: ()) -> ImmutableString { + let _ = unit; + ctx.engine().const_empty_string() + } + /// Convert the unit into a string in debug format. + #[rhai_fn(name = "debug", name = "to_debug")] + pub fn debug_unit(unit: ()) -> ImmutableString { + let _ = unit; + "()".into() + } + /// Convert the value of `number` into a string. #[cfg(not(feature = "no_float"))] #[rhai_fn(name = "print", name = "to_string")] diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index f51b27d8..b8c3f4c6 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -6,9 +6,12 @@ use std::{any::TypeId, mem}; use super::string_basic::{print_with_func, FUNC_TO_STRING}; +#[cfg(not(feature = "no_index"))] +use crate::Blob; + def_package! { /// Package of additional string utilities over [`BasicStringPackage`][super::BasicStringPackage] - crate::MoreStringPackage => |lib| { + pub MoreStringPackage(lib) { lib.standard = true; combine_with_exported_module!(lib, "string", string_functions); @@ -19,7 +22,7 @@ def_package! { mod string_functions { use crate::{ImmutableString, SmartString}; - #[rhai_fn(name = "+", name = "append")] + #[rhai_fn(name = "+")] pub fn add_append( ctx: NativeCallContext, string: ImmutableString, @@ -33,6 +36,14 @@ mod string_functions { format!("{}{}", string, s).into() } } + #[rhai_fn(name = "+=", name = "append")] + pub fn add(ctx: NativeCallContext, string: &mut ImmutableString, mut item: Dynamic) { + let s = print_with_func(FUNC_TO_STRING, &ctx, &mut item); + + if !s.is_empty() { + *string = format!("{}{}", string, s).into(); + } + } #[rhai_fn(name = "+", pure)] pub fn add_prepend( ctx: NativeCallContext, @@ -48,11 +59,13 @@ mod string_functions { s } - #[rhai_fn(name = "+", name = "append")] + // The following are needed in order to override the generic versions with `Dynamic` parameters. + + #[rhai_fn(name = "+")] pub fn add_append_str(string1: ImmutableString, string2: ImmutableString) -> ImmutableString { string1 + string2 } - #[rhai_fn(name = "+", name = "append")] + #[rhai_fn(name = "+")] pub fn add_append_char(string: ImmutableString, character: char) -> ImmutableString { string + character } @@ -61,7 +74,7 @@ mod string_functions { format!("{}{}", character, string).into() } - #[rhai_fn(name = "+", name = "append")] + #[rhai_fn(name = "+")] pub fn add_append_unit(string: ImmutableString, item: ()) -> ImmutableString { let _item = item; string @@ -71,6 +84,30 @@ mod string_functions { string } + #[cfg(not(feature = "no_index"))] + pub mod blob_functions { + #[rhai_fn(name = "+")] + pub fn add_append_blob(string: ImmutableString, utf8: Blob) -> ImmutableString { + if utf8.is_empty() { + string + } else if string.is_empty() { + String::from_utf8_lossy(&utf8).into_owned().into() + } else { + let mut s = crate::SmartString::from(string); + s.push_str(&String::from_utf8_lossy(&utf8)); + s.into() + } + } + #[rhai_fn(name = "append")] + pub fn add_blob(string: &mut ImmutableString, utf8: Blob) { + let mut s = crate::SmartString::from(string.as_str()); + if !utf8.is_empty() { + s.push_str(&String::from_utf8_lossy(&utf8)); + *string = s.into(); + } + } + } + /// Return the length of the string, in number of characters. /// /// # Example @@ -105,6 +142,25 @@ mod string_functions { string.len() as INT } } + /// Convert the string into an UTF-8 encoded byte-stream as a BLOB. + /// + /// # Example + /// + /// ```rhai + /// let text = "朝には紅顔ありて夕べには白骨となる"; + /// + /// let bytes = text.to_blob(); + /// + /// print(bytes.len()); // prints 51 + /// ``` + #[cfg(not(feature = "no_index"))] + pub fn to_blob(string: &str) -> crate::Blob { + if string.is_empty() { + crate::Blob::new() + } else { + string.as_bytes().into() + } + } /// Remove all occurrences of a sub-string from the string. /// /// # Example diff --git a/src/packages/time_basic.rs b/src/packages/time_basic.rs index 3eacee0b..b631a59d 100644 --- a/src/packages/time_basic.rs +++ b/src/packages/time_basic.rs @@ -15,7 +15,7 @@ use instant::{Duration, Instant}; def_package! { /// Package of basic timing utilities. - crate::BasicTimePackage => |lib| { + pub BasicTimePackage(lib) { lib.standard = true; // Register date/time functions diff --git a/src/parser.rs b/src/parser.rs index 8f5a28ca..74f97c2a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,22 +1,25 @@ //! Main module defining the lexer and parser. use crate::api::custom_syntax::{markers::*, CustomSyntax}; +use crate::api::events::VarDefInfo; use crate::api::options::LanguageOptions; use crate::ast::{ BinaryExpr, ConditionalStmtBlock, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, OpAssignment, ScriptFnDef, Stmt, SwitchCases, TryCatchBlock, AST_OPTION_FLAGS::*, }; use crate::engine::{Precedence, KEYWORD_THIS, OP_CONTAINS}; +use crate::eval::{EvalState, GlobalRuntimeState}; use crate::func::hashing::get_hasher; use crate::tokenizer::{ - is_keyword_function, is_valid_function_name, is_valid_identifier, Token, TokenStream, + is_keyword_function, is_valid_function_name, is_valid_identifier, Span, Token, TokenStream, TokenizerControl, }; use crate::types::dynamic::AccessMode; use crate::types::StringsInterner; use crate::{ - calc_fn_hash, Dynamic, Engine, ExclusiveRange, Identifier, ImmutableString, InclusiveRange, - LexError, OptimizationLevel, ParseError, Position, Scope, Shared, StaticVec, AST, INT, PERR, + calc_fn_hash, Dynamic, Engine, EvalAltResult, EvalContext, ExclusiveRange, Identifier, + ImmutableString, InclusiveRange, LexError, OptimizationLevel, ParseError, Position, Scope, + Shared, StaticVec, AST, INT, PERR, }; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -47,7 +50,7 @@ pub struct ParseState<'e> { /// Interned strings. pub interned_strings: StringsInterner, /// Encapsulates a local stack with variable names to simulate an actual runtime scope. - pub stack: StaticVec<(Identifier, AccessMode)>, + pub stack: Scope<'e>, /// Size of the local variables stack upon entry of the current block scope. pub entry_stack_len: usize, /// Tracks a list of external variables (variables that are not explicitly declared in the scope). @@ -89,7 +92,7 @@ impl<'e> ParseState<'e> { #[cfg(not(feature = "no_closure"))] allow_capture: true, interned_strings: StringsInterner::new(), - stack: StaticVec::new_const(), + stack: Scope::new(), entry_stack_len: 0, #[cfg(not(feature = "no_module"))] imports: StaticVec::new_const(), @@ -107,18 +110,17 @@ impl<'e> ParseState<'e> { #[inline] #[must_use] pub fn access_var(&mut self, name: &str, pos: Position) -> Option { - let mut barrier = false; + let mut hit_barrier = false; let _pos = pos; let index = self .stack - .iter() - .rev() + .iter_rev_raw() .enumerate() - .find(|(.., (n, ..))| { + .find(|&(.., (n, ..))| { if n == SCOPE_SEARCH_BARRIER_MARKER { // Do not go beyond the barrier - barrier = true; + hit_barrier = true; false } else { n == name @@ -138,7 +140,7 @@ impl<'e> ParseState<'e> { self.allow_capture = true } - if barrier { + if hit_barrier { None } else { index @@ -1291,8 +1293,9 @@ fn parse_primary( let mut segments = StaticVec::::new(); match input.next().expect(NEVER_ENDS) { + (Token::InterpolatedString(s), ..) if s.is_empty() => (), (Token::InterpolatedString(s), pos) => { - segments.push(Expr::StringConstant(s.into(), pos)); + segments.push(Expr::StringConstant(s.into(), pos)) } token => unreachable!("Token::InterpolatedString expected but gets {:?}", token), } @@ -1302,7 +1305,10 @@ fn parse_primary( block @ Stmt::Block(..) => Expr::Stmt(Box::new(block.into())), stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt), }; - segments.push(expr); + match expr { + Expr::StringConstant(s, ..) if s.is_empty() => (), + _ => segments.push(expr), + } // Make sure to parse the following as text let mut control = state.tokenizer_control.get(); @@ -1332,8 +1338,12 @@ fn parse_primary( } } - segments.shrink_to_fit(); - Expr::InterpolatedString(segments.into(), settings.pos) + if segments.is_empty() { + Expr::StringConstant(state.get_interned_string("", ""), settings.pos) + } else { + segments.shrink_to_fit(); + Expr::InterpolatedString(segments.into(), settings.pos) + } } // Array literal @@ -1792,7 +1802,11 @@ fn make_assignment_stmt( || index.expect("either long or short index is `None`").get(), |n| n.get() as usize, ); - match state.stack[state.stack.len() - index].1 { + match state + .stack + .get_mut_by_index(state.stack.len() - index) + .access_mode() + { AccessMode::ReadWrite => Ok(Stmt::Assignment( (op_info, (lhs, rhs).into()).into(), op_pos, @@ -2185,7 +2199,7 @@ fn parse_custom_syntax( // Add a barrier variable to the stack so earlier variables will not be matched. // Variable searches stop at the first barrier. let marker = state.get_identifier("", SCOPE_SEARCH_BARRIER_MARKER); - state.stack.push((marker, AccessMode::ReadWrite)); + state.stack.push(marker, ()); } let parse_func = syntax.parse.as_ref(); @@ -2552,12 +2566,12 @@ fn parse_for( let counter_var = counter_name.map(|name| { let name = state.get_identifier("", name); let pos = counter_pos.expect("`Some`"); - state.stack.push((name.clone(), AccessMode::ReadWrite)); + state.stack.push(name.clone(), ()); Ident { name, pos } }); let loop_var = state.get_identifier("", name); - state.stack.push((loop_var.clone(), AccessMode::ReadWrite)); + state.stack.push(loop_var.clone(), ()); let loop_var = Ident { name: loop_var, pos: name_pos, @@ -2566,7 +2580,7 @@ fn parse_for( settings.is_breakable = true; let body = parse_block(input, state, lib, settings.level_up())?; - state.stack.truncate(prev_stack_len); + state.stack.rewind(prev_stack_len); Ok(Stmt::For( expr, @@ -2600,6 +2614,36 @@ fn parse_let( return Err(PERR::VariableExists(name.to_string()).into_err(pos)); } + if let Some(ref filter) = state.engine.def_var_filter { + let will_shadow = state.stack.iter().any(|(v, ..)| v == name.as_ref()); + let level = settings.level; + let is_const = var_type == AccessMode::ReadOnly; + let info = VarDefInfo { + name: &name, + is_const, + nesting_level: level, + will_shadow, + }; + let context = EvalContext { + engine: state.engine, + scope: &mut state.stack, + global: &mut GlobalRuntimeState::new(state.engine), + state: &mut EvalState::new(), + lib: &[], + this_ptr: &mut None, + level, + }; + + match filter(false, info, &context) { + Ok(true) => (), + Ok(false) => return Err(PERR::ForbiddenVariable(name.to_string()).into_err(pos)), + Err(err) => match *err { + EvalAltResult::ErrorParsing(perr, pos) => return Err(perr.into_err(pos)), + _ => return Err(PERR::ForbiddenVariable(name.to_string()).into_err(pos)), + }, + } + } + let name = state.get_identifier("", name); let var_def = Ident { name: name.clone(), @@ -2614,8 +2658,6 @@ fn parse_let( Expr::Unit(Position::NONE) }; - state.stack.push((name, var_type)); - let export = if is_export { AST_OPTION_EXPORTED } else { @@ -2624,14 +2666,20 @@ fn parse_let( match var_type { // let name = expr - AccessMode::ReadWrite => Ok(Stmt::Var(expr, var_def.into(), export, settings.pos)), + AccessMode::ReadWrite => { + state.stack.push(name, ()); + Ok(Stmt::Var(expr, var_def.into(), export, settings.pos)) + } // const name = { expr:constant } - AccessMode::ReadOnly => Ok(Stmt::Var( - expr, - var_def.into(), - AST_OPTION_CONSTANT + export, - settings.pos, - )), + AccessMode::ReadOnly => { + state.stack.push_constant(name, ()); + Ok(Stmt::Var( + expr, + var_def.into(), + AST_OPTION_CONSTANT + export, + settings.pos, + )) + } } } @@ -2810,7 +2858,7 @@ fn parse_block( } }; - state.stack.truncate(state.entry_stack_len); + state.stack.rewind(state.entry_stack_len); state.entry_stack_len = prev_entry_stack_len; #[cfg(not(feature = "no_module"))] @@ -2818,7 +2866,7 @@ fn parse_block( Ok(Stmt::Block( statements.into_boxed_slice(), - (settings.pos, end_pos), + Span::new(settings.pos, end_pos), )) } @@ -3096,7 +3144,7 @@ fn parse_try_catch( } let name = state.get_identifier("", name); - state.stack.push((name.clone(), AccessMode::ReadWrite)); + state.stack.push(name.clone(), ()); Some(Ident { name, pos }) } else { None @@ -3107,7 +3155,7 @@ fn parse_try_catch( if catch_var.is_some() { // Remove the error variable from the stack - state.stack.pop().unwrap(); + state.stack.rewind(state.stack.len() - 1); } Ok(Stmt::TryCatch( @@ -3166,7 +3214,7 @@ fn parse_fn( ); } let s = state.get_identifier("", s); - state.stack.push((s.clone(), AccessMode::ReadWrite)); + state.stack.push(s.clone(), ()); params.push((s, pos)) } (Token::LexError(err), pos) => return Err(err.into_err(pos)), @@ -3226,7 +3274,7 @@ fn parse_fn( fn make_curry_from_externals( state: &mut ParseState, fn_expr: Expr, - externals: StaticVec, + externals: StaticVec, pos: Position, ) -> Expr { // If there are no captured variables, no need to curry @@ -3239,21 +3287,26 @@ fn make_curry_from_externals( args.push(fn_expr); - args.extend(externals.iter().cloned().map(|x| { - Expr::Variable( - None, - Position::NONE, - ( - None, - #[cfg(not(feature = "no_module"))] - None, - #[cfg(feature = "no_module")] - (), - x, - ) - .into(), - ) - })); + args.extend( + externals + .iter() + .cloned() + .map(|crate::ast::Ident { name, pos }| { + Expr::Variable( + None, + pos, + ( + None, + #[cfg(not(feature = "no_module"))] + None, + #[cfg(feature = "no_module")] + (), + name, + ) + .into(), + ) + }), + ); let expr = FnCallExpr { name: state.get_identifier("", crate::engine::KEYWORD_FN_PTR_CURRY), @@ -3270,7 +3323,11 @@ fn make_curry_from_externals( // Convert the entire expression into a statement block, then insert the relevant // [`Share`][Stmt::Share] statements. let mut statements = StaticVec::with_capacity(externals.len() + 1); - statements.extend(externals.into_iter().map(Stmt::Share)); + statements.extend( + externals + .into_iter() + .map(|crate::ast::Ident { name, pos }| Stmt::Share(name, pos)), + ); statements.push(Stmt::Expr(expr)); Expr::Stmt(crate::ast::StmtBlock::new(statements, pos, Position::NONE).into()) } @@ -3300,7 +3357,7 @@ fn parse_anon_fn( ); } let s = state.get_identifier("", s); - state.stack.push((s.clone(), AccessMode::ReadWrite)); + state.stack.push(s.clone(), ()); params_list.push(s) } (Token::LexError(err), pos) => return Err(err.into_err(pos)), @@ -3336,14 +3393,14 @@ fn parse_anon_fn( // so extract them into a list. #[cfg(not(feature = "no_closure"))] let (mut params, externals) = { - let externals: StaticVec = state - .external_vars - .iter() - .map(|crate::ast::Ident { name, .. }| name.clone()) - .collect(); + let externals: StaticVec<_> = state.external_vars.iter().cloned().collect(); let mut params = StaticVec::with_capacity(params_list.len() + externals.len()); - params.extend(externals.iter().cloned()); + params.extend( + externals + .iter() + .map(|crate::ast::Ident { name, .. }| name.clone()), + ); (params, externals) }; diff --git a/src/reify.rs b/src/reify.rs index e582b5cb..bdbe2db8 100644 --- a/src/reify.rs +++ b/src/reify.rs @@ -1,12 +1,19 @@ -/// Runs `$code` if `$old` is of type `$t`. +/// Macro to cast an identifier or expression to another type with type checks. /// -/// This macro is primarily used for type casting between known types. +/// Runs _code_ if _variable_ or _expression_ is of type _type_, otherwise run _fallback_. +/// +/// # Syntax +/// +/// * `reify!(`_variable_ or _expression_`,|`_temp-variable_`: `_type_`|` _code_`,` `||` _fallback_ `)` +/// * `reify!(`_variable_ or _expression_`,|`_temp-variable_`: `_type_`|` _code_ `)` +/// * `reify!(`_variable_ or _expression_ `=>` `Option<`_type_`>` `)` +/// * `reify!(`_variable_ or _expression_ `=>` _type_ `)` #[macro_export] macro_rules! reify { ($old:ident, |$new:ident : $t:ty| $code:expr, || $fallback:expr) => {{ if std::any::TypeId::of::<$t>() == std::any::Any::type_id(&$old) { - // SAFETY: This is safe because we check to make sure the two types are - // actually the same type. + // SAFETY: This is safe because we already checked to make sure the two types + // are actually the same. let $new: $t = unsafe { std::mem::transmute_copy(&std::mem::ManuallyDrop::new($old)) }; $code } else { @@ -17,6 +24,7 @@ macro_rules! reify { let old = $old; reify!(old, |$new: $t| $code, || $fallback) }}; + ($old:ident, |$new:ident : $t:ty| $code:expr) => { reify!($old, |$new: $t| $code, || ()) }; diff --git a/src/tokenizer.rs b/src/tokenizer.rs index ecc887aa..8d3c70b2 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -59,10 +59,10 @@ pub type TokenStream<'a> = Peekable>; /// Advancing beyond the maximum line length or maximum number of lines is not an error but has no effect. #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy)] pub struct Position { - /// Line number - 0 = none + /// Line number: 0 = none #[cfg(not(feature = "no_position"))] line: u16, - /// Character position - 0 = BOL + /// Character position: 0 = BOL #[cfg(not(feature = "no_position"))] pos: u16, } @@ -226,11 +226,9 @@ impl Position { /// Print this [`Position`] for debug purposes. #[inline] pub(crate) fn debug_print(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { - #[cfg(not(feature = "no_position"))] if !self.is_none() { write!(_f, " @ {:?}", self)?; } - Ok(()) } } @@ -259,16 +257,19 @@ impl fmt::Display for Position { impl fmt::Debug for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - #[cfg(not(feature = "no_position"))] - if self.is_beginning_of_line() { - write!(f, "{}", self.line)?; + if self.is_none() { + f.write_str("none") } else { - write!(f, "{}:{}", self.line, self.pos)?; - } - #[cfg(feature = "no_position")] - f.write_str("none")?; + #[cfg(not(feature = "no_position"))] + if self.is_beginning_of_line() { + write!(f, "{}", self.line) + } else { + write!(f, "{}:{}", self.line, self.pos) + } - Ok(()) + #[cfg(feature = "no_position")] + unreachable!(); + } } } @@ -300,6 +301,71 @@ impl AddAssign for Position { } } +/// _(internals)_ A span consisting of a starting and an ending [positions][Position]. +/// Exported under the `internals` feature only. +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, Default)] +pub struct Span { + /// Starting [position][Position]. + start: Position, + /// Ending [position][Position]. + end: Position, +} + +impl Span { + pub const NONE: Self = Self::new(Position::NONE, Position::NONE); + + /// Create a new [`Span`]. + #[inline(always)] + #[must_use] + pub const fn new(start: Position, end: Position) -> Self { + Self { start, end } + } + /// Is this [`Span`] non-existent? + #[inline(always)] + #[must_use] + pub const fn is_none(&self) -> bool { + self.start.is_none() && self.end.is_none() + } + /// Get the [`Span`]'s starting [position][Position]. + #[inline(always)] + #[must_use] + pub const fn start(&self) -> Position { + self.start + } + /// Get the [`Span`]'s ending [position][Position]. + #[inline(always)] + #[must_use] + pub const fn end(&self) -> Position { + self.end + } +} + +impl fmt::Display for Span { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match (self.start().is_none(), self.end().is_none()) { + (false, false) if self.start().line() != self.end().line() => { + write!(f, "{:?}-{:?}", self.start(), self.end()) + } + (false, false) => write!( + f, + "{}:{}-{}", + self.start().line().unwrap(), + self.start().position().unwrap_or(0), + self.end().position().unwrap_or(0) + ), + (true, false) => write!(f, "..{:?}", self.end()), + (false, true) => write!(f, "{:?}", self.start()), + (true, true) => write!(f, "{:?}", Position::NONE), + } + } +} + +impl fmt::Debug for Span { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + /// _(internals)_ A Rhai language token. /// Exported under the `internals` feature only. #[derive(Debug, PartialEq, Clone, Hash)] @@ -1066,11 +1132,12 @@ pub fn parse_string_literal( verbatim: bool, allow_line_continuation: bool, allow_interpolation: bool, -) -> Result<(Box, bool), (LexError, Position)> { +) -> Result<(Box, bool, Position), (LexError, Position)> { let mut result = String::with_capacity(12); let mut escape = String::with_capacity(12); let start = *pos; + let mut first_char = Position::NONE; let mut interpolated = false; #[cfg(not(feature = "no_position"))] let mut skip_whitespace_until = 0; @@ -1123,6 +1190,21 @@ pub fn parse_string_literal( } } + // Close wrapper + if termination_char == next_char && escape.is_empty() { + // Double wrapper + if stream.peek_next().map_or(false, |c| c == termination_char) { + eat_next(stream, pos); + } else { + state.is_within_text_terminated_by = None; + break; + } + } + + if first_char.is_none() { + first_char = *pos; + } + match next_char { // \r - ignore if followed by \n '\r' if stream.peek_next().map(|ch| ch == '\n').unwrap_or(false) => (), @@ -1190,21 +1272,6 @@ pub fn parse_string_literal( result.push(next_char) } - // Double wrapper - _ if termination_char == next_char - && escape.is_empty() - && stream.peek_next().map_or(false, |c| c == termination_char) => - { - eat_next(stream, pos); - result.push(termination_char) - } - - // Close wrapper - _ if termination_char == next_char && escape.is_empty() => { - state.is_within_text_terminated_by = None; - break; - } - // Verbatim '\n' if verbatim => { assert_eq!(escape, "", "verbatim strings should not have any escapes"); @@ -1262,7 +1329,7 @@ pub fn parse_string_literal( } } - Ok((result.into(), interpolated)) + Ok((result.into(), interpolated, first_char)) } /// Consume the next character. @@ -1397,11 +1464,9 @@ fn get_next_token_inner( // Within text? if let Some(ch) = state.is_within_text_terminated_by.take() { - let start_pos = *pos; - return parse_string_literal(stream, state, pos, ch, true, false, true).map_or_else( |(err, err_pos)| Some((Token::LexError(err), err_pos)), - |(result, interpolated)| { + |(result, interpolated, start_pos)| { if interpolated { Some((Token::InterpolatedString(result), start_pos)) } else { @@ -1612,7 +1677,7 @@ fn get_next_token_inner( return parse_string_literal(stream, state, pos, c, true, false, true).map_or_else( |(err, err_pos)| Some((Token::LexError(err), err_pos)), - |(result, interpolated)| { + |(result, interpolated, ..)| { if interpolated { Some((Token::InterpolatedString(result), start_pos)) } else { diff --git a/src/types/error.rs b/src/types/error.rs index 32d3195b..9eaf7907 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -13,6 +13,10 @@ use std::prelude::v1::*; /// /// All wrapped [`Position`] values represent the location in the script where the error occurs. /// +/// Some errors never appear when certain features are turned on. +/// They still exist so that the application can turn features on and off without going through +/// massive code changes to remove/add back enum variants in match statements. +/// /// # Thread Safety /// /// Currently, [`EvalAltResult`] is neither [`Send`] nor [`Sync`]. @@ -32,8 +36,12 @@ pub enum EvalAltResult { /// Shadowing of an existing variable disallowed. Wrapped value is the variable name. ErrorVariableExists(String, Position), - /// Usage of an unknown variable. Wrapped value is the variable name. + /// Forbidden variable name. Wrapped value is the variable name. + ErrorForbiddenVariable(String, Position), + /// Access of an unknown variable. Wrapped value is the variable name. ErrorVariableNotFound(String, Position), + /// Access of an unknown object map property. Wrapped value is the property name. + ErrorPropertyNotFound(String, Position), /// Call to an unknown function. Wrapped value is the function signature. ErrorFunctionNotFound(String, Position), /// Usage of an unknown [module][crate::Module]. Wrapped value is the [module][crate::Module] name. @@ -142,7 +150,9 @@ impl fmt::Display for EvalAltResult { Self::ErrorInModule(s, err, ..) => write!(f, "Error in module {}: {}", s, err)?, Self::ErrorVariableExists(s, ..) => write!(f, "Variable is already defined: {}", s)?, + Self::ErrorForbiddenVariable(s, ..) => write!(f, "Forbidden variable name: {}", s)?, Self::ErrorVariableNotFound(s, ..) => write!(f, "Variable not found: {}", s)?, + Self::ErrorPropertyNotFound(s, ..) => write!(f, "Property not found: {}", s)?, Self::ErrorFunctionNotFound(s, ..) => write!(f, "Function not found: {}", s)?, Self::ErrorModuleNotFound(s, ..) => write!(f, "Module not found: {}", s)?, Self::ErrorDataRace(s, ..) => { @@ -278,7 +288,9 @@ impl EvalAltResult { | Self::ErrorIndexingType(..) | Self::ErrorFor(..) | Self::ErrorVariableExists(..) + | Self::ErrorForbiddenVariable(..) | Self::ErrorVariableNotFound(..) + | Self::ErrorPropertyNotFound(..) | Self::ErrorModuleNotFound(..) | Self::ErrorDataRace(..) | Self::ErrorAssignmentToConstant(..) @@ -369,7 +381,9 @@ impl EvalAltResult { map.insert("type".into(), t.into()); } Self::ErrorVariableExists(v, ..) + | Self::ErrorForbiddenVariable(v, ..) | Self::ErrorVariableNotFound(v, ..) + | Self::ErrorPropertyNotFound(v, ..) | Self::ErrorDataRace(v, ..) | Self::ErrorAssignmentToConstant(v, ..) => { map.insert("variable".into(), v.into()); @@ -431,7 +445,9 @@ impl EvalAltResult { | Self::ErrorIndexingType(.., pos) | Self::ErrorFor(pos) | Self::ErrorVariableExists(.., pos) + | Self::ErrorForbiddenVariable(.., pos) | Self::ErrorVariableNotFound(.., pos) + | Self::ErrorPropertyNotFound(.., pos) | Self::ErrorModuleNotFound(.., pos) | Self::ErrorDataRace(.., pos) | Self::ErrorAssignmentToConstant(.., pos) @@ -480,7 +496,9 @@ impl EvalAltResult { | Self::ErrorIndexingType(.., pos) | Self::ErrorFor(pos) | Self::ErrorVariableExists(.., pos) + | Self::ErrorForbiddenVariable(.., pos) | Self::ErrorVariableNotFound(.., pos) + | Self::ErrorPropertyNotFound(.., pos) | Self::ErrorModuleNotFound(.., pos) | Self::ErrorDataRace(.., pos) | Self::ErrorAssignmentToConstant(.., pos) diff --git a/src/types/parse_error.rs b/src/types/parse_error.rs index 4ae403ba..fb922e15 100644 --- a/src/types/parse_error.rs +++ b/src/types/parse_error.rs @@ -10,8 +10,7 @@ use std::fmt; #[cfg(feature = "no_std")] use std::prelude::v1::*; -/// _(internals)_ Error encountered when tokenizing the script text. -/// Exported under the `internals` feature only. +/// Error encountered when tokenizing the script text. #[derive(Debug, Eq, PartialEq, Clone, Hash)] #[non_exhaustive] pub enum LexError { @@ -89,21 +88,12 @@ pub enum ParseErrorType { MalformedCallExpr(String), /// An expression in indexing brackets `[]` has syntax error. Wrapped value is the error /// description (if any). - /// - /// Never appears under the `no_index` feature. MalformedIndexExpr(String), - /// An expression in an `in` expression has syntax error. Wrapped value is the error description - /// (if any). - /// - /// Never appears under the `no_object` and `no_index` features combination. + /// An expression in an `in` expression has syntax error. Wrapped value is the error description (if any). MalformedInExpr(String), /// A capturing has syntax error. Wrapped value is the error description (if any). - /// - /// Never appears under the `no_closure` feature. MalformedCapture(String), /// A map definition has duplicated property names. Wrapped value is the property name. - /// - /// Never appears under the `no_object` feature. DuplicatedProperty(String), /// A `switch` case is duplicated. DuplicatedSwitchCase, @@ -116,11 +106,11 @@ pub enum ParseErrorType { /// The case condition of a `switch` statement is not appropriate. WrongSwitchCaseCondition, /// Missing a property name for custom types and maps. - /// - /// Never appears under the `no_object` feature. PropertyExpected, /// Missing a variable name after the `let`, `const`, `for` or `catch` keywords. VariableExpected, + /// Forbidden variable name. Wrapped value is the variable name. + ForbiddenVariable(String), /// An identifier is a reserved symbol. Reserved(String), /// An expression is of the wrong type. @@ -129,38 +119,22 @@ pub enum ParseErrorType { /// Missing an expression. Wrapped value is the expression type. ExprExpected(String), /// Defining a doc-comment in an appropriate place (e.g. not at global level). - /// - /// Never appears under the `no_function` feature. WrongDocComment, /// Defining a function `fn` in an appropriate place (e.g. inside another function). - /// - /// Never appears under the `no_function` feature. WrongFnDefinition, /// Defining a function with a name that conflicts with an existing function. /// Wrapped values are the function name and number of parameters. - /// - /// Never appears under the `no_object` feature. FnDuplicatedDefinition(String, usize), /// Missing a function name after the `fn` keyword. - /// - /// Never appears under the `no_function` feature. FnMissingName, /// A function definition is missing the parameters list. Wrapped value is the function name. - /// - /// Never appears under the `no_function` feature. FnMissingParams(String), /// A function definition has duplicated parameters. Wrapped values are the function name and /// parameter name. - /// - /// Never appears under the `no_function` feature. FnDuplicatedParam(String, String), /// A function definition is missing the body. Wrapped value is the function name. - /// - /// Never appears under the `no_function` feature. FnMissingBody(String), /// Export statement not at global level. - /// - /// Never appears under the `no_module` feature. WrongExport, /// Assignment to an a constant variable. Wrapped value is the constant variable name. AssignmentToConstant(String), @@ -178,16 +152,10 @@ pub enum ParseErrorType { /// An imported module is not found. /// /// Only appears when strict variables mode is enabled. - /// - /// Never appears under the `no_module` feature. ModuleUndefined(String), /// Expression exceeding the maximum levels of complexity. - /// - /// Never appears under the `unchecked` feature. ExprTooDeep, /// Literal exceeding the maximum size. Wrapped values are the data type name and the maximum size. - /// - /// Never appears under the `unchecked` feature. LiteralTooLarge(String, usize), /// Break statement not inside a loop. LoopBreak, @@ -274,6 +242,7 @@ impl fmt::Display for ParseErrorType { Self::WrongSwitchCaseCondition => f.write_str("This switch case cannot have a condition"), Self::PropertyExpected => f.write_str("Expecting name of a property"), Self::VariableExpected => f.write_str("Expecting name of a variable"), + Self::ForbiddenVariable(s) => write!(f, "Forbidden variable name: {}", s), Self::WrongFnDefinition => f.write_str("Function definitions must be at global level and cannot be inside a block or another function"), Self::FnMissingName => f.write_str("Expecting function name in function declaration"), Self::WrongDocComment => f.write_str("Doc-comment must be followed immediately by a function definition"), diff --git a/src/types/scope.rs b/src/types/scope.rs index 74a667e7..85fd70d4 100644 --- a/src/types/scope.rs +++ b/src/types/scope.rs @@ -595,6 +595,16 @@ impl Scope<'_> { .zip(self.values.iter()) .map(|((name, ..), value)| (name.as_ref(), value.is_read_only(), value)) } + /// Get a reverse iterator to entries in the [`Scope`]. + /// Shared values are not expanded. + #[inline] + pub(crate) fn iter_rev_raw(&self) -> impl Iterator { + self.names + .iter() + .rev() + .zip(self.values.iter().rev()) + .map(|((name, ..), value)| (name.as_ref(), value.is_read_only(), value)) + } /// Remove a range of entries within the [`Scope`]. /// /// # Panics diff --git a/tests/maps.rs b/tests/maps.rs index 5dceecb4..36ff7e84 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -107,6 +107,23 @@ b`: 1}; y["a\nb"] Ok(()) } +#[test] +fn test_map_prop() -> Result<(), Box> { + let mut engine = Engine::new(); + + assert_eq!(engine.eval::<()>("let x = #{a: 42}; x.b")?, ()); + + engine.set_fail_on_invalid_map_property(true); + + assert!( + matches!(*engine.eval::<()>("let x = #{a: 42}; x.b").expect_err("should error"), + EvalAltResult::ErrorPropertyNotFound(prop, _) if prop == "b" + ) + ); + + Ok(()) +} + #[test] fn test_map_assign() -> Result<(), Box> { let engine = Engine::new(); diff --git a/tests/var_scope.rs b/tests/var_scope.rs index d8543b81..84678747 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult, Position, Scope, INT}; +use rhai::{Engine, EvalAltResult, ParseErrorType, Position, Scope, INT}; #[test] fn test_var_scope() -> Result<(), Box> { @@ -125,7 +125,10 @@ fn test_var_resolver() -> Result<(), Box> { fn test_var_def_filter() -> Result<(), Box> { let mut engine = Engine::new(); - engine.on_def_var(|name, _, scope_level, _, _| match (name, scope_level) { + let ast = engine.compile("let x = 42;")?; + engine.run_ast(&ast)?; + + engine.on_def_var(|_, info, _| match (info.name, info.nesting_level) { ("x", 0 | 1) => Ok(false), _ => Ok(true), }); @@ -135,7 +138,14 @@ fn test_var_def_filter() -> Result<(), Box> { 124 ); - assert!(engine.run("let x = 42;").is_err()); + assert!(matches!( + *engine.compile("let x = 42;").expect_err("should error").0, + ParseErrorType::ForbiddenVariable(s) if s == "x" + )); + assert!(matches!( + *engine.run_ast(&ast).expect_err("should err"), + EvalAltResult::ErrorForbiddenVariable(s, _) if s == "x" + )); assert!(engine.run("const x = 42;").is_err()); assert!(engine.run("let y = 42; { let x = y + 1; }").is_err()); assert!(engine.run("let y = 42; { let x = y + 1; }").is_err());