From e637bfa51d9841f4a7642dbad04a73dd6c6a1e18 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 25 Sep 2020 09:39:18 +0800 Subject: [PATCH 01/25] Change pure to independent. --- doc/src/language/fn-namespaces.md | 4 ++-- doc/src/rust/modules/resolvers.md | 12 ++++++------ src/module.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/src/language/fn-namespaces.md b/doc/src/language/fn-namespaces.md index 27f005c3..0621472f 100644 --- a/doc/src/language/fn-namespaces.md +++ b/doc/src/language/fn-namespaces.md @@ -10,7 +10,7 @@ Each Function is a Separate Compilation Unit This means that individual functions can be separated, exported, re-grouped, imported, and generally mix-'n-match-ed with other completely unrelated scripts. -For example, the `AST::merge` method allows Global all functions in one [`AST`] into another, +For example, the `AST::merge` method allows merging all functions in one [`AST`] into another, forming a new, combined, group of functions. In general, there are two types of _namespaces_ where functions are looked up: @@ -136,7 +136,7 @@ the subsequent call using the _namespace-qualified_ function name fails to find function named '`message`' in the global namespace. Therefore, when writing functions for a [module] intended for the [`GlobalFileModuleResolver`][module resolver], -make sure that those functions are as _pure_ as possible and avoid cross-calling them from each other. +make sure that those functions are as independent as possible and avoid cross-calling them from each other. A [function pointer] is a valid technique to call another function in an environment-independent manner: diff --git a/doc/src/rust/modules/resolvers.md b/doc/src/rust/modules/resolvers.md index 92fa6074..451388af 100644 --- a/doc/src/rust/modules/resolvers.md +++ b/doc/src/rust/modules/resolvers.md @@ -16,12 +16,12 @@ which simply loads a script file based on the path (with `.rhai` extension attac Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace. -| Module Resolver | Description | Namespace | -| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------: | -| `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | Module (cannot access global namespace) | -| `GlobalFileModuleResolver` | A simpler but more efficient version of `FileModuleResolver`, intended for short utility modules. Not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
**Note:** All functions are assumed absolutely _pure_ and cannot cross-call each other.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | Global | -| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | Global | -| `ModuleResolversCollection` | A collection of module resolvers. Modules will be resolved from each resolver in sequential order.
This is useful when multiple types of modules are needed simultaneously. | Global | +| Module Resolver | Description | Namespace | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------: | +| `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | Module (cannot access global namespace) | +| `GlobalFileModuleResolver` | A simpler but more efficient version of `FileModuleResolver`, intended for short utility modules. Not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
**Note:** All functions are assumed independent and cannot cross-call each other.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | Global | +| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | Global | +| `ModuleResolversCollection` | A collection of module resolvers. Modules will be resolved from each resolver in sequential order.
This is useful when multiple types of modules are needed simultaneously. | Global | Set into `Engine` diff --git a/src/module.rs b/src/module.rs index 74c2b8da..532467d9 100644 --- a/src/module.rs +++ b/src/module.rs @@ -1400,7 +1400,7 @@ mod file { /// Module resolution service that loads module script files from the file system. /// - /// All functions in each module are treated as strictly _pure_ and cannot refer to + /// All functions in each module are treated as strictly independent and cannot refer to /// other functions within the same module. Functions are searched in the _global_ namespace. /// /// For simple utility libraries, this usually performs better than the full `FileModuleResolver`. From 93f53fa41766cc3da910ac80e501eee9ef76af67 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 25 Sep 2020 10:59:21 +0800 Subject: [PATCH 02/25] Refactor module.rs into files structure. --- src/lib.rs | 7 +- src/{module.rs => module/mod.rs} | 587 +--------------------------- src/module/resolvers/collection.rs | 80 ++++ src/module/resolvers/file.rs | 209 ++++++++++ src/module/resolvers/global_file.rs | 189 +++++++++ src/module/resolvers/mod.rs | 40 ++ src/module/resolvers/stat.rs | 97 +++++ 7 files changed, 629 insertions(+), 580 deletions(-) rename src/{module.rs => module/mod.rs} (70%) create mode 100644 src/module/resolvers/collection.rs create mode 100644 src/module/resolvers/file.rs create mode 100644 src/module/resolvers/global_file.rs create mode 100644 src/module/resolvers/mod.rs create mode 100644 src/module/resolvers/stat.rs diff --git a/src/lib.rs b/src/lib.rs index c0d00fab..e88cf7e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,8 +93,13 @@ pub use result::EvalAltResult; pub use scope::Scope; pub use syntax::{EvalContext, Expression}; pub use token::Position; + +#[cfg(feature = "internals")] pub use utils::calc_fn_hash; +#[cfg(not(feature = "internals"))] +pub(crate) use utils::calc_fn_hash; + pub use rhai_codegen::*; #[cfg(not(feature = "no_function"))] @@ -118,8 +123,6 @@ pub use parser::FLOAT; pub use module::ModuleResolver; /// Module containing all built-in _module resolvers_ available to Rhai. -/// -/// Not available under the `no_module` feature. #[cfg(not(feature = "no_module"))] pub mod module_resolvers { pub use crate::module::resolvers::*; diff --git a/src/module.rs b/src/module/mod.rs similarity index 70% rename from src/module.rs rename to src/module/mod.rs index 532467d9..ea773abc 100644 --- a/src/module.rs +++ b/src/module/mod.rs @@ -38,16 +38,6 @@ use crate::stdlib::{ vec::Vec, }; -#[cfg(not(feature = "no_std"))] -#[cfg(not(feature = "no_module"))] -#[cfg(not(feature = "sync"))] -use crate::stdlib::cell::RefCell; - -#[cfg(not(feature = "no_std"))] -#[cfg(not(feature = "no_module"))] -#[cfg(feature = "sync")] -use crate::stdlib::sync::RwLock; - /// Return type of module-level Rust function. pub type FuncReturn = Result>; @@ -494,8 +484,12 @@ impl Module { self.set_fn(name, Public, arg_types, Func::from_method(Box::new(f))) } - /// Set a raw function but with a signature that is a scripted function, but the implementation is in Rust. + /// Set a raw function but with a signature that is a scripted function (meaning that the types + /// are not determined), but the implementation is in Rust. #[cfg(not(feature = "no_function"))] + #[cfg(not(feature = "no_module"))] + #[cfg(not(feature = "no_std"))] + #[cfg(not(target_arch = "wasm32"))] pub(crate) fn set_raw_fn_as_scripted( &mut self, name: impl Into, @@ -1372,573 +1366,10 @@ impl ModuleRef { } } -/// Trait that encapsulates a module resolution service. -pub trait ModuleResolver: SendSync { - /// Resolve a module based on a path string. - fn resolve(&self, _: &Engine, path: &str, pos: Position) -> Result>; -} +/// Re-export module resolver trait. +#[cfg(not(feature = "no_module"))] +pub use resolvers::ModuleResolver; /// Re-export module resolvers. #[cfg(not(feature = "no_module"))] -pub mod resolvers { - pub use super::collection::ModuleResolversCollection; - #[cfg(not(feature = "no_std"))] - #[cfg(not(target_arch = "wasm32"))] - pub use super::file::{FileModuleResolver, GlobalFileModuleResolver}; - pub use super::stat::StaticModuleResolver; -} -#[cfg(feature = "no_module")] -pub mod resolvers {} - -/// Script file-based module resolver. -#[cfg(not(feature = "no_module"))] -#[cfg(not(feature = "no_std"))] -#[cfg(not(target_arch = "wasm32"))] -mod file { - use super::*; - use crate::stdlib::path::PathBuf; - - /// Module resolution service that loads module script files from the file system. - /// - /// All functions in each module are treated as strictly independent and cannot refer to - /// other functions within the same module. Functions are searched in the _global_ namespace. - /// - /// For simple utility libraries, this usually performs better than the full `FileModuleResolver`. - /// - /// Script files are cached so they are are not reloaded and recompiled in subsequent requests. - /// - /// The `new_with_path` and `new_with_path_and_extension` constructor functions - /// allow specification of a base directory with module path used as a relative path offset - /// to the base directory. The script file is then forced to be in a specified extension - /// (default `.rhai`). - /// - /// # Examples - /// - /// ``` - /// use rhai::Engine; - /// use rhai::module_resolvers::GlobalFileModuleResolver; - /// - /// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory - /// // with file extension '.x'. - /// let resolver = GlobalFileModuleResolver::new_with_path_and_extension("./scripts", "x"); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(resolver)); - /// ``` - #[derive(Debug)] - pub struct GlobalFileModuleResolver { - path: PathBuf, - extension: String, - - #[cfg(not(feature = "sync"))] - cache: RefCell>, - - #[cfg(feature = "sync")] - cache: RwLock>, - } - - impl Default for GlobalFileModuleResolver { - fn default() -> Self { - Self::new_with_path(PathBuf::default()) - } - } - - impl GlobalFileModuleResolver { - /// Create a new `GlobalFileModuleResolver` with a specific base path. - /// - /// # Examples - /// - /// ``` - /// use rhai::Engine; - /// use rhai::module_resolvers::GlobalFileModuleResolver; - /// - /// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory - /// // with file extension '.rhai' (the default). - /// let resolver = GlobalFileModuleResolver::new_with_path("./scripts"); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(resolver)); - /// ``` - pub fn new_with_path>(path: P) -> Self { - Self::new_with_path_and_extension(path, "rhai") - } - - /// Create a new `GlobalFileModuleResolver` with a specific base path and file extension. - /// - /// The default extension is `.rhai`. - /// - /// # Examples - /// - /// ``` - /// use rhai::Engine; - /// use rhai::module_resolvers::GlobalFileModuleResolver; - /// - /// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory - /// // with file extension '.x'. - /// let resolver = GlobalFileModuleResolver::new_with_path_and_extension("./scripts", "x"); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(resolver)); - /// ``` - pub fn new_with_path_and_extension, E: Into>( - path: P, - extension: E, - ) -> Self { - Self { - path: path.into(), - extension: extension.into(), - cache: Default::default(), - } - } - - /// Create a new `GlobalFileModuleResolver` with the current directory as base path. - /// - /// # Examples - /// - /// ``` - /// use rhai::Engine; - /// use rhai::module_resolvers::GlobalFileModuleResolver; - /// - /// // Create a new 'GlobalFileModuleResolver' loading scripts from the current directory - /// // with file extension '.rhai' (the default). - /// let resolver = GlobalFileModuleResolver::new(); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(resolver)); - /// ``` - pub fn new() -> Self { - Default::default() - } - - /// Create a `Module` from a file path. - pub fn create_module>( - &self, - engine: &Engine, - path: &str, - ) -> Result> { - self.resolve(engine, path, Default::default()) - } - } - - impl ModuleResolver for GlobalFileModuleResolver { - fn resolve( - &self, - engine: &Engine, - path: &str, - pos: Position, - ) -> Result> { - // Construct the script file path - let mut file_path = self.path.clone(); - file_path.push(path); - file_path.set_extension(&self.extension); // Force extension - - let scope = Default::default(); - - // See if it is cached - let (module, ast) = { - #[cfg(not(feature = "sync"))] - let c = self.cache.borrow(); - #[cfg(feature = "sync")] - let c = self.cache.read().unwrap(); - - if let Some(ast) = c.get(&file_path) { - ( - Module::eval_ast_as_new(scope, ast, engine) - .map_err(|err| err.new_position(pos))?, - None, - ) - } else { - // Load the file and compile it if not found - let ast = engine - .compile_file(file_path.clone()) - .map_err(|err| err.new_position(pos))?; - - ( - Module::eval_ast_as_new(scope, &ast, engine) - .map_err(|err| err.new_position(pos))?, - Some(ast), - ) - } - }; - - if let Some(ast) = ast { - // Put it into the cache - #[cfg(not(feature = "sync"))] - self.cache.borrow_mut().insert(file_path, ast); - #[cfg(feature = "sync")] - self.cache.write().unwrap().insert(file_path, ast); - } - - Ok(module) - } - } - - /// Module resolution service that loads module script files from the file system. - /// - /// Script files are cached so they are are not reloaded and recompiled in subsequent requests. - /// - /// The `new_with_path` and `new_with_path_and_extension` constructor functions - /// allow specification of a base directory with module path used as a relative path offset - /// to the base directory. The script file is then forced to be in a specified extension - /// (default `.rhai`). - /// - /// # Examples - /// - /// ``` - /// use rhai::Engine; - /// use rhai::module_resolvers::FileModuleResolver; - /// - /// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory - /// // with file extension '.x'. - /// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x"); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(resolver)); - /// ``` - #[derive(Debug)] - pub struct FileModuleResolver { - path: PathBuf, - extension: String, - - #[cfg(not(feature = "sync"))] - cache: RefCell>, - - #[cfg(feature = "sync")] - cache: RwLock>, - } - - impl Default for FileModuleResolver { - fn default() -> Self { - Self::new_with_path(PathBuf::default()) - } - } - - impl FileModuleResolver { - /// Create a new `FileModuleResolver` with a specific base path. - /// - /// # Examples - /// - /// ``` - /// use rhai::Engine; - /// use rhai::module_resolvers::FileModuleResolver; - /// - /// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory - /// // with file extension '.rhai' (the default). - /// let resolver = FileModuleResolver::new_with_path("./scripts"); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(resolver)); - /// ``` - pub fn new_with_path>(path: P) -> Self { - Self::new_with_path_and_extension(path, "rhai") - } - - /// Create a new `FileModuleResolver` with a specific base path and file extension. - /// - /// The default extension is `.rhai`. - /// - /// # Examples - /// - /// ``` - /// use rhai::Engine; - /// use rhai::module_resolvers::FileModuleResolver; - /// - /// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory - /// // with file extension '.x'. - /// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x"); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(resolver)); - /// ``` - pub fn new_with_path_and_extension, E: Into>( - path: P, - extension: E, - ) -> Self { - Self { - path: path.into(), - extension: extension.into(), - cache: Default::default(), - } - } - - /// Create a new `FileModuleResolver` with the current directory as base path. - /// - /// # Examples - /// - /// ``` - /// use rhai::Engine; - /// use rhai::module_resolvers::FileModuleResolver; - /// - /// // Create a new 'FileModuleResolver' loading scripts from the current directory - /// // with file extension '.rhai' (the default). - /// let resolver = FileModuleResolver::new(); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(resolver)); - /// ``` - pub fn new() -> Self { - Default::default() - } - - /// Create a `Module` from a file path. - pub fn create_module>( - &self, - engine: &Engine, - path: &str, - ) -> Result> { - self.resolve(engine, path, Default::default()) - } - } - - impl ModuleResolver for FileModuleResolver { - fn resolve( - &self, - engine: &Engine, - path: &str, - pos: Position, - ) -> Result> { - // Construct the script file path - let mut file_path = self.path.clone(); - file_path.push(path); - file_path.set_extension(&self.extension); // Force extension - - // See if it is cached - let exists = { - #[cfg(not(feature = "sync"))] - let c = self.cache.borrow(); - #[cfg(feature = "sync")] - let c = self.cache.read().unwrap(); - - c.contains_key(&file_path) - }; - - if !exists { - // Load the file and compile it if not found - let ast = engine - .compile_file(file_path.clone()) - .map_err(|err| err.new_position(pos))?; - - // Put it into the cache - #[cfg(not(feature = "sync"))] - self.cache.borrow_mut().insert(file_path.clone(), ast); - #[cfg(feature = "sync")] - self.cache.write().unwrap().insert(file_path.clone(), ast); - } - - #[cfg(not(feature = "sync"))] - let c = self.cache.borrow(); - #[cfg(feature = "sync")] - let c = self.cache.read().unwrap(); - - let ast = c.get(&file_path).unwrap(); - - let mut _module = Module::eval_ast_as_new(Scope::new(), ast, engine)?; - - #[cfg(not(feature = "no_function"))] - ast.iter_functions(|access, name, num_args| match access { - FnAccess::Private => (), - FnAccess::Public => { - let fn_name = name.to_string(); - let ast_lib = ast.lib().clone(); - - _module.set_raw_fn_as_scripted( - name, - num_args, - move |engine: &Engine, _, args: &mut [&mut Dynamic]| { - engine.call_fn_dynamic_raw( - &mut Scope::new(), - &ast_lib, - &fn_name, - &mut None, - args, - ) - }, - ); - } - }); - - Ok(_module) - } - } -} - -/// Static module resolver. -#[cfg(not(feature = "no_module"))] -mod stat { - use super::*; - - /// Module resolution service that serves modules added into it. - /// - /// # Examples - /// - /// ``` - /// use rhai::{Engine, Module}; - /// use rhai::module_resolvers::StaticModuleResolver; - /// - /// let mut resolver = StaticModuleResolver::new(); - /// - /// let module = Module::new(); - /// resolver.insert("hello".to_string(), module); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(resolver)); - /// ``` - #[derive(Debug, Clone, Default)] - pub struct StaticModuleResolver(HashMap); - - impl StaticModuleResolver { - /// Create a new `StaticModuleResolver`. - /// - /// # Examples - /// - /// ``` - /// use rhai::{Engine, Module}; - /// use rhai::module_resolvers::StaticModuleResolver; - /// - /// let mut resolver = StaticModuleResolver::new(); - /// - /// let module = Module::new(); - /// resolver.insert("hello", module); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(resolver)); - /// ``` - pub fn new() -> Self { - Default::default() - } - } - - impl StaticModuleResolver { - /// Add a module keyed by its path. - pub fn insert>(&mut self, path: S, mut module: Module) { - module.index_all_sub_modules(); - self.0.insert(path.into(), module); - } - /// Remove a module given its path. - pub fn remove(&mut self, path: &str) -> Option { - self.0.remove(path) - } - /// Does the path exist? - pub fn contains_path(&self, path: &str) -> bool { - self.0.contains_key(path) - } - /// Get an iterator of all the modules. - pub fn iter(&self) -> impl Iterator { - self.0.iter().map(|(k, v)| (k.as_str(), v)) - } - /// Get a mutable iterator of all the modules. - pub fn iter_mut(&mut self) -> impl Iterator { - self.0.iter_mut().map(|(k, v)| (k.as_str(), v)) - } - /// Get an iterator of all the module paths. - pub fn paths(&self) -> impl Iterator { - self.0.keys().map(String::as_str) - } - /// Get an iterator of all the modules. - pub fn values(&self) -> impl Iterator { - self.0.values() - } - /// Get a mutable iterator of all the modules. - pub fn values_mut(&mut self) -> impl Iterator { - self.0.values_mut() - } - /// Remove all modules. - pub fn clear(&mut self) { - self.0.clear(); - } - } - - impl ModuleResolver for StaticModuleResolver { - fn resolve( - &self, - _: &Engine, - path: &str, - pos: Position, - ) -> Result> { - self.0 - .get(path) - .cloned() - .ok_or_else(|| EvalAltResult::ErrorModuleNotFound(path.into(), pos).into()) - } - } -} - -/// Module resolver collection. -#[cfg(not(feature = "no_module"))] -mod collection { - use super::*; - - /// Module resolution service that holds a collection of module resolves, - /// to be searched in sequential order. - /// - /// # Examples - /// - /// ``` - /// use rhai::{Engine, Module}; - /// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection}; - /// - /// let mut collection = ModuleResolversCollection::new(); - /// - /// let resolver = StaticModuleResolver::new(); - /// collection.push(resolver); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(collection)); - /// ``` - #[derive(Default)] - pub struct ModuleResolversCollection(Vec>); - - impl ModuleResolversCollection { - /// Create a new `ModuleResolversCollection`. - /// - /// # Examples - /// - /// ``` - /// use rhai::{Engine, Module}; - /// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection}; - /// - /// let mut collection = ModuleResolversCollection::new(); - /// - /// let resolver = StaticModuleResolver::new(); - /// collection.push(resolver); - /// - /// let mut engine = Engine::new(); - /// engine.set_module_resolver(Some(collection)); - /// ``` - pub fn new() -> Self { - Default::default() - } - } - - impl ModuleResolversCollection { - /// Add a module keyed by its path. - pub fn push(&mut self, resolver: impl ModuleResolver + 'static) { - self.0.push(Box::new(resolver)); - } - /// Get an iterator of all the module resolvers. - pub fn iter(&self) -> impl Iterator { - self.0.iter().map(|v| v.as_ref()) - } - /// Remove all module resolvers. - pub fn clear(&mut self) { - self.0.clear(); - } - } - - impl ModuleResolver for ModuleResolversCollection { - fn resolve( - &self, - engine: &Engine, - path: &str, - pos: Position, - ) -> Result> { - for resolver in self.0.iter() { - if let Ok(module) = resolver.resolve(engine, path, pos) { - return Ok(module); - } - } - - EvalAltResult::ErrorModuleNotFound(path.into(), pos).into() - } - } -} +pub mod resolvers; diff --git a/src/module/resolvers/collection.rs b/src/module/resolvers/collection.rs new file mode 100644 index 00000000..b8cc2729 --- /dev/null +++ b/src/module/resolvers/collection.rs @@ -0,0 +1,80 @@ +use crate::engine::Engine; +use crate::module::{Module, ModuleResolver}; +use crate::result::EvalAltResult; +use crate::token::Position; + +use crate::stdlib::{boxed::Box, vec::Vec}; + +/// Module resolution service that holds a collection of module resolves, +/// to be searched in sequential order. +/// +/// # Examples +/// +/// ``` +/// use rhai::{Engine, Module}; +/// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection}; +/// +/// let mut collection = ModuleResolversCollection::new(); +/// +/// let resolver = StaticModuleResolver::new(); +/// collection.push(resolver); +/// +/// let mut engine = Engine::new(); +/// engine.set_module_resolver(Some(collection)); +/// ``` +#[derive(Default)] +pub struct ModuleResolversCollection(Vec>); + +impl ModuleResolversCollection { + /// Create a new `ModuleResolversCollection`. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Engine, Module}; + /// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection}; + /// + /// let mut collection = ModuleResolversCollection::new(); + /// + /// let resolver = StaticModuleResolver::new(); + /// collection.push(resolver); + /// + /// let mut engine = Engine::new(); + /// engine.set_module_resolver(Some(collection)); + /// ``` + pub fn new() -> Self { + Default::default() + } +} + +impl ModuleResolversCollection { + /// Add a module keyed by its path. + pub fn push(&mut self, resolver: impl ModuleResolver + 'static) { + self.0.push(Box::new(resolver)); + } + /// Get an iterator of all the module resolvers. + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|v| v.as_ref()) + } + /// Remove all module resolvers. + pub fn clear(&mut self) { + self.0.clear(); + } +} + +impl ModuleResolver for ModuleResolversCollection { + fn resolve( + &self, + engine: &Engine, + path: &str, + pos: Position, + ) -> Result> { + for resolver in self.0.iter() { + if let Ok(module) = resolver.resolve(engine, path, pos) { + return Ok(module); + } + } + + EvalAltResult::ErrorModuleNotFound(path.into(), pos).into() + } +} diff --git a/src/module/resolvers/file.rs b/src/module/resolvers/file.rs new file mode 100644 index 00000000..92243cb3 --- /dev/null +++ b/src/module/resolvers/file.rs @@ -0,0 +1,209 @@ +use crate::any::Dynamic; +use crate::engine::Engine; +use crate::module::{Module, ModuleResolver}; +use crate::parser::{FnAccess, AST}; +use crate::result::EvalAltResult; +use crate::scope::Scope; +use crate::token::Position; + +use crate::stdlib::{ + boxed::Box, + collections::HashMap, + path::PathBuf, + string::{String, ToString}, +}; + +#[cfg(not(feature = "sync"))] +use crate::stdlib::cell::RefCell; + +#[cfg(feature = "sync")] +use crate::stdlib::sync::RwLock; + +/// Module resolution service that loads module script files from the file system. +/// +/// Script files are cached so they are are not reloaded and recompiled in subsequent requests. +/// +/// The `new_with_path` and `new_with_path_and_extension` constructor functions +/// allow specification of a base directory with module path used as a relative path offset +/// to the base directory. The script file is then forced to be in a specified extension +/// (default `.rhai`). +/// +/// # Examples +/// +/// ``` +/// use rhai::Engine; +/// use rhai::module_resolvers::FileModuleResolver; +/// +/// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory +/// // with file extension '.x'. +/// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x"); +/// +/// let mut engine = Engine::new(); +/// +/// engine.set_module_resolver(Some(resolver)); +/// ``` +#[derive(Debug)] +pub struct FileModuleResolver { + path: PathBuf, + extension: String, + + #[cfg(not(feature = "sync"))] + cache: RefCell>, + + #[cfg(feature = "sync")] + cache: RwLock>, +} + +impl Default for FileModuleResolver { + fn default() -> Self { + Self::new_with_path(PathBuf::default()) + } +} + +impl FileModuleResolver { + /// Create a new `FileModuleResolver` with a specific base path. + /// + /// # Examples + /// + /// ``` + /// use rhai::Engine; + /// use rhai::module_resolvers::FileModuleResolver; + /// + /// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory + /// // with file extension '.rhai' (the default). + /// let resolver = FileModuleResolver::new_with_path("./scripts"); + /// + /// let mut engine = Engine::new(); + /// engine.set_module_resolver(Some(resolver)); + /// ``` + pub fn new_with_path>(path: P) -> Self { + Self::new_with_path_and_extension(path, "rhai") + } + + /// Create a new `FileModuleResolver` with a specific base path and file extension. + /// + /// The default extension is `.rhai`. + /// + /// # Examples + /// + /// ``` + /// use rhai::Engine; + /// use rhai::module_resolvers::FileModuleResolver; + /// + /// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory + /// // with file extension '.x'. + /// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x"); + /// + /// let mut engine = Engine::new(); + /// engine.set_module_resolver(Some(resolver)); + /// ``` + pub fn new_with_path_and_extension, E: Into>( + path: P, + extension: E, + ) -> Self { + Self { + path: path.into(), + extension: extension.into(), + cache: Default::default(), + } + } + + /// Create a new `FileModuleResolver` with the current directory as base path. + /// + /// # Examples + /// + /// ``` + /// use rhai::Engine; + /// use rhai::module_resolvers::FileModuleResolver; + /// + /// // Create a new 'FileModuleResolver' loading scripts from the current directory + /// // with file extension '.rhai' (the default). + /// let resolver = FileModuleResolver::new(); + /// + /// let mut engine = Engine::new(); + /// engine.set_module_resolver(Some(resolver)); + /// ``` + pub fn new() -> Self { + Default::default() + } + + /// Create a `Module` from a file path. + pub fn create_module>( + &self, + engine: &Engine, + path: &str, + ) -> Result> { + self.resolve(engine, path, Default::default()) + } +} + +impl ModuleResolver for FileModuleResolver { + fn resolve( + &self, + engine: &Engine, + path: &str, + pos: Position, + ) -> Result> { + // Construct the script file path + let mut file_path = self.path.clone(); + file_path.push(path); + file_path.set_extension(&self.extension); // Force extension + + // See if it is cached + let exists = { + #[cfg(not(feature = "sync"))] + let c = self.cache.borrow(); + #[cfg(feature = "sync")] + let c = self.cache.read().unwrap(); + + c.contains_key(&file_path) + }; + + if !exists { + // Load the file and compile it if not found + let ast = engine + .compile_file(file_path.clone()) + .map_err(|err| err.new_position(pos))?; + + // Put it into the cache + #[cfg(not(feature = "sync"))] + self.cache.borrow_mut().insert(file_path.clone(), ast); + #[cfg(feature = "sync")] + self.cache.write().unwrap().insert(file_path.clone(), ast); + } + + #[cfg(not(feature = "sync"))] + let c = self.cache.borrow(); + #[cfg(feature = "sync")] + let c = self.cache.read().unwrap(); + + let ast = c.get(&file_path).unwrap(); + + let mut _module = Module::eval_ast_as_new(Scope::new(), ast, engine)?; + + #[cfg(not(feature = "no_function"))] + ast.iter_functions(|access, name, num_args| match access { + FnAccess::Private => (), + FnAccess::Public => { + let fn_name = name.to_string(); + let ast_lib = ast.lib().clone(); + + _module.set_raw_fn_as_scripted( + name, + num_args, + move |engine: &Engine, _, args: &mut [&mut Dynamic]| { + engine.call_fn_dynamic_raw( + &mut Scope::new(), + &ast_lib, + &fn_name, + &mut None, + args, + ) + }, + ); + } + }); + + Ok(_module) + } +} diff --git a/src/module/resolvers/global_file.rs b/src/module/resolvers/global_file.rs new file mode 100644 index 00000000..f0b0cce8 --- /dev/null +++ b/src/module/resolvers/global_file.rs @@ -0,0 +1,189 @@ +use crate::engine::Engine; +use crate::module::{Module, ModuleResolver}; +use crate::parser::AST; +use crate::result::EvalAltResult; +use crate::token::Position; + +use crate::stdlib::{boxed::Box, collections::HashMap, path::PathBuf, string::String}; + +#[cfg(not(feature = "sync"))] +use crate::stdlib::cell::RefCell; + +#[cfg(feature = "sync")] +use crate::stdlib::sync::RwLock; + +/// Module resolution service that loads module script files from the file system. +/// +/// All functions in each module are treated as strictly independent and cannot refer to +/// other functions within the same module. Functions are searched in the _global_ namespace. +/// +/// For simple utility libraries, this usually performs better than the full `FileModuleResolver`. +/// +/// Script files are cached so they are are not reloaded and recompiled in subsequent requests. +/// +/// The `new_with_path` and `new_with_path_and_extension` constructor functions +/// allow specification of a base directory with module path used as a relative path offset +/// to the base directory. The script file is then forced to be in a specified extension +/// (default `.rhai`). +/// +/// # Examples +/// +/// ``` +/// use rhai::Engine; +/// use rhai::module_resolvers::GlobalFileModuleResolver; +/// +/// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory +/// // with file extension '.x'. +/// let resolver = GlobalFileModuleResolver::new_with_path_and_extension("./scripts", "x"); +/// +/// let mut engine = Engine::new(); +/// +/// engine.set_module_resolver(Some(resolver)); +/// ``` +#[derive(Debug)] +pub struct GlobalFileModuleResolver { + path: PathBuf, + extension: String, + + #[cfg(not(feature = "sync"))] + cache: RefCell>, + + #[cfg(feature = "sync")] + cache: RwLock>, +} + +impl Default for GlobalFileModuleResolver { + fn default() -> Self { + Self::new_with_path(PathBuf::default()) + } +} + +impl GlobalFileModuleResolver { + /// Create a new `GlobalFileModuleResolver` with a specific base path. + /// + /// # Examples + /// + /// ``` + /// use rhai::Engine; + /// use rhai::module_resolvers::GlobalFileModuleResolver; + /// + /// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory + /// // with file extension '.rhai' (the default). + /// let resolver = GlobalFileModuleResolver::new_with_path("./scripts"); + /// + /// let mut engine = Engine::new(); + /// engine.set_module_resolver(Some(resolver)); + /// ``` + pub fn new_with_path>(path: P) -> Self { + Self::new_with_path_and_extension(path, "rhai") + } + + /// Create a new `GlobalFileModuleResolver` with a specific base path and file extension. + /// + /// The default extension is `.rhai`. + /// + /// # Examples + /// + /// ``` + /// use rhai::Engine; + /// use rhai::module_resolvers::GlobalFileModuleResolver; + /// + /// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory + /// // with file extension '.x'. + /// let resolver = GlobalFileModuleResolver::new_with_path_and_extension("./scripts", "x"); + /// + /// let mut engine = Engine::new(); + /// engine.set_module_resolver(Some(resolver)); + /// ``` + pub fn new_with_path_and_extension, E: Into>( + path: P, + extension: E, + ) -> Self { + Self { + path: path.into(), + extension: extension.into(), + cache: Default::default(), + } + } + + /// Create a new `GlobalFileModuleResolver` with the current directory as base path. + /// + /// # Examples + /// + /// ``` + /// use rhai::Engine; + /// use rhai::module_resolvers::GlobalFileModuleResolver; + /// + /// // Create a new 'GlobalFileModuleResolver' loading scripts from the current directory + /// // with file extension '.rhai' (the default). + /// let resolver = GlobalFileModuleResolver::new(); + /// + /// let mut engine = Engine::new(); + /// engine.set_module_resolver(Some(resolver)); + /// ``` + pub fn new() -> Self { + Default::default() + } + + /// Create a `Module` from a file path. + pub fn create_module>( + &self, + engine: &Engine, + path: &str, + ) -> Result> { + self.resolve(engine, path, Default::default()) + } +} + +impl ModuleResolver for GlobalFileModuleResolver { + fn resolve( + &self, + engine: &Engine, + path: &str, + pos: Position, + ) -> Result> { + // Construct the script file path + let mut file_path = self.path.clone(); + file_path.push(path); + file_path.set_extension(&self.extension); // Force extension + + let scope = Default::default(); + + // See if it is cached + let (module, ast) = { + #[cfg(not(feature = "sync"))] + let c = self.cache.borrow(); + #[cfg(feature = "sync")] + let c = self.cache.read().unwrap(); + + if let Some(ast) = c.get(&file_path) { + ( + Module::eval_ast_as_new(scope, ast, engine) + .map_err(|err| err.new_position(pos))?, + None, + ) + } else { + // Load the file and compile it if not found + let ast = engine + .compile_file(file_path.clone()) + .map_err(|err| err.new_position(pos))?; + + ( + Module::eval_ast_as_new(scope, &ast, engine) + .map_err(|err| err.new_position(pos))?, + Some(ast), + ) + } + }; + + if let Some(ast) = ast { + // Put it into the cache + #[cfg(not(feature = "sync"))] + self.cache.borrow_mut().insert(file_path, ast); + #[cfg(feature = "sync")] + self.cache.write().unwrap().insert(file_path, ast); + } + + Ok(module) + } +} diff --git a/src/module/resolvers/mod.rs b/src/module/resolvers/mod.rs new file mode 100644 index 00000000..b9da048d --- /dev/null +++ b/src/module/resolvers/mod.rs @@ -0,0 +1,40 @@ +use crate::engine::Engine; +use crate::fn_native::SendSync; +use crate::module::Module; +use crate::result::EvalAltResult; +use crate::token::Position; + +use crate::stdlib::boxed::Box; + +mod collection; +pub use collection::ModuleResolversCollection; + +#[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +mod file; + +#[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +pub use file::FileModuleResolver; + +#[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +mod global_file; + +#[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +pub use global_file::GlobalFileModuleResolver; + +mod stat; +pub use stat::StaticModuleResolver; + +/// Trait that encapsulates a module resolution service. +pub trait ModuleResolver: SendSync { + /// Resolve a module based on a path string. + fn resolve( + &self, + engine: &Engine, + path: &str, + pos: Position, + ) -> Result>; +} diff --git a/src/module/resolvers/stat.rs b/src/module/resolvers/stat.rs new file mode 100644 index 00000000..5903ce57 --- /dev/null +++ b/src/module/resolvers/stat.rs @@ -0,0 +1,97 @@ +use crate::engine::Engine; +use crate::module::{Module, ModuleResolver}; +use crate::result::EvalAltResult; +use crate::token::Position; + +use crate::stdlib::{boxed::Box, collections::HashMap, string::String}; + +/// Module resolution service that serves modules added into it. +/// +/// # Examples +/// +/// ``` +/// use rhai::{Engine, Module}; +/// use rhai::module_resolvers::StaticModuleResolver; +/// +/// let mut resolver = StaticModuleResolver::new(); +/// +/// let module = Module::new(); +/// resolver.insert("hello".to_string(), module); +/// +/// let mut engine = Engine::new(); +/// +/// engine.set_module_resolver(Some(resolver)); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct StaticModuleResolver(HashMap); + +impl StaticModuleResolver { + /// Create a new `StaticModuleResolver`. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Engine, Module}; + /// use rhai::module_resolvers::StaticModuleResolver; + /// + /// let mut resolver = StaticModuleResolver::new(); + /// + /// let module = Module::new(); + /// resolver.insert("hello", module); + /// + /// let mut engine = Engine::new(); + /// engine.set_module_resolver(Some(resolver)); + /// ``` + pub fn new() -> Self { + Default::default() + } +} + +impl StaticModuleResolver { + /// Add a module keyed by its path. + pub fn insert>(&mut self, path: S, mut module: Module) { + module.index_all_sub_modules(); + self.0.insert(path.into(), module); + } + /// Remove a module given its path. + pub fn remove(&mut self, path: &str) -> Option { + self.0.remove(path) + } + /// Does the path exist? + pub fn contains_path(&self, path: &str) -> bool { + self.0.contains_key(path) + } + /// Get an iterator of all the modules. + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|(k, v)| (k.as_str(), v)) + } + /// Get a mutable iterator of all the modules. + pub fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut().map(|(k, v)| (k.as_str(), v)) + } + /// Get an iterator of all the module paths. + pub fn paths(&self) -> impl Iterator { + self.0.keys().map(String::as_str) + } + /// Get an iterator of all the modules. + pub fn values(&self) -> impl Iterator { + self.0.values() + } + /// Get a mutable iterator of all the modules. + pub fn values_mut(&mut self) -> impl Iterator { + self.0.values_mut() + } + /// Remove all modules. + pub fn clear(&mut self) { + self.0.clear(); + } +} + +impl ModuleResolver for StaticModuleResolver { + fn resolve(&self, _: &Engine, path: &str, pos: Position) -> Result> { + self.0 + .get(path) + .cloned() + .ok_or_else(|| EvalAltResult::ErrorModuleNotFound(path.into(), pos).into()) + } +} From 599fe846cb78dafdc70233cb5d538a1d795bd393 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 25 Sep 2020 18:07:39 +0800 Subject: [PATCH 03/25] Add complete_namespace to Module::eval_ast_as_new. --- RELEASES.md | 3 + doc/src/language/modules/ast.md | 8 +- doc/src/language/modules/export.md | 14 ++- src/module/mod.rs | 156 ++++++++++++++++++++++------ src/module/resolvers/file.rs | 78 +++++--------- src/module/resolvers/global_file.rs | 27 +++-- src/result.rs | 7 ++ 7 files changed, 191 insertions(+), 102 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 78716df6..a6e8e522 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -18,6 +18,9 @@ Breaking changes * `Engine::register_set_result` and `Engine::register_indexer_set_result` now take a function that returns `Result<(), Box>`. * `Engine::register_indexer_XXX` and `Module::set_indexer_XXX` panic when the type is `Arrary`, `Map` or `String`. +* `EvalAltResult` has a new variant `ErrorInModule` which holds errors when loading an external module. +* `Module::eval_ast_as_new` now takes an extra boolean parameter, indicating whether to encapsulate the entire module into a separate namespace. +* Functions in `FileModuleResolver` loaded modules now can cross-call each other, but cannot access the global namespace. For the old behavior, use `MergingFileModuleResolver` instead. New features ------------ diff --git a/doc/src/language/modules/ast.md b/doc/src/language/modules/ast.md index 4bf39ace..56cde70d 100644 --- a/doc/src/language/modules/ast.md +++ b/doc/src/language/modules/ast.md @@ -44,7 +44,13 @@ let ast = engine.compile(r#" "#)?; // Convert the 'AST' into a module, using the 'Engine' to evaluate it first -let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; +// +// The second parameter ('private_namespace'), when set to true, will encapsulate +// a copy of the entire 'AST' into each function, allowing functions in the module script +// to cross-call each other. Otherwise module script functions access the global namespace. +// +// This incurs additional overhead, avoidable by setting 'private_namespace' to false. +let module = Module::eval_ast_as_new(Scope::new(), &ast, true, &engine)?; // 'module' now can be loaded into a custom 'Scope' for future use. It contains: // - sub-module: 'foobar' (renamed from 'extra') diff --git a/doc/src/language/modules/export.md b/doc/src/language/modules/export.md index 546b5952..46416bcc 100644 --- a/doc/src/language/modules/export.md +++ b/doc/src/language/modules/export.md @@ -3,10 +3,10 @@ Export Variables, Functions and Sub-Modules in Module {{#include ../../links.md}} -A _module_ is a single script (or pre-compiled [`AST`]) containing global variables, functions and sub-modules. +A _module_ can be created from a single script (or pre-compiled [`AST`]) containing global variables, +functions and sub-modules via the `Module::eval_ast_as_new` method. -A module can be created from a script via the `Module::eval_ast_as_new` method. When given an [`AST`], -it is first evaluated, then the following items are exposed as members of the new module: +When given an [`AST`], it is first evaluated, then the following items are exposed as members of the new module: * Global variables - essentially all variables that remain in the [`Scope`] at the end of a script run - that are exported. Variables not exported (via the `export` statement) remain hidden. @@ -14,6 +14,14 @@ it is first evaluated, then the following items are exposed as members of the ne * Global modules that remain in the [`Scope`] at the end of a script run. +The parameter `private_namespace` in `Module::eval_ast_as_new` determines the exact behavior of +functions exposed by the module and the namespace that they can access: + +| `private_namespace` value | Behavior of module functions | Namespace | Call global functions | Call functions in same module | +| :-----------------------: | ---------------------------------------------------- | :-------: | :-------------------: | :---------------------------: | +| `true` | encapsulate the entire `AST` into each function call | module | no | yes | +| `false` | register each function independently | global | yes | no | + Global Variables ---------------- diff --git a/src/module/mod.rs b/src/module/mod.rs index ea773abc..75abda36 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -3,7 +3,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; use crate::engine::Engine; -use crate::fn_native::{CallableFunction as Func, FnCallArgs, IteratorFn, SendSync}; +use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; use crate::fn_register::by_value as cast_arg; use crate::parser::{FnAccess, FnAccess::Public}; use crate::result::EvalAltResult; @@ -41,6 +41,14 @@ use crate::stdlib::{ /// Return type of module-level Rust function. pub type FuncReturn = Result>; +pub type FuncInfo = ( + String, + FnAccess, + usize, + Option>, + CallableFunction, +); + /// An imported module, which may contain variables, sub-modules, /// external Rust functions, and script-defined functions. /// @@ -57,18 +65,14 @@ pub struct Module { all_variables: HashMap, /// External Rust functions. - functions: HashMap< - u64, - (String, FnAccess, usize, Option>, Func), - StraightHasherBuilder, - >, + functions: HashMap, /// Iterator functions, keyed by the type producing the iterator. type_iterators: HashMap, /// Flattened collection of all external Rust functions, native or scripted, /// including those in sub-modules. - all_functions: HashMap, + all_functions: HashMap, /// Is the module indexed? indexed: bool, @@ -381,7 +385,7 @@ impl Module { name: impl Into, access: FnAccess, arg_types: &[TypeId], - func: Func, + func: CallableFunction, ) -> u64 { let name = name.into(); @@ -481,7 +485,12 @@ impl Module { let f = move |engine: &Engine, lib: &Module, args: &mut FnCallArgs| { func(engine, lib, args).map(Dynamic::from) }; - self.set_fn(name, Public, arg_types, Func::from_method(Box::new(f))) + self.set_fn( + name, + Public, + arg_types, + CallableFunction::from_method(Box::new(f)), + ) } /// Set a raw function but with a signature that is a scripted function (meaning that the types @@ -507,7 +516,7 @@ impl Module { FnAccess::Public, num_args, None, - Func::from_pure(Box::new(f)), + CallableFunction::from_pure(Box::new(f)), ), ); self.indexed = false; @@ -534,7 +543,12 @@ impl Module { ) -> u64 { let f = move |_: &Engine, _: &Module, _: &mut FnCallArgs| func().map(Dynamic::from); let arg_types = []; - self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f))) + self.set_fn( + name, + Public, + &arg_types, + CallableFunction::from_pure(Box::new(f)), + ) } /// Set a Rust function taking one parameter into the module, returning a hash key. @@ -559,7 +573,12 @@ impl Module { func(cast_arg::(&mut args[0])).map(Dynamic::from) }; let arg_types = [TypeId::of::()]; - self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f))) + self.set_fn( + name, + Public, + &arg_types, + CallableFunction::from_pure(Box::new(f)), + ) } /// Set a Rust function taking one mutable parameter into the module, returning a hash key. @@ -584,7 +603,12 @@ impl Module { func(&mut args[0].write_lock::().unwrap()).map(Dynamic::from) }; let arg_types = [TypeId::of::()]; - self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f))) + self.set_fn( + name, + Public, + &arg_types, + CallableFunction::from_method(Box::new(f)), + ) } /// Set a Rust getter function taking one mutable parameter, returning a hash key. @@ -636,7 +660,12 @@ impl Module { func(a, b).map(Dynamic::from) }; let arg_types = [TypeId::of::(), TypeId::of::()]; - self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f))) + self.set_fn( + name, + Public, + &arg_types, + CallableFunction::from_pure(Box::new(f)), + ) } /// Set a Rust function taking two parameters (the first one mutable) into the module, @@ -667,7 +696,12 @@ impl Module { func(a, b).map(Dynamic::from) }; let arg_types = [TypeId::of::(), TypeId::of::()]; - self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f))) + self.set_fn( + name, + Public, + &arg_types, + CallableFunction::from_method(Box::new(f)), + ) } /// Set a Rust setter function taking two parameters (the first one mutable) into the module, @@ -772,7 +806,12 @@ impl Module { func(a, b, c).map(Dynamic::from) }; let arg_types = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; - self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f))) + self.set_fn( + name, + Public, + &arg_types, + CallableFunction::from_pure(Box::new(f)), + ) } /// Set a Rust function taking three parameters (the first one mutable) into the module, @@ -809,7 +848,12 @@ impl Module { func(a, b, c).map(Dynamic::from) }; let arg_types = [TypeId::of::(), TypeId::of::(), TypeId::of::()]; - self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f))) + self.set_fn( + name, + Public, + &arg_types, + CallableFunction::from_method(Box::new(f)), + ) } /// Set a Rust index setter taking three parameters (the first one mutable) into the module, @@ -865,7 +909,7 @@ impl Module { FN_IDX_SET, Public, &arg_types, - Func::from_method(Box::new(f)), + CallableFunction::from_method(Box::new(f)), ) } @@ -949,7 +993,12 @@ impl Module { TypeId::of::(), TypeId::of::(), ]; - self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f))) + self.set_fn( + name, + Public, + &arg_types, + CallableFunction::from_pure(Box::new(f)), + ) } /// Set a Rust function taking four parameters (the first one mutable) into the module, @@ -993,14 +1042,19 @@ impl Module { TypeId::of::(), TypeId::of::(), ]; - self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f))) + self.set_fn( + name, + Public, + &arg_types, + CallableFunction::from_method(Box::new(f)), + ) } /// Get a Rust function. /// /// The `u64` hash is calculated by the function `crate::calc_fn_hash`. /// It is also returned by the `set_fn_XXX` calls. - pub(crate) fn get_fn(&self, hash_fn: u64, public_only: bool) -> Option<&Func> { + pub(crate) fn get_fn(&self, hash_fn: u64, public_only: bool) -> Option<&CallableFunction> { if hash_fn == 0 { None } else { @@ -1019,7 +1073,7 @@ impl Module { /// /// The `u64` hash is calculated by the function `crate::calc_fn_hash` and must match /// the hash calculated by `index_all_sub_modules`. - pub(crate) fn get_qualified_fn(&self, hash_qualified_fn: u64) -> Option<&Func> { + pub(crate) fn get_qualified_fn(&self, hash_qualified_fn: u64) -> Option<&CallableFunction> { self.all_functions.get(&hash_qualified_fn) } @@ -1084,7 +1138,9 @@ impl Module { .iter() .filter(|(_, (_, _, _, _, v))| match v { #[cfg(not(feature = "no_function"))] - Func::Script(ref f) => _filter(f.access, f.name.as_str(), f.params.len()), + CallableFunction::Script(f) => { + _filter(f.access, f.name.as_str(), f.params.len()) + } _ => true, }) .map(|(&k, v)| (k, v.clone())), @@ -1106,7 +1162,7 @@ impl Module { mut filter: impl FnMut(FnAccess, &str, usize) -> bool, ) -> &mut Self { self.functions.retain(|_, (_, _, _, _, v)| match v { - Func::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()), + CallableFunction::Script(f) => filter(f.access, f.name.as_str(), f.params.len()), _ => true, }); @@ -1135,9 +1191,7 @@ impl Module { } /// Get an iterator to the functions in the module. - pub(crate) fn iter_fn( - &self, - ) -> impl Iterator>, Func)> { + pub(crate) fn iter_fn(&self) -> impl Iterator { self.functions.values() } @@ -1156,13 +1210,21 @@ impl Module { self.functions .iter() .for_each(|(_, (_, _, _, _, v))| match v { - Func::Script(ref f) => action(f.access, f.name.as_str(), f.params.len()), + CallableFunction::Script(f) => action(f.access, f.name.as_str(), f.params.len()), _ => (), }); } /// Create a new `Module` by evaluating an `AST`. /// + /// ### `private_namespace` parameter + /// + /// If `true`, the entire `AST` is encapsulated into each function as a private namespace, + /// allowing functions to cross-call each other. + /// + /// If `false`, each function is registered independently and cannot cross-call + /// each other. Functions are searched in the global namespace. + /// /// # Examples /// /// ``` @@ -1171,14 +1233,19 @@ impl Module { /// /// let engine = Engine::new(); /// let ast = engine.compile("let answer = 42; export answer;")?; - /// let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; + /// let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine, true)?; /// assert!(module.contains_var("answer")); /// assert_eq!(module.get_var_value::("answer").unwrap(), 42); /// # Ok(()) /// # } /// ``` #[cfg(not(feature = "no_module"))] - pub fn eval_ast_as_new(mut scope: Scope, ast: &AST, engine: &Engine) -> FuncReturn { + pub fn eval_ast_as_new( + mut scope: Scope, + ast: &AST, + private_namespace: bool, + engine: &Engine, + ) -> FuncReturn { let mut mods = Imports::new(); // Run the script @@ -1201,7 +1268,32 @@ impl Module { module.modules.insert(alias.to_string(), m); }); - module.merge(ast.lib()); + #[cfg(not(feature = "no_function"))] + if private_namespace { + ast.iter_functions(|access, name, num_args| match access { + FnAccess::Private => (), + FnAccess::Public => { + let fn_name = name.to_string(); + let ast_lib = ast.lib().clone(); + + module.set_raw_fn_as_scripted( + name, + num_args, + move |engine: &Engine, _, args: &mut [&mut Dynamic]| { + engine.call_fn_dynamic_raw( + &mut Scope::new(), + &ast_lib, + &fn_name, + &mut None, + args, + ) + }, + ); + } + }); + } else { + module.merge(ast.lib()); + } Ok(module) } @@ -1215,7 +1307,7 @@ impl Module { module: &'a Module, qualifiers: &mut Vec<&'a str>, variables: &mut Vec<(u64, Dynamic)>, - functions: &mut Vec<(u64, Func)>, + functions: &mut Vec<(u64, CallableFunction)>, ) { for (name, m) in &module.modules { // Index all the sub-modules first. diff --git a/src/module/resolvers/file.rs b/src/module/resolvers/file.rs index 92243cb3..bd49b429 100644 --- a/src/module/resolvers/file.rs +++ b/src/module/resolvers/file.rs @@ -1,17 +1,10 @@ -use crate::any::Dynamic; use crate::engine::Engine; use crate::module::{Module, ModuleResolver}; -use crate::parser::{FnAccess, AST}; +use crate::parser::AST; use crate::result::EvalAltResult; -use crate::scope::Scope; use crate::token::Position; -use crate::stdlib::{ - boxed::Box, - collections::HashMap, - path::PathBuf, - string::{String, ToString}, -}; +use crate::stdlib::{boxed::Box, collections::HashMap, path::PathBuf, string::String}; #[cfg(not(feature = "sync"))] use crate::stdlib::cell::RefCell; @@ -149,61 +142,42 @@ impl ModuleResolver for FileModuleResolver { file_path.push(path); file_path.set_extension(&self.extension); // Force extension + let scope = Default::default(); + let module; + // See if it is cached - let exists = { + let ast = { #[cfg(not(feature = "sync"))] let c = self.cache.borrow(); #[cfg(feature = "sync")] let c = self.cache.read().unwrap(); - c.contains_key(&file_path) + if let Some(ast) = c.get(&file_path) { + module = Module::eval_ast_as_new(scope, ast, true, engine).map_err(|err| { + Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)) + })?; + None + } else { + // Load the file and compile it if not found + let ast = engine.compile_file(file_path.clone()).map_err(|err| { + Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)) + })?; + + module = Module::eval_ast_as_new(scope, &ast, true, engine).map_err(|err| { + Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)) + })?; + Some(ast) + } }; - if !exists { - // Load the file and compile it if not found - let ast = engine - .compile_file(file_path.clone()) - .map_err(|err| err.new_position(pos))?; - + if let Some(ast) = ast { // Put it into the cache #[cfg(not(feature = "sync"))] - self.cache.borrow_mut().insert(file_path.clone(), ast); + self.cache.borrow_mut().insert(file_path, ast); #[cfg(feature = "sync")] - self.cache.write().unwrap().insert(file_path.clone(), ast); + self.cache.write().unwrap().insert(file_path, ast); } - #[cfg(not(feature = "sync"))] - let c = self.cache.borrow(); - #[cfg(feature = "sync")] - let c = self.cache.read().unwrap(); - - let ast = c.get(&file_path).unwrap(); - - let mut _module = Module::eval_ast_as_new(Scope::new(), ast, engine)?; - - #[cfg(not(feature = "no_function"))] - ast.iter_functions(|access, name, num_args| match access { - FnAccess::Private => (), - FnAccess::Public => { - let fn_name = name.to_string(); - let ast_lib = ast.lib().clone(); - - _module.set_raw_fn_as_scripted( - name, - num_args, - move |engine: &Engine, _, args: &mut [&mut Dynamic]| { - engine.call_fn_dynamic_raw( - &mut Scope::new(), - &ast_lib, - &fn_name, - &mut None, - args, - ) - }, - ); - } - }); - - Ok(_module) + Ok(module) } } diff --git a/src/module/resolvers/global_file.rs b/src/module/resolvers/global_file.rs index f0b0cce8..f9445d09 100644 --- a/src/module/resolvers/global_file.rs +++ b/src/module/resolvers/global_file.rs @@ -148,31 +148,30 @@ impl ModuleResolver for GlobalFileModuleResolver { file_path.set_extension(&self.extension); // Force extension let scope = Default::default(); + let module; // See if it is cached - let (module, ast) = { + let ast = { #[cfg(not(feature = "sync"))] let c = self.cache.borrow(); #[cfg(feature = "sync")] let c = self.cache.read().unwrap(); if let Some(ast) = c.get(&file_path) { - ( - Module::eval_ast_as_new(scope, ast, engine) - .map_err(|err| err.new_position(pos))?, - None, - ) + module = Module::eval_ast_as_new(scope, ast, false, engine).map_err(|err| { + Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)) + })?; + None } else { // Load the file and compile it if not found - let ast = engine - .compile_file(file_path.clone()) - .map_err(|err| err.new_position(pos))?; + let ast = engine.compile_file(file_path.clone()).map_err(|err| { + Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)) + })?; - ( - Module::eval_ast_as_new(scope, &ast, engine) - .map_err(|err| err.new_position(pos))?, - Some(ast), - ) + module = Module::eval_ast_as_new(scope, &ast, false, engine).map_err(|err| { + Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)) + })?; + Some(ast) } }; diff --git a/src/result.rs b/src/result.rs index 40f2935c..e1f9ebf3 100644 --- a/src/result.rs +++ b/src/result.rs @@ -39,6 +39,9 @@ pub enum EvalAltResult { /// An error has occurred inside a called function. /// Wrapped values are the name of the function and the interior error. ErrorInFunctionCall(String, Box, Position), + /// An error has occurred while loading a module. + /// Wrapped value are the name of the module and the interior error. + ErrorInModule(String, Box, Position), /// Access to `this` that is not bound. ErrorUnboundThis(Position), /// Non-boolean operand encountered for boolean operator. Wrapped value is the operator. @@ -113,6 +116,7 @@ impl EvalAltResult { Self::ErrorParsing(p, _) => p.desc(), Self::ErrorInFunctionCall(_, _, _) => "Error in called function", + Self::ErrorInModule(_, _, _) => "Error in module", Self::ErrorFunctionNotFound(_, _) => "Function not found", Self::ErrorUnboundThis(_) => "'this' is not bound", Self::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands", @@ -180,6 +184,7 @@ impl fmt::Display for EvalAltResult { Self::ErrorInFunctionCall(s, err, _) => { write!(f, "Error in call to function '{}' : {}", s, err)? } + Self::ErrorInModule(s, err, _) => write!(f, "Error in module '{}' : {}", s, err)?, Self::ErrorFunctionNotFound(s, _) | Self::ErrorVariableNotFound(s, _) @@ -280,6 +285,7 @@ impl EvalAltResult { Self::ErrorParsing(_, pos) | Self::ErrorFunctionNotFound(_, pos) | Self::ErrorInFunctionCall(_, _, pos) + | Self::ErrorInModule(_, _, pos) | Self::ErrorUnboundThis(pos) | Self::ErrorBooleanArgMismatch(_, pos) | Self::ErrorCharMismatch(pos) @@ -321,6 +327,7 @@ impl EvalAltResult { Self::ErrorParsing(_, pos) | Self::ErrorFunctionNotFound(_, pos) | Self::ErrorInFunctionCall(_, _, pos) + | Self::ErrorInModule(_, _, pos) | Self::ErrorUnboundThis(pos) | Self::ErrorBooleanArgMismatch(_, pos) | Self::ErrorCharMismatch(pos) From f406fc0ac0c870d3b2df0c7d265a2ad858825e6d Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 25 Sep 2020 19:07:24 +0800 Subject: [PATCH 04/25] Fix function calls. --- src/fn_call.rs | 105 +++++++++++++++++++++++++++------------------- tests/modules.rs | 2 +- tests/packages.rs | 9 ++-- 3 files changed, 69 insertions(+), 47 deletions(-) diff --git a/src/fn_call.rs b/src/fn_call.rs index c40d352f..1aeaa473 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -221,6 +221,7 @@ impl Engine { let func = self .global_module .get_fn(hash_fn, pub_only) + .or_else(|| lib.get_fn(hash_fn, pub_only)) .or_else(|| self.packages.get_fn(hash_fn, pub_only)); if let Some(func) = func { @@ -439,9 +440,9 @@ impl Engine { // First check script-defined functions lib.contains_fn(hash_script, pub_only) - //|| lib.contains_fn(hash_fn, pub_only) + || lib.contains_fn(hash_fn, pub_only) // Then check registered functions - //|| self.global_module.contains_fn(hash_script, pub_only) + || self.global_module.contains_fn(hash_script, pub_only) || self.global_module.contains_fn(hash_fn, pub_only) // Then check packages || self.packages.contains_fn(hash_script, pub_only) @@ -522,58 +523,76 @@ impl Engine { .into() } - // Normal script function call + // Script-like function found #[cfg(not(feature = "no_function"))] - _ if lib.contains_fn(hash_script, pub_only) + _ if self.global_module.contains_fn(hash_script, pub_only) + || lib.contains_fn(hash_script, pub_only) || self.packages.contains_fn(hash_script, pub_only) => { - // Get scripted function - let func = lib + // Get function + let func = self + .global_module .get_fn(hash_script, pub_only) + .or_else(|| lib.get_fn(hash_script, pub_only)) .or_else(|| self.packages.get_fn(hash_script, pub_only)) - .unwrap() - .get_fn_def(); + .unwrap(); - let scope = &mut Scope::new(); - let mods = &mut Imports::new(); + if func.is_script() { + let func = func.get_fn_def(); - // Add captured variables into scope - #[cfg(not(feature = "no_closure"))] - if let Some(captured) = _capture { - add_captured_variables_into_scope(&func.externals, captured, scope); - } + let scope = &mut Scope::new(); + let mods = &mut Imports::new(); - let result = if _is_method { - // Method call of script function - map first argument to `this` - let (first, rest) = args.split_first_mut().unwrap(); - self.call_script_fn( - scope, - mods, + // Add captured variables into scope + #[cfg(not(feature = "no_closure"))] + if let Some(captured) = _capture { + add_captured_variables_into_scope(&func.externals, captured, scope); + } + + let result = if _is_method { + // Method call of script function - map first argument to `this` + let (first, rest) = args.split_first_mut().unwrap(); + self.call_script_fn( + scope, + mods, + state, + lib, + &mut Some(*first), + fn_name, + func, + rest, + _level, + )? + } else { + // Normal call of script function - map first argument to `this` + // The first argument is a reference? + let mut backup: ArgBackup = Default::default(); + backup.change_first_arg_to_copy(is_ref, args); + + let result = self.call_script_fn( + scope, mods, state, lib, &mut None, fn_name, func, args, _level, + ); + + // Restore the original reference + backup.restore_first_arg(args); + + result? + }; + + Ok((result, false)) + } else { + // If it is a native function, redirect it + self.call_native_fn( state, lib, - &mut Some(*first), fn_name, - func, - rest, - _level, - )? - } else { - // Normal call of script function - map first argument to `this` - // The first argument is a reference? - let mut backup: ArgBackup = Default::default(); - backup.change_first_arg_to_copy(is_ref, args); - - let result = self.call_script_fn( - scope, mods, state, lib, &mut None, fn_name, func, args, _level, - ); - - // Restore the original reference - backup.restore_first_arg(args); - - result? - }; - - Ok((result, false)) + hash_script, + args, + is_ref, + pub_only, + def_val, + ) + } } // Normal native function call diff --git a/tests/modules.rs b/tests/modules.rs index e3475584..b0134594 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -262,7 +262,7 @@ fn test_module_from_ast() -> Result<(), Box> { engine.set_module_resolver(Some(resolver1)); - let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; + let module = Module::eval_ast_as_new(Scope::new(), &ast, true, &engine)?; let mut resolver2 = StaticModuleResolver::new(); resolver2.insert("testing", module); diff --git a/tests/packages.rs b/tests/packages.rs index 6e871496..b0235b6b 100644 --- a/tests/packages.rs +++ b/tests/packages.rs @@ -34,12 +34,15 @@ fn test_packages() -> Result<(), Box> { #[test] fn test_packages_with_script() -> Result<(), Box> { let mut engine = Engine::new(); - let ast = engine.compile("fn foo(x) { x + 1 }")?; - let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; + let ast = engine.compile("fn foo(x) { x + 1 } fn bar(x) { foo(x) + 1 }")?; + let module = Module::eval_ast_as_new(Scope::new(), &ast, false, &engine)?; engine.load_package(module); - assert_eq!(engine.eval::("foo(41)")?, 42); + let module = Module::eval_ast_as_new(Scope::new(), &ast, true, &engine)?; + engine.load_package(module); + assert_eq!(engine.eval::("bar(40)")?, 42); + Ok(()) } From 39bbff878ced1da61dd7f6b2662a257a96df41b4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 25 Sep 2020 20:00:49 +0800 Subject: [PATCH 05/25] Fix doc test. --- src/module/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/module/mod.rs b/src/module/mod.rs index 75abda36..fbaf0a0e 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1233,7 +1233,7 @@ impl Module { /// /// let engine = Engine::new(); /// let ast = engine.compile("let answer = 42; export answer;")?; - /// let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine, true)?; + /// let module = Module::eval_ast_as_new(Scope::new(), &ast, true, &engine)?; /// assert!(module.contains_var("answer")); /// assert_eq!(module.get_var_value::("answer").unwrap(), 42); /// # Ok(()) From da9aa602565e50f0b67327362884a0a0d857b616 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 25 Sep 2020 20:24:03 +0800 Subject: [PATCH 06/25] Wrap file module functions in ErrorInModule. --- RELEASES.md | 1 + src/module/mod.rs | 23 ++++++++++++++++------- src/result.rs | 7 +++++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index a6e8e522..bd65e687 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -21,6 +21,7 @@ Breaking changes * `EvalAltResult` has a new variant `ErrorInModule` which holds errors when loading an external module. * `Module::eval_ast_as_new` now takes an extra boolean parameter, indicating whether to encapsulate the entire module into a separate namespace. * Functions in `FileModuleResolver` loaded modules now can cross-call each other, but cannot access the global namespace. For the old behavior, use `MergingFileModuleResolver` instead. +* New `EvalAltResult::ErrorInModule` variant capturing errors when loading a module from a script file. New features ------------ diff --git a/src/module/mod.rs b/src/module/mod.rs index fbaf0a0e..b6511eb8 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1280,13 +1280,22 @@ impl Module { name, num_args, move |engine: &Engine, _, args: &mut [&mut Dynamic]| { - engine.call_fn_dynamic_raw( - &mut Scope::new(), - &ast_lib, - &fn_name, - &mut None, - args, - ) + engine + .call_fn_dynamic_raw( + &mut Scope::new(), + &ast_lib, + &fn_name, + &mut None, + args, + ) + .map_err(|err| { + // Wrap the error in a module-error + Box::new(EvalAltResult::ErrorInModule( + "".to_string(), + err, + Position::none(), + )) + }) }, ); } diff --git a/src/result.rs b/src/result.rs index e1f9ebf3..16f18981 100644 --- a/src/result.rs +++ b/src/result.rs @@ -182,9 +182,12 @@ impl fmt::Display for EvalAltResult { Self::ErrorParsing(p, _) => write!(f, "Syntax error: {}", p)?, Self::ErrorInFunctionCall(s, err, _) => { - write!(f, "Error in call to function '{}' : {}", s, err)? + write!(f, "Error in call to function '{}': {}", s, err)? } - Self::ErrorInModule(s, err, _) => write!(f, "Error in module '{}' : {}", s, err)?, + Self::ErrorInModule(s, err, _) if s.is_empty() => { + write!(f, "Error in module: {}", err)? + } + Self::ErrorInModule(s, err, _) => write!(f, "Error in module '{}': {}", s, err)?, Self::ErrorFunctionNotFound(s, _) | Self::ErrorVariableNotFound(s, _) From 4efe6b90e7cfb0020f1fc262d41eea45b37223fb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 25 Sep 2020 23:02:49 +0800 Subject: [PATCH 07/25] Add string::split(). --- doc/src/language/string-fn.md | 1 + src/packages/string_more.rs | 24 +++++++++++++++++------- tests/string.rs | 21 +++++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/doc/src/language/string-fn.md b/doc/src/language/string-fn.md index 504ea0a6..5c3be69c 100644 --- a/doc/src/language/string-fn.md +++ b/doc/src/language/string-fn.md @@ -16,6 +16,7 @@ using a [raw `Engine`]) operate on [strings]: | `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | | `index_of` | character/sub-string to search for, start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found | | `sub_string` | start index, length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) | +| `split` | delimiter character/string | splits the string by the specified delimiter, returning an [array] of string segments | | `crop` | start index, length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) | | `replace` | target character/sub-string, replacement character/string | replaces a sub-string with another | | `trim` | _none_ | trims the string of whitespace at the beginning and end | diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index c6ac9bc6..331ff4d9 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -187,9 +187,8 @@ mod string_functions { pub fn contains_char(s: &str, ch: char) -> bool { s.contains(ch) } - #[rhai_fn(name = "contains")] #[inline(always)] - pub fn contains_string(s: &str, find: ImmutableString) -> bool { + pub fn contains(s: &str, find: ImmutableString) -> bool { s.contains(find.as_str()) } @@ -230,13 +229,12 @@ mod string_functions { .unwrap_or(-1 as INT) } #[rhai_fn(name = "index_of")] - pub fn index_of_string(s: &str, find: ImmutableString) -> INT { + pub fn index_of(s: &str, find: ImmutableString) -> INT { s.find(find.as_str()) .map(|index| s[0..index].chars().count() as INT) .unwrap_or(-1 as INT) } - #[rhai_fn(name = "sub_string")] pub fn sub_string(s: &str, start: INT, len: INT) -> ImmutableString { let offset = if s.is_empty() || len <= 0 { return "".to_string().into(); @@ -272,7 +270,7 @@ mod string_functions { } #[rhai_fn(name = "crop")] - pub fn crop_string(s: &mut ImmutableString, start: INT, len: INT) { + pub fn crop(s: &mut ImmutableString, start: INT, len: INT) { let offset = if s.is_empty() || len <= 0 { s.make_mut().clear(); return; @@ -300,12 +298,12 @@ mod string_functions { #[rhai_fn(name = "crop")] #[inline(always)] pub fn crop_string_starting_from(s: &mut ImmutableString, start: INT) { - crop_string(s, start, s.len() as INT); + crop(s, start, s.len() as INT); } #[rhai_fn(name = "replace")] #[inline(always)] - pub fn replace_string(s: &mut ImmutableString, find: ImmutableString, sub: ImmutableString) { + pub fn replace(s: &mut ImmutableString, find: ImmutableString, sub: ImmutableString) { *s = s.replace(find.as_str(), sub.as_str()).into(); } #[rhai_fn(name = "replace")] @@ -338,6 +336,18 @@ mod string_functions { pub fn prepend(x: &mut Array, y: &str) -> String { format!("{:?}{}", x, y) } + + #[inline(always)] + pub fn split(s: &str, delimiter: ImmutableString) -> Array { + s.split(delimiter.as_str()) + .map(Into::::into) + .collect() + } + #[rhai_fn(name = "split")] + #[inline(always)] + pub fn split_char(s: &str, delimiter: char) -> Array { + s.split(delimiter).map(Into::::into).collect() + } } #[cfg(not(feature = "no_object"))] diff --git a/tests/string.rs b/tests/string.rs index 908b4c6c..786ec106 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -186,3 +186,24 @@ fn test_string_fn() -> Result<(), Box> { Ok(()) } + +#[cfg(not(feature = "no_object"))] +#[test] +fn test_string_split() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.split(' ').len"# + )?, + 3 + ); + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.split("hello").len"# + )?, + 2 + ); + + Ok(()) +} From 6b13e266b911733d1797c750aca01c87fe724fc8 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 25 Sep 2020 23:07:18 +0800 Subject: [PATCH 08/25] Allow actual function name that is the same as a #[rhai_fn(name = "...")] rename. --- codegen/src/rhai_module.rs | 40 ++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/codegen/src/rhai_module.rs b/codegen/src/rhai_module.rs index b65d891b..473c3398 100644 --- a/codegen/src/rhai_module.rs +++ b/codegen/src/rhai_module.rs @@ -183,25 +183,29 @@ pub(crate) fn flatten_type_groups(ty: &syn::Type) -> &syn::Type { } pub(crate) fn check_rename_collisions(fns: &Vec) -> Result<(), syn::Error> { + fn make_key(name: String, itemfn: &ExportedFn) -> String { + itemfn.arg_list().fold(name, |mut argstr, fnarg| { + let type_string: String = match fnarg { + syn::FnArg::Receiver(_) => unimplemented!("receiver rhai_fns not implemented"), + syn::FnArg::Typed(syn::PatType { ref ty, .. }) => { + ty.as_ref().to_token_stream().to_string() + } + }; + argstr.push('.'); + argstr.push_str(&type_string); + argstr + }) + } + let mut renames = HashMap::::new(); - let mut names = HashMap::::new(); + let mut fn_names = HashMap::::new(); + let mut fn_sig = HashMap::::new(); + for itemfn in fns.iter() { if let Some(ref names) = itemfn.params().name { for name in names { let current_span = itemfn.params().span.as_ref().unwrap(); - let key = itemfn.arg_list().fold(name.clone(), |mut argstr, fnarg| { - let type_string: String = match fnarg { - syn::FnArg::Receiver(_) => { - unimplemented!("receiver rhai_fns not implemented") - } - syn::FnArg::Typed(syn::PatType { ref ty, .. }) => { - ty.as_ref().to_token_stream().to_string() - } - }; - argstr.push('.'); - argstr.push_str(&type_string); - argstr - }); + let key = make_key(name.clone(), itemfn); if let Some(other_span) = renames.insert(key, *current_span) { let mut err = syn::Error::new( *current_span, @@ -216,7 +220,7 @@ pub(crate) fn check_rename_collisions(fns: &Vec) -> Result<(), syn:: } } else { let ident = itemfn.name(); - if let Some(other_span) = names.insert(ident.to_string(), ident.span()) { + if let Some(other_span) = fn_names.insert(ident.to_string(), ident.span()) { let mut err = syn::Error::new( ident.span(), format!("duplicate function '{}'", ident.to_string()), @@ -227,11 +231,13 @@ pub(crate) fn check_rename_collisions(fns: &Vec) -> Result<(), syn:: )); return Err(err); } + let key = make_key(ident.to_string(), itemfn); + fn_sig.insert(key, ident.span()); } } + for (new_name, attr_span) in renames.drain() { - let new_name = new_name.split('.').next().unwrap(); - if let Some(fn_span) = names.get(new_name) { + if let Some(fn_span) = fn_sig.get(&new_name) { let mut err = syn::Error::new( attr_span, format!("duplicate Rhai signature for '{}'", &new_name), From a2551a4650b76abe9998b3a36852a938419dfc4e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 26 Sep 2020 00:30:30 +0800 Subject: [PATCH 09/25] Check rename collisions with special names. --- codegen/src/function.rs | 42 ++++++++++++------- codegen/src/rhai_module.rs | 26 ++++++++---- ...n_rename_collision_oneattr_multiple.stderr | 14 +++---- 3 files changed, 52 insertions(+), 30 deletions(-) diff --git a/codegen/src/function.rs b/codegen/src/function.rs index 2126fd73..72ad2b64 100644 --- a/codegen/src/function.rs +++ b/codegen/src/function.rs @@ -43,6 +43,30 @@ impl Default for FnSpecialAccess { } } +impl FnSpecialAccess { + pub fn get_fn_name(&self) -> Option<(String, String, proc_macro2::Span)> { + match self { + FnSpecialAccess::None => None, + FnSpecialAccess::Property(Property::Get(ref g)) => { + Some((format!("get${}", g.to_string()), g.to_string(), g.span())) + } + FnSpecialAccess::Property(Property::Set(ref s)) => { + Some((format!("set${}", s.to_string()), s.to_string(), s.span())) + } + FnSpecialAccess::Index(Index::Get) => Some(( + FN_IDX_GET.to_string(), + "index_get".to_string(), + proc_macro2::Span::call_site(), + )), + FnSpecialAccess::Index(Index::Set) => Some(( + FN_IDX_SET.to_string(), + "index_set".to_string(), + proc_macro2::Span::call_site(), + )), + } + } +} + #[derive(Debug, Default)] pub(crate) struct ExportedFnParams { pub name: Option>, @@ -363,22 +387,8 @@ impl ExportedFn { }) .unwrap_or_else(|| Vec::new()); - match self.params.special { - FnSpecialAccess::None => {} - FnSpecialAccess::Property(Property::Get(ref g)) => literals.push(syn::LitStr::new( - &format!("get${}", g.to_string()), - g.span(), - )), - FnSpecialAccess::Property(Property::Set(ref s)) => literals.push(syn::LitStr::new( - &format!("set${}", s.to_string()), - s.span(), - )), - FnSpecialAccess::Index(Index::Get) => { - literals.push(syn::LitStr::new(FN_IDX_GET, proc_macro2::Span::call_site())) - } - FnSpecialAccess::Index(Index::Set) => { - literals.push(syn::LitStr::new(FN_IDX_SET, proc_macro2::Span::call_site())) - } + if let Some((s, _, span)) = self.params.special.get_fn_name() { + literals.push(syn::LitStr::new(&s, span)); } if literals.is_empty() { diff --git a/codegen/src/rhai_module.rs b/codegen/src/rhai_module.rs index 473c3398..ab0dd117 100644 --- a/codegen/src/rhai_module.rs +++ b/codegen/src/rhai_module.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use quote::{quote, ToTokens}; use crate::attrs::ExportScope; -use crate::function::ExportedFn; +use crate::function::{ExportedFn, FnSpecialAccess}; use crate::module::Module; pub(crate) type ExportedConst = (String, Box, syn::Expr); @@ -202,18 +202,29 @@ pub(crate) fn check_rename_collisions(fns: &Vec) -> Result<(), syn:: let mut fn_sig = HashMap::::new(); for itemfn in fns.iter() { - if let Some(ref names) = itemfn.params().name { - for name in names { + if itemfn.params().name.is_some() || itemfn.params().special != FnSpecialAccess::None { + let mut names = itemfn + .params() + .name + .as_ref() + .map(|v| v.iter().map(|n| (n.clone(), n.clone())).collect()) + .unwrap_or_else(|| Vec::new()); + + if let Some((s, n, _)) = itemfn.params().special.get_fn_name() { + names.push((s, n)); + } + + for (name, fn_name) in names { let current_span = itemfn.params().span.as_ref().unwrap(); let key = make_key(name.clone(), itemfn); if let Some(other_span) = renames.insert(key, *current_span) { let mut err = syn::Error::new( *current_span, - format!("duplicate Rhai signature for '{}'", &name), + format!("duplicate Rhai signature for '{}'", &fn_name), ); err.combine(syn::Error::new( other_span, - format!("duplicated function renamed '{}'", &name), + format!("duplicated function renamed '{}'", &fn_name), )); return Err(err); } @@ -237,14 +248,15 @@ pub(crate) fn check_rename_collisions(fns: &Vec) -> Result<(), syn:: } for (new_name, attr_span) in renames.drain() { + let fn_name = new_name.split('.').next().unwrap(); if let Some(fn_span) = fn_sig.get(&new_name) { let mut err = syn::Error::new( attr_span, - format!("duplicate Rhai signature for '{}'", &new_name), + format!("duplicate Rhai signature for '{}'", &fn_name), ); err.combine(syn::Error::new( *fn_span, - format!("duplicated function '{}'", &new_name), + format!("duplicated function '{}'", &fn_name), )); return Err(err); } diff --git a/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr b/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr index 77375279..78770b8d 100644 --- a/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr +++ b/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr @@ -1,15 +1,15 @@ -error: duplicate Rhai signature for 'foo' +error: duplicate Rhai signature for 'bar' + --> $DIR/rhai_fn_rename_collision_oneattr_multiple.rs:17:15 + | +17 | #[rhai_fn(get = "bar")] + | ^^^ + +error: duplicated function renamed 'bar' --> $DIR/rhai_fn_rename_collision_oneattr_multiple.rs:12:15 | 12 | #[rhai_fn(name = "foo", get = "bar")] | ^^^^^^^^^^^^^^^^^^^^^^^^^ -error: duplicated function 'foo' - --> $DIR/rhai_fn_rename_collision_oneattr_multiple.rs:18:12 - | -18 | pub fn foo(input: Point) -> bool { - | ^^^ - error[E0433]: failed to resolve: use of undeclared crate or module `test_module` --> $DIR/rhai_fn_rename_collision_oneattr_multiple.rs:25:8 | From 371b7fd00bb738863e4b105abc4cd96f355c642c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 26 Sep 2020 00:46:47 +0800 Subject: [PATCH 10/25] Fix tests. --- .../ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr | 2 +- tests/string.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr b/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr index 78770b8d..67b9eef5 100644 --- a/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr +++ b/codegen/ui_tests/rhai_fn_rename_collision_oneattr_multiple.stderr @@ -2,7 +2,7 @@ error: duplicate Rhai signature for 'bar' --> $DIR/rhai_fn_rename_collision_oneattr_multiple.rs:17:15 | 17 | #[rhai_fn(get = "bar")] - | ^^^ + | ^^^^^^^^^^^ error: duplicated function renamed 'bar' --> $DIR/rhai_fn_rename_collision_oneattr_multiple.rs:12:15 diff --git a/tests/string.rs b/tests/string.rs index 786ec106..13f78640 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -188,6 +188,7 @@ fn test_string_fn() -> Result<(), Box> { } #[cfg(not(feature = "no_object"))] +#[cfg(not(feature = "no_index"))] #[test] fn test_string_split() -> Result<(), Box> { let engine = Engine::new(); From 0d7d54e21c49f2ba5090dd9df7247f3485a9a96a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 26 Sep 2020 12:08:15 +0800 Subject: [PATCH 11/25] Simplify rename checking. --- codegen/src/rhai_module.rs | 61 ++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/codegen/src/rhai_module.rs b/codegen/src/rhai_module.rs index ab0dd117..f6939ae7 100644 --- a/codegen/src/rhai_module.rs +++ b/codegen/src/rhai_module.rs @@ -183,23 +183,24 @@ pub(crate) fn flatten_type_groups(ty: &syn::Type) -> &syn::Type { } pub(crate) fn check_rename_collisions(fns: &Vec) -> Result<(), syn::Error> { - fn make_key(name: String, itemfn: &ExportedFn) -> String { - itemfn.arg_list().fold(name, |mut argstr, fnarg| { - let type_string: String = match fnarg { - syn::FnArg::Receiver(_) => unimplemented!("receiver rhai_fns not implemented"), - syn::FnArg::Typed(syn::PatType { ref ty, .. }) => { - ty.as_ref().to_token_stream().to_string() - } - }; - argstr.push('.'); - argstr.push_str(&type_string); - argstr - }) + fn make_key(name: impl ToString, itemfn: &ExportedFn) -> String { + itemfn + .arg_list() + .fold(name.to_string(), |mut argstr, fnarg| { + let type_string: String = match fnarg { + syn::FnArg::Receiver(_) => unimplemented!("receiver rhai_fns not implemented"), + syn::FnArg::Typed(syn::PatType { ref ty, .. }) => { + ty.as_ref().to_token_stream().to_string() + } + }; + argstr.push('.'); + argstr.push_str(&type_string); + argstr + }) } let mut renames = HashMap::::new(); - let mut fn_names = HashMap::::new(); - let mut fn_sig = HashMap::::new(); + let mut fn_defs = HashMap::::new(); for itemfn in fns.iter() { if itemfn.params().name.is_some() || itemfn.params().special != FnSpecialAccess::None { @@ -216,7 +217,7 @@ pub(crate) fn check_rename_collisions(fns: &Vec) -> Result<(), syn:: for (name, fn_name) in names { let current_span = itemfn.params().span.as_ref().unwrap(); - let key = make_key(name.clone(), itemfn); + let key = make_key(&name, itemfn); if let Some(other_span) = renames.insert(key, *current_span) { let mut err = syn::Error::new( *current_span, @@ -231,7 +232,7 @@ pub(crate) fn check_rename_collisions(fns: &Vec) -> Result<(), syn:: } } else { let ident = itemfn.name(); - if let Some(other_span) = fn_names.insert(ident.to_string(), ident.span()) { + if let Some(other_span) = fn_defs.insert(ident.to_string(), ident.span()) { let mut err = syn::Error::new( ident.span(), format!("duplicate function '{}'", ident.to_string()), @@ -242,24 +243,20 @@ pub(crate) fn check_rename_collisions(fns: &Vec) -> Result<(), syn:: )); return Err(err); } - let key = make_key(ident.to_string(), itemfn); - fn_sig.insert(key, ident.span()); + let key = make_key(ident, itemfn); + if let Some(fn_span) = renames.get(&key) { + let mut err = syn::Error::new( + ident.span(), + format!("duplicate Rhai signature for '{}'", &ident), + ); + err.combine(syn::Error::new( + *fn_span, + format!("duplicated function '{}'", &ident), + )); + return Err(err); + } } } - for (new_name, attr_span) in renames.drain() { - let fn_name = new_name.split('.').next().unwrap(); - if let Some(fn_span) = fn_sig.get(&new_name) { - let mut err = syn::Error::new( - attr_span, - format!("duplicate Rhai signature for '{}'", &fn_name), - ); - err.combine(syn::Error::new( - *fn_span, - format!("duplicated function '{}'", &fn_name), - )); - return Err(err); - } - } Ok(()) } From 984ef4874173037f24341823b485931e011362c4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 26 Sep 2020 12:17:16 +0800 Subject: [PATCH 12/25] Fix test. --- .../rhai_fn_rename_collision_oneattr.stderr | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr b/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr index b10e434c..1a9ea35f 100644 --- a/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr +++ b/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr @@ -1,17 +1,17 @@ error: duplicate Rhai signature for 'foo' - --> $DIR/rhai_fn_rename_collision_oneattr.rs:12:15 - | -12 | #[rhai_fn(name = "foo")] - | ^^^^^^^^^^^^ - -error: duplicated function 'foo' - --> $DIR/rhai_fn_rename_collision_oneattr.rs:17:12 +Error: --> $DIR/rhai_fn_rename_collision_oneattr.rs:17:12 | 17 | pub fn foo(input: Point) -> bool { | ^^^ +error: duplicated function 'foo' +Error: --> $DIR/rhai_fn_rename_collision_oneattr.rs:12:15 + | +12 | #[rhai_fn(name = "foo")] + | ^^^^^^^^^^^^ + error[E0433]: failed to resolve: use of undeclared crate or module `test_module` - --> $DIR/rhai_fn_rename_collision_oneattr.rs:27:8 +Error: --> $DIR/rhai_fn_rename_collision_oneattr.rs:27:8 | 27 | if test_module::test_fn(n) { | ^^^^^^^^^^^ use of undeclared crate or module `test_module` From 91886e30d27b4cb7e01f9561eebc0875ed509bbe Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 26 Sep 2020 12:28:28 +0800 Subject: [PATCH 13/25] Fix test output. --- codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr b/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr index 1a9ea35f..fc5170a6 100644 --- a/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr +++ b/codegen/ui_tests/rhai_fn_rename_collision_oneattr.stderr @@ -1,17 +1,17 @@ error: duplicate Rhai signature for 'foo' -Error: --> $DIR/rhai_fn_rename_collision_oneattr.rs:17:12 + --> $DIR/rhai_fn_rename_collision_oneattr.rs:17:12 | 17 | pub fn foo(input: Point) -> bool { | ^^^ error: duplicated function 'foo' -Error: --> $DIR/rhai_fn_rename_collision_oneattr.rs:12:15 + --> $DIR/rhai_fn_rename_collision_oneattr.rs:12:15 | 12 | #[rhai_fn(name = "foo")] | ^^^^^^^^^^^^ error[E0433]: failed to resolve: use of undeclared crate or module `test_module` -Error: --> $DIR/rhai_fn_rename_collision_oneattr.rs:27:8 + --> $DIR/rhai_fn_rename_collision_oneattr.rs:27:8 | 27 | if test_module::test_fn(n) { | ^^^^^^^^^^^ use of undeclared crate or module `test_module` From e62d1cd3ff0bcee504e4a1896ffd07519bc4288d Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 26 Sep 2020 11:35:18 +0800 Subject: [PATCH 14/25] Better error message for setter. --- src/fn_call.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/fn_call.rs b/src/fn_call.rs index 1aeaa473..eeba460a 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -288,7 +288,7 @@ impl Engine { if let Some(prop) = extract_prop_from_getter(fn_name) { return EvalAltResult::ErrorDotExpr( format!( - "Unknown property '{}' for {}, or it is write-only", + "Failed to get property '{}' of '{}' - the property may not exist, or it may be write-only", prop, self.map_type_name(args[0].type_name()) ), @@ -301,9 +301,10 @@ impl Engine { if let Some(prop) = extract_prop_from_setter(fn_name) { return EvalAltResult::ErrorDotExpr( format!( - "Unknown property '{}' for {}, or it is read-only", + "Failed to set property '{}' of '{}' - the property may not exist, may be read-only, or '{}' is the wrong type", prop, - self.map_type_name(args[0].type_name()) + self.map_type_name(args[0].type_name()), + self.map_type_name(args[1].type_name()), ), Position::none(), ) From 03dce8632843e149b95a20d5f63f4b8f0c8901a1 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 26 Sep 2020 16:27:38 +0800 Subject: [PATCH 15/25] Support Dynamic as function default return value. --- RELEASES.md | 2 ++ doc/src/language/string-fn.md | 28 ++++++++++++++-------------- src/engine.rs | 35 ++++++++++++++++++++--------------- src/fn_call.rs | 10 +++++----- src/fn_native.rs | 2 +- src/optimize.rs | 2 +- src/parser.rs | 2 +- 7 files changed, 44 insertions(+), 37 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index bd65e687..a4c31fee 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -32,6 +32,8 @@ New features * `AST::iter_functions` and `Module::iter_script_fn_info` to iterate functions. * Functions iteration functions now take `FnMut` instead of `Fn`. * New `FileModuleResolver` that encapsulates the entire `AST` of the module script, allowing function cross-calling. The old version is renamed `MergingFileModuleResolver`. +* `split` function for splitting strings. + Version 0.18.3 ============== diff --git a/doc/src/language/string-fn.md b/doc/src/language/string-fn.md index 5c3be69c..a9caaa94 100644 --- a/doc/src/language/string-fn.md +++ b/doc/src/language/string-fn.md @@ -6,20 +6,20 @@ Built-in String Functions The following standard methods (mostly defined in the [`MoreStringPackage`][packages] but excluded if using a [raw `Engine`]) operate on [strings]: -| Function | Parameter(s) | Description | -| ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | -| `len` method and property | _none_ | returns the number of characters (not number of bytes) in the string | -| `pad` | character to pad, target length | pads the string with an character to at least a specified length | -| `+=` operator, `append` | character/string to append | Adds a character or a string to the end of another string | -| `clear` | _none_ | empties the string | -| `truncate` | target length | cuts off the string at exactly a specified number of characters | -| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | -| `index_of` | character/sub-string to search for, start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found | -| `sub_string` | start index, length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) | -| `split` | delimiter character/string | splits the string by the specified delimiter, returning an [array] of string segments | -| `crop` | start index, length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) | -| `replace` | target character/sub-string, replacement character/string | replaces a sub-string with another | -| `trim` | _none_ | trims the string of whitespace at the beginning and end | +| Function | Parameter(s) | Description | +| ------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `len` method and property | _none_ | returns the number of characters (not number of bytes) in the string | +| `pad` | character to pad, target length | pads the string with an character to at least a specified length | +| `+=` operator, `append` | character/string to append | Adds a character or a string to the end of another string | +| `clear` | _none_ | empties the string | +| `truncate` | target length | cuts off the string at exactly a specified number of characters | +| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | +| `index_of` | character/sub-string to search for, start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found | +| `sub_string` | start index, length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) | +| `split` | delimiter character/string | splits the string by the specified delimiter, returning an [array] of string segments; not available under [`no_index`] | +| `crop` | start index, length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) | +| `replace` | target character/sub-string, replacement character/string | replaces a sub-string with another | +| `trim` | _none_ | trims the string of whitespace at the beginning and end | Examples -------- diff --git a/src/engine.rs b/src/engine.rs index d4715eca..cceda886 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -769,7 +769,7 @@ impl Engine { let args = &mut [val, &mut idx_val2, &mut new_val]; self.exec_fn_call( - state, lib, FN_IDX_SET, 0, args, is_ref, true, false, None, None, + state, lib, FN_IDX_SET, 0, args, is_ref, true, false, None, &None, level, ) .map_err(|err| match *err { @@ -798,8 +798,9 @@ impl Engine { // xxx.fn_name(arg_expr_list) Expr::FnCall(x) if x.1.is_none() => { let ((name, native, _, pos), _, hash, _, def_val) = x.as_ref(); + let def_val = def_val.map(Into::::into); self.make_method_call( - state, lib, name, *hash, target, idx_val, *def_val, *native, false, + state, lib, name, *hash, target, idx_val, &def_val, *native, false, level, ) .map_err(|err| err.new_position(*pos)) @@ -833,7 +834,7 @@ impl Engine { let mut new_val = new_val; let mut args = [target.as_mut(), new_val.as_mut().unwrap()]; self.exec_fn_call( - state, lib, setter, 0, &mut args, is_ref, true, false, None, None, + state, lib, setter, 0, &mut args, is_ref, true, false, None, &None, level, ) .map(|(v, _)| (v, true)) @@ -844,7 +845,7 @@ impl Engine { let ((_, getter, _), pos) = x.as_ref(); let mut args = [target.as_mut()]; self.exec_fn_call( - state, lib, getter, 0, &mut args, is_ref, true, false, None, None, + state, lib, getter, 0, &mut args, is_ref, true, false, None, &None, level, ) .map(|(v, _)| (v, false)) @@ -865,9 +866,10 @@ impl Engine { // {xxx:map}.fn_name(arg_expr_list)[expr] | {xxx:map}.fn_name(arg_expr_list).expr Expr::FnCall(x) if x.1.is_none() => { let ((name, native, _, pos), _, hash, _, def_val) = x.as_ref(); + let def_val = def_val.map(Into::::into); let (val, _) = self .make_method_call( - state, lib, name, *hash, target, idx_val, *def_val, + state, lib, name, *hash, target, idx_val, &def_val, *native, false, level, ) .map_err(|err| err.new_position(*pos))?; @@ -898,7 +900,7 @@ impl Engine { let (mut val, updated) = self .exec_fn_call( state, lib, getter, 0, args, is_ref, true, false, None, - None, level, + &None, level, ) .map_err(|err| err.new_position(*pos))?; @@ -924,7 +926,7 @@ impl Engine { arg_values[1] = val; self.exec_fn_call( state, lib, setter, 0, arg_values, is_ref, true, false, - None, None, level, + None, &None, level, ) .or_else( |err| match *err { @@ -942,9 +944,10 @@ impl Engine { // xxx.fn_name(arg_expr_list)[expr] | xxx.fn_name(arg_expr_list).expr Expr::FnCall(x) if x.1.is_none() => { let ((name, native, _, pos), _, hash, _, def_val) = x.as_ref(); + let def_val = def_val.map(Into::::into); let (mut val, _) = self .make_method_call( - state, lib, name, *hash, target, idx_val, *def_val, + state, lib, name, *hash, target, idx_val, &def_val, *native, false, level, ) .map_err(|err| err.new_position(*pos))?; @@ -1202,7 +1205,7 @@ impl Engine { let mut idx = idx; let args = &mut [val, &mut idx]; self.exec_fn_call( - state, _lib, FN_IDX_GET, 0, args, is_ref, true, false, None, None, _level, + state, _lib, FN_IDX_GET, 0, args, is_ref, true, false, None, &None, _level, ) .map(|(v, _)| v.into()) .map_err(|err| match *err { @@ -1245,8 +1248,9 @@ impl Engine { let op = "=="; // Call the `==` operator to compare each value + let def_value = Some(false.into()); + for value in rhs_value.iter_mut() { - let def_value = Some(false); let args = &mut [&mut lhs_value.clone(), value]; // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. @@ -1254,7 +1258,7 @@ impl Engine { calc_fn_hash(empty(), op, args.len(), args.iter().map(|a| a.type_id())); if self - .call_native_fn(state, lib, op, hash, args, false, false, def_value) + .call_native_fn(state, lib, op, hash, args, false, false, &def_value) .map_err(|err| err.new_position(rhs.position()))? .0 .as_bool() @@ -1264,7 +1268,7 @@ impl Engine { } } - Ok(false.into()) + Ok(def_value.unwrap()) } #[cfg(not(feature = "no_object"))] Dynamic(Union::Map(rhs_value)) => match lhs_value { @@ -1395,7 +1399,7 @@ impl Engine { // Run function let (value, _) = self .exec_fn_call( - state, lib, op, 0, args, false, false, false, None, None, + state, lib, op, 0, args, false, false, false, None, &None, level, ) .map_err(|err| err.new_position(*op_pos))?; @@ -1431,7 +1435,7 @@ impl Engine { &mut rhs_val, ]; self.exec_fn_call( - state, lib, op, 0, args, false, false, false, None, None, level, + state, lib, op, 0, args, false, false, false, None, &None, level, ) .map(|(v, _)| v) .map_err(|err| err.new_position(*op_pos))? @@ -1499,8 +1503,9 @@ impl Engine { // Normal function call Expr::FnCall(x) if x.1.is_none() => { let ((name, native, capture, pos), _, hash, args_expr, def_val) = x.as_ref(); + let def_val = def_val.map(Into::::into); self.make_function_call( - scope, mods, state, lib, this_ptr, name, args_expr, *def_val, *hash, *native, + scope, mods, state, lib, this_ptr, name, args_expr, &def_val, *hash, *native, false, *capture, level, ) .map_err(|err| err.new_position(*pos)) diff --git a/src/fn_call.rs b/src/fn_call.rs index eeba460a..9ee74b86 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -211,7 +211,7 @@ impl Engine { args: &mut FnCallArgs, is_ref: bool, pub_only: bool, - def_val: Option, + def_val: &Option, ) -> Result<(Dynamic, bool), Box> { self.inc_operations(state)?; @@ -281,7 +281,7 @@ impl Engine { // Return default value (if any) if let Some(val) = def_val { - return Ok((val.into(), false)); + return Ok((val.clone(), false)); } // Getter function not found? @@ -469,7 +469,7 @@ impl Engine { _is_method: bool, pub_only: bool, _capture: Option, - def_val: Option, + def_val: &Option, _level: usize, ) -> Result<(Dynamic, bool), Box> { // Check for data race. @@ -669,7 +669,7 @@ impl Engine { hash_script: u64, target: &mut Target, idx_val: Dynamic, - def_val: Option, + def_val: &Option, native: bool, pub_only: bool, level: usize, @@ -815,7 +815,7 @@ impl Engine { this_ptr: &mut Option<&mut Dynamic>, name: &str, args_expr: &[Expr], - def_val: Option, + def_val: &Option, mut hash_script: u64, native: bool, pub_only: bool, diff --git a/src/fn_native.rs b/src/fn_native.rs index 820e5dc7..041ff224 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -145,7 +145,7 @@ impl FnPtr { has_this, true, None, - None, + &None, 0, ) .map(|(v, _)| v) diff --git a/src/optimize.rs b/src/optimize.rs index 60bfb2cc..0e916878 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -142,7 +142,7 @@ fn call_fn_with_constant_arguments( arg_values.iter_mut().collect::>().as_mut(), false, true, - None, + &None, ) .ok() .map(|(v, _)| v) diff --git a/src/parser.rs b/src/parser.rs index 9366a3d6..98de18fb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -790,7 +790,7 @@ pub enum Expr { Option>, u64, StaticVec, - Option, + Option, // Default value is `bool` in order for `Expr` to be `Hash`. )>, ), /// expr op= expr From e0483f2a509938b090b006309acda9503e1efd5b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 26 Sep 2020 19:41:04 +0800 Subject: [PATCH 16/25] Implement Dynamic::From --- src/any.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/any.rs b/src/any.rs index a1e532aa..12836aff 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1256,3 +1256,9 @@ impl From> for Dynamic { Self(Union::FnPtr(value)) } } +impl From for Dynamic { + #[inline(always)] + fn from(value: Instant) -> Self { + Self(Union::Variant(Box::new(Box::new(value)))) + } +} From 726b0306161b4d883fcb55f6ef8f985a286687fa Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 26 Sep 2020 19:45:33 +0800 Subject: [PATCH 17/25] Implement +/- operators for timestamp. --- doc/src/language/timestamps.md | 10 +-- src/packages/time_basic.rs | 117 ++++++++++++++++++++++++++++++--- tests/time.rs | 57 ++++++++++++++++ 3 files changed, 170 insertions(+), 14 deletions(-) diff --git a/doc/src/language/timestamps.md b/doc/src/language/timestamps.md index 8fef7cdd..0a8a0adc 100644 --- a/doc/src/language/timestamps.md +++ b/doc/src/language/timestamps.md @@ -18,10 +18,12 @@ Built-in Functions The following methods (defined in the [`BasicTimePackage`][packages] but excluded if using a [raw `Engine`]) operate on timestamps: -| Function | Parameter(s) | Description | -| ----------------------------- | ---------------------------------- | -------------------------------------------------------- | -| `elapsed` method and property | _none_ | returns the number of seconds since the timestamp | -| `-` operator | later timestamp, earlier timestamp | returns the number of seconds between the two timestamps | +| Function | Parameter(s) | Description | +| ----------------------------- | ------------------------------------------- | -------------------------------------------------------- | +| `elapsed` method and property | _none_ | returns the number of seconds since the timestamp | +| `-` operator | 1) later timestamp
2) earlier timestamp | returns the number of seconds between the two timestamps | +| `+` operator | number of seconds to add | returns a new timestamp | +| `-` operator | number of seconds to subtract | returns a new timestamp | Examples diff --git a/src/packages/time_basic.rs b/src/packages/time_basic.rs index caa5fc47..b6aabdd6 100644 --- a/src/packages/time_basic.rs +++ b/src/packages/time_basic.rs @@ -1,23 +1,23 @@ #![cfg(not(feature = "no_std"))] -#[cfg(feature = "no_float")] -use super::{arithmetic::make_err, math_basic::MAX_INT}; +use super::{arithmetic::make_err as make_arithmetic_err, math_basic::MAX_INT}; +use crate::any::Dynamic; use crate::def_package; +use crate::parser::INT; use crate::plugin::*; use crate::result::EvalAltResult; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; -#[cfg(feature = "no_float")] -use crate::parser::INT; +use crate::stdlib::boxed::Box; #[cfg(not(target_arch = "wasm32"))] -use crate::stdlib::time::Instant; +use crate::stdlib::time::{Duration, Instant}; #[cfg(target_arch = "wasm32")] -use instant::Instant; +use instant::{Duration, Instant}; def_package!(crate:BasicTimePackage:"Basic timing utilities.", lib, { // Register date/time functions @@ -43,7 +43,7 @@ mod time_functions { let seconds = timestamp.elapsed().as_secs(); if cfg!(not(feature = "unchecked")) && seconds > (MAX_INT as u64) { - Err(make_err(format!( + Err(make_arithmetic_err(format!( "Integer overflow for timestamp.elapsed: {}", seconds ))) @@ -70,18 +70,18 @@ mod time_functions { let seconds = (ts2 - ts1).as_secs(); if cfg!(not(feature = "unchecked")) && seconds > (MAX_INT as u64) { - Err(make_err(format!( + Err(make_arithmetic_err(format!( "Integer overflow for timestamp duration: -{}", seconds ))) } else { - Ok(Dynamic::from(-(seconds as INT))) + Ok((-(seconds as INT)).into()) } } else { let seconds = (ts1 - ts2).as_secs(); if cfg!(not(feature = "unchecked")) && seconds > (MAX_INT as u64) { - Err(make_err(format!( + Err(make_arithmetic_err(format!( "Integer overflow for timestamp duration: {}", seconds ))) @@ -91,6 +91,103 @@ mod time_functions { } } + #[cfg(not(feature = "no_float"))] + pub mod float_functions { + #[rhai_fn(return_raw, name = "+")] + pub fn add(x: Instant, seconds: FLOAT) -> Result> { + if seconds < 0.0 { + return subtract(x, -seconds); + } + + if cfg!(not(feature = "unchecked")) { + if seconds > (MAX_INT as FLOAT) { + Err(make_arithmetic_err(format!( + "Integer overflow for timestamp add: {}", + seconds + ))) + } else { + x.checked_add(Duration::from_millis((seconds * 1000.0) as u64)) + .ok_or_else(|| { + make_arithmetic_err(format!( + "Timestamp overflow when adding {} second(s)", + seconds + )) + }) + .map(Into::::into) + } + } else { + Ok((x + Duration::from_millis((seconds * 1000.0) as u64)).into()) + } + } + + #[rhai_fn(return_raw, name = "-")] + pub fn subtract(x: Instant, seconds: FLOAT) -> Result> { + if seconds < 0.0 { + return add(x, -seconds); + } + + if cfg!(not(feature = "unchecked")) { + if seconds > (MAX_INT as FLOAT) { + Err(make_arithmetic_err(format!( + "Integer overflow for timestamp add: {}", + seconds + ))) + } else { + x.checked_sub(Duration::from_millis((seconds * 1000.0) as u64)) + .ok_or_else(|| { + make_arithmetic_err(format!( + "Timestamp overflow when adding {} second(s)", + seconds + )) + }) + .map(Into::::into) + } + } else { + Ok((x - Duration::from_millis((seconds * 1000.0) as u64)).into()) + } + } + } + + #[rhai_fn(return_raw, name = "+")] + pub fn add(x: Instant, seconds: INT) -> Result> { + if seconds < 0 { + return subtract(x, -seconds); + } + + if cfg!(not(feature = "unchecked")) { + x.checked_add(Duration::from_secs(seconds as u64)) + .ok_or_else(|| { + make_arithmetic_err(format!( + "Timestamp overflow when adding {} second(s)", + seconds + )) + }) + .map(Into::::into) + } else { + Ok((x + Duration::from_secs(seconds as u64)).into()) + } + } + + #[rhai_fn(return_raw, name = "-")] + pub fn subtract(x: Instant, seconds: INT) -> Result> { + if seconds < 0 { + return add(x, -seconds); + } + + if cfg!(not(feature = "unchecked")) { + x.checked_sub(Duration::from_secs(seconds as u64)) + .ok_or_else(|| { + make_arithmetic_err(format!( + "Timestamp overflow when adding {} second(s)", + seconds + )) + }) + .map(Into::::into) + } else { + Ok((x - Duration::from_secs(seconds as u64)).into()) + } + } + #[rhai_fn(name = "==")] #[inline(always)] pub fn eq(x: Instant, y: Instant) -> bool { diff --git a/tests/time.rs b/tests/time.rs index ad7fc4c9..54240db8 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -50,3 +50,60 @@ fn test_timestamp() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_timestamp_op() -> Result<(), Box> { + let engine = Engine::new(); + + #[cfg(not(feature = "no_float"))] + assert!( + (engine.eval::( + r#" + let time1 = timestamp(); + let time2 = time1 + 123.45; + time2 - time1 + "# + )? - 123.45) + .abs() + < 0.001 + ); + + #[cfg(not(feature = "no_float"))] + assert!( + (engine.eval::( + r#" + let time1 = timestamp(); + let time2 = time1 - 123.45; + time1 - time2 + "# + )? - 123.45) + .abs() + < 0.001 + ); + + #[cfg(feature = "no_float")] + assert_eq!( + engine.eval::( + r#" + let time1 = timestamp(); + let time2 = time1 + 42; + time2 - time1 + "# + )?, + 42 + ); + + #[cfg(feature = "no_float")] + assert_eq!( + engine.eval::( + r#" + let time1 = timestamp(); + let time2 = time1 - 42; + time1 - time2 + "# + )?, + 42 + ); + + Ok(()) +} From a2694114d12881120f1a799f93b2c428ef7fee4d Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 27 Sep 2020 11:14:50 +0800 Subject: [PATCH 18/25] Better panic messages. --- src/any.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/any.rs b/src/any.rs index 12836aff..75c88683 100644 --- a/src/any.rs +++ b/src/any.rs @@ -629,7 +629,7 @@ impl Dynamic { }; #[cfg(feature = "no_closure")] - unimplemented!() + panic!("'no_closure' feature does not support converting into a shared value"); } /// Convert the `Dynamic` value into specific type. @@ -781,7 +781,20 @@ impl Dynamic { /// ``` #[inline(always)] pub fn cast(self) -> T { - self.try_cast::().unwrap() + let self_type_name = if self.is_shared() { + // Avoid panics/deadlocks with shared values + "" + } else { + self.type_name() + }; + + self.try_cast::().unwrap_or_else(|| { + panic!( + "value is {} and cannot be cast to {}", + self_type_name, + type_name::() + ) + }) } /// Flatten the `Dynamic` and clone it. From 516f7b60d9e0ec2acc8437bbcbb397db9d3d9940 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 27 Sep 2020 18:47:20 +0800 Subject: [PATCH 19/25] Fix no_std build. --- src/any.rs | 1 + src/module/mod.rs | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/any.rs b/src/any.rs index 75c88683..d9dada0d 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1269,6 +1269,7 @@ impl From> for Dynamic { Self(Union::FnPtr(value)) } } +#[cfg(not(feature = "no_std"))] impl From for Dynamic { #[inline(always)] fn from(value: Instant) -> Self { diff --git a/src/module/mod.rs b/src/module/mod.rs index b6511eb8..bb107ad7 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -497,8 +497,6 @@ impl Module { /// are not determined), but the implementation is in Rust. #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_module"))] - #[cfg(not(feature = "no_std"))] - #[cfg(not(target_arch = "wasm32"))] pub(crate) fn set_raw_fn_as_scripted( &mut self, name: impl Into, From e1ce67adc257bb1350d2cc0d5d0c5734d786761b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 27 Sep 2020 22:15:35 +0800 Subject: [PATCH 20/25] Add TimeStamp variant. --- src/any.rs | 58 +++++++++++++++++++----- src/packages/time_basic.rs | 91 ++++++++++++++++++++++++-------------- 2 files changed, 104 insertions(+), 45 deletions(-) diff --git a/src/any.rs b/src/any.rs index d9dada0d..7cc67870 100644 --- a/src/any.rs +++ b/src/any.rs @@ -159,6 +159,8 @@ pub enum Union { #[cfg(not(feature = "no_object"))] Map(Box), FnPtr(Box), + #[cfg(not(feature = "no_std"))] + TimeStamp(Box), Variant(Box>), @@ -313,6 +315,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(_) => TypeId::of::(), Union::FnPtr(_) => TypeId::of::(), + #[cfg(not(feature = "no_std"))] + Union::TimeStamp(_) => TypeId::of::(), Union::Variant(value) => (***value).type_id(), @@ -345,9 +349,9 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(_) => "map", Union::FnPtr(_) => "Fn", - #[cfg(not(feature = "no_std"))] - Union::Variant(value) if value.is::() => "timestamp", + Union::TimeStamp(_) => "timestamp", + Union::Variant(value) => (***value).type_name(), #[cfg(not(feature = "no_closure"))] @@ -375,10 +379,6 @@ pub(crate) fn map_std_type_name(name: &str) -> &str { } else if name == type_name::() { "Fn" } else { - #[cfg(not(feature = "no_std"))] - if name == type_name::() { - return "timestamp"; - } #[cfg(not(feature = "no_index"))] if name == type_name::() { return "array"; @@ -387,6 +387,10 @@ pub(crate) fn map_std_type_name(name: &str) -> &str { if name == type_name::() { return "map"; } + #[cfg(not(feature = "no_std"))] + if name == type_name::() { + return "timestamp"; + } name } @@ -410,9 +414,9 @@ impl fmt::Display for Dynamic { fmt::Debug::fmt(value, f) } Union::FnPtr(value) => fmt::Display::fmt(value, f), - #[cfg(not(feature = "no_std"))] - Union::Variant(value) if value.is::() => f.write_str(""), + Union::TimeStamp(_) => f.write_str(""), + Union::Variant(value) => f.write_str((*value).type_name()), #[cfg(not(feature = "no_closure"))] @@ -449,9 +453,9 @@ impl fmt::Debug for Dynamic { fmt::Debug::fmt(value, f) } Union::FnPtr(value) => fmt::Debug::fmt(value, f), - #[cfg(not(feature = "no_std"))] - Union::Variant(value) if value.is::() => write!(f, ""), + Union::TimeStamp(_) => write!(f, ""), + Union::Variant(value) => write!(f, "{}", (*value).type_name()), #[cfg(not(feature = "no_closure"))] @@ -485,6 +489,8 @@ impl Clone for Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(ref value) => Self(Union::Map(value.clone())), Union::FnPtr(ref value) => Self(Union::FnPtr(value.clone())), + #[cfg(not(feature = "no_std"))] + Union::TimeStamp(ref value) => Self(Union::TimeStamp(value.clone())), Union::Variant(ref value) => (***value).clone_into_dynamic(), @@ -601,6 +607,14 @@ impl Dynamic { Err(val) => val, }; + #[cfg(not(feature = "no_std"))] + { + boxed = match unsafe_cast_box::<_, Instant>(boxed) { + Ok(timestamp) => return (*timestamp).into(), + Err(val) => val, + } + } + Self(Union::Variant(Box::new(boxed))) } @@ -738,6 +752,14 @@ impl Dynamic { }; } + #[cfg(not(feature = "no_std"))] + if TypeId::of::() == TypeId::of::() { + return match self.0 { + Union::TimeStamp(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), + _ => None, + }; + } + if TypeId::of::() == TypeId::of::<()>() { return match self.0 { Union::Unit(value) => unsafe_try_cast(value), @@ -993,6 +1015,13 @@ impl Dynamic { _ => None, }; } + #[cfg(not(feature = "no_std"))] + if TypeId::of::() == TypeId::of::() { + return match &self.0 { + Union::TimeStamp(value) => ::downcast_ref::(value.as_ref()), + _ => None, + }; + } if TypeId::of::() == TypeId::of::<()>() { return match &self.0 { Union::Unit(value) => ::downcast_ref::(value), @@ -1068,6 +1097,13 @@ impl Dynamic { _ => None, }; } + #[cfg(not(feature = "no_std"))] + if TypeId::of::() == TypeId::of::() { + return match &mut self.0 { + Union::TimeStamp(value) => ::downcast_mut::(value.as_mut()), + _ => None, + }; + } if TypeId::of::() == TypeId::of::<()>() { return match &mut self.0 { Union::Unit(value) => ::downcast_mut::(value), @@ -1273,6 +1309,6 @@ impl From> for Dynamic { impl From for Dynamic { #[inline(always)] fn from(value: Instant) -> Self { - Self(Union::Variant(Box::new(Box::new(value)))) + Self(Union::TimeStamp(Box::new(value))) } } diff --git a/src/packages/time_basic.rs b/src/packages/time_basic.rs index b6aabdd6..1fee278b 100644 --- a/src/packages/time_basic.rs +++ b/src/packages/time_basic.rs @@ -93,13 +93,10 @@ mod time_functions { #[cfg(not(feature = "no_float"))] pub mod float_functions { - #[rhai_fn(return_raw, name = "+")] - pub fn add(x: Instant, seconds: FLOAT) -> Result> { + fn add_impl(x: Instant, seconds: FLOAT) -> Result> { if seconds < 0.0 { - return subtract(x, -seconds); - } - - if cfg!(not(feature = "unchecked")) { + subtract_impl(x, -seconds) + } else if cfg!(not(feature = "unchecked")) { if seconds > (MAX_INT as FLOAT) { Err(make_arithmetic_err(format!( "Integer overflow for timestamp add: {}", @@ -113,20 +110,15 @@ mod time_functions { seconds )) }) - .map(Into::::into) } } else { - Ok((x + Duration::from_millis((seconds * 1000.0) as u64)).into()) + Ok(x + Duration::from_millis((seconds * 1000.0) as u64)) } } - - #[rhai_fn(return_raw, name = "-")] - pub fn subtract(x: Instant, seconds: FLOAT) -> Result> { + fn subtract_impl(x: Instant, seconds: FLOAT) -> Result> { if seconds < 0.0 { - return add(x, -seconds); - } - - if cfg!(not(feature = "unchecked")) { + add_impl(x, -seconds) + } else if cfg!(not(feature = "unchecked")) { if seconds > (MAX_INT as FLOAT) { Err(make_arithmetic_err(format!( "Integer overflow for timestamp add: {}", @@ -140,21 +132,39 @@ mod time_functions { seconds )) }) - .map(Into::::into) } } else { - Ok((x - Duration::from_millis((seconds * 1000.0) as u64)).into()) + Ok(x - Duration::from_millis((seconds * 1000.0) as u64)) } } + + #[rhai_fn(return_raw, name = "+")] + pub fn add(x: Instant, seconds: FLOAT) -> Result> { + add_impl(x, seconds).map(Into::::into) + } + #[rhai_fn(return_raw, name = "+=")] + pub fn add_assign(x: &mut Instant, seconds: FLOAT) -> Result> { + *x = add_impl(*x, seconds)?; + Ok(().into()) + } + #[rhai_fn(return_raw, name = "-")] + pub fn subtract(x: Instant, seconds: FLOAT) -> Result> { + subtract_impl(x, seconds).map(Into::::into) + } + #[rhai_fn(return_raw, name = "-=")] + pub fn subtract_assign( + x: &mut Instant, + seconds: FLOAT, + ) -> Result> { + *x = subtract_impl(*x, seconds)?; + Ok(().into()) + } } - #[rhai_fn(return_raw, name = "+")] - pub fn add(x: Instant, seconds: INT) -> Result> { + fn add_impl(x: Instant, seconds: INT) -> Result> { if seconds < 0 { - return subtract(x, -seconds); - } - - if cfg!(not(feature = "unchecked")) { + subtract_impl(x, -seconds) + } else if cfg!(not(feature = "unchecked")) { x.checked_add(Duration::from_secs(seconds as u64)) .ok_or_else(|| { make_arithmetic_err(format!( @@ -162,19 +172,14 @@ mod time_functions { seconds )) }) - .map(Into::::into) } else { - Ok((x + Duration::from_secs(seconds as u64)).into()) + Ok(x + Duration::from_secs(seconds as u64)) } } - - #[rhai_fn(return_raw, name = "-")] - pub fn subtract(x: Instant, seconds: INT) -> Result> { + fn subtract_impl(x: Instant, seconds: INT) -> Result> { if seconds < 0 { - return add(x, -seconds); - } - - if cfg!(not(feature = "unchecked")) { + add_impl(x, -seconds) + } else if cfg!(not(feature = "unchecked")) { x.checked_sub(Duration::from_secs(seconds as u64)) .ok_or_else(|| { make_arithmetic_err(format!( @@ -182,12 +187,30 @@ mod time_functions { seconds )) }) - .map(Into::::into) } else { - Ok((x - Duration::from_secs(seconds as u64)).into()) + Ok(x - Duration::from_secs(seconds as u64)) } } + #[rhai_fn(return_raw, name = "+")] + pub fn add(x: Instant, seconds: INT) -> Result> { + add_impl(x, seconds).map(Into::::into) + } + #[rhai_fn(return_raw, name = "+=")] + pub fn add_assign(x: &mut Instant, seconds: INT) -> Result> { + *x = add_impl(*x, seconds)?; + Ok(().into()) + } + #[rhai_fn(return_raw, name = "-")] + pub fn subtract(x: Instant, seconds: INT) -> Result> { + subtract_impl(x, seconds).map(Into::::into) + } + #[rhai_fn(return_raw, name = "-=")] + pub fn subtract_assign(x: &mut Instant, seconds: INT) -> Result> { + *x = subtract_impl(*x, seconds)?; + Ok(().into()) + } + #[rhai_fn(name = "==")] #[inline(always)] pub fn eq(x: Instant, y: Instant) -> bool { From f92d99216582e328cd4c6a9e31196bfd3a733ac9 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 27 Sep 2020 22:34:07 +0800 Subject: [PATCH 21/25] Handle timestamps in serde. --- src/serde/de.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/serde/de.rs b/src/serde/de.rs index 6b0b8a53..69a45765 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -160,9 +160,8 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { #[cfg(not(feature = "no_object"))] Union::Map(_) => self.deserialize_map(visitor), Union::FnPtr(_) => self.type_error(), - #[cfg(not(feature = "no_std"))] - Union::Variant(value) if value.is::() => self.type_error(), + Union::TimeStamp(_) => self.type_error(), Union::Variant(value) if value.is::() => self.deserialize_i8(visitor), Union::Variant(value) if value.is::() => self.deserialize_i16(visitor), From a1cf852bb946195c9ec5b0d8d383a007b007ddaf Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 28 Sep 2020 11:01:25 +0800 Subject: [PATCH 22/25] Merge in function pointer arguments. --- src/module/mod.rs | 40 +++++++++++++++++++++++++++++------- src/module/resolvers/stat.rs | 3 +-- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/module/mod.rs b/src/module/mod.rs index bb107ad7..674d93fe 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -3,7 +3,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; use crate::engine::Engine; -use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; +use crate::fn_native::{CallableFunction, FnCallArgs, FnPtr, IteratorFn, SendSync}; use crate::fn_register::by_value as cast_arg; use crate::parser::{FnAccess, FnAccess::Public}; use crate::result::EvalAltResult; @@ -1217,11 +1217,16 @@ impl Module { /// /// ### `private_namespace` parameter /// - /// If `true`, the entire `AST` is encapsulated into each function as a private namespace, - /// allowing functions to cross-call each other. + /// * If `true`, the entire `AST` is encapsulated into each function as a private namespace, + /// allowing functions to cross-call each other. Functions are searched in the module's + /// private namespace, and the global namespace cannot be accessed. /// - /// If `false`, each function is registered independently and cannot cross-call - /// each other. Functions are searched in the global namespace. + /// Under this scenario, if any argument is a function pointer, then the function referred + /// to by the function pointer is also _merged_ into the module's private namespace before + /// the function call, so passing simple closures to functions work as expected. + /// + /// * If `false`, each function is registered independently and cannot cross-call + /// each other. Functions are searched in the global namespace. /// /// # Examples /// @@ -1277,11 +1282,32 @@ impl Module { module.set_raw_fn_as_scripted( name, num_args, - move |engine: &Engine, _, args: &mut [&mut Dynamic]| { + move |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { + // Get anonymous functions + let anon_fns: StaticVec<_> = args + .iter() + .filter(|&arg| arg.is::()) + .map(|arg| arg.read_lock::().unwrap().get_fn_name().clone()) + .collect(); + + let mut lib_merged; + + let call_lib = if anon_fns.is_empty() { + // No anonymous functions - just pass the AST through + &ast_lib + } else { + // If there are anonymous functions, merge them in + lib_merged = ast_lib.clone(); + lib_merged.merge_filtered(&lib, &mut |_, name, _| { + anon_fns.iter().find(|s| s.as_str() == name).is_some() + }); + &lib_merged + }; + engine .call_fn_dynamic_raw( &mut Scope::new(), - &ast_lib, + call_lib, &fn_name, &mut None, args, diff --git a/src/module/resolvers/stat.rs b/src/module/resolvers/stat.rs index 5903ce57..563f5074 100644 --- a/src/module/resolvers/stat.rs +++ b/src/module/resolvers/stat.rs @@ -49,8 +49,7 @@ impl StaticModuleResolver { impl StaticModuleResolver { /// Add a module keyed by its path. - pub fn insert>(&mut self, path: S, mut module: Module) { - module.index_all_sub_modules(); + pub fn insert>(&mut self, path: S, module: Module) { self.0.insert(path.into(), module); } /// Remove a module given its path. From 5e43f2e5a4b56ea8789c2090390f287474e9ac04 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 28 Sep 2020 11:19:49 +0800 Subject: [PATCH 23/25] Better error handling messages. --- src/any.rs | 4 ++-- src/module/resolvers/collection.rs | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/any.rs b/src/any.rs index 7cc67870..10133b43 100644 --- a/src/any.rs +++ b/src/any.rs @@ -643,7 +643,7 @@ impl Dynamic { }; #[cfg(feature = "no_closure")] - panic!("'no_closure' feature does not support converting into a shared value"); + panic!("converting into a shared value is not supported under 'no_closure'"); } /// Convert the `Dynamic` value into specific type. @@ -812,7 +812,7 @@ impl Dynamic { self.try_cast::().unwrap_or_else(|| { panic!( - "value is {} and cannot be cast to {}", + "cannot cast {} value and to {}", self_type_name, type_name::() ) diff --git a/src/module/resolvers/collection.rs b/src/module/resolvers/collection.rs index b8cc2729..e513f51e 100644 --- a/src/module/resolvers/collection.rs +++ b/src/module/resolvers/collection.rs @@ -70,8 +70,13 @@ impl ModuleResolver for ModuleResolversCollection { pos: Position, ) -> Result> { for resolver in self.0.iter() { - if let Ok(module) = resolver.resolve(engine, path, pos) { - return Ok(module); + match resolver.resolve(engine, path, pos) { + Ok(module) => return Ok(module), + Err(err) => match *err { + EvalAltResult::ErrorModuleNotFound(_, _) => continue, + EvalAltResult::ErrorInModule(_, err, _) => return Err(err), + _ => panic!("ModuleResolver::resolve returns error that is not ErrorModuleNotFound or ErrorInModule"), + }, } } From 2123b0a2794b52374cf8a4a1a1c36b186432de46 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 28 Sep 2020 18:53:03 +0800 Subject: [PATCH 24/25] Module::eval_ast_as_new will merge global and module namespaces if private_namespace is true. --- src/module/mod.rs | 43 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/src/module/mod.rs b/src/module/mod.rs index 674d93fe..d44e5fbe 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -3,7 +3,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; use crate::engine::Engine; -use crate::fn_native::{CallableFunction, FnCallArgs, FnPtr, IteratorFn, SendSync}; +use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; use crate::fn_register::by_value as cast_arg; use crate::parser::{FnAccess, FnAccess::Public}; use crate::result::EvalAltResult; @@ -1215,15 +1215,12 @@ impl Module { /// Create a new `Module` by evaluating an `AST`. /// - /// ### `private_namespace` parameter + /// ### `merge_namespaces` parameter /// - /// * If `true`, the entire `AST` is encapsulated into each function as a private namespace, - /// allowing functions to cross-call each other. Functions are searched in the module's - /// private namespace, and the global namespace cannot be accessed. - /// - /// Under this scenario, if any argument is a function pointer, then the function referred - /// to by the function pointer is also _merged_ into the module's private namespace before - /// the function call, so passing simple closures to functions work as expected. + /// * If `true`, the entire `AST` is encapsulated into each function, allowing functions + /// to cross-call each other. Functions in the global namespace, plus all functions + /// defined in the module, are _merged_ into a _unified_ namespace before each call. + /// Therefore, all functions will be found, at the expense of some performance. /// /// * If `false`, each function is registered independently and cannot cross-call /// each other. Functions are searched in the global namespace. @@ -1246,7 +1243,7 @@ impl Module { pub fn eval_ast_as_new( mut scope: Scope, ast: &AST, - private_namespace: bool, + merge_namespaces: bool, engine: &Engine, ) -> FuncReturn { let mut mods = Imports::new(); @@ -1272,7 +1269,7 @@ impl Module { }); #[cfg(not(feature = "no_function"))] - if private_namespace { + if merge_namespaces { ast.iter_functions(|access, name, num_args| match access { FnAccess::Private => (), FnAccess::Public => { @@ -1283,31 +1280,13 @@ impl Module { name, num_args, move |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { - // Get anonymous functions - let anon_fns: StaticVec<_> = args - .iter() - .filter(|&arg| arg.is::()) - .map(|arg| arg.read_lock::().unwrap().get_fn_name().clone()) - .collect(); - - let mut lib_merged; - - let call_lib = if anon_fns.is_empty() { - // No anonymous functions - just pass the AST through - &ast_lib - } else { - // If there are anonymous functions, merge them in - lib_merged = ast_lib.clone(); - lib_merged.merge_filtered(&lib, &mut |_, name, _| { - anon_fns.iter().find(|s| s.as_str() == name).is_some() - }); - &lib_merged - }; + let mut lib_merged = lib.clone(); + lib_merged.merge(&ast_lib); engine .call_fn_dynamic_raw( &mut Scope::new(), - call_lib, + &lib_merged, &fn_name, &mut None, args, From 64c421b3d736628da3557866a1865a3b9cddbb9c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 28 Sep 2020 22:14:19 +0800 Subject: [PATCH 25/25] Revise docs. --- README.md | 21 ++- RELEASES.md | 13 +- doc/src/SUMMARY.md | 6 +- doc/src/about/features.md | 2 +- doc/src/appendix/keywords.md | 60 ++++---- doc/src/appendix/literals.md | 22 +-- doc/src/appendix/operators.md | 20 +-- doc/src/engine/custom-syntax.md | 8 +- doc/src/engine/func.md | 14 +- doc/src/engine/optimize/semantics.md | 10 +- doc/src/engine/raw.md | 1 - doc/src/engine/scope.md | 7 +- doc/src/language/arrays.md | 32 ++--- doc/src/language/constants.md | 39 +++++- doc/src/language/convert.md | 32 +++++ doc/src/language/dynamic.md | 27 ++-- doc/src/language/fn-anon.md | 4 +- doc/src/language/fn-closure.md | 50 +++---- doc/src/language/fn-namespaces.md | 6 +- doc/src/language/fn-ptr.md | 4 +- doc/src/language/json.md | 16 ++- doc/src/language/method.md | 8 +- doc/src/language/modules/ast.md | 59 -------- doc/src/language/modules/export.md | 29 +--- doc/src/language/object-maps-oop.md | 14 +- doc/src/language/object-maps.md | 22 +-- doc/src/language/string-fn.md | 28 ++-- doc/src/language/strings-chars.md | 22 +-- doc/src/language/type-of.md | 21 +++ doc/src/links.md | 2 + doc/src/patterns/config.md | 16 ++- doc/src/patterns/control.md | 6 + doc/src/patterns/singleton.md | 78 +++++++---- doc/src/plugins/function.md | 10 +- doc/src/plugins/module.md | 22 +-- doc/src/rust/getters-setters.md | 8 +- doc/src/rust/modules/ast.md | 88 ++++++++++++ doc/src/rust/modules/create.md | 36 +++-- doc/src/rust/modules/imp-resolver.md | 19 ++- doc/src/rust/modules/resolvers.md | 128 ++++++++++++++++-- doc/src/rust/packages/create.md | 8 +- doc/src/rust/packages/index.md | 8 +- doc/src/rust/register-raw.md | 12 +- doc/src/rust/serde.md | 11 ++ doc/src/safety/sandbox.md | 14 +- ...{macro_register.rs => plugins_register.rs} | 0 tests/{macro_unroll.rs => plugins_unroll.rs} | 0 47 files changed, 686 insertions(+), 377 deletions(-) delete mode 100644 doc/src/language/modules/ast.md create mode 100644 doc/src/rust/modules/ast.md rename tests/{macro_register.rs => plugins_register.rs} (100%) rename tests/{macro_unroll.rs => plugins_unroll.rs} (100%) diff --git a/README.md b/README.md index fbd2f3d2..5b06f34c 100644 --- a/README.md +++ b/README.md @@ -23,22 +23,23 @@ Standard features ----------------- * Easy-to-use language similar to JavaScript+Rust with dynamic typing. +* Fairly low compile-time overhead. +* Fairly efficient evaluation (1 million iterations in 0.3 sec on a single core, 2.3 GHz Linux VM). * Tight integration with native Rust [functions](https://schungx.github.io/rhai/rust/functions.html) and [types]([#custom-types-and-methods](https://schungx.github.io/rhai/rust/custom.html)), including [getters/setters](https://schungx.github.io/rhai/rust/getters-setters.html), [methods](https://schungx.github.io/rhai/rust/custom.html) and [indexers](https://schungx.github.io/rhai/rust/indexers.html). * Freely pass Rust variables/constants into a script via an external [`Scope`](https://schungx.github.io/rhai/rust/scope.html). * Easily [call a script-defined function](https://schungx.github.io/rhai/engine/call-fn.html) from Rust. -* Fairly low compile-time overhead. -* Fairly efficient evaluation (1 million iterations in 0.3 sec on a single core, 2.3 GHz Linux VM). * Relatively little `unsafe` code (yes there are some for performance reasons). +* Few dependencies (currently only [`smallvec`](https://crates.io/crates/smallvec)). * Re-entrant scripting engine can be made `Send + Sync` (via the `sync` feature). -* [Function overloading](https://schungx.github.io/rhai/language/overload.html). -* [Operator overloading](https://schungx.github.io/rhai/rust/operators.html). -* Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html) with additional support for [currying](https://schungx.github.io/rhai/language/fn-curry.html) and [closures](https://schungx.github.io/rhai/language/fn-closure.html). -* Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). +* Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations. +* Easy custom API development via [plugins](https://schungx.github.io/rhai/plugins/index.html) system powered by procedural macros. +* [Function overloading](https://schungx.github.io/rhai/language/overload.html) and [operator overloading](https://schungx.github.io/rhai/rust/operators.html). +* Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html) with additional support for [currying](https://schungx.github.io/rhai/language/fn-curry.html). +* [Closures](https://schungx.github.io/rhai/language/fn-closure.html) (anonymous functions) that can capture shared values. +* Some syntactic support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). * Serialization/deserialization support via [serde](https://crates.io/crates/serde) (requires the `serde` feature). -* Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations. * Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html). -* Easy custom API development via [plugins](https://schungx.github.io/rhai/plugins/index.html) system powered by procedural macros. Protection against attacks -------------------------- @@ -69,13 +70,11 @@ Scripts can be evaluated directly from the editor. License ------- -Licensed under either: +Licensed under either of the following, at your choice: * [Apache License, Version 2.0](https://github.com/jonathandturner/rhai/blob/master/LICENSE-APACHE.txt), or * [MIT license](https://github.com/jonathandturner/rhai/blob/master/LICENSE-MIT.txt) -at your choice. - Unless explicitly stated otherwise, any contribution intentionally submitted for inclusion in this crate, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions. diff --git a/RELEASES.md b/RELEASES.md index a4c31fee..3503baf7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,9 @@ Rhai Release Notes Version 0.19.0 ============== +The major new feature for this version is _Plugins_ support, powered by procedural macros. +Plugins make it extremely easy to develop and register Rust functions with an `Engine`. + Bug fixes --------- @@ -17,10 +20,10 @@ Breaking changes ---------------- * `Engine::register_set_result` and `Engine::register_indexer_set_result` now take a function that returns `Result<(), Box>`. -* `Engine::register_indexer_XXX` and `Module::set_indexer_XXX` panic when the type is `Arrary`, `Map` or `String`. +* `Engine::register_indexer_XXX` and `Module::set_indexer_XXX` panic when the type is `Array`, `Map` or `String`. * `EvalAltResult` has a new variant `ErrorInModule` which holds errors when loading an external module. * `Module::eval_ast_as_new` now takes an extra boolean parameter, indicating whether to encapsulate the entire module into a separate namespace. -* Functions in `FileModuleResolver` loaded modules now can cross-call each other, but cannot access the global namespace. For the old behavior, use `MergingFileModuleResolver` instead. +* Functions in `FileModuleResolver` loaded modules now can cross-call each other in addition to functions in the global namespace. For the old behavior, use `MergingFileModuleResolver` instead. * New `EvalAltResult::ErrorInModule` variant capturing errors when loading a module from a script file. New features @@ -28,11 +31,11 @@ New features * Plugins support via procedural macros. * Scripted functions are allowed in packages. -* `parse_int` and `parse_float` functions. +* `parse_int` and `parse_float` functions for parsing numbers; `split` function for splitting strings. * `AST::iter_functions` and `Module::iter_script_fn_info` to iterate functions. -* Functions iteration functions now take `FnMut` instead of `Fn`. +* Functions iteration functions for `AST` and `Module` now take `FnMut` instead of `Fn`. * New `FileModuleResolver` that encapsulates the entire `AST` of the module script, allowing function cross-calling. The old version is renamed `MergingFileModuleResolver`. -* `split` function for splitting strings. +* `+` and `-` operators for timestamps to increment/decrement by seconds. Version 0.18.3 diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 23f39757..5193b7bb 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -81,15 +81,15 @@ The Rhai Scripting Language 2. [Overloading](language/overload.md) 3. [Namespaces](language/fn-namespaces.md) 4. [Function Pointers](language/fn-ptr.md) - 5. [Anonymous Functions](language/fn-anon.md) - 6. [Currying](language/fn-curry.md) + 5. [Currying](language/fn-curry.md) + 6. [Anonymous Functions](language/fn-anon.md) 7. [Closures](language/fn-closure.md) 16. [Print and Debug](language/print-debug.md) 17. [Modules](language/modules/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) 2. [Import Modules](language/modules/import.md) 3. [Create from Rust](rust/modules/create.md) - 4. [Create from AST](language/modules/ast.md) + 4. [Create from AST](rust/modules/ast.md) 5. [Module Resolvers](rust/modules/resolvers.md) 1. [Custom Implementation](rust/modules/imp-resolver.md) 18. [Eval Statement](language/eval.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index cacc1885..bcfe5a6e 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -37,7 +37,7 @@ Dynamic * Dynamic dispatch via [function pointers] with additional support for [currying]. -* Closures via [automatic currying] with capturing shared variables from the external scope. +* [Closures] that can capture shared variables. * Some support for [object-oriented programming (OOP)][OOP]. diff --git a/doc/src/appendix/keywords.md b/doc/src/appendix/keywords.md index 1b52d049..47a7b446 100644 --- a/doc/src/appendix/keywords.md +++ b/doc/src/appendix/keywords.md @@ -3,36 +3,36 @@ Keywords List {{#include ../links.md}} -| Keyword | Description | Inactive under | Overloadable | -| :-------------------: | ---------------------------------------- | :-------------: | :----------: | -| `true` | boolean true literal | | no | -| `false` | boolean false literal | | no | -| `let` | variable declaration | | no | -| `const` | constant declaration | | no | -| `is_shared` | is a value shared? | | no | -| `if` | if statement | | no | -| `else` | else block of if statement | | no | -| `while` | while loop | | no | -| `loop` | infinite loop | | no | -| `for` | for loop | | no | -| `in` | containment test, part of for loop | | no | -| `continue` | continue a loop at the next iteration | | no | -| `break` | loop breaking | | no | -| `return` | return value | | no | -| `throw` | throw exception | | no | -| `import` | import module | [`no_module`] | no | -| `export` | export variable | [`no_module`] | no | -| `as` | alias for variable export | [`no_module`] | no | -| `private` | mark function private | [`no_function`] | no | -| `fn` (lower-case `f`) | function definition | [`no_function`] | no | -| `Fn` (capital `F`) | function to create a [function pointer] | | yes | -| `call` | call a [function pointer] | | no | -| `curry` | curry a [function pointer] | | no | -| `this` | reference to base object for method call | [`no_function`] | no | -| `type_of` | get type name of value | | yes | -| `print` | print value | | yes | -| `debug` | print value in debug format | | yes | -| `eval` | evaluate script | | yes | +| Keyword | Description | Inactive under | Overloadable | +| :-------------------: | ------------------------------------------- | :-------------: | :----------: | +| `true` | boolean true literal | | no | +| `false` | boolean false literal | | no | +| `let` | variable declaration | | no | +| `const` | constant declaration | | no | +| `is_shared` | is a value shared? | | no | +| `if` | if statement | | no | +| `else` | else block of if statement | | no | +| `while` | while loop | | no | +| `loop` | infinite loop | | no | +| `for` | for loop | | no | +| `in` | 1) containment test
2) part of for loop | | no | +| `continue` | continue a loop at the next iteration | | no | +| `break` | break out of loop iteration | | no | +| `return` | return value | | no | +| `throw` | throw exception | | no | +| `import` | import module | [`no_module`] | no | +| `export` | export variable | [`no_module`] | no | +| `as` | alias for variable export | [`no_module`] | no | +| `private` | mark function private | [`no_function`] | no | +| `fn` (lower-case `f`) | function definition | [`no_function`] | no | +| `Fn` (capital `F`) | create a [function pointer] | | yes | +| `call` | call a [function pointer] | | no | +| `curry` | curry a [function pointer] | | no | +| `this` | reference to base object for method call | [`no_function`] | no | +| `type_of` | get type name of value | | yes | +| `print` | print value | | yes | +| `debug` | print value in debug format | | yes | +| `eval` | evaluate script | | yes | Reserved Keywords diff --git a/doc/src/appendix/literals.md b/doc/src/appendix/literals.md index a308e951..2d51ba36 100644 --- a/doc/src/appendix/literals.md +++ b/doc/src/appendix/literals.md @@ -3,14 +3,14 @@ Literals Syntax {{#include ../links.md}} -| Type | Literal syntax | -| :--------------------------------: | :------------------------------------------------------------------------------: | -| `INT` | `42`, `-123`, `0`,
`0x????..` (hex), `0b????..` (binary), `0o????..` (octal) | -| `FLOAT` | `42.0`, `-123.456`, `0.0` | -| [String] | `"... \x?? \u???? \U???????? ..."` | -| Character | `"... \x?? \u???? \U???????? ..."` | -| [`Array`] | `[ ???, ???, ??? ]` | -| [Object map] | `#{ a: ???, b: ???, c: ???, "def": ??? }` | -| Boolean true | `true` | -| Boolean false | `false` | -| `Nothing`/`null`/`nil`/`void`/Unit | `()` | +| Type | Literal syntax | +| :--------------------------------: | :-----------------------------------------------------------------------------------------: | +| `INT` | decimal: `42`, `-123`, `0`
hex: `0x????..`
binary: `0b????..`
octal: `0o????..` | +| `FLOAT` | `42.0`, `-123.456`, `0.0` | +| [String] | `"... \x?? \u???? \U???????? ..."` | +| Character | single: `'?'`
ASCII hex: `'\x??'`
Unicode: `'\u????'`, `'\U????????'` | +| [`Array`] | `[ ???, ???, ??? ]` | +| [Object map] | `#{ a: ???, b: ???, c: ???, "def": ??? }` | +| Boolean true | `true` | +| Boolean false | `false` | +| `Nothing`/`null`/`nil`/`void`/Unit | `()` | diff --git a/doc/src/appendix/operators.md b/doc/src/appendix/operators.md index 812966a8..e53ba01b 100644 --- a/doc/src/appendix/operators.md +++ b/doc/src/appendix/operators.md @@ -40,13 +40,13 @@ Symbols | ------------ | ------------------------ | | `:` | property value separator | | `::` | module path separator | -| `#` | _Reserved_ | -| `=>` | _Reserved_ | -| `->` | _Reserved_ | -| `<-` | _Reserved_ | -| `===` | _Reserved_ | -| `!==` | _Reserved_ | -| `:=` | _Reserved_ | -| `::<` .. `>` | _Reserved_ | -| `@` | _Reserved_ | -| `(*` .. `*)` | _Reserved_ | +| `#` | _reserved_ | +| `=>` | _reserved_ | +| `->` | _reserved_ | +| `<-` | _reserved_ | +| `===` | _reserved_ | +| `!==` | _reserved_ | +| `:=` | _reserved_ | +| `::<` .. `>` | _reserved_ | +| `@` | _reserved_ | +| `(*` .. `*)` | _reserved_ | diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md index 8d0820c8..3f88112c 100644 --- a/doc/src/engine/custom-syntax.md +++ b/doc/src/engine/custom-syntax.md @@ -4,7 +4,7 @@ Extend Rhai with Custom Syntax {{#include ../links.md}} -For the ultimate advantageous, there is a built-in facility to _extend_ the Rhai language +For the ultimate adventurous, there is a built-in facility to _extend_ the Rhai language with custom-defined _syntax_. But before going off to define the next weird statement type, heed this warning: @@ -28,7 +28,7 @@ Where This Might Be Useful * Where a custom syntax _significantly_ simplifies the code and _significantly_ enhances understanding of the code's intent. -* Where certain logic cannot be easily encapsulated inside a function. This is usually the case where _closures_ are required, because Rhai does not have closures. +* Where certain logic cannot be easily encapsulated inside a function. * Where you just want to confuse your user and make their lives miserable, because you can. @@ -154,10 +154,10 @@ let result = engine.eval_expression_tree(context, scope, expr)?; New variables maybe declared (usually with a variable name that is passed in via `$ident$). -It can simply be pushed into the [`scope`]. +It can simply be pushed into the [`Scope`]. However, beware that all new variables must be declared _prior_ to evaluating any expression tree. -In other words, any `scope.push(...)` calls must come _before_ any `engine::eval_expression_tree(...)` calls. +In other words, any `Scope::push` calls must come _before_ any `Engine::eval_expression_tree` calls. ```rust let var_name = inputs[0].get_variable_name().unwrap().to_string(); diff --git a/doc/src/engine/func.md b/doc/src/engine/func.md index f545b926..d0ce6784 100644 --- a/doc/src/engine/func.md +++ b/doc/src/engine/func.md @@ -11,9 +11,9 @@ Creating them is accomplished via the `Func` trait which contains `create_from_s (as well as its companion method `create_from_ast`): ```rust -use rhai::{Engine, Func}; // use 'Func' for 'create_from_script' +use rhai::{Engine, Func}; // use 'Func' for 'create_from_script' -let engine = Engine::new(); // create a new 'Engine' just for this +let engine = Engine::new(); // create a new 'Engine' just for this let script = "fn calc(x, y) { x + y.len < 42 }"; @@ -25,14 +25,14 @@ let script = "fn calc(x, y) { x + y.len < 42 }"; let func = Func::<(i64, String), bool>::create_from_script( // ^^^^^^^^^^^^^ function parameter types in tuple - engine, // the 'Engine' is consumed into the closure - script, // the script, notice number of parameters must match - "calc" // the entry-point function name + engine, // the 'Engine' is consumed into the closure + script, // the script, notice number of parameters must match + "calc" // the entry-point function name )?; -func(123, "hello".to_string())? == false; // call the closure +func(123, "hello".to_string())? == false; // call the closure -schedule_callback(func); // pass it as a callback to another function +schedule_callback(func); // pass it as a callback to another function // Although there is nothing you can't do by manually writing out the closure yourself... let engine = Engine::new(); diff --git a/doc/src/engine/optimize/semantics.md b/doc/src/engine/optimize/semantics.md index 021173ef..db014688 100644 --- a/doc/src/engine/optimize/semantics.md +++ b/doc/src/engine/optimize/semantics.md @@ -8,13 +8,13 @@ Some optimizations can alter subtle semantics of the script. For example: ```rust -if true { // condition always true - 123.456; // eliminated - hello; // eliminated, EVEN THOUGH the variable doesn't exist! - foo(42) // promoted up-level +if true { // condition always true + 123.456; // eliminated + hello; // eliminated, EVEN THOUGH the variable doesn't exist! + foo(42) // promoted up-level } -foo(42) // <- the above optimizes to this +foo(42) // <- the above optimizes to this ``` If the original script were evaluated instead, it would have been an error - the variable `hello` does not exist, diff --git a/doc/src/engine/raw.md b/doc/src/engine/raw.md index 06e4ff33..cf867246 100644 --- a/doc/src/engine/raw.md +++ b/doc/src/engine/raw.md @@ -1,4 +1,3 @@ - Raw `Engine` =========== diff --git a/doc/src/engine/scope.md b/doc/src/engine/scope.md index daa08a15..3deecc7d 100644 --- a/doc/src/engine/scope.md +++ b/doc/src/engine/scope.md @@ -31,19 +31,20 @@ let mut scope = Scope::new(); scope .push("y", 42_i64) .push("z", 999_i64) + .push_constant("MY_NUMBER", 123_i64) // constants can also be added .set_value("s", "hello, world!".to_string()); //'set_value' adds a variable when one doesn't exist - // remember to use 'String', not '&str' + // remember to use 'String', not '&str' // First invocation engine.eval_with_scope::<()>(&mut scope, r" - let x = 4 + 5 - y + z + s.len; + let x = 4 + 5 - y + z + MY_NUMBER + s.len; y = 1; ")?; // Second invocation using the same state let result = engine.eval_with_scope::(&mut scope, "x")?; -println!("result: {}", result); // prints 979 +println!("result: {}", result); // prints 1102 // Variable y is changed in the script - read it with 'get_value' assert_eq!(scope.get_value::("y").expect("variable y should exist"), 1); diff --git a/doc/src/language/arrays.md b/doc/src/language/arrays.md index a4a8e458..29e1aafb 100644 --- a/doc/src/language/arrays.md +++ b/doc/src/language/arrays.md @@ -30,22 +30,22 @@ Built-in Functions The following methods (mostly defined in the [`BasicArrayPackage`][packages] but excluded if using a [raw `Engine`]) operate on arrays: -| Function | Parameter(s) | Description | -| ------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `push` | element to insert | inserts an element at the end | -| `+=` operator | array, element to insert (not another array) | inserts an element at the end | -| `append` | array to append | concatenates the second array to the end of the first | -| `+=` operator | array, array to append | concatenates the second array to the end of the first | -| `+` operator | first array, second array | concatenates the first array with the second | -| `insert` | element to insert, position
(beginning if <= 0, end if >= length) | inserts an element at a certain index | -| `pop` | _none_ | removes the last element and returns it ([`()`] if empty) | -| `shift` | _none_ | removes the first element and returns it ([`()`] if empty) | -| `remove` | index | removes an element at a particular index and returns it, or returns [`()`] if the index is not valid | -| `reverse` | _none_ | reverses the array | -| `len` method and property | _none_ | returns the number of elements | -| `pad` | element to pad, target length | pads the array with an element to at least a specified length | -| `clear` | _none_ | empties the array | -| `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | +| Function | Parameter(s) | Description | +| ------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `push` | element to insert | inserts an element at the end | +| `append` | array to append | concatenates the second array to the end of the first | +| `+=` operator | 1) array
2) element to insert (not another array) | inserts an element at the end | +| `+=` operator | 1) array
2) array to append | concatenates the second array to the end of the first | +| `+` operator | 1) first array
2) second array | concatenates the first array with the second | +| `insert` | 1) element to insert
2) position (beginning if <= 0, end if >= length) | inserts an element at a certain index | +| `pop` | _none_ | removes the last element and returns it ([`()`] if empty) | +| `shift` | _none_ | removes the first element and returns it ([`()`] if empty) | +| `remove` | index | removes an element at a particular index and returns it, or returns [`()`] if the index is not valid | +| `reverse` | _none_ | reverses the array | +| `len` method and property | _none_ | returns the number of elements | +| `pad` | 1) target length
2) element to pad | pads the array with an element to at least a specified length | +| `clear` | _none_ | empties the array | +| `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | Use Custom Types With Arrays diff --git a/doc/src/language/constants.md b/doc/src/language/constants.md index d88ecd4f..81ad1ce6 100644 --- a/doc/src/language/constants.md +++ b/doc/src/language/constants.md @@ -16,8 +16,45 @@ x = 123; // <- syntax error: cannot assign to constant ``` Unlike variables which need not have initial values (default to [`()`]), -constants must be assigned one, and it must be a constant _value_, not an expression. +constants must be assigned one, and it must be a [_literal value_](../appendix/literals.md), +not an expression. ```rust const x = 40 + 2; // <- syntax error: cannot assign expression to constant ``` + + +Manually Add Constant into Custom Scope +-------------------------------------- + +It is possible to add a constant into a custom [`Scope`] so it'll be available to scripts +running with that [`Scope`]. + +When added to a custom [`Scope`], a constant can hold any value, not just a literal value. + +It is very useful to have a constant value hold a [custom type], which essentially acts +as a [_singleton_](../patterns/singleton.md). The singleton object can be modified via its +registered API - being a constant only prevents it from being re-assigned or operated upon by Rhai; +mutating it via a Rust function is still allowed. + +```rust +use rhai::{Engine, Scope}; + +struct TestStruct(i64); // custom type + +let engine = Engine::new() + .register_type_with_name::("TestStruct") // register custom type + .register_get_set("value", + |obj: &mut TestStruct| obj.0, // property getter + |obj: &mut TestStruct, value: i64| obj.0 = value // property setter + ); + +let mut scope = Scope::new(); // create custom scope + +scope.push_constant("MY_NUMBER", TestStruct(123_i64)); // add constant variable + +engine.consume_with_scope(&mut scope, r" + MY_NUMBER.value = 42; // constant objects can be modified + print(MY_NUMBER.value); // prints 42 +")?; +``` diff --git a/doc/src/language/convert.md b/doc/src/language/convert.md index 0c2226eb..a14feed3 100644 --- a/doc/src/language/convert.md +++ b/doc/src/language/convert.md @@ -3,6 +3,10 @@ Value Conversions {{#include ../links.md}} + +Convert Between Integer and Floating-Point +----------------------------------------- + The `to_float` function converts a supported number to `FLOAT` (defaults to `f64`). The `to_int` function converts a supported number to `INT` (`i32` or `i64` depending on [`only_i32`]). @@ -22,3 +26,31 @@ let c = 'X'; // character print("c is '" + c + "' and its code is " + c.to_int()); // prints "c is 'X' and its code is 88" ``` + + +Parse String into Number +------------------------ + +The `parse_float` function converts a [string] into a `FLOAT` (defaults to `f64`). + +The `parse_int` function converts a [string] into an `INT` (`i32` or `i64` depending on [`only_i32`]). +An optional radix (2-36) can be provided to parse the [string] into a number of the specified radix. + +```rust +let x = parse_float("123.4"); // parse as floating-point +x == 123.4; +type_of(x) == "f64"; + +let dec = parse_int("42"); // parse as decimal +let dec = parse_int("42", 10); // radix = 10 is the default +dec == 42; +type_of(dec) == "i64"; + +let bin = parse_int("110", 2); // parse as binary (radix = 2) +bin == 0b110; +type_of(bin) == "i64"; + +let hex = parse_int("ab", 16); // parse as hex (radix = 16) +hex == 0xab; +type_of(hex) == "i64"; +``` diff --git a/doc/src/language/dynamic.md b/doc/src/language/dynamic.md index 7909d162..9de60a1c 100644 --- a/doc/src/language/dynamic.md +++ b/doc/src/language/dynamic.md @@ -58,15 +58,15 @@ The `cast` method then converts the value into a specific, known type. Alternatively, use the `try_cast` method which does not panic but returns `None` when the cast fails. ```rust -let list: Array = engine.eval("...")?; // return type is 'Array' -let item = list[0]; // an element in an 'Array' is 'Dynamic' +let list: Array = engine.eval("...")?; // return type is 'Array' +let item = list[0]; // an element in an 'Array' is 'Dynamic' -item.is::() == true; // 'is' returns whether a 'Dynamic' value is of a particular type +item.is::() == true; // 'is' returns whether a 'Dynamic' value is of a particular type -let value = item.cast::(); // if the element is 'i64', this succeeds; otherwise it panics -let value: i64 = item.cast(); // type can also be inferred +let value = item.cast::(); // if the element is 'i64', this succeeds; otherwise it panics +let value: i64 = item.cast(); // type can also be inferred -let value = item.try_cast::()?; // 'try_cast' does not panic when the cast fails, but returns 'None' +let value = item.try_cast::()?; // 'try_cast' does not panic when the cast fails, but returns 'None' ``` Type Name @@ -76,17 +76,21 @@ The `type_name` method gets the name of the actual type as a static string slice which can be `match`-ed against. ```rust -let list: Array = engine.eval("...")?; // return type is 'Array' -let item = list[0]; // an element in an 'Array' is 'Dynamic' +let list: Array = engine.eval("...")?; // return type is 'Array' +let item = list[0]; // an element in an 'Array' is 'Dynamic' -match item.type_name() { // 'type_name' returns the name of the actual Rust type +match item.type_name() { // 'type_name' returns the name of the actual Rust type "i64" => ... "alloc::string::String" => ... "bool" => ... - "path::to::module::TestStruct" => ... + "crate::path::to::module::TestStruct" => ... } ``` +**Note:** `type_name` always returns the _full_ Rust path name of the type, even when the type +has been registered with a friendly name via `Engine::register_type_with_name`. This behavior +is different from that of the [`type_of`][`type_of()`] function in Rhai. + Conversion Traits ---------------- @@ -100,4 +104,5 @@ The following conversion traits are implemented for `Dynamic`: * `From` * `From` * `From>` (into an [array]) -* `From>` (into an [object map]). +* `From>` (into an [object map]) +* `From` (into a [timestamp] if not [`no_std`]) diff --git a/doc/src/language/fn-anon.md b/doc/src/language/fn-anon.md index 8b514509..49ee13c2 100644 --- a/doc/src/language/fn-anon.md +++ b/doc/src/language/fn-anon.md @@ -10,8 +10,8 @@ This scenario is especially common when simulating object-oriented programming ( // Define object let obj = #{ data: 42, - increment: Fn("inc_obj"), // use function pointers to - decrement: Fn("dec_obj"), // refer to method functions + increment: Fn("inc_obj"), // use function pointers to + decrement: Fn("dec_obj"), // refer to method functions print: Fn("print_obj") }; diff --git a/doc/src/language/fn-closure.md b/doc/src/language/fn-closure.md index 7f04dbac..b620e503 100644 --- a/doc/src/language/fn-closure.md +++ b/doc/src/language/fn-closure.md @@ -23,43 +23,25 @@ Therefore, similar to closures in many languages, these captured shared values p reference counting, and may be read or modified even after the variables that hold them go out of scope and no longer exist. -Use the `is_shared` function to check whether a particular value is a shared value. +Use the `Dynamic::is_shared` function to check whether a particular value is a shared value. Automatic currying can be turned off via the [`no_closure`] feature. -Actual Implementation ---------------------- - -The actual implementation de-sugars to: - -1. Keeping track of what variables are accessed inside the anonymous function, - -2. If a variable is not defined within the anonymous function's scope, it is looked up _outside_ the function and - in the current execution scope - where the anonymous function is created. - -3. The variable is added to the parameters list of the anonymous function, at the front. - -4. The variable is then converted into a **reference-counted shared value**. - - An [anonymous function] which captures an external variable is the only way to create a reference-counted shared value in Rhai. - -5. The shared value is then [curried][currying] into the [function pointer] itself, essentially carrying a reference to that shared value - and inserting it into future calls of the function. - - This process is called _Automatic Currying_, and is the mechanism through which Rhai simulates normal closures. - - Examples -------- ```rust let x = 1; // a normal variable +x.is_shared() == false; + let f = |y| x + y; // variable 'x' is auto-curried (captured) into 'f' x.is_shared() == true; // 'x' is now a shared value! +f.call(2) == 3; // 1 + 2 == 3 + x = 40; // changing 'x'... f.call(2) == 42; // the value of 'x' is 40 because 'x' is shared @@ -117,6 +99,8 @@ will occur and the script will terminate with an error. ```rust let x = 20; +x.is_shared() == false; // 'x' is not shared, so no data race is possible + let f = |a| this += x + a; // 'x' is captured in this closure x.is_shared() == true; // now 'x' is shared @@ -152,6 +136,26 @@ x.call(f, 2); TL;DR ----- +### Q: How is it actually implemented? + +The actual implementation of closures de-sugars to: + +1. Keeping track of what variables are accessed inside the anonymous function, + +2. If a variable is not defined within the anonymous function's scope, it is looked up _outside_ the function and + in the current execution scope - where the anonymous function is created. + +3. The variable is added to the parameters list of the anonymous function, at the front. + +4. The variable is then converted into a **reference-counted shared value**. + + An [anonymous function] which captures an external variable is the only way to create a reference-counted shared value in Rhai. + +5. The shared value is then [curried][currying] into the [function pointer] itself, essentially carrying a reference to that shared value + and inserting it into future calls of the function. + + This process is called _Automatic Currying_, and is the mechanism through which Rhai simulates normal closures. + ### Q: Why are closures implemented as automatic currying? In concept, a closure _closes_ over captured variables from the outer scope - that's why diff --git a/doc/src/language/fn-namespaces.md b/doc/src/language/fn-namespaces.md index 0621472f..e6844f9a 100644 --- a/doc/src/language/fn-namespaces.md +++ b/doc/src/language/fn-namespaces.md @@ -43,10 +43,12 @@ This aspect is very similar to JavaScript before ES6 modules. // Compile a script into AST let ast1 = engine.compile( r#" - fn get_message() { "Hello!" } // greeting message + fn get_message() { + "Hello!" // greeting message + } fn say_hello() { - print(get_message()); // prints message + print(get_message()); // prints message } say_hello(); diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md index 833cfaa2..8c294a83 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -61,8 +61,8 @@ hello.call(0); // error: function not found - 'hello_world (i64)' Global Namespace Only -------------------- -Because of their dynamic nature, function pointers cannot refer to functions in a _module_ [namespace][function namespace] -(i.e. functions in [`import`]-ed modules). They can only refer to functions within the global [namespace][function namespace]. +Because of their dynamic nature, function pointers cannot refer to functions in [`import`]-ed [modules]. +They can only refer to functions within the global [namespace][function namespace]. See [function namespaces] for more details. ```rust diff --git a/doc/src/language/json.md b/doc/src/language/json.md index 951f0cbe..0d82b64a 100644 --- a/doc/src/language/json.md +++ b/doc/src/language/json.md @@ -44,10 +44,13 @@ Representation of Numbers ------------------------ JSON numbers are all floating-point while Rhai supports integers (`INT`) and floating-point (`FLOAT`) if -the [`no_float`] feature is not used. Most common generators of JSON data distinguish between -integer and floating-point values by always serializing a floating-point number with a decimal point -(i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully -with Rhai [object maps]. +the [`no_float`] feature is not used. + +Most common generators of JSON data distinguish between integer and floating-point values by always +serializing a floating-point number with a decimal point (i.e. `123.0` instead of `123` which is +assumed to be an integer). + +This style can be used successfully with Rhai [object maps]. Parse JSON with Sub-Objects @@ -68,9 +71,10 @@ A JSON object hash starting with `#{` is handled transparently by `Engine::parse // JSON with sub-object 'b'. let json = r#"{"a":1, "b":{"x":true, "y":false}}"#; -let new_json = json.replace("{" "#{"); +// Our JSON text does not contain the '{' character, so off we go! +let new_json = json.replace("{", "#{"); -// The leading '{' will also be replaced to '#{', but parse_json can handle this. +// The leading '{' will also be replaced to '#{', but 'parse_json' handles this just fine. let map = engine.parse_json(&new_json, false)?; map.len() == 2; // 'map' contains two properties: 'a' and 'b' diff --git a/doc/src/language/method.md b/doc/src/language/method.md index 4b88fe68..01633350 100644 --- a/doc/src/language/method.md +++ b/doc/src/language/method.md @@ -47,10 +47,10 @@ an equivalent method coded in script, where the object is accessed via the `this The following table illustrates the differences: -| Function type | Parameters | Object reference | Function signature | -| :-----------: | :--------: | :--------------------: | :-----------------------------------------------------: | -| Native Rust | _n_ + 1 | first `&mut` parameter | `fn method`
`(obj: &mut T, x: U, y: V) {}` | -| Rhai script | _n_ | `this` | `fn method(x, y) {}` | +| Function type | Parameters | Object reference | Function signature | +| :-----------: | :--------: | :-----------------------: | :---------------------------: | +| Native Rust | _N_ + 1 | first `&mut T` parameter | `Fn(obj: &mut T, x: U, y: V)` | +| Rhai script | _N_ | `this` (of type `&mut T`) | `Fn(x: U, y: V)` | `&mut` is Efficient, Except for `ImmutableString` diff --git a/doc/src/language/modules/ast.md b/doc/src/language/modules/ast.md deleted file mode 100644 index 56cde70d..00000000 --- a/doc/src/language/modules/ast.md +++ /dev/null @@ -1,59 +0,0 @@ -Create a Module from an AST -========================== - -{{#include ../../links.md}} - -It is easy to convert a pre-compiled [`AST`] into a module: just use `Module::eval_ast_as_new`. - -Don't forget the [`export`] statement, otherwise there will be no variables exposed by the module -other than non-[`private`] functions (unless that's intentional). - -```rust -use rhai::{Engine, Module}; - -let engine = Engine::new(); - -// Compile a script into an 'AST' -let ast = engine.compile(r#" - // Functions become module functions - fn calc(x) { - x + 1 - } - fn add_len(x, y) { - x + y.len - } - - // Imported modules can become sub-modules - import "another module" as extra; - - // Variables defined at global level can become module variables - const x = 123; - let foo = 41; - let hello; - - // Variable values become constant module variable values - foo = calc(foo); - hello = "hello, " + foo + " worlds!"; - - // Finally, export the variables and modules - export - x as abc, // aliased variable name - foo, - hello, - extra as foobar; // export sub-module -"#)?; - -// Convert the 'AST' into a module, using the 'Engine' to evaluate it first -// -// The second parameter ('private_namespace'), when set to true, will encapsulate -// a copy of the entire 'AST' into each function, allowing functions in the module script -// to cross-call each other. Otherwise module script functions access the global namespace. -// -// This incurs additional overhead, avoidable by setting 'private_namespace' to false. -let module = Module::eval_ast_as_new(Scope::new(), &ast, true, &engine)?; - -// 'module' now can be loaded into a custom 'Scope' for future use. It contains: -// - sub-module: 'foobar' (renamed from 'extra') -// - functions: 'calc', 'add_len' -// - variables: 'abc' (renamed from 'x'), 'foo', 'hello' -``` diff --git a/doc/src/language/modules/export.md b/doc/src/language/modules/export.md index 46416bcc..aaa8eb2d 100644 --- a/doc/src/language/modules/export.md +++ b/doc/src/language/modules/export.md @@ -3,35 +3,16 @@ Export Variables, Functions and Sub-Modules in Module {{#include ../../links.md}} -A _module_ can be created from a single script (or pre-compiled [`AST`]) containing global variables, -functions and sub-modules via the `Module::eval_ast_as_new` method. -When given an [`AST`], it is first evaluated, then the following items are exposed as members of the new module: - -* Global variables - essentially all variables that remain in the [`Scope`] at the end of a script run - that are exported. Variables not exported (via the `export` statement) remain hidden. - -* Functions not specifically marked `private`. - -* Global modules that remain in the [`Scope`] at the end of a script run. - -The parameter `private_namespace` in `Module::eval_ast_as_new` determines the exact behavior of -functions exposed by the module and the namespace that they can access: - -| `private_namespace` value | Behavior of module functions | Namespace | Call global functions | Call functions in same module | -| :-----------------------: | ---------------------------------------------------- | :-------: | :-------------------: | :---------------------------: | -| `true` | encapsulate the entire `AST` into each function call | module | no | yes | -| `false` | register each function independently | global | yes | no | - - -Global Variables ----------------- +Export Global Variables +---------------------- The `export` statement, which can only be at global level, exposes selected variables as members of a module. Variables not exported are _private_ and hidden. They are merely used to initialize the module, but cannot be accessed from outside. -Everything exported from a module is **constant** (**read-only**). +Everything exported from a module is **constant** (i.e. read-only). ```rust // This is a module script. @@ -53,8 +34,8 @@ export x as answer; // the variable 'x' is exported under the alias 'answer' ``` -Functions ---------- +Export Functions +---------------- All functions are automatically exported, _unless_ it is explicitly opt-out with the [`private`] prefix. diff --git a/doc/src/language/object-maps-oop.md b/doc/src/language/object-maps-oop.md index 0c2b840d..f96fda04 100644 --- a/doc/src/language/object-maps-oop.md +++ b/doc/src/language/object-maps-oop.md @@ -10,25 +10,23 @@ If an [object map]'s property holds a [function pointer], the property can simpl a normal method in method-call syntax. This is a _short-hand_ to avoid the more verbose syntax of using the `call` function keyword. -When a property holding a [function pointer] (which incudes [closures]) is called like a method, +When a property holding a [function pointer] or a [closure] is called like a method, what happens next depends on whether the target function is a native Rust function or a script-defined function. -If it is a registered native Rust method function, it is called directly. +* If it is a registered native Rust function, it is called directly in _method-call_ style with the [object map] inserted as the first argument. -If it is a script-defined function, the `this` variable within the function body is bound -to the [object map] before the function is called. There is no way to simulate this behavior -via a normal function-call syntax because all scripted function arguments are passed by value. +* If it is a script-defined function, the `this` variable within the function body is bound to the [object map] before the function is called. ```rust let obj = #{ data: 40, - action: || this.data += x // 'action' holds a function pointer which is a closure + action: || this.data += x // 'action' holds a closure }; -obj.action(2); // Calls the function pointer with `this` bound to 'obj' +obj.action(2); // calls the function pointer with `this` bound to 'obj' -obj.call(obj.action, 2); // The above de-sugars to this +obj.call(obj.action, 2); // <- the above de-sugars to this obj.data == 42; diff --git a/doc/src/language/object-maps.md b/doc/src/language/object-maps.md index c2c0d20e..937b1818 100644 --- a/doc/src/language/object-maps.md +++ b/doc/src/language/object-maps.md @@ -57,17 +57,17 @@ Built-in Functions The following methods (defined in the [`BasicMapPackage`][packages] but excluded if using a [raw `Engine`]) operate on object maps: -| Function | Parameter(s) | Description | -| ---------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `has` | property name | does the object map contain a property of a particular name? | -| `len` | _none_ | returns the number of properties | -| `clear` | _none_ | empties the object map | -| `remove` | property name | removes a certain property and returns it ([`()`] if the property does not exist) | -| `+=` operator, `mixin` | second object map | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) | -| `+` operator | first object map, second object map | merges the first object map with the second | -| `fill_with` | second object map | adds in all properties of the second object map that do not exist in the object map | -| `keys` | _none_ | returns an [array] of all the property names (in random order), not available under [`no_index`] | -| `values` | _none_ | returns an [array] of all the property values (in random order), not available under [`no_index`] | +| Function | Parameter(s) | Description | +| ---------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `has` | property name | does the object map contain a property of a particular name? | +| `len` | _none_ | returns the number of properties | +| `clear` | _none_ | empties the object map | +| `remove` | property name | removes a certain property and returns it ([`()`] if the property does not exist) | +| `+=` operator, `mixin` | second object map | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) | +| `+` operator | 1) first object map
2) second object map | merges the first object map with the second | +| `fill_with` | second object map | adds in all properties of the second object map that do not exist in the object map | +| `keys` | _none_ | returns an [array] of all the property names (in random order), not available under [`no_index`] | +| `values` | _none_ | returns an [array] of all the property values (in random order), not available under [`no_index`] | Examples diff --git a/doc/src/language/string-fn.md b/doc/src/language/string-fn.md index a9caaa94..d6e373a5 100644 --- a/doc/src/language/string-fn.md +++ b/doc/src/language/string-fn.md @@ -6,20 +6,20 @@ Built-in String Functions The following standard methods (mostly defined in the [`MoreStringPackage`][packages] but excluded if using a [raw `Engine`]) operate on [strings]: -| Function | Parameter(s) | Description | -| ------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -| `len` method and property | _none_ | returns the number of characters (not number of bytes) in the string | -| `pad` | character to pad, target length | pads the string with an character to at least a specified length | -| `+=` operator, `append` | character/string to append | Adds a character or a string to the end of another string | -| `clear` | _none_ | empties the string | -| `truncate` | target length | cuts off the string at exactly a specified number of characters | -| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | -| `index_of` | character/sub-string to search for, start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found | -| `sub_string` | start index, length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) | -| `split` | delimiter character/string | splits the string by the specified delimiter, returning an [array] of string segments; not available under [`no_index`] | -| `crop` | start index, length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) | -| `replace` | target character/sub-string, replacement character/string | replaces a sub-string with another | -| `trim` | _none_ | trims the string of whitespace at the beginning and end | +| Function | Parameter(s) | Description | +| ------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `len` method and property | _none_ | returns the number of characters (not number of bytes) in the string | +| `pad` | 1) character to pad
2) target length | pads the string with an character to at least a specified length | +| `+=` operator, `append` | character/string to append | Adds a character or a string to the end of another string | +| `clear` | _none_ | empties the string | +| `truncate` | target length | cuts off the string at exactly a specified number of characters | +| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | +| `index_of` | 1) character/sub-string to search for
2) start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found | +| `sub_string` | 1) start index
2) length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) | +| `split` | delimiter character/string | splits the string by the specified delimiter, returning an [array] of string segments; not available under [`no_index`] | +| `crop` | 1) start index
2) length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) | +| `replace` | 1) target character/sub-string
2) replacement character/string | replaces a sub-string with another | +| `trim` | _none_ | trims the string of whitespace at the beginning and end | Examples -------- diff --git a/doc/src/language/strings-chars.md b/doc/src/language/strings-chars.md index 81ca6258..a402d485 100644 --- a/doc/src/language/strings-chars.md +++ b/doc/src/language/strings-chars.md @@ -43,17 +43,17 @@ Hex sequences map to ASCII characters, while '`\u`' maps to 16-bit common Unicod Standard escape sequences: -| Escape sequence | Meaning | -| --------------- | ------------------------------ | -| `\\` | back-slash `\` | -| `\t` | tab | -| `\r` | carriage-return `CR` | -| `\n` | line-feed `LF` | -| `\"` | double-quote `"` in strings | -| `\'` | single-quote `'` in characters | -| `\x`_xx_ | Unicode in 2-digit hex | -| `\u`_xxxx_ | Unicode in 4-digit hex | -| `\U`_xxxxxxxx_ | Unicode in 8-digit hex | +| Escape sequence | Meaning | +| --------------- | -------------------------------- | +| `\\` | back-slash `\` | +| `\t` | tab | +| `\r` | carriage-return `CR` | +| `\n` | line-feed `LF` | +| `\"` | double-quote `"` | +| `\'` | single-quote `'` | +| `\x`_xx_ | ASCII character in 2-digit hex | +| `\u`_xxxx_ | Unicode character in 4-digit hex | +| `\U`_xxxxxxxx_ | Unicode character in 8-digit hex | Differences from Rust Strings diff --git a/doc/src/language/type-of.md b/doc/src/language/type-of.md index 02ddbc28..87171d06 100644 --- a/doc/src/language/type-of.md +++ b/doc/src/language/type-of.md @@ -24,3 +24,24 @@ if type_of(x) == "string" { do_something_with_string(x); } ``` + + +Custom Types +------------ + +`type_of()` a [custom type] returns: + +* if registered via `Engine::register_type_with_name` - the registered name + +* if registered via `Engine::register_type` - the full Rust path name + +```rust +struct TestStruct1; +struct TestStruct2; + +engine + // type_of(struct1) == "crate::path::to::module::TestStruct1" + .register_type::() + // type_of(struct2) == "MyStruct" + .register_type_with_name::("MyStruct"); +``` diff --git a/doc/src/links.md b/doc/src/links.md index 0231689b..2fd9d152 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -49,6 +49,8 @@ [`Dynamic`]: {{rootUrl}}/language/dynamic.md [`to_int`]: {{rootUrl}}/language/convert.md [`to_float`]: {{rootUrl}}/language/convert.md +[`parse_int`]: {{rootUrl}}/language/convert.md +[`parse_float`]: {{rootUrl}}/language/convert.md [custom type]: {{rootUrl}}/rust/custom.md [custom types]: {{rootUrl}}/rust/custom.md diff --git a/doc/src/patterns/config.md b/doc/src/patterns/config.md index c87a0fb3..3017bb1d 100644 --- a/doc/src/patterns/config.md +++ b/doc/src/patterns/config.md @@ -51,6 +51,12 @@ let config: Rc> = Rc::new(RefCell::new(Default::default())); ### Register Config API +The trick to building a Config API is to clone the shared configuration object and +move it into each function registration as a closure. + +It is not possible to use a [plugin module] to achieve this, so each function must +be registered one after another. + ```rust // Notice 'move' is used to move the shared configuration object into the closure. let cfg = config.clone(); @@ -66,27 +72,27 @@ engine.register_fn("config_set", move |value: i64| *cfg.borrow_mut().some_field let cfg = config.clone(); engine.register_fn("config_add", move |value: String| - cfg.borrow_mut().some_list.push(value) + cfg.borrow_mut().some_list.push(value) ); let cfg = config.clone(); engine.register_fn("config_add", move |values: &mut Array| - cfg.borrow_mut().some_list.extend(values.into_iter().map(|v| v.to_string())) + cfg.borrow_mut().some_list.extend(values.into_iter().map(|v| v.to_string())) ); let cfg = config.clone(); engine.register_fn("config_add", move |key: String, value: bool| - cfg.borrow_mut().some_map.insert(key, value) + cfg.borrow_mut().some_map.insert(key, value) ); let cfg = config.clone(); engine.register_fn("config_contains", move |value: String| - cfg.borrow().some_list.contains(&value) + cfg.borrow().some_list.contains(&value) ); let cfg = config.clone(); engine.register_fn("config_is_set", move |value: String| - cfg.borrow().some_map.get(&value).cloned().unwrap_or(false) + cfg.borrow().some_map.get(&value).cloned().unwrap_or(false) ); ``` diff --git a/doc/src/patterns/control.md b/doc/src/patterns/control.md index 52b6b23c..c3624442 100644 --- a/doc/src/patterns/control.md +++ b/doc/src/patterns/control.md @@ -64,6 +64,12 @@ let bunny: Rc> = Rc::new(RefCell::(EnergizerBunny::new() ### Register Control API +The trick to building a Control API is to clone the shared API object and +move it into each function registration as a closure. + +It is not possible to use a [plugin module] to achieve this, so each function must +be registered one after another. + ```rust // Notice 'move' is used to move the shared API object into the closure. let b = bunny.clone(); diff --git a/doc/src/patterns/singleton.md b/doc/src/patterns/singleton.md index c38df1d5..fa7c9d75 100644 --- a/doc/src/patterns/singleton.md +++ b/doc/src/patterns/singleton.md @@ -58,7 +58,7 @@ impl EnergizerBunny { pub fn new () -> Self { ... } pub fn go (&mut self) { ... } pub fn stop (&mut self) { ... } - pub fn is_going (&self) { ... } + pub fn is_going (&self) -> bol { ... } pub fn get_speed (&self) -> i64 { ... } pub fn set_speed (&mut self, speed: i64) { ... } pub fn turn (&mut self, left_turn: bool) { ... } @@ -77,37 +77,52 @@ let SharedBunnyType = Rc>; engine.register_type_with_name::("EnergizerBunny"); ``` -### Register Methods and Getters/Setters +### Develop a Plugin with Methods and Getters/Setters + +The easiest way to develop a complete set of API for a [custom type] is via a [plugin module]. ```rust -engine - .register_get_set("power", - |bunny: &mut SharedBunnyType| bunny.borrow().is_going(), - |bunny: &mut SharedBunnyType, on: bool| { - if on { - if bunny.borrow().is_going() { - println!("Still going..."); - } else { - bunny.borrow_mut().go(); - } +use rhai::plugins::*; + +#[export_module] +pub mod bunny_api { + pub const MAX_SPEED: i64 = 100; + + #[rhai_fn(get = "power")] + pub fn get_power(bunny: &mut SharedBunnyType) -> bool { + bunny.borrow().is_going() + } + #[rhai_fn(set = "power")] + pub fn set_power(bunny: &mut SharedBunnyType, on: bool) { + if on { + if bunny.borrow().is_going() { + println!("Still going..."); } else { - if bunny.borrow().is_going() { - bunny.borrow_mut().stop(); - } else { - println!("Already out of battery!"); - } + bunny.borrow_mut().go(); + } + } else { + if bunny.borrow().is_going() { + bunny.borrow_mut().stop(); + } else { + println!("Already out of battery!"); } } - ).register_get("speed", |bunny: &mut SharedBunnyType| { + } + #[rhai_fn(get = "speed")] + pub fn get_speed(bunny: &mut SharedBunnyType) -> i64 { if bunny.borrow().is_going() { bunny.borrow().get_speed() } else { 0 } - }).register_set_result("speed", |bunny: &mut SharedBunnyType, speed: i64| { + } + #[rhai_fn(set = "speed", return_raw)] + pub fn set_speed(bunny: &mut SharedBunnyType, speed: i64) + -> Result> + { if speed <= 0 { Err("Speed must be positive!".into()) - } else if speed > 100 { + } else if speed > MAX_SPEED { Err("Bunny will be going too fast!".into()) } else if !bunny.borrow().is_going() { Err("Bunny is not yet going!".into()) @@ -115,15 +130,20 @@ engine b.borrow_mut().set_speed(speed); Ok(().into()) } - }).register_fn("turn_left", |bunny: &mut SharedBunnyType| { + } + pub fn turn_left(bunny: &mut SharedBunnyType) { if bunny.borrow().is_going() { bunny.borrow_mut().turn(true); } - }).register_fn("turn_right", |bunny: &mut SharedBunnyType| { + } + pub fn turn_right(bunny: &mut SharedBunnyType) { if bunny.borrow().is_going() { bunny.borrow_mut().turn(false); } - }); + } +} + +engine.load_package(exported_module!(bunny_api)); ``` ### Push Constant Command Object into Custom Scope @@ -132,7 +152,9 @@ engine let bunny: SharedBunnyType = Rc::new(RefCell::(EnergizerBunny::new())); let mut scope = Scope::new(); -scope.push_constant("BUNNY", bunny.clone()); + +// Add the command object into a custom Scope. +scope.push_constant("Bunny", bunny.clone()); engine.consume_with_scope(&mut scope, script)?; ``` @@ -140,11 +162,11 @@ engine.consume_with_scope(&mut scope, script)?; ### Use the Command API in Script ```rust -// Access the command object via constant variable 'BUNNY'. +// Access the command object via constant variable 'Bunny'. -if !BUNNY.power { BUNNY.power = true; } +if !Bunny.power { Bunny.power = true; } -if BUNNY.speed > 50 { BUNNY.speed = 50; } +if Bunny.speed > 50 { Bunny.speed = 50; } -BUNNY.turn_left(); +Bunny.turn_left(); ``` diff --git a/doc/src/plugins/function.md b/doc/src/plugins/function.md index d0d9b5fd..388c9b84 100644 --- a/doc/src/plugins/function.md +++ b/doc/src/plugins/function.md @@ -11,11 +11,11 @@ individual functions instead of a full-blown [plugin module]. Macros ------ -| Macro | Apply to | Description | -| ----------------------- | --------------------------------------------------------------- | ------------------------------------------------------------- | -| `#[export_fn]` | rust function defined in a Rust module | exports the function | -| `register_exported_fn!` | [`Engine`] instance, register name string, use path to function | registers the function into an [`Engine`] under specific name | -| `set_exported_fn!` | [`Module`] instance, register name string, use path to function | registers the function into an [`Module`] under specific name | +| Macro | Signature | Description | +| ----------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- | +| `#[export_fn]` | apply to rust function defined in a Rust module | exports the function | +| `register_exported_fn!` | `register_exported_fn!(&mut `_engine_`, "`_name_`", `_function_`)` | registers the function into an [`Engine`] under a specific name | +| `set_exported_fn!` | `set_exported_fn!(&mut `_module_`, "`_name_`", `_function_`)` | registers the function into a [`Module`] under a specific name | `#[export_fn]` and `register_exported_fn!` diff --git a/doc/src/plugins/module.md b/doc/src/plugins/module.md index 2d001f68..97e2ffa6 100644 --- a/doc/src/plugins/module.md +++ b/doc/src/plugins/module.md @@ -29,9 +29,9 @@ use rhai::plugins::*; // a "prelude" import for macros #[export_module] mod my_module { - // This constant will be registered as the constant variable 'SOME_NUMBER'. + // This constant will be registered as the constant variable 'MY_NUMBER'. // Ignored when loaded as a package. - pub const SOME_NUMBER: i64 = 42; + pub const MY_NUMBER: i64 = 42; // This function will be registered as 'greet'. pub fn greet(name: &str) -> String { @@ -260,12 +260,12 @@ Inner attributes can be applied to the inner items of a module to tweak the expo Parameters should be set on inner attributes to specify the desired behavior. -| Attribute Parameter | Use with | Apply to | Description | -| ------------------- | --------------------------- | -------------------------------------------------------- | ------------------------------------------------------ | -| `skip` | `#[rhai_fn]`, `#[rhai_mod]` | function or sub-module | do not export this function/sub-module | -| `name = "..."` | `#[rhai_fn]`, `#[rhai_mod]` | function or sub-module | registers function/sub-module under the specified name | -| `get = "..."` | `#[rhai_fn]` | function with `&mut` first parameter | registers a getter for the named property | -| `set = "..."` | `#[rhai_fn]` | function with `&mut` first parameter | registers a setter for the named property | -| `index_get` | `#[rhai_fn]` | function with `&mut` first parameter | registers an index getter | -| `index_set` | `#[rhai_fn]` | function with `&mut` first parameter | registers an index setter | -| `return_raw` | `#[rhai_fn]` | function returning `Result>` | marks this as a [fallible function] | +| Attribute Parameter | Use with | Apply to | Description | +| ------------------- | --------------------------- | ----------------------------------------------------- | ------------------------------------------------------ | +| `skip` | `#[rhai_fn]`, `#[rhai_mod]` | function or sub-module | do not export this function/sub-module | +| `name = "..."` | `#[rhai_fn]`, `#[rhai_mod]` | function or sub-module | registers function/sub-module under the specified name | +| `get = "..."` | `#[rhai_fn]` | `pub fn (&mut Type) -> Value` | registers a getter for the named property | +| `set = "..."` | `#[rhai_fn]` | `pub fn (&mut Type, Value)` | registers a setter for the named property | +| `index_get` | `#[rhai_fn]` | `pub fn (&mut Type, INT) -> Value` | registers an index getter | +| `index_set` | `#[rhai_fn]` | `pub fn (&mut Type, INT, Value)` | registers an index setter | +| `return_raw` | `#[rhai_fn]` | `pub fn (...) -> Result>` | marks this as a [fallible function] | diff --git a/doc/src/rust/getters-setters.md b/doc/src/rust/getters-setters.md index efc19514..bb07989d 100644 --- a/doc/src/rust/getters-setters.md +++ b/doc/src/rust/getters-setters.md @@ -53,10 +53,10 @@ impl TestStruct { let mut engine = Engine::new(); - engine - .register_type::() - .register_get_set("xyz", TestStruct::get_field, TestStruct::set_field) - .register_fn("new_ts", TestStruct::new); +engine + .register_type::() + .register_get_set("xyz", TestStruct::get_field, TestStruct::set_field) + .register_fn("new_ts", TestStruct::new); let result = engine.eval::(r#"let a = new_ts(); a.xyz = "42"; a.xyz"#)?; diff --git a/doc/src/rust/modules/ast.md b/doc/src/rust/modules/ast.md new file mode 100644 index 00000000..0348579a --- /dev/null +++ b/doc/src/rust/modules/ast.md @@ -0,0 +1,88 @@ +Create a Module from an AST +========================== + +{{#include ../../links.md}} + + +`Module::eval_ast_as_new` +------------------------ + +A _module_ can be created from a single script (or pre-compiled [`AST`]) containing global variables, +functions and sub-modules via the `Module::eval_ast_as_new` method. + +When given an [`AST`], it is first evaluated, then the following items are exposed as members of the new module: + +* Global variables - essentially all variables that remain in the [`Scope`] at the end of a script run - that are exported. Variables not exported (via the `export` statement) remain hidden. + +* Functions not specifically marked `private`. + +* Global modules that remain in the [`Scope`] at the end of a script run. + + +`merge_namespaces` Parameter +--------------------------- + +The parameter `merge_namespaces` in `Module::eval_ast_as_new` determines the exact behavior of +functions exposed by the module and the namespace that they can access: + +| `merge_namespaces` value | Description | Namespace | Performance | Call global functions | Call functions in same module | +| :----------------------: | ------------------------------------------------ | :-----------------: | :---------: | :-------------------: | :---------------------------: | +| `true` | encapsulate entire `AST` into each function call | module, then global | slower | yes | yes | +| `false` | register each function independently | global only | fast | yes | no | + + +Examples +-------- + +Don't forget the [`export`] statement, otherwise there will be no variables exposed by the module +other than non-[`private`] functions (unless that's intentional). + +```rust +use rhai::{Engine, Module}; + +let engine = Engine::new(); + +// Compile a script into an 'AST' +let ast = engine.compile(r#" + // Functions become module functions + fn calc(x) { + x + 1 + } + fn add_len(x, y) { + x + y.len + } + + // Imported modules can become sub-modules + import "another module" as extra; + + // Variables defined at global level can become module variables + const x = 123; + let foo = 41; + let hello; + + // Variable values become constant module variable values + foo = calc(foo); + hello = "hello, " + foo + " worlds!"; + + // Finally, export the variables and modules + export + x as abc, // aliased variable name + foo, + hello, + extra as foobar; // export sub-module +"#)?; + +// Convert the 'AST' into a module, using the 'Engine' to evaluate it first +// +// The second parameter ('merge_namespaces'), when set to true, will encapsulate +// a copy of the entire 'AST' into each function, allowing functions in the module script +// to cross-call each other. +// +// This incurs additional overhead, avoidable by setting 'merge_namespaces' to false. +let module = Module::eval_ast_as_new(Scope::new(), &ast, true, &engine)?; + +// 'module' now contains: +// - sub-module: 'foobar' (renamed from 'extra') +// - functions: 'calc', 'add_len' +// - constants: 'abc' (renamed from 'x'), 'foo', 'hello' +``` diff --git a/doc/src/rust/modules/create.md b/doc/src/rust/modules/create.md index d2f68e54..51462b22 100644 --- a/doc/src/rust/modules/create.md +++ b/doc/src/rust/modules/create.md @@ -22,19 +22,38 @@ For the complete `Module` API, refer to the [documentation](https://docs.rs/rhai Make the `Module` Available to the `Engine` ------------------------------------------ -In order to _use_ a custom module, there must be a [module resolver], which serves the module when -loaded via `import` statements. +`Engine::load_package` supports loading a [module] as a [package]. + +Since it acts as a [package], all functions will be registered into the _global_ namespace +and can be accessed without _module qualifiers_. + +```rust +use rhai::{Engine, Module}; + +let mut module = Module::new(); // new module +module.set_fn_1("inc", |x: i64| Ok(x+1)); // use the 'set_fn_XXX' API to add functions + +// Load the module into the Engine as a new package. +let mut engine = Engine::new(); +engine.load_package(module); + +engine.eval::("inc(41)")? == 42; // no need to import module +``` + + +Make the `Module` Dynamically Loadable +------------------------------------- + +In order to dynamically load a custom module, there must be a [module resolver] which serves +the module when loaded via `import` statements. The easiest way is to use, for example, the [`StaticModuleResolver`][module resolver] to hold such a custom module. ```rust -use rhai::{Engine, Scope, Module, i64}; +use rhai::{Engine, Scope, Module}; use rhai::module_resolvers::StaticModuleResolver; -let mut engine = Engine::new(); -let mut scope = Scope::new(); - let mut module = Module::new(); // new module module.set_var("answer", 41_i64); // variable 'answer' under module module.set_fn_1("inc", |x: i64| Ok(x+1)); // use the 'set_fn_XXX' API to add functions @@ -47,11 +66,12 @@ let mut resolver = StaticModuleResolver::new(); resolver.insert("question", module); // Set the module resolver into the 'Engine' +let mut engine = Engine::new(); engine.set_module_resolver(Some(resolver)); // Use module-qualified variables -engine.eval::(&scope, r#"import "question" as q; q::answer + 1"#)? == 42; +engine.eval::(r#"import "question" as q; q::answer + 1"#)? == 42; // Call module-qualified functions -engine.eval::(&scope, r#"import "question" as q; q::inc(q::answer)"#)? == 42; +engine.eval::(r#"import "question" as q; q::inc(q::answer)"#)? == 42; ``` diff --git a/doc/src/rust/modules/imp-resolver.md b/doc/src/rust/modules/imp-resolver.md index d1d9ed38..58aada13 100644 --- a/doc/src/rust/modules/imp-resolver.md +++ b/doc/src/rust/modules/imp-resolver.md @@ -11,8 +11,14 @@ A module resolver must implement the trait [`rhai::ModuleResolver`][traits], which contains only one function: `resolve`. When Rhai prepares to load a module, `ModuleResolver::resolve` is called with the name -of the _module path_ (i.e. the path specified in the [`import`] statement). Upon success, it should -return a [`Module`]; if the module cannot be load, return `EvalAltResult::ErrorModuleNotFound`. +of the _module path_ (i.e. the path specified in the [`import`] statement). + +* Upon success, it should return a [`Module`]. + +* If the path does not resolve to a valid module, return `EvalAltResult::ErrorModuleNotFound`. + +* If the module failed to load, return `EvalAltResult::ErrorInModule`. + Example ------- @@ -35,9 +41,12 @@ impl ModuleResolver for MyModuleResolver { // Check module path. if is_valid_module_path(path) { // Load the custom module. - let module: Module = load_secret_module(path); - Ok(module) + load_secret_module(path).map_err(|err| + // Return EvalAltResult::ErrorInModule upon loading error + EvalAltResult::ErrorInModule(err.to_string(), pos).into() + ) } else { + // Return EvalAltResult::ErrorModuleNotFound if the path is invalid Err(EvalAltResult::ErrorModuleNotFound(path.into(), pos).into()) } } @@ -50,7 +59,7 @@ engine.set_module_resolver(Some(MyModuleResolver {})); engine.consume(r#" import "hello" as foo; // this 'import' statement will call - // 'MyModuleResolver::resolve' with "hello" as path + // 'MyModuleResolver::resolve' with "hello" as `path` foo:bar(); "#)?; ``` diff --git a/doc/src/rust/modules/resolvers.md b/doc/src/rust/modules/resolvers.md index 451388af..ebb73698 100644 --- a/doc/src/rust/modules/resolvers.md +++ b/doc/src/rust/modules/resolvers.md @@ -12,16 +12,120 @@ Built-In Module Resolvers ------------------------ There are a number of standard resolvers built into Rhai, the default being the `FileModuleResolver` -which simply loads a script file based on the path (with `.rhai` extension attached) and execute it to form a module. +which simply loads a script file based on the path (with `.rhai` extension attached) +and execute it to form a module. Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace. -| Module Resolver | Description | Namespace | -| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------: | -| `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | Module (cannot access global namespace) | -| `GlobalFileModuleResolver` | A simpler but more efficient version of `FileModuleResolver`, intended for short utility modules. Not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.
**Note:** All functions are assumed independent and cannot cross-call each other.
The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.
`FileModuleResolver::create_module()` loads a script file and returns a module. | Global | -| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | Global | -| `ModuleResolversCollection` | A collection of module resolvers. Modules will be resolved from each resolver in sequential order.
This is useful when multiple types of modules are needed simultaneously. | Global | + +`FileModuleResolver` (default) +----------------------------- + +The _default_ module resolution service, not available for [`no_std`] or [WASM] builds. +Loads a script file (based off the current directory) with `.rhai` extension. + +All functions in the _global_ namespace, plus all those defined in the same module, +are _merged_ into a _unified_ namespace. + +```rust +------------------ +| my_module.rhai | +------------------ + +private fn inner_message() { "hello! from module!" } + +fn greet(callback) { print(callback.call()); } + +------------- +| main.rhai | +------------- + +fn main_message() { "hi! from main!" } + +import "my_module" as m; + +m::greet(|| "hello, " + "world!"); // works - anonymous function in global + +m::greet(|| inner_message()); // works - function in module + +m::greet(|| main_message()); // works - function in global +``` + +The base directory can be changed via the `FileModuleResolver::new_with_path` constructor function. + +`FileModuleResolver::create_module` loads a script file and returns a module. + + +`GlobalFileModuleResolver` +------------------------- + +A simpler but more efficient version of `FileModuleResolver`, intended for short utility modules. +Not available for [`no_std`] or [WASM] builds. +Loads a script file (based off the current directory) with `.rhai` extension. + +All functions are assumed **independent** and _cannot_ cross-call each other. +Functions are searched _only_ in the _global_ namespace. + +```rust +------------------ +| my_module.rhai | +------------------ + +private fn inner_message() { "hello! from module!" } + +fn greet_inner() { + print(inner_message()); // cross-calling a module function! + // there will be trouble because each function + // in the module is supposed to be independent + // of each other +} + +fn greet() { + print(main_message()); // function is searched in global namespace +} + +------------- +| main.rhai | +------------- + +fn main_message() { "hi! from main!" } + +import "my_module" as m; + +m::greet_inner(); // <- function not found: 'inner_message' + +m::greet(); // works because 'main_message' exists in + // the global namespace +``` + +The base directory can be changed via the `FileModuleResolver::new_with_path` constructor function. + +`GlobalFileModuleResolver::create_module` loads a script file and returns a module. + + +`StaticModuleResolver` +--------------------- + +Loads modules that are statically added. This can be used under [`no_std`]. + +Functions are searched in the _global_ namespace by default. + +```rust +use rhai::{Module, module_resolvers::StaticModuleResolver}; + +let module: Module = create_a_module(); + +let mut resolver = StaticModuleResolver::new(); +resolver.insert("my_module", module); +``` + + +`ModuleResolversCollection` +-------------------------- + +A collection of module resolvers. Modules will be resolved from each resolver in sequential order. + +This is useful when multiple types of modules are needed simultaneously. Set into `Engine` @@ -30,8 +134,14 @@ Set into `Engine` An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`: ```rust -// Use the 'StaticModuleResolver' -let resolver = rhai::module_resolvers::StaticModuleResolver::new(); +use rhai::module_resolvers::StaticModuleResolver; + +// Create a module resolver +let resolver = StaticModuleResolver::new(); + +// Register functions into 'resolver'... + +// Use the module resolver engine.set_module_resolver(Some(resolver)); // Effectively disable 'import' statements by setting module resolver to 'None' diff --git a/doc/src/rust/packages/create.md b/doc/src/rust/packages/create.md index 01d11b12..698be745 100644 --- a/doc/src/rust/packages/create.md +++ b/doc/src/rust/packages/create.md @@ -34,9 +34,11 @@ Macro Parameters ```rust // Import necessary types and traits. use rhai::{ - def_package, - packages::Package, - packages::{ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage} + def_package, // 'def_package!' macro + packages::Package, // 'Package' trait + packages::{ // pre-defined packages + ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage + } }; // Define the package 'MyPackage'. diff --git a/doc/src/rust/packages/index.md b/doc/src/rust/packages/index.md index f2e6b598..020097cc 100644 --- a/doc/src/rust/packages/index.md +++ b/doc/src/rust/packages/index.md @@ -9,10 +9,12 @@ Packages reside under `rhai::packages::*` and the trait `rhai::packages::Package packages to be used. Packages typically contain Rust functions that are callable within a Rhai script. -All functions registered in a package is loaded under the _global namespace_ (i.e. they're available without module qualifiers). +All functions registered in a package is loaded under the _global namespace_ +(i.e. they're available without module qualifiers). -Once a package is created (e.g. via `Package::new`), it can be _shared_ (via `Package::get`) among multiple instances of [`Engine`], -even across threads (under [`sync`]). Therefore, a package only has to be created _once_. +Once a package is created (e.g. via `Package::new`), it can be _shared_ (via `Package::get`) +among multiple instances of [`Engine`], even across threads (under [`sync`]). +Therefore, a package only has to be created _once_. ```rust use rhai::Engine; diff --git a/doc/src/rust/register-raw.md b/doc/src/rust/register-raw.md index 965a103b..b77ebab5 100644 --- a/doc/src/rust/register-raw.md +++ b/doc/src/rust/register-raw.md @@ -84,12 +84,12 @@ Extract Arguments To extract an argument from the `args` parameter (`&mut [&mut Dynamic]`), use the following: -| Argument type | Access (`n` = argument position) | Result | -| ------------------------------ | ------------------------------------- | --------------------------------------------------------- | -| [Primary type][standard types] | `args[n].clone().cast::()` | copy of value | -| Custom type | `args[n].read_lock::().unwrap()` | immutable reference to value | -| Custom type (consumed) | `std::mem::take(args[n]).cast::()` | the _consumed_ value.
The original value becomes `()` | -| `this` object | `args[0].write_lock::().unwrap()` | mutable reference to value | +| Argument type | Access (`n` = argument position) | Result | +| ------------------------------ | ------------------------------------- | ----------------------------------------------------- | +| [Primary type][standard types] | `args[n].clone().cast::()` | copy of value | +| [Custom type] | `args[n].read_lock::().unwrap()` | immutable reference to value | +| [Custom type] (consumed) | `std::mem::take(args[n]).cast::()` | the _consumed_ value; the original value becomes `()` | +| `this` object | `args[0].write_lock::().unwrap()` | mutable reference to value | When there is a mutable reference to the `this` object (i.e. the first argument), there can be no other immutable references to `args`, otherwise the Rust borrow checker will complain. diff --git a/doc/src/rust/serde.md b/doc/src/rust/serde.md index 19c3a22e..7b9a12f1 100644 --- a/doc/src/rust/serde.md +++ b/doc/src/rust/serde.md @@ -105,6 +105,17 @@ let x: MyStruct = from_dynamic(&result)?; ``` +Cannot Deserialize Shared Values +------------------------------- + +A [`Dynamic`] containing a _shared_ value cannot be deserialized - i.e. it will give a type error. + +Use `Dynamic::flatten` to obtain a cloned copy before deserialization +(if the value is not shared, it is simply returned and not cloned). + +Shared values are turned off via the [`no_closure`] feature. + + Lighter Alternative ------------------- diff --git a/doc/src/safety/sandbox.md b/doc/src/safety/sandbox.md index a85a5f9e..60e35534 100644 --- a/doc/src/safety/sandbox.md +++ b/doc/src/safety/sandbox.md @@ -11,11 +11,15 @@ Furthermore, an [`Engine`] created non-`mut` cannot mutate any state, including It is highly recommended that [`Engine`]'s be created immutable as much as possible. ```rust -// Use the fluent API to configure an 'Engine' and then keep an immutable instance. -let engine = Engine::new() - .register_get("field", get_field) - .register_set("field", set_field) - .register_fn("do_work", action); +let mut engine = Engine::new(); + +// Use the fluent API to configure an 'Engine' +engine.register_get("field", get_field) + .register_set("field", set_field) + .register_fn("do_work", action); + +// Then turn it into an immutable instance +let engine = engine; // 'engine' is immutable... ``` diff --git a/tests/macro_register.rs b/tests/plugins_register.rs similarity index 100% rename from tests/macro_register.rs rename to tests/plugins_register.rs diff --git a/tests/macro_unroll.rs b/tests/plugins_unroll.rs similarity index 100% rename from tests/macro_unroll.rs rename to tests/plugins_unroll.rs