diff --git a/CHANGELOG.md b/CHANGELOG.md index bc16d51d..a9a6e926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,17 @@ Version 1.9.0 The minimum Rust version is now `1.60.0` in order to use the `dep:` syntax for dependencies. +Bug fixes +--------- + +* `switch` cases with conditions that evaluate to constant `()` no longer optimize to `false` (should raise a type error during runtime). +* Fixes concatenation of BLOB's and strings, where the BLOB's should be interpreted as UTF-8 encoded strings. + New features ------------ * A new feature, `no_custom_syntax`, is added to remove custom syntax support from Rhai for applications that do not require it (which should be most). +* Comment lines beginning with `//!` (requires the `metadata` feature) are now collected as the script file's _module documentation_. Enhancements ------------ @@ -25,6 +32,8 @@ Enhancements * `EvalContext::eval_expression_tree_raw` and `Expression::eval_with_context_raw` are added to allow for not rewinding the `Scope` at the end of a statements block. * A new `range` function variant that takes an exclusive range with a step. +* `as_string` is added to BLOB's to convert it into a string by interpreting it as a UTF-8 byte stream. +* `FnAccess::is_private`, `FnAccess::is_public`, `FnNamespace::is_module_namespace` and `FnNameSpace::is_global_namespace` are added for convenience. Version 1.8.0 diff --git a/Cargo.toml b/Cargo.toml index e571dc48..7678fb27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ name = "rhai" version = "1.8.0" rust-version = "1.60.0" edition = "2018" +resolver = "2" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung", "jhwgh1968"] description = "Embedded scripting for Rust" homepage = "https://rhai.rs" diff --git a/codegen/src/rhai_module.rs b/codegen/src/rhai_module.rs index dc26e958..b5c8df4a 100644 --- a/codegen/src/rhai_module.rs +++ b/codegen/src/rhai_module.rs @@ -200,7 +200,7 @@ pub fn generate_body( let ns_str = syn::Ident::new( match namespace { - FnNamespaceAccess::Unset => unreachable!(), + FnNamespaceAccess::Unset => unreachable!("`namespace` should be set"), FnNamespaceAccess::Global => "Global", FnNamespaceAccess::Internal => "Internal", }, diff --git a/examples/event_handler_js/script.rhai b/examples/event_handler_js/script.rhai index 7c488515..0667575f 100644 --- a/examples/event_handler_js/script.rhai +++ b/examples/event_handler_js/script.rhai @@ -1,4 +1,4 @@ -// Implementation of the Event Handler With State Pattern - JS Style +//! Implementation of the Event Handler With State Pattern - JS Style /// Initialize user-provided state. fn init() { diff --git a/examples/event_handler_main/script.rhai b/examples/event_handler_main/script.rhai index 641c2372..a2097e12 100644 --- a/examples/event_handler_main/script.rhai +++ b/examples/event_handler_main/script.rhai @@ -1,4 +1,4 @@ -// Implementation of the Event Handler With State Pattern - Main Style +//! Implementation of the Event Handler With State Pattern - Main Style /// Initialize user-provided state (shadows system-provided state, if any). fn init() { diff --git a/examples/event_handler_map/script.rhai b/examples/event_handler_map/script.rhai index 988ca170..59c55f79 100644 --- a/examples/event_handler_map/script.rhai +++ b/examples/event_handler_map/script.rhai @@ -1,4 +1,4 @@ -// Implementation of the Event Handler With State Pattern - Map Style +//! Implementation of the Event Handler With State Pattern - Map Style /// Initialize user-provided state. /// State is stored inside an object map bound to 'state'. diff --git a/scripts/assignment.rhai b/scripts/assignment.rhai index 2d713b43..68802a75 100644 --- a/scripts/assignment.rhai +++ b/scripts/assignment.rhai @@ -1,4 +1,4 @@ -// This script contains a single assignment statement. +//! This script contains a single assignment statement. let x = 78; diff --git a/scripts/doc-comments.rhai b/scripts/doc-comments.rhai index 6717039f..f2a56ffe 100644 --- a/scripts/doc-comments.rhai +++ b/scripts/doc-comments.rhai @@ -1,3 +1,5 @@ +//! This script illustrates how to put doc-comments on functions. + /// The function `foo`, which prints `hello, world!` and a magic number, /// accepts three parameters. /// diff --git a/scripts/fibonacci.rhai b/scripts/fibonacci.rhai index 97c93d08..92015718 100644 --- a/scripts/fibonacci.rhai +++ b/scripts/fibonacci.rhai @@ -1,5 +1,5 @@ -// This script calculates the n-th Fibonacci number using a really dumb algorithm -// to test the speed of the scripting engine. +//! This script calculates the n-th Fibonacci number using a really dumb algorithm +//! to test the speed of the scripting engine. const TARGET = 28; const REPEAT = 5; diff --git a/scripts/for1.rhai b/scripts/for1.rhai index fe022762..6d4062e8 100644 --- a/scripts/for1.rhai +++ b/scripts/for1.rhai @@ -1,4 +1,4 @@ -// This script runs for-loops. +//! This script runs for-loops. let arr = [1, true, 123.456, "hello", 3, 42]; diff --git a/scripts/for2.rhai b/scripts/for2.rhai index 478f53c9..010a3a00 100644 --- a/scripts/for2.rhai +++ b/scripts/for2.rhai @@ -1,4 +1,4 @@ -// This script runs for-loops +//! This script runs for-loops const MAX = 1_000_000; diff --git a/scripts/for3.rhai b/scripts/for3.rhai index d370a388..eac49194 100644 --- a/scripts/for3.rhai +++ b/scripts/for3.rhai @@ -1,4 +1,4 @@ -// This script runs for-loops with closures. +//! This script runs for-loops with closures. const MAX = 100; const CHECK = ((MAX - 1) ** 2) * MAX; diff --git a/scripts/function_decl1.rhai b/scripts/function_decl1.rhai index 8c237760..d358b4b3 100644 --- a/scripts/function_decl1.rhai +++ b/scripts/function_decl1.rhai @@ -1,4 +1,4 @@ -// This script defines a function and calls it. +//! This script defines a function and calls it. fn call_me() { return 3; diff --git a/scripts/function_decl2.rhai b/scripts/function_decl2.rhai index 3640537f..7ce09e3a 100644 --- a/scripts/function_decl2.rhai +++ b/scripts/function_decl2.rhai @@ -1,4 +1,4 @@ -// This script defines a function with two parameters and local variables. +//! This script defines a function with two parameters and local variables. let a = 3; diff --git a/scripts/function_decl3.rhai b/scripts/function_decl3.rhai index 2cc9c6d9..9ce0b508 100644 --- a/scripts/function_decl3.rhai +++ b/scripts/function_decl3.rhai @@ -1,5 +1,4 @@ -// This script defines a function with many parameters. -// +//! This script defines a function with many parameters. const KEY = 38; diff --git a/scripts/function_decl4.rhai b/scripts/function_decl4.rhai index e5bc9271..07ae2bef 100644 --- a/scripts/function_decl4.rhai +++ b/scripts/function_decl4.rhai @@ -1,4 +1,4 @@ -// This script defines a function that acts as a method. +//! This script defines a function that acts as a method. // Use 'this' to refer to the object of a method call fn action(x, y) { diff --git a/scripts/if1.rhai b/scripts/if1.rhai index ed74c227..92830b3f 100644 --- a/scripts/if1.rhai +++ b/scripts/if1.rhai @@ -1,4 +1,4 @@ -// This script runs if statements. +//! This script runs if statements. let a = 42; let b = 123; diff --git a/scripts/if2.rhai b/scripts/if2.rhai index 202fac89..15e855ba 100644 --- a/scripts/if2.rhai +++ b/scripts/if2.rhai @@ -1,4 +1,4 @@ -// This script runs an if expression. +//! This script runs an if expression. let a = 42; let b = 123; diff --git a/scripts/loop.rhai b/scripts/loop.rhai index 0cfab44d..8f843c22 100644 --- a/scripts/loop.rhai +++ b/scripts/loop.rhai @@ -1,4 +1,4 @@ -// This script runs an infinite loop, ending it with a break statement. +//! This script runs an infinite loop, ending it with a break statement. let x = 10; diff --git a/scripts/mat_mul.rhai b/scripts/mat_mul.rhai index 40283aa7..9830a66f 100644 --- a/scripts/mat_mul.rhai +++ b/scripts/mat_mul.rhai @@ -1,4 +1,4 @@ -// This script simulates multi-dimensional matrix calculations. +//! This script simulates multi-dimensional matrix calculations. const SIZE = 50; diff --git a/scripts/module.rhai b/scripts/module.rhai index 29a6704d..fc92ec19 100644 --- a/scripts/module.rhai +++ b/scripts/module.rhai @@ -1,4 +1,4 @@ -// This script imports an external script as a module. +//! This script imports an external script as a module. import "loop" as x; diff --git a/scripts/oop.rhai b/scripts/oop.rhai index 8caf6d87..2c2ecd3d 100644 --- a/scripts/oop.rhai +++ b/scripts/oop.rhai @@ -1,4 +1,4 @@ -// This script simulates object-oriented programming (OOP) techniques using closures. +//! This script simulates object-oriented programming (OOP) techniques using closures. // External variable that will be captured. let last_value = (); diff --git a/scripts/op1.rhai b/scripts/op1.rhai index 27333f9a..daf13f49 100644 --- a/scripts/op1.rhai +++ b/scripts/op1.rhai @@ -1,4 +1,4 @@ -// This script runs a single expression. +//! This script runs a single expression. print("The result should be 46:"); diff --git a/scripts/op2.rhai b/scripts/op2.rhai index 37481557..94163a8b 100644 --- a/scripts/op2.rhai +++ b/scripts/op2.rhai @@ -1,4 +1,4 @@ -// This script runs a complex expression. +//! This script runs a complex expression. print("The result should be 182:"); diff --git a/scripts/op3.rhai b/scripts/op3.rhai index 30f57dcc..dc1bd997 100644 --- a/scripts/op3.rhai +++ b/scripts/op3.rhai @@ -1,4 +1,4 @@ -// This script runs a complex expression. +//! This script runs a complex expression. print("The result should be 230:"); diff --git a/scripts/primes.rhai b/scripts/primes.rhai index a827de5f..81db2b2c 100644 --- a/scripts/primes.rhai +++ b/scripts/primes.rhai @@ -1,4 +1,4 @@ -// This script uses the Sieve of Eratosthenes to calculate prime numbers. +//! This script uses the Sieve of Eratosthenes to calculate prime numbers. let now = timestamp(); diff --git a/scripts/speed_test.rhai b/scripts/speed_test.rhai index 219ddb71..a5207dcb 100644 --- a/scripts/speed_test.rhai +++ b/scripts/speed_test.rhai @@ -1,4 +1,4 @@ -// This script runs 1 million iterations to test the speed of the scripting engine. +//! This script runs 1 million iterations to test the speed of the scripting engine. let now = timestamp(); let x = 1_000_000; diff --git a/scripts/static.d.rhai b/scripts/static.d.rhai new file mode 100644 index 00000000..b6c104dc --- /dev/null +++ b/scripts/static.d.rhai @@ -0,0 +1,170 @@ +/// This definition file extends the scope of all scripts. +/// +/// The items defined here simply exist and are available. +/// everywhere. +/// +/// These definitions should be used for built-in functions and +/// local domain-specific environment-provided values. +module static; + +/// Display any data to the standard output. +/// +/// # Example +/// +/// ```rhai +/// let answer = 42; +/// +/// print(`The Answer is ${answer}`); +/// ``` +fn print(data: ?); + +/// Display any data to the standard output in debug format. +/// +/// # Example +/// +/// ```rhai +/// let answer = 42; +/// +/// debug(answer); +/// ``` +fn debug(data: ?); + +/// Get the type of a value. +/// +/// # Example +/// +/// ```rhai +/// let x = "hello, world!"; +/// +/// print(x.type_of()); // prints "string" +/// ``` +fn type_of(data: ?) -> String; + +/// Create a function pointer to a named function. +/// +/// If the specified name is not a valid function name, an error is raised. +/// +/// # Example +/// +/// ```rhai +/// let f = Fn("foo"); // function pointer to 'foo' +/// +/// f.call(42); // call: foo(42) +/// ``` +fn Fn(fn_name: String) -> FnPtr; + +/// Call a function pointed to by a function pointer, +/// passing following arguments to the function call. +/// +/// If an appropriate function is not found, an error is raised. +/// +/// # Example +/// +/// ```rhai +/// let f = Fn("foo"); // function pointer to 'foo' +/// +/// f.call(1, 2, 3); // call: foo(1, 2, 3) +/// ``` +fn call(fn_ptr: FnPtr, ...args: ?) -> ?; + +/// Call a function pointed to by a function pointer, binding the `this` pointer +/// to the object of the method call, and passing on following arguments to the function call. +/// +/// If an appropriate function is not found, an error is raised. +/// +/// # Example +/// +/// ```rhai +/// fn add(x) { +/// this + x +/// } +/// +/// let f = Fn("add"); // function pointer to 'add' +/// +/// let x = 41; +/// +/// let r = x.call(f, 1); // call: add(1) with 'this' = 'x' +/// +/// print(r); // prints 42 +/// ``` +fn call(obj: ?, fn_ptr: FnPtr, ...args: ?) -> ?; + +/// Curry a number of arguments into a function pointer and return it as a new function pointer. +/// +/// # Example +/// +/// ```rhai +/// fn foo(x, y, z) { +/// x + y + z +/// } +/// +/// let f = Fn("foo"); +/// +/// let g = f.curry(1, 2); // curried arguments: 1, 2 +/// +/// g.call(3); // call: foo(1, 2, 3) +/// ``` +fn curry(fn_ptr: FnPtr, ...args: ?) -> FnPtr; + +/// Return `true` if a script-defined function exists with a specified name and +/// number of parameters. +/// +/// # Example +/// +/// ```rhai +/// fn foo(x) { } +/// +/// print(is_def_fn("foo", 1)); // prints true +/// print(is_def_fn("foo", 2)); // prints false +/// print(is_def_fn("foo", 0)); // prints false +/// print(is_def_fn("bar", 1)); // prints false +/// ``` +fn is_def_fn(fn_name: String, num_params: i64) -> bool; + +/// Return `true` if a variable matching a specified name is defined. +/// +/// # Example +/// +/// ```rhai +/// let x = 42; +/// +/// print(is_def_var("x")); // prints true +/// print(is_def_var("foo")); // prints false +/// +/// { +/// let y = 1; +/// print(is_def_var("y")); // prints true +/// } +/// +/// print(is_def_var("y")); // prints false +/// ``` +fn is_def_var(var_name: String) -> bool; + +/// Return `true` if the variable is shared. +/// +/// # Example +/// +/// ```rhai +/// let x = 42; +/// +/// print(is_shared(x)); // prints false +/// +/// let f = || x; // capture 'x', making it shared +/// +/// print(is_shared(x)); // prints true +/// ``` +fn is_shared(variable: ?) -> bool; + +/// Evaluate a text script within the current scope. +/// +/// # Example +/// +/// ```rhai +/// let x = 42; +/// +/// eval("let y = x; x = 123;"); +/// +/// print(x); // prints 123 +/// print(y); // prints 42 +/// ``` +fn eval(script: String) -> ?; diff --git a/scripts/string.rhai b/scripts/string.rhai index 12d9a75e..66f51834 100644 --- a/scripts/string.rhai +++ b/scripts/string.rhai @@ -1,4 +1,4 @@ -// This script tests string operations. +//! This script tests string operations. print("hello"); print("this\nis \\ nice"); // escape sequences diff --git a/scripts/strings_map.rhai b/scripts/strings_map.rhai index 0acaf361..88ae45df 100644 --- a/scripts/strings_map.rhai +++ b/scripts/strings_map.rhai @@ -1,4 +1,4 @@ -// This script tests object maps and strings. +//! This script tests object maps and strings. print("Ready... Go!"); diff --git a/scripts/switch.rhai b/scripts/switch.rhai index 77292630..8e2bb6e1 100644 --- a/scripts/switch.rhai +++ b/scripts/switch.rhai @@ -1,4 +1,4 @@ -// This script runs a switch statement in a for-loop. +//! This script runs a switch statement in a for-loop. let arr = [42, 123.456, "hello", true, "hey", 'x', 999, 1, 2, 3, 4]; diff --git a/scripts/while.rhai b/scripts/while.rhai index 0dd575c1..21c0681d 100644 --- a/scripts/while.rhai +++ b/scripts/while.rhai @@ -1,4 +1,4 @@ -// This script runs a while loop. +//! This script runs a while loop. let x = 10; diff --git a/src/api/compile.rs b/src/api/compile.rs index 9aed8eed..b9e95e50 100644 --- a/src/api/compile.rs +++ b/src/api/compile.rs @@ -222,7 +222,10 @@ impl Engine { self.token_mapper.as_ref().map(<_>::as_ref), ); let mut state = ParseState::new(self, scope, tokenizer_control); - self.parse(&mut stream.peekable(), &mut state, optimization_level) + let mut ast = self.parse(&mut stream.peekable(), &mut state, optimization_level)?; + #[cfg(feature = "metadata")] + ast.set_doc(state.tokenizer_control.borrow().global_comments.join("\n")); + Ok(ast) } /// Compile a string containing an expression into an [`AST`], /// which can be used later for evaluation. diff --git a/src/api/custom_syntax.rs b/src/api/custom_syntax.rs index 4769e96e..6c7e2525 100644 --- a/src/api/custom_syntax.rs +++ b/src/api/custom_syntax.rs @@ -78,7 +78,7 @@ impl Expression<'_> { /// /// The following option is available: /// - /// * whether to rewind the [`Scope`] after evaluation if the expression is a [`StmtBlock`][crate::ast::StmtBlock] + /// * whether to rewind the [`Scope`][crate::Scope] after evaluation if the expression is a [`StmtBlock`][crate::ast::StmtBlock] /// /// # WARNING - Unstable API /// diff --git a/src/api/deprecated.rs b/src/api/deprecated.rs index 49dfb94f..208c3f2a 100644 --- a/src/api/deprecated.rs +++ b/src/api/deprecated.rs @@ -318,7 +318,7 @@ impl crate::Expression<'_> { /// /// # Deprecated /// - /// This method is deprecated. Use [`get_string_value`][Expression::get_string_value] instead. + /// This method is deprecated. Use [`get_string_value`][crate::Expression::get_string_value] instead. /// /// This method will be removed in the next major version. #[deprecated(since = "1.4.0", note = "use `get_string_value` instead")] diff --git a/src/api/optimize.rs b/src/api/optimize.rs index fca162f2..726b12f9 100644 --- a/src/api/optimize.rs +++ b/src/api/optimize.rs @@ -2,6 +2,7 @@ #![cfg(not(feature = "no_optimize"))] use crate::{Engine, OptimizationLevel, Scope, AST}; +use std::mem; impl Engine { /// Control whether and how the [`Engine`] will optimize an [`AST`] after compilation. @@ -59,13 +60,18 @@ impl Engine { .map(|f| f.func.get_script_fn_def().unwrap().clone()) .collect(); - crate::optimizer::optimize_into_ast( + let mut new_ast = crate::optimizer::optimize_into_ast( self, scope, ast.take_statements(), #[cfg(not(feature = "no_function"))] lib, optimization_level, - ) + ); + + #[cfg(feature = "metadata")] + new_ast.set_doc(mem::take(ast.doc_mut())); + + new_ast } } diff --git a/src/api/run.rs b/src/api/run.rs index 1c937956..5352e761 100644 --- a/src/api/run.rs +++ b/src/api/run.rs @@ -25,9 +25,7 @@ impl Engine { let (stream, tokenizer_control) = self.lex_raw(&scripts, self.token_mapper.as_ref().map(<_>::as_ref)); let mut state = ParseState::new(self, scope, tokenizer_control); - let ast = self.parse(&mut stream.peekable(), &mut state, self.optimization_level)?; - self.run_ast_with_scope(scope, &ast) } /// Evaluate an [`AST`], returning any error (if any). diff --git a/src/ast/ast.rs b/src/ast/ast.rs index 8a033e2d..0c201593 100644 --- a/src/ast/ast.rs +++ b/src/ast/ast.rs @@ -1,7 +1,7 @@ //! Module defining the AST (abstract syntax tree). use super::{ASTFlags, Expr, FnAccess, Stmt, StmtBlock, StmtBlockContainer}; -use crate::{Dynamic, FnNamespace, Identifier, Position}; +use crate::{Dynamic, FnNamespace, Identifier, Position, SmartString}; #[cfg(feature = "no_std")] use std::prelude::v1::*; use std::{ @@ -21,6 +21,9 @@ pub struct AST { /// Source of the [`AST`]. /// No source if string is empty. source: Identifier, + /// [`AST`] documentation. + #[cfg(feature = "metadata")] + doc: SmartString, /// Global statements. body: StmtBlock, /// Script-defined functions. @@ -42,13 +45,11 @@ impl fmt::Debug for AST { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut fp = f.debug_struct("AST"); - if !self.source.is_empty() { - fp.field("source: ", &self.source); - } + fp.field("source", &self.source); + #[cfg(feature = "metadata")] + fp.field("doc", &self.doc); #[cfg(not(feature = "no_module"))] - if let Some(ref resolver) = self.resolver { - fp.field("resolver: ", resolver); - } + fp.field("resolver", &self.resolver); fp.field("body", &self.body.as_slice()); @@ -74,6 +75,8 @@ impl AST { ) -> Self { Self { source: Identifier::new_const(), + #[cfg(feature = "metadata")] + doc: SmartString::new_const(), body: StmtBlock::new(statements, Position::NONE, Position::NONE), #[cfg(not(feature = "no_function"))] lib: functions.into(), @@ -92,6 +95,8 @@ impl AST { ) -> Self { Self { source: Identifier::new_const(), + #[cfg(feature = "metadata")] + doc: SmartString::new_const(), body: StmtBlock::new(statements, Position::NONE, Position::NONE), #[cfg(not(feature = "no_function"))] lib: functions.into(), @@ -140,6 +145,8 @@ impl AST { pub fn empty() -> Self { Self { source: Identifier::new_const(), + #[cfg(feature = "metadata")] + doc: SmartString::new_const(), body: StmtBlock::NONE, #[cfg(not(feature = "no_function"))] lib: crate::Module::new().into(), @@ -180,6 +187,41 @@ impl AST { self.source.clear(); self } + /// Get the documentation (if any). + /// Exported under the `metadata` feature only. + /// + /// Documentation is a collection of all comment lines beginning with `//!`. + /// + /// Leading white-spaces are stripped, and each line always starts with `//!`. + #[cfg(feature = "metadata")] + #[inline(always)] + pub fn doc(&self) -> &str { + &self.doc + } + /// Clear the documentation. + /// Exported under the `metadata` feature only. + #[cfg(feature = "metadata")] + #[inline(always)] + pub fn clear_doc(&mut self) -> &mut Self { + self.doc.clear(); + self + } + /// Get a mutable reference to the documentation. + /// + /// Only available under `metadata`. + #[cfg(feature = "metadata")] + #[inline(always)] + pub(crate) fn doc_mut(&mut self) -> &mut SmartString { + &mut self.doc + } + /// Set the documentation. + /// + /// Only available under `metadata`. + #[cfg(feature = "metadata")] + #[inline(always)] + pub(crate) fn set_doc(&mut self, doc: impl Into) { + self.doc = doc.into(); + } /// Get the statements. #[cfg(not(feature = "internals"))] #[inline(always)] @@ -292,6 +334,8 @@ impl AST { lib.merge_filtered(&self.lib, &filter); Self { source: self.source.clone(), + #[cfg(feature = "metadata")] + doc: self.doc.clone(), body: StmtBlock::NONE, lib: lib.into(), #[cfg(not(feature = "no_module"))] @@ -305,6 +349,8 @@ impl AST { pub fn clone_statements_only(&self) -> Self { Self { source: self.source.clone(), + #[cfg(feature = "metadata")] + doc: self.doc.clone(), body: self.body.clone(), #[cfg(not(feature = "no_function"))] lib: crate::Module::new().into(), @@ -543,6 +589,14 @@ impl AST { } } + #[cfg(feature = "metadata")] + if !other.doc.is_empty() { + if !_ast.doc.is_empty() { + _ast.doc.push('\n'); + } + _ast.doc.push_str(other.doc()); + } + _ast } /// Combine one [`AST`] with another. The second [`AST`] is consumed. @@ -636,6 +690,14 @@ impl AST { crate::func::shared_make_mut(&mut self.lib).merge_filtered(&other.lib, &_filter); } + #[cfg(feature = "metadata")] + if !other.doc.is_empty() { + if !self.doc.is_empty() { + self.doc.push('\n'); + } + self.doc.push_str(&other.doc); + } + self } /// Filter out the functions, retaining only some based on a filter predicate. diff --git a/src/ast/flags.rs b/src/ast/flags.rs index 837a9d80..1bd080cb 100644 --- a/src/ast/flags.rs +++ b/src/ast/flags.rs @@ -14,6 +14,27 @@ pub enum FnAccess { Public, } +impl FnAccess { + /// Is this function private? + #[inline(always)] + #[must_use] + pub fn is_private(self) -> bool { + match self { + Self::Private => true, + Self::Public => false, + } + } + /// Is this function public? + #[inline(always)] + #[must_use] + pub fn is_public(self) -> bool { + match self { + Self::Private => false, + Self::Public => true, + } + } +} + bitflags! { /// _(internals)_ Bit-flags containing [`AST`][crate::AST] node configuration options. /// Exported under the `internals` feature only. diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8d9cacf2..00719a31 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -22,8 +22,8 @@ pub use script_fn::EncapsulatedEnviron; #[cfg(not(feature = "no_function"))] pub use script_fn::{ScriptFnDef, ScriptFnMetadata}; pub use stmt::{ - CaseBlocksList, ConditionalStmtBlock, OpAssignment, RangeCase, Stmt, StmtBlock, - StmtBlockContainer, SwitchCasesCollection, TryCatchBlock, + CaseBlocksList, ConditionalExpr, OpAssignment, RangeCase, Stmt, StmtBlock, StmtBlockContainer, + SwitchCasesCollection, TryCatchBlock, }; #[cfg(not(feature = "no_float"))] diff --git a/src/ast/script_fn.rs b/src/ast/script_fn.rs index 38edeea1..e7c44a87 100644 --- a/src/ast/script_fn.rs +++ b/src/ast/script_fn.rs @@ -45,6 +45,16 @@ pub struct ScriptFnDef { pub params: StaticVec, /// _(metadata)_ Function doc-comments (if any). /// Exported under the `metadata` feature only. + /// + /// Doc-comments are comment lines beginning with `///` or comment blocks beginning with `/**`, + /// placed immediately before a function definition. + /// + /// Block doc-comments are kept in a single string slice with line-breaks within. + /// + /// Line doc-comments are kept in one string slice per line without the termination line-break. + /// + /// Leading white-spaces are stripped, and each string slice always starts with the + /// corresponding doc-comment leader: `///` or `/**`. #[cfg(feature = "metadata")] pub comments: Box<[Box]>, } @@ -85,13 +95,15 @@ pub struct ScriptFnMetadata<'a> { /// _(metadata)_ Function doc-comments (if any). /// Exported under the `metadata` feature only. /// + /// Doc-comments are comment lines beginning with `///` or comment blocks beginning with `/**`, + /// placed immediately before a function definition. + /// /// Block doc-comments are kept in a single string slice with line-breaks within. /// /// Line doc-comments are kept in one string slice per line without the termination line-break. /// /// Leading white-spaces are stripped, and each string slice always starts with the /// corresponding doc-comment leader: `///` or `/**`. - /// Function access mode. #[cfg(feature = "metadata")] pub comments: Vec<&'a str>, } diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index 842c476c..c2cad9da 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -122,33 +122,54 @@ impl fmt::Debug for OpAssignment { } } -/// A statements block with a condition. +/// An expression with a condition. /// /// The condition may simply be [`Expr::BoolConstant`] with `true` if there is actually no condition. #[derive(Debug, Clone, Default, Hash)] -pub struct ConditionalStmtBlock { +pub struct ConditionalExpr { /// Condition. pub condition: Expr, - /// Statements block. - pub statements: StmtBlock, + /// Expression. + pub expr: Expr, } -impl> From for ConditionalStmtBlock { +impl> From for ConditionalExpr { #[inline(always)] - fn from(value: B) -> Self { + fn from(value: E) -> Self { Self { condition: Expr::BoolConstant(true, Position::NONE), - statements: value.into(), + expr: value.into(), } } } -impl> From<(Expr, B)> for ConditionalStmtBlock { +impl> From<(Expr, E)> for ConditionalExpr { #[inline(always)] - fn from(value: (Expr, B)) -> Self { + fn from(value: (Expr, E)) -> Self { Self { condition: value.0, - statements: value.1.into(), + expr: value.1.into(), + } + } +} + +impl ConditionalExpr { + /// Is the condition always `true`? + #[inline(always)] + #[must_use] + pub fn is_always_true(&self) -> bool { + match self.condition { + Expr::BoolConstant(true, ..) => true, + _ => false, + } + } + /// Is the condition always `false`? + #[inline(always)] + #[must_use] + pub fn is_always_false(&self) -> bool { + match self.condition { + Expr::BoolConstant(false, ..) => true, + _ => false, } } } @@ -262,14 +283,14 @@ pub type CaseBlocksList = smallvec::SmallVec<[usize; 1]>; /// Exported under the `internals` feature only. #[derive(Debug, Clone, Hash)] pub struct SwitchCasesCollection { - /// List of [`ConditionalStmtBlock`]'s. - pub case_blocks: StaticVec, - /// Dictionary mapping value hashes to [`ConditionalStmtBlock`]'s. + /// List of [`ConditionalExpr`]'s. + pub expressions: StaticVec, + /// Dictionary mapping value hashes to [`ConditionalExpr`]'s. pub cases: BTreeMap, - /// Statements block for the default case (there can be no condition for the default case). - pub def_case: usize, /// List of range cases. pub ranges: StaticVec, + /// Statements block for the default case (there can be no condition for the default case). + pub def_case: Option, } /// _(internals)_ A `try-catch` block. @@ -757,17 +778,15 @@ impl Stmt { let (expr, sw) = &**x; expr.is_pure() && sw.cases.values().flat_map(|cases| cases.iter()).all(|&c| { - let block = &sw.case_blocks[c]; - block.condition.is_pure() && block.statements.iter().all(Stmt::is_pure) + let block = &sw.expressions[c]; + block.condition.is_pure() && block.expr.is_pure() }) && sw.ranges.iter().all(|r| { - let block = &sw.case_blocks[r.index()]; - block.condition.is_pure() && block.statements.iter().all(Stmt::is_pure) + let block = &sw.expressions[r.index()]; + block.condition.is_pure() && block.expr.is_pure() }) - && sw.case_blocks[sw.def_case] - .statements - .iter() - .all(Stmt::is_pure) + && sw.def_case.is_some() + && sw.expressions[sw.def_case.unwrap()].expr.is_pure() } // Loops that exit can be pure because it can never be infinite. @@ -910,32 +929,28 @@ impl Stmt { } for (.., blocks) in &sw.cases { for &b in blocks { - let block = &sw.case_blocks[b]; + let block = &sw.expressions[b]; if !block.condition.walk(path, on_node) { return false; } - for s in &block.statements { - if !s.walk(path, on_node) { - return false; - } - } - } - } - for r in &sw.ranges { - let block = &sw.case_blocks[r.index()]; - - if !block.condition.walk(path, on_node) { - return false; - } - for s in &block.statements { - if !s.walk(path, on_node) { + if !block.expr.walk(path, on_node) { return false; } } } - for s in &sw.case_blocks[sw.def_case].statements { - if !s.walk(path, on_node) { + for r in &sw.ranges { + let block = &sw.expressions[r.index()]; + + if !block.condition.walk(path, on_node) { + return false; + } + if !block.expr.walk(path, on_node) { + return false; + } + } + if let Some(index) = sw.def_case { + if !sw.expressions[index].expr.walk(path, on_node) { return false; } } diff --git a/src/eval/global_state.rs b/src/eval/global_state.rs index 1ed3b3af..35b15416 100644 --- a/src/eval/global_state.rs +++ b/src/eval/global_state.rs @@ -156,13 +156,11 @@ impl GlobalRuntimeState<'_> { pub fn find_import(&self, name: &str) -> Option { let len = self.keys.len(); - self.keys.iter().rev().enumerate().find_map(|(i, key)| { - if key == name { - Some(len - 1 - i) - } else { - None - } - }) + self.keys + .iter() + .rev() + .position(|key| key == name) + .map(|i| len - 1 - i) } /// Push an imported [module][crate::Module] onto the stack. /// diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index 3c7eaf41..86a06e04 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -394,7 +394,7 @@ impl Engine { let ( expr, SwitchCasesCollection { - case_blocks, + expressions, cases, def_case, ranges, @@ -405,7 +405,7 @@ impl Engine { self.eval_expr(scope, global, caches, lib, this_ptr, expr, level); if let Ok(value) = value_result { - let stmt_block_result = if value.is_hashable() { + let expr_result = if value.is_hashable() { let hasher = &mut get_hasher(); value.hash(hasher); let hash = hasher.finish(); @@ -417,7 +417,7 @@ impl Engine { let mut result = Ok(None); for &index in case_blocks_list { - let block = &case_blocks[index]; + let block = &expressions[index]; let cond_result = match block.condition { Expr::BoolConstant(b, ..) => Ok(b), @@ -435,7 +435,7 @@ impl Engine { match cond_result { Ok(true) => { - result = Ok(Some(&block.statements)); + result = Ok(Some(&block.expr)); break; } Ok(false) => (), @@ -453,7 +453,7 @@ impl Engine { let mut result = Ok(None); for r in ranges.iter().filter(|r| r.contains(value)) { - let block = &case_blocks[r.index()]; + let block = &expressions[r.index()]; let cond_result = match block.condition { Expr::BoolConstant(b, ..) => Ok(b), @@ -470,7 +470,7 @@ impl Engine { }; match cond_result { - Ok(true) => result = Ok(Some(&block.statements)), + Ok(true) => result = Ok(Some(&block.expr)), Ok(false) => continue, _ => result = cond_result.map(|_| None), } @@ -488,27 +488,18 @@ impl Engine { Ok(None) }; - if let Ok(Some(statements)) = stmt_block_result { - if !statements.is_empty() { - self.eval_stmt_block( - scope, global, caches, lib, this_ptr, statements, true, level, - ) - } else { - Ok(Dynamic::UNIT) - } - } else if let Ok(None) = stmt_block_result { + if let Ok(Some(expr)) = expr_result { + self.eval_expr(scope, global, caches, lib, this_ptr, expr, level) + } else if let Ok(None) = expr_result { // Default match clause - let def_case = &case_blocks[*def_case].statements; - - if !def_case.is_empty() { - self.eval_stmt_block( - scope, global, caches, lib, this_ptr, def_case, true, level, - ) + if let Some(index) = def_case { + let def_expr = &expressions[*index].expr; + self.eval_expr(scope, global, caches, lib, this_ptr, def_expr, level) } else { Ok(Dynamic::UNIT) } } else { - stmt_block_result.map(|_| Dynamic::UNIT) + expr_result.map(|_| Dynamic::UNIT) } } else { value_result diff --git a/src/func/builtin.rs b/src/func/builtin.rs index 5477893c..3e728764 100644 --- a/src/func/builtin.rs +++ b/src/func/builtin.rs @@ -293,10 +293,15 @@ pub fn get_builtin_binary_op_fn(op: &str, x: &Dynamic, y: &Dynamic) -> Option None, }; } - if type1 == type2 { + if type2 == TypeId::of::() { return match op { - "==" => Some(impl_op!(Blob == Blob)), - "!=" => Some(impl_op!(Blob != Blob)), + "+" => Some(|_, args| { + let mut buf = [0_u8; 4]; + let mut blob = args[0].read_lock::().expect(BUILTIN).clone(); + let x = args[1].as_char().expect("`char`").encode_utf8(&mut buf); + blob.extend(x.as_bytes()); + Ok(Dynamic::from_blob(blob)) + }), _ => None, }; } @@ -503,6 +508,33 @@ pub fn get_builtin_binary_op_fn(op: &str, x: &Dynamic, y: &Dynamic) -> Option() { + use crate::Blob; + + return match op { + "+" => Some(|_, args| { + let blob1 = &*args[0].read_lock::().expect(BUILTIN); + let blob2 = &*args[1].read_lock::().expect(BUILTIN); + + Ok(Dynamic::from_blob(if !blob2.is_empty() { + if blob1.is_empty() { + blob2.clone() + } else { + let mut blob = blob1.clone(); + blob.extend(blob2); + blob + } + } else { + blob1.clone() + })) + }), + "==" => Some(impl_op!(Blob == Blob)), + "!=" => Some(impl_op!(Blob != Blob)), + _ => None, + }; + } + if type1 == TypeId::of::<()>() { return match op { "==" => Some(|_, _| Ok(Dynamic::TRUE)), @@ -684,71 +716,39 @@ pub fn get_builtin_op_assignment_fn(op: &str, x: &Dynamic, y: &Dynamic) -> Optio #[cfg(not(feature = "no_index"))] { - // 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; + use crate::Blob; + // blob op= int + if types_pair == (TypeId::of::(), TypeId::of::()) { 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()) + let x = args[1].as_int().expect("`INT`"); + let blob = &mut *args[0].write_lock::().expect(BUILTIN); + Ok(crate::packages::blob_basic::blob_functions::push(blob, x).into()) }), _ => None, }; } // blob op= char - if types_pair == (TypeId::of::(), TypeId::of::()) { - use crate::Blob; - + if types_pair == (TypeId::of::(), TypeId::of::()) { 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()) + let x = args[1].as_char().expect("`char`"); + let blob = &mut *args[0].write_lock::().expect(BUILTIN); + Ok(crate::packages::blob_basic::blob_functions::append_char(blob, x).into()) }), _ => None, }; } // blob op= string - if types_pair == (TypeId::of::(), TypeId::of::()) { - use crate::Blob; - + if types_pair == (TypeId::of::(), TypeId::of::()) { 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()) + let s = std::mem::take(args[1]).cast::(); + let blob = &mut *args[0].write_lock::().expect(BUILTIN); + Ok(crate::packages::blob_basic::blob_functions::append_str(blob, &s).into()) }), _ => None, }; @@ -838,14 +838,13 @@ pub fn get_builtin_op_assignment_fn(op: &str, x: &Dynamic, y: &Dynamic) -> Optio #[cfg(not(feature = "no_index"))] if type1 == TypeId::of::() { - use crate::packages::blob_basic::blob_functions::*; use crate::Blob; return match op { "+=" => Some(|_, args| { let blob2 = std::mem::take(args[1]).cast::(); let blob1 = &mut *args[0].write_lock::().expect(BUILTIN); - Ok(append(blob1, blob2).into()) + Ok(crate::packages::blob_basic::blob_functions::append(blob1, blob2).into()) }), _ => None, }; diff --git a/src/lib.rs b/src/lib.rs index 915b9966..e476fb15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -284,7 +284,7 @@ pub use parser::ParseState; #[cfg(feature = "internals")] pub use ast::{ - ASTFlags, ASTNode, BinaryExpr, ConditionalStmtBlock, Expr, FnCallExpr, FnCallHashes, Ident, + ASTFlags, ASTNode, BinaryExpr, ConditionalExpr, Expr, FnCallExpr, FnCallHashes, Ident, OpAssignment, RangeCase, ScriptFnDef, Stmt, StmtBlock, SwitchCasesCollection, TryCatchBlock, }; diff --git a/src/module/mod.rs b/src/module/mod.rs index 18d996c1..38bb887a 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -32,6 +32,27 @@ pub enum FnNamespace { Global, } +impl FnNamespace { + /// Is this a module namespace? + #[inline(always)] + #[must_use] + pub fn is_module_namespace(self) -> bool { + match self { + Self::Internal => true, + Self::Global => false, + } + } + /// Is this a global namespace? + #[inline(always)] + #[must_use] + pub fn is_global_namespace(self) -> bool { + match self { + Self::Internal => false, + Self::Global => true, + } + } +} + /// A type containing all metadata for a registered function. #[derive(Debug, Clone, Eq, PartialEq, Hash)] #[non_exhaustive] diff --git a/src/optimizer.rs b/src/optimizer.rs index 2fb671ec..f0f7461c 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -213,9 +213,9 @@ fn optimize_stmt_block( // Flatten blocks loop { - if let Some(n) = statements.iter().enumerate().find_map(|(i, s)| match s { - Stmt::Block(block, ..) if !block.iter().any(Stmt::is_block_dependent) => Some(i), - _ => None, + if let Some(n) = statements.iter().position(|s| match s { + Stmt::Block(block, ..) if !block.iter().any(Stmt::is_block_dependent) => true, + _ => false, }) { let (first, second) = statements.split_at_mut(n); let stmt = mem::take(&mut second[0]); @@ -527,7 +527,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b let ( match_expr, SwitchCasesCollection { - case_blocks, + expressions, cases, ranges, def_case, @@ -544,36 +544,36 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b match &case_blocks_list[..] { [] => (), [index] => { - let mut b = mem::take(&mut case_blocks[*index]); + let mut b = mem::take(&mut expressions[*index]); cases.clear(); - match b.condition { - Expr::BoolConstant(true, ..) => { - // Promote the matched case - let statements: StmtBlockContainer = mem::take(&mut b.statements); - let statements = - optimize_stmt_block(statements, state, true, true, false); - *stmt = (statements, b.statements.span()).into(); - } - ref mut condition => { - // switch const { case if condition => stmt, _ => def } => if condition { stmt } else { def } - optimize_expr(condition, state, false); + if b.is_always_true() { + // Promote the matched case + let mut statements = Stmt::Expr(mem::take(&mut b.expr).into()); + optimize_stmt(&mut statements, state, true); + *stmt = statements; + } else { + // switch const { case if condition => stmt, _ => def } => if condition { stmt } else { def } + optimize_expr(&mut b.condition, state, false); - let def_case = &mut case_blocks[*def_case].statements; - let def_span = def_case.span_or_else(*pos, Position::NONE); - let def_case: StmtBlockContainer = mem::take(def_case); - let def_stmt = - optimize_stmt_block(def_case, state, true, true, false); - *stmt = Stmt::If( - ( - mem::take(condition), - mem::take(&mut b.statements), - StmtBlock::new_with_span(def_stmt, def_span), - ) - .into(), - match_expr.start_position(), - ); - } + let else_stmt = if let Some(index) = def_case { + let mut def_stmt = + Stmt::Expr(mem::take(&mut expressions[*index].expr).into()); + optimize_stmt(&mut def_stmt, state, true); + def_stmt.into() + } else { + StmtBlock::NONE + }; + + *stmt = Stmt::If( + ( + mem::take(&mut b.condition), + Stmt::Expr(mem::take(&mut b.expr).into()).into(), + else_stmt, + ) + .into(), + match_expr.start_position(), + ); } state.set_dirty(); @@ -581,20 +581,15 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } _ => { for &index in case_blocks_list { - let mut b = mem::take(&mut case_blocks[index]); + let mut b = mem::take(&mut expressions[index]); - match b.condition { - Expr::BoolConstant(true, ..) => { - // Promote the matched case - let statements: StmtBlockContainer = - mem::take(&mut b.statements); - let statements = - optimize_stmt_block(statements, state, true, true, false); - *stmt = (statements, b.statements.span()).into(); - state.set_dirty(); - return; - } - _ => (), + if b.is_always_true() { + // Promote the matched case + let mut statements = Stmt::Expr(mem::take(&mut b.expr).into()); + optimize_stmt(&mut statements, state, true); + *stmt = statements; + state.set_dirty(); + return; } } } @@ -607,47 +602,42 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // Only one range or all ranges without conditions if ranges.len() == 1 - || ranges.iter().all(|r| { - matches!( - case_blocks[r.index()].condition, - Expr::BoolConstant(true, ..) - ) - }) + || ranges + .iter() + .all(|r| expressions[r.index()].is_always_true()) { for r in ranges.iter().filter(|r| r.contains(value)) { - let condition = mem::take(&mut case_blocks[r.index()].condition); + let range_block = &mut expressions[r.index()]; - match condition { - Expr::BoolConstant(true, ..) => { - // Promote the matched case - let block = &mut case_blocks[r.index()]; - let statements = mem::take(&mut *block.statements); - let statements = - optimize_stmt_block(statements, state, true, true, false); - *stmt = (statements, block.statements.span()).into(); - } - mut condition => { - // switch const { range if condition => stmt, _ => def } => if condition { stmt } else { def } - optimize_expr(&mut condition, state, false); + if range_block.is_always_true() { + // Promote the matched case + let block = &mut expressions[r.index()]; + let mut statements = Stmt::Expr(mem::take(&mut block.expr).into()); + optimize_stmt(&mut statements, state, true); + *stmt = statements; + } else { + let mut condition = mem::take(&mut range_block.condition); - let def_case = &mut case_blocks[*def_case].statements; - let def_span = def_case.span_or_else(*pos, Position::NONE); - let def_case: StmtBlockContainer = mem::take(def_case); - let def_stmt = - optimize_stmt_block(def_case, state, true, true, false); + // switch const { range if condition => stmt, _ => def } => if condition { stmt } else { def } + optimize_expr(&mut condition, state, false); - let statements = mem::take(&mut case_blocks[r.index()].statements); + let else_stmt = if let Some(index) = def_case { + let mut def_stmt = + Stmt::Expr(mem::take(&mut expressions[*index].expr).into()); + optimize_stmt(&mut def_stmt, state, true); + def_stmt.into() + } else { + StmtBlock::NONE + }; - *stmt = Stmt::If( - ( - condition, - statements, - StmtBlock::new_with_span(def_stmt, def_span), - ) - .into(), - match_expr.start_position(), - ); - } + let if_stmt = + Stmt::Expr(mem::take(&mut expressions[r.index()].expr).into()) + .into(); + + *stmt = Stmt::If( + (condition, if_stmt, else_stmt).into(), + match_expr.start_position(), + ); } state.set_dirty(); @@ -669,20 +659,9 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } for r in &*ranges { - let b = &mut case_blocks[r.index()]; - let statements = mem::take(&mut *b.statements); - *b.statements = - optimize_stmt_block(statements, state, preserve_result, true, false); - + let b = &mut expressions[r.index()]; optimize_expr(&mut b.condition, state, false); - - match b.condition { - Expr::Unit(pos) => { - b.condition = Expr::BoolConstant(true, pos); - state.set_dirty() - } - _ => (), - } + optimize_expr(&mut b.expr, state, false); } return; } @@ -690,18 +669,21 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // Promote the default case state.set_dirty(); - let def_case = &mut case_blocks[*def_case].statements; - let def_span = def_case.span_or_else(*pos, Position::NONE); - let def_case: StmtBlockContainer = mem::take(def_case); - let def_stmt = optimize_stmt_block(def_case, state, true, true, false); - *stmt = (def_stmt, def_span).into(); + + if let Some(index) = def_case { + let mut def_stmt = Stmt::Expr(mem::take(&mut expressions[*index].expr).into()); + optimize_stmt(&mut def_stmt, state, true); + *stmt = def_stmt; + } else { + *stmt = StmtBlock::empty(*pos).into(); + } } // switch Stmt::Switch(x, ..) => { let ( match_expr, SwitchCasesCollection { - case_blocks, + expressions, cases, ranges, def_case, @@ -712,78 +694,76 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b optimize_expr(match_expr, state, false); // Optimize blocks - for b in case_blocks.iter_mut() { - let statements = mem::take(&mut *b.statements); - *b.statements = - optimize_stmt_block(statements, state, preserve_result, true, false); - + for b in expressions.iter_mut() { optimize_expr(&mut b.condition, state, false); + optimize_expr(&mut b.expr, state, false); - match b.condition { - Expr::Unit(pos) => { - b.condition = Expr::BoolConstant(true, pos); + if b.is_always_false() { + if !b.expr.is_unit() { + b.expr = Expr::Unit(b.expr.position()); state.set_dirty(); } - Expr::BoolConstant(false, ..) => { - if !b.statements.is_empty() { - b.statements = StmtBlock::NONE; - state.set_dirty(); - } - } - _ => (), } } // Remove false cases - let cases_len = cases.len(); cases.retain(|_, list| { // Remove all entries that have false conditions - list.retain(|index| match case_blocks[*index].condition { - Expr::BoolConstant(false, ..) => false, - _ => true, + list.retain(|index| { + if expressions[*index].is_always_false() { + state.set_dirty(); + false + } else { + true + } }); // Remove all entries after a `true` condition if let Some(n) = list .iter() - .position(|&index| match case_blocks[index].condition { - Expr::BoolConstant(true, ..) => true, - _ => false, - }) + .position(|&index| expressions[index].is_always_true()) { - list.truncate(n + 1); + if n + 1 < list.len() { + state.set_dirty(); + list.truncate(n + 1); + } } // Remove if no entry left - !list.is_empty() + match list.is_empty() { + true => { + state.set_dirty(); + false + } + false => true, + } }); - if cases.len() != cases_len { - state.set_dirty(); - } + // Remove false ranges - ranges.retain(|r| match case_blocks[r.index()].condition { - Expr::BoolConstant(false, ..) => { + ranges.retain(|r| { + if expressions[r.index()].is_always_false() { state.set_dirty(); false + } else { + true } - _ => true, }); - let def_stmt_block = &mut case_blocks[*def_case].statements; - let def_block = mem::take(&mut **def_stmt_block); - **def_stmt_block = optimize_stmt_block(def_block, state, preserve_result, true, false); + if let Some(index) = def_case { + optimize_expr(&mut expressions[*index].expr, state, false); + } // Remove unused block statements - for index in 0..case_blocks.len() { - if *def_case == index + for index in 0..expressions.len() { + if *def_case == Some(index) || cases.values().flat_map(|c| c.iter()).any(|&n| n == index) || ranges.iter().any(|r| r.index() == index) { continue; } - let b = &mut case_blocks[index]; + let b = &mut expressions[index]; - if !b.statements.is_empty() { - b.statements = StmtBlock::NONE; + if !b.expr.is_unit() { + b.expr = Expr::Unit(b.expr.position()); state.set_dirty(); } } @@ -904,6 +884,21 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b optimize_stmt_block(mem::take(&mut *x.catch_block), state, false, true, false); } + // expr(stmt) + Stmt::Expr(expr) if matches!(**expr, Expr::Stmt(..)) => { + state.set_dirty(); + match expr.as_mut() { + Expr::Stmt(block) if !block.is_empty() => { + let mut stmt_block = *mem::take(block); + *stmt_block = + optimize_stmt_block(mem::take(&mut *stmt_block), state, true, true, false); + *stmt = stmt_block.into(); + } + Expr::Stmt(..) => *stmt = Stmt::Noop(expr.position()), + _ => unreachable!("`Expr::Stmt`"), + } + } + Stmt::Expr(expr) => { optimize_expr(expr, state, false); @@ -948,6 +943,16 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) { match expr { // {} Expr::Stmt(x) if x.is_empty() => { state.set_dirty(); *expr = Expr::Unit(x.position()) } + Expr::Stmt(x) if x.len() == 1 && matches!(x.statements()[0], Stmt::Expr(..)) => { + state.set_dirty(); + match x.take_statements().remove(0) { + Stmt::Expr(mut e) => { + optimize_expr(&mut e, state, false); + *expr = *e; + } + _ => unreachable!("`Expr::Stmt`") + } + } // { stmt; ... } - do not count promotion as dirty because it gets turned back into an array Expr::Stmt(x) => { ***x = optimize_stmt_block(mem::take(&mut **x), state, true, true, false); diff --git a/src/packages/blob_basic.rs b/src/packages/blob_basic.rs index 6cd8ebff..ad26ccac 100644 --- a/src/packages/blob_basic.rs +++ b/src/packages/blob_basic.rs @@ -8,7 +8,7 @@ use crate::{ }; #[cfg(feature = "no_std")] use std::prelude::v1::*; -use std::{any::TypeId, mem}; +use std::{any::TypeId, borrow::Cow, mem}; #[cfg(not(feature = "no_float"))] use crate::{FLOAT, FLOAT_BYTES}; @@ -104,6 +104,27 @@ pub mod blob_functions { pub fn to_array(blob: &mut Blob) -> Array { blob.iter().map(|&ch| (ch as INT).into()).collect() } + /// Convert the BLOB into a string. + /// + /// The byte stream must be valid UTF-8, otherwise an error is raised. + /// + /// # Example + /// + /// ```rhai + /// let b = blob(5, 0x42); + /// + /// let x = b.as_string(); + /// + /// print(x); // prints "FFFFF" + /// ``` + pub fn as_string(blob: Blob) -> String { + let s = String::from_utf8_lossy(&blob); + + match s { + Cow::Borrowed(_) => String::from_utf8(blob).unwrap(), + Cow::Owned(_) => s.into_owned(), + } + } /// Return the length of the BLOB. /// /// # Example @@ -200,6 +221,7 @@ pub mod blob_functions { /// /// print(b); // prints "[42]" /// ``` + #[rhai_fn(name = "push", name = "append")] pub fn push(blob: &mut Blob, value: INT) { blob.push((value & 0x000000ff) as u8); } @@ -235,13 +257,13 @@ pub mod blob_functions { /// /// print(b); // prints "[424242424268656c 6c6f]" /// ``` - #[rhai_fn(name = "+=", name = "append")] + #[rhai_fn(name = "append")] pub fn append_str(blob: &mut Blob, string: &str) { if !string.is_empty() { blob.extend(string.as_bytes()); } } - /// Add a string (as UTF-8 encoded byte-stream) to the end of the BLOB + /// Add a character (as UTF-8 encoded byte-stream) to the end of the BLOB /// /// # Example /// @@ -252,38 +274,12 @@ pub mod blob_functions { /// /// print(b); // prints "[424242424221]" /// ``` - #[rhai_fn(name = "+=", name = "append")] + #[rhai_fn(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 - /// - /// ```rhai - /// let b1 = blob(5, 0x42); - /// let b2 = blob(3, 0x11); - /// - /// print(b1 + b2); // prints "[4242424242111111]" - /// - /// print(b1); // prints "[4242424242]" - /// ``` - #[rhai_fn(name = "+")] - pub fn concat(blob1: Blob, blob2: Blob) -> Blob { - if !blob2.is_empty() { - if blob1.is_empty() { - blob2 - } else { - let mut blob = blob1; - blob.extend(blob2); - blob - } - } else { - blob1 - } - } /// Add a byte `value` to the BLOB at a particular `index` position. /// /// * If `index` < 0, position counts from the end of the BLOB (`-1` is the last byte). diff --git a/src/packages/iter_basic.rs b/src/packages/iter_basic.rs index 367bf5ff..4f6d4317 100644 --- a/src/packages/iter_basic.rs +++ b/src/packages/iter_basic.rs @@ -105,7 +105,7 @@ impl Iterator for StepRange { self.dir = 0; } } else { - unreachable!(); + unreachable!("`dir` != 0"); } Some(v) diff --git a/src/packages/mod.rs b/src/packages/mod.rs index a4d4d612..853ec95a 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -4,20 +4,20 @@ use crate::{Module, Shared}; pub(crate) mod arithmetic; pub(crate) mod array_basic; -mod bit_field; +pub(crate) mod bit_field; pub(crate) mod blob_basic; -mod debugging; -mod fn_basic; +pub(crate) mod debugging; +pub(crate) mod fn_basic; pub(crate) mod iter_basic; -mod lang_core; -mod logic; -mod map_basic; -mod math_basic; -mod pkg_core; -mod pkg_std; -mod string_basic; -mod string_more; -mod time_basic; +pub(crate) mod lang_core; +pub(crate) mod logic; +pub(crate) mod map_basic; +pub(crate) mod math_basic; +pub(crate) mod pkg_core; +pub(crate) mod pkg_std; +pub(crate) mod string_basic; +pub(crate) mod string_more; +pub(crate) mod time_basic; pub use arithmetic::ArithmeticPackage; #[cfg(not(feature = "no_index"))] diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 6a0c2cad..dd28d34c 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -6,9 +6,6 @@ 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] pub MoreStringPackage(lib) { @@ -86,26 +83,50 @@ mod string_functions { #[cfg(not(feature = "no_index"))] pub mod blob_functions { + use crate::Blob; + #[rhai_fn(name = "+", pure)] - pub fn add_append_blob(string: &mut ImmutableString, utf8: Blob) -> ImmutableString { + pub fn add_append(string: &mut ImmutableString, utf8: Blob) -> ImmutableString { if utf8.is_empty() { - string.clone() - } else if string.is_empty() { - String::from_utf8_lossy(&utf8).into_owned().into() + return string.clone(); + } + + let s = String::from_utf8_lossy(&utf8); + + if string.is_empty() { + match s { + std::borrow::Cow::Borrowed(_) => String::from_utf8(utf8).unwrap(), + std::borrow::Cow::Owned(_) => s.into_owned(), + } + .into() } else { - let mut s = crate::SmartString::from(string.as_str()); - s.push_str(&String::from_utf8_lossy(&utf8)); - s.into() + let mut x = SmartString::from(string.as_str()); + x.push_str(s.as_ref()); + x.into() } } - #[rhai_fn(name = "append")] - pub fn add_blob(string: &mut ImmutableString, utf8: Blob) { + #[rhai_fn(name = "+=", name = "append")] + pub fn add(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(); } } + #[rhai_fn(name = "+")] + pub fn add_prepend(utf8: Blob, string: ImmutableString) -> ImmutableString { + let s = String::from_utf8_lossy(&utf8); + let mut s = match s { + std::borrow::Cow::Borrowed(_) => String::from_utf8(utf8).unwrap(), + std::borrow::Cow::Owned(_) => s.into_owned(), + }; + + if !string.is_empty() { + s.push_str(&string); + } + + s.into() + } } /// Return the length of the string, in number of characters. diff --git a/src/parser.rs b/src/parser.rs index 0f36dd2b..af64546f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,9 +3,9 @@ use crate::api::events::VarDefInfo; use crate::api::options::LangOptions; use crate::ast::{ - ASTFlags, BinaryExpr, CaseBlocksList, ConditionalStmtBlock, Expr, FnCallExpr, FnCallHashes, - Ident, OpAssignment, RangeCase, ScriptFnDef, Stmt, StmtBlockContainer, SwitchCasesCollection, - TryCatchBlock, + ASTFlags, BinaryExpr, CaseBlocksList, ConditionalExpr, Expr, FnCallExpr, FnCallHashes, Ident, + OpAssignment, RangeCase, ScriptFnDef, Stmt, StmtBlock, StmtBlockContainer, + SwitchCasesCollection, TryCatchBlock, }; use crate::engine::{Precedence, KEYWORD_THIS, OP_CONTAINS}; use crate::eval::GlobalRuntimeState; @@ -798,7 +798,7 @@ impl Engine { match token { Token::LeftBracket => ASTFlags::NONE, Token::QuestionBracket => ASTFlags::NEGATED, - _ => unreachable!(), + _ => unreachable!("`[` or `?[`"), }, false, settings.level_up(), @@ -1055,11 +1055,11 @@ impl Engine { } } - let mut case_blocks = StaticVec::::new(); + let mut expressions = StaticVec::::new(); let mut cases = BTreeMap::::new(); let mut ranges = StaticVec::::new(); - let mut def_stmt_pos = Position::NONE; - let mut def_stmt_index = None; + let mut def_case = None; + let mut def_case_pos = Position::NONE; loop { const MISSING_RBRACE: &str = "to end this switch block"; @@ -1075,8 +1075,8 @@ impl Engine { .into_err(*pos), ) } - (Token::Underscore, pos) if def_stmt_index.is_none() => { - def_stmt_pos = *pos; + (Token::Underscore, pos) if def_case.is_none() => { + def_case_pos = *pos; eat_token(input, Token::Underscore); let (if_clause, if_pos) = match_token(input, Token::If); @@ -1087,8 +1087,8 @@ impl Engine { (Default::default(), Expr::BoolConstant(true, Position::NONE)) } - _ if def_stmt_index.is_some() => { - return Err(PERR::WrongSwitchDefaultCase.into_err(def_stmt_pos)) + _ if def_case.is_some() => { + return Err(PERR::WrongSwitchDefaultCase.into_err(def_case_pos)) } _ => { @@ -1142,8 +1142,9 @@ impl Engine { let need_comma = !stmt.is_self_terminated(); let has_condition = !matches!(condition, Expr::BoolConstant(true, ..)); - case_blocks.push((condition, stmt).into()); - let index = case_blocks.len() - 1; + let stmt_block: StmtBlock = stmt.into(); + expressions.push((condition, Expr::Stmt(stmt_block.into())).into()); + let index = expressions.len() - 1; if !case_expr_list.is_empty() { for expr in case_expr_list { @@ -1197,7 +1198,7 @@ impl Engine { .or_insert_with(|| [index].into()); } } else { - def_stmt_index = Some(index); + def_case = Some(index); } match input.peek().expect(NEVER_ENDS) { @@ -1223,13 +1224,8 @@ impl Engine { } } - let def_case = def_stmt_index.unwrap_or_else(|| { - case_blocks.push(Default::default()); - case_blocks.len() - 1 - }); - let cases = SwitchCasesCollection { - case_blocks, + expressions, cases, def_case, ranges, @@ -1408,9 +1404,7 @@ impl Engine { } // Make sure to parse the following as text - let mut control = state.tokenizer_control.get(); - control.is_within_text = true; - state.tokenizer_control.set(control); + state.tokenizer_control.borrow_mut().is_within_text = true; match input.next().expect(NEVER_ENDS) { (Token::StringConstant(s), pos) => { @@ -1697,7 +1691,7 @@ impl Engine { let opt = match token { Token::LeftBracket => ASTFlags::NONE, Token::QuestionBracket => ASTFlags::NEGATED, - _ => unreachable!(), + _ => unreachable!("`[` or `?[`"), }; self.parse_index_chain(input, state, lib, expr, opt, true, settings.level_up())? } @@ -1721,7 +1715,7 @@ impl Engine { let op_flags = match op { Token::Period => ASTFlags::NONE, Token::Elvis => ASTFlags::NEGATED, - _ => unreachable!(), + _ => unreachable!("`.` or `?.`"), }; Self::make_dot_expr(state, expr, rhs, ASTFlags::NONE, op_flags, tail_pos)? } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index eda7720c..5b827c38 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -10,7 +10,7 @@ use crate::{Engine, Identifier, LexError, SmartString, StaticVec, INT, UNSIGNED_ use std::prelude::v1::*; use std::{ borrow::Cow, - cell::Cell, + cell::RefCell, char, fmt, iter::{FusedIterator, Peekable}, num::NonZeroUsize, @@ -20,11 +20,14 @@ use std::{ }; /// _(internals)_ A type containing commands to control the tokenizer. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct TokenizerControlBlock { /// Is the current tokenizer position within an interpolated text string? /// This flag allows switching the tokenizer back to _text_ parsing after an interpolation stream. pub is_within_text: bool, + /// Collection of global comments. + #[cfg(feature = "metadata")] + pub global_comments: Vec, } impl TokenizerControlBlock { @@ -34,12 +37,14 @@ impl TokenizerControlBlock { pub const fn new() -> Self { Self { is_within_text: false, + #[cfg(feature = "metadata")] + global_comments: Vec::new(), } } } /// _(internals)_ A shared object that allows control of the tokenizer from outside. -pub type TokenizerControl = Rc>; +pub type TokenizerControl = Rc>; type LERR = LexError; @@ -248,7 +253,7 @@ impl fmt::Debug for Position { } #[cfg(feature = "no_position")] - unreachable!(); + unreachable!("no position"); } } } @@ -1098,12 +1103,14 @@ impl From for String { /// _(internals)_ State of the tokenizer. /// Exported under the `internals` feature only. -#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct TokenizeState { /// Maximum length of a string. pub max_string_size: Option, /// Can the next token be a unary operator? pub next_token_cannot_be_unary: bool, + /// Shared object to allow controlling the tokenizer externally. + pub tokenizer_control: TokenizerControl, /// Is the tokenizer currently inside a block comment? pub comment_level: usize, /// Include comments? @@ -1866,6 +1873,11 @@ fn get_next_token_inner( _ => Some("///".into()), } } + #[cfg(feature = "metadata")] + Some('!') => { + eat_next(stream, pos); + Some("//!".into()) + } _ if state.include_comments => Some("//".into()), _ => None, }; @@ -1890,7 +1902,15 @@ fn get_next_token_inner( } if let Some(comment) = comment { - return Some((Token::Comment(comment), start_pos)); + match comment { + #[cfg(feature = "metadata")] + _ if comment.starts_with("//!") => state + .tokenizer_control + .borrow_mut() + .global_comments + .push(comment), + _ => return Some((Token::Comment(comment), start_pos)), + } } } ('/', '*') => { @@ -2300,8 +2320,6 @@ pub struct TokenIterator<'a> { pub state: TokenizeState, /// Current position. pub pos: Position, - /// Shared object to allow controlling the tokenizer externally. - pub tokenizer_control: TokenizerControl, /// Input character stream. pub stream: MultiInputsStream<'a>, /// A processor function that maps a token to another. @@ -2312,14 +2330,15 @@ impl<'a> Iterator for TokenIterator<'a> { type Item = (Token, Position); fn next(&mut self) -> Option { - let mut control = self.tokenizer_control.get(); + { + let control = &mut *self.state.tokenizer_control.borrow_mut(); - if control.is_within_text { - // Switch to text mode terminated by back-tick - self.state.is_within_text_terminated_by = Some('`'); - // Reset it - control.is_within_text = false; - self.tokenizer_control.set(control); + if control.is_within_text { + // Switch to text mode terminated by back-tick + self.state.is_within_text_terminated_by = Some('`'); + // Reset it + control.is_within_text = false; + } } let (token, pos) = match get_next_token(&mut self.stream, &mut self.state, &mut self.pos) { @@ -2450,7 +2469,7 @@ impl Engine { input: impl IntoIterator + 'a)>, token_mapper: Option<&'a OnParseTokenCallback>, ) -> (TokenIterator<'a>, TokenizerControl) { - let buffer: TokenizerControl = Cell::new(TokenizerControlBlock::new()).into(); + let buffer: TokenizerControl = RefCell::new(TokenizerControlBlock::new()).into(); let buffer2 = buffer.clone(); ( @@ -2462,12 +2481,12 @@ impl Engine { #[cfg(feature = "unchecked")] max_string_size: None, next_token_cannot_be_unary: false, + tokenizer_control: buffer, comment_level: 0, include_comments: false, is_within_text_terminated_by: None, }, pos: Position::new(1, 0), - tokenizer_control: buffer, stream: MultiInputsStream { buf: None, streams: input diff --git a/tests/optimizer.rs b/tests/optimizer.rs index 3a203ac2..da8ecd68 100644 --- a/tests/optimizer.rs +++ b/tests/optimizer.rs @@ -68,6 +68,7 @@ fn test_optimizer_run() -> Result<(), Box> { Ok(()) } +#[cfg(feature = "metadata")] #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_position"))] @@ -79,30 +80,39 @@ fn test_optimizer_parse() -> Result<(), Box> { let ast = engine.compile("{ const DECISION = false; if DECISION { 42 } else { 123 } }")?; - assert_eq!(format!("{:?}", ast), "AST { body: [Expr(123 @ 1:53)] }"); + assert_eq!( + format!("{:?}", ast), + r#"AST { source: "", doc: "", resolver: None, body: [Expr(123 @ 1:53)] }"# + ); let ast = engine.compile("const DECISION = false; if DECISION { 42 } else { 123 }")?; assert_eq!( format!("{:?}", ast), - r#"AST { body: [Var(("DECISION" @ 1:7, false @ 1:18, None), CONSTANT, 1:1), Expr(123 @ 1:51)] }"# + r#"AST { source: "", doc: "", resolver: None, body: [Var(("DECISION" @ 1:7, false @ 1:18, None), CONSTANT, 1:1), Expr(123 @ 1:51)] }"# ); let ast = engine.compile("if 1 == 2 { 42 }")?; - assert_eq!(format!("{:?}", ast), "AST { body: [] }"); + assert_eq!( + format!("{:?}", ast), + r#"AST { source: "", doc: "", resolver: None, body: [] }"# + ); engine.set_optimization_level(OptimizationLevel::Full); let ast = engine.compile("abs(-42)")?; - assert_eq!(format!("{:?}", ast), "AST { body: [Expr(42 @ 1:1)] }"); + assert_eq!( + format!("{:?}", ast), + r#"AST { source: "", doc: "", resolver: None, body: [Expr(42 @ 1:1)] }"# + ); let ast = engine.compile("NUMBER")?; assert_eq!( format!("{:?}", ast), - "AST { body: [Expr(Variable(NUMBER) @ 1:1)] }" + r#"AST { source: "", doc: "", resolver: None, body: [Expr(Variable(NUMBER) @ 1:1)] }"# ); let mut module = Module::new(); @@ -112,7 +122,10 @@ fn test_optimizer_parse() -> Result<(), Box> { let ast = engine.compile("NUMBER")?; - assert_eq!(format!("{:?}", ast), "AST { body: [Expr(42 @ 1:1)] }"); + assert_eq!( + format!("{:?}", ast), + r#"AST { source: "", doc: "", resolver: None, body: [Expr(42 @ 1:1)] }"# + ); Ok(()) }