diff --git a/Cargo.toml b/Cargo.toml index 5bae3961..c1ea251a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] default = [] +plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync no_optimize = [] # no script optimizer diff --git a/README.md b/README.md index 78fbae94..401b49fc 100644 --- a/README.md +++ b/README.md @@ -1499,6 +1499,11 @@ record == "Bob X. Davis: age 42 ❤\n"; "Davis" in record == true; 'X' in record == true; 'C' in record == false; + +// Strings can be iterated with a 'for' statement, yielding characters +for ch in record { + print(ch); +} ``` The maximum allowed length of a string can be controlled via `Engine::set_max_string_size` @@ -2011,9 +2016,18 @@ loop { Iterating through a range or an [array] is provided by the `for` ... `in` loop. ```rust -let array = [1, 3, 5, 7, 9, 42]; +// Iterate through string, yielding characters +let s = "hello, world!"; + +for ch in s { + if ch > 'z' { continue; } // skip to the next iteration + print(ch); + if x == '@' { break; } // break out of for loop +} // Iterate through array +let array = [1, 3, 5, 7, 9, 42]; + for x in array { if x > 10 { continue; } // skip to the next iteration print(x); diff --git a/examples/repl.rs b/examples/repl.rs index 4a3bd561..f18a56e5 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -125,7 +125,7 @@ fn main() { match engine .compile_with_scope(&scope, &script) - .map_err(|err| err.into()) + .map_err(Into::into) .and_then(|r| { ast_u = r.clone(); diff --git a/src/engine.rs b/src/engine.rs index 8ca14356..c94d1423 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -230,6 +230,9 @@ pub fn get_script_function_by_signature<'a>( /// /// Currently, `Engine` is neither `Send` nor `Sync`. Use the `sync` feature to make it `Send + Sync`. pub struct Engine { + /// A unique ID identifying this scripting `Engine`. + pub id: Option, + /// A module containing all functions directly loaded into the Engine. pub(crate) global_module: Module, /// A collection of all library packages loaded into the Engine. @@ -274,6 +277,8 @@ impl Default for Engine { fn default() -> Self { // Create the new scripting Engine let mut engine = Self { + id: None, + packages: Default::default(), global_module: Default::default(), @@ -431,6 +436,8 @@ impl Engine { /// Use the `load_package` method to load additional packages of functions. pub fn new_raw() -> Self { Self { + id: None, + packages: Default::default(), global_module: Default::default(), module_resolver: None, @@ -483,6 +490,15 @@ impl Engine { self.optimization_level = optimization_level } + /// The current optimization level. + /// It controls whether and how the `Engine` will optimize an AST after compilation. + /// + /// Not available under the `no_optimize` feature. + #[cfg(not(feature = "no_optimize"))] + pub fn optimization_level(&self) -> OptimizationLevel { + self.optimization_level + } + /// Set the maximum levels of function calls allowed for a script in order to avoid /// infinite recursion and stack overflows. #[cfg(not(feature = "unchecked"))] @@ -490,11 +506,27 @@ impl Engine { self.max_call_stack_depth = levels } + /// The maximum levels of function calls allowed for a script. + #[cfg(not(feature = "unchecked"))] + pub fn max_call_levels(&self) -> usize { + self.max_call_stack_depth + } + /// Set the maximum number of operations allowed for a script to run to avoid /// consuming too much resources (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_operations(&mut self, operations: u64) { - self.max_operations = operations; + self.max_operations = if operations == u64::MAX { + 0 + } else { + operations + }; + } + + /// The maximum number of operations allowed for a script to run (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_operations(&self) -> u64 { + self.max_operations } /// Set the maximum number of imported modules allowed for a script. @@ -503,31 +535,77 @@ impl Engine { self.max_modules = modules; } - /// Set the depth limits for expressions/statements (0 for unlimited). + /// The maximum number of imported modules allowed for a script. + #[cfg(not(feature = "unchecked"))] + pub fn max_modules(&self) -> usize { + self.max_modules + } + + /// Set the depth limits for expressions (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_expr_depths(&mut self, max_expr_depth: usize, max_function_expr_depth: usize) { - self.max_expr_depth = max_expr_depth; - self.max_function_expr_depth = max_function_expr_depth; + self.max_expr_depth = if max_expr_depth == usize::MAX { + 0 + } else { + max_expr_depth + }; + self.max_function_expr_depth = if max_function_expr_depth == usize::MAX { + 0 + } else { + max_function_expr_depth + }; + } + + /// The depth limit for expressions (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_expr_depth(&self) -> usize { + self.max_expr_depth + } + + /// The depth limit for expressions in functions (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_function_expr_depth(&self) -> usize { + self.max_function_expr_depth } /// Set the maximum length of strings (0 for unlimited). #[cfg(not(feature = "unchecked"))] pub fn set_max_string_size(&mut self, max_size: usize) { - self.max_string_size = max_size; + self.max_string_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of strings (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + pub fn max_string_size(&self) -> usize { + self.max_string_size } /// Set the maximum length of arrays (0 for unlimited). #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "no_index"))] pub fn set_max_array_size(&mut self, max_size: usize) { - self.max_array_size = max_size; + self.max_array_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of arrays (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_index"))] + pub fn max_array_size(&self) -> usize { + self.max_array_size } /// Set the maximum length of object maps (0 for unlimited). #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "no_object"))] pub fn set_max_map_size(&mut self, max_size: usize) { - self.max_map_size = max_size; + self.max_map_size = if max_size == usize::MAX { 0 } else { max_size }; + } + + /// The maximum length of object maps (0 for unlimited). + #[cfg(not(feature = "unchecked"))] + #[cfg(not(feature = "no_object"))] + pub fn max_map_size(&self) -> usize { + self.max_map_size } /// Set the module resolution service used by the `Engine`. @@ -1705,6 +1783,7 @@ impl Engine { self.call_script_fn(&mut scope, state, lib, name, fn_def, args, level) .map_err(|err| EvalAltResult::new_position(err, *pos)) } + Ok(f) if f.is_plugin_fn() => f.get_plugin_fn().call(args.as_mut(), *pos), Ok(f) => { f.get_native_fn()(self, args.as_mut()).map_err(|err| err.new_position(*pos)) } @@ -2186,14 +2265,14 @@ fn run_builtin_binary_op( #[cfg(not(feature = "unchecked"))] match op { - "+" => return add(x, y).map(Into::::into).map(Some), - "-" => return sub(x, y).map(Into::::into).map(Some), - "*" => return mul(x, y).map(Into::::into).map(Some), - "/" => return div(x, y).map(Into::::into).map(Some), - "%" => return modulo(x, y).map(Into::::into).map(Some), - "~" => return pow_i_i(x, y).map(Into::::into).map(Some), - ">>" => return shr(x, y).map(Into::::into).map(Some), - "<<" => return shl(x, y).map(Into::::into).map(Some), + "+" => return add(x, y).map(Into::into).map(Some), + "-" => return sub(x, y).map(Into::into).map(Some), + "*" => return mul(x, y).map(Into::into).map(Some), + "/" => return div(x, y).map(Into::into).map(Some), + "%" => return modulo(x, y).map(Into::into).map(Some), + "~" => return pow_i_i(x, y).map(Into::into).map(Some), + ">>" => return shr(x, y).map(Into::into).map(Some), + "<<" => return shl(x, y).map(Into::into).map(Some), _ => (), } @@ -2204,9 +2283,9 @@ fn run_builtin_binary_op( "*" => return Ok(Some((x * y).into())), "/" => return Ok(Some((x / y).into())), "%" => return Ok(Some((x % y).into())), - "~" => return pow_i_i_u(x, y).map(Into::::into).map(Some), - ">>" => return shr_u(x, y).map(Into::::into).map(Some), - "<<" => return shl_u(x, y).map(Into::::into).map(Some), + "~" => return pow_i_i_u(x, y).map(Into::into).map(Some), + ">>" => return shr_u(x, y).map(Into::into).map(Some), + "<<" => return shl_u(x, y).map(Into::into).map(Some), _ => (), } @@ -2280,7 +2359,7 @@ fn run_builtin_binary_op( "*" => return Ok(Some((x * y).into())), "/" => return Ok(Some((x / y).into())), "%" => return Ok(Some((x % y).into())), - "~" => return pow_f_f(x, y).map(Into::::into).map(Some), + "~" => return pow_f_f(x, y).map(Into::into).map(Some), "==" => return Ok(Some((x == y).into())), "!=" => return Ok(Some((x != y).into())), ">" => return Ok(Some((x > y).into())), diff --git a/src/fn_native.rs b/src/fn_native.rs index b7805ff5..fc6e7673 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,6 +1,7 @@ use crate::any::Dynamic; use crate::engine::Engine; use crate::parser::ScriptFnDef; +use crate::plugin::PluginFunction; use crate::result::EvalAltResult; use crate::stdlib::{boxed::Box, fmt, rc::Rc, sync::Arc}; @@ -59,6 +60,11 @@ pub type FnAny = pub type IteratorFn = fn(Dynamic) -> Box>; +#[cfg(feature = "sync")] +pub type SharedPluginFunction = Arc; +#[cfg(not(feature = "sync"))] +pub type SharedPluginFunction = Rc; + #[cfg(not(feature = "sync"))] pub type Callback = Box R + 'static>; #[cfg(feature = "sync")] @@ -74,6 +80,8 @@ pub enum CallableFunction { Method(Shared), /// An iterator function. Iterator(IteratorFn), + /// A plugin-defined function, + Plugin(SharedPluginFunction), /// A script-defined function. Script(Shared), } @@ -84,6 +92,7 @@ impl fmt::Debug for CallableFunction { Self::Pure(_) => write!(f, "NativePureFunction"), Self::Method(_) => write!(f, "NativeMethod"), Self::Iterator(_) => write!(f, "NativeIterator"), + Self::Plugin(_) => write!(f, "PluginFunction"), Self::Script(fn_def) => fmt::Debug::fmt(fn_def, f), } } @@ -95,6 +104,7 @@ impl CallableFunction { match self { Self::Pure(_) => true, Self::Method(_) | Self::Iterator(_) | Self::Script(_) => false, + Self::Plugin(_) => false, } } /// Is this a pure native Rust method-call? @@ -102,6 +112,7 @@ impl CallableFunction { match self { Self::Method(_) => true, Self::Pure(_) | Self::Iterator(_) | Self::Script(_) => false, + Self::Plugin(_) => false, } } /// Is this an iterator function? @@ -109,6 +120,7 @@ impl CallableFunction { match self { Self::Iterator(_) => true, Self::Pure(_) | Self::Method(_) | Self::Script(_) => false, + Self::Plugin(_) => false, } } /// Is this a Rhai-scripted function? @@ -116,6 +128,14 @@ impl CallableFunction { match self { Self::Script(_) => true, Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => false, + Self::Plugin(_) => false, + } + } + /// Is this a plugin-defined function? + pub fn is_plugin_fn(&self) -> bool { + match self { + Self::Plugin(_) => true, + Self::Pure(_) | Self::Method(_) | Self::Iterator(_) | Self::Script(_) => false, } } /// Get a reference to a native Rust function. @@ -127,6 +147,7 @@ impl CallableFunction { match self { Self::Pure(f) | Self::Method(f) => f.as_ref(), Self::Iterator(_) | Self::Script(_) => panic!(), + Self::Plugin(_) => panic!(), } } /// Get a shared reference to a script-defined function definition. @@ -136,7 +157,7 @@ impl CallableFunction { /// Panics if the `CallableFunction` is not `Script`. pub fn get_shared_fn_def(&self) -> Shared { match self { - Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), + Self::Pure(_) | Self::Method(_) | Self::Plugin(_) | Self::Iterator(_) => panic!(), Self::Script(f) => f.clone(), } } @@ -149,6 +170,7 @@ impl CallableFunction { match self { Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => panic!(), Self::Script(f) => f, + Self::Plugin(_) => panic!(), } } /// Get a reference to an iterator function. @@ -160,6 +182,18 @@ impl CallableFunction { match self { Self::Iterator(f) => *f, Self::Pure(_) | Self::Method(_) | Self::Script(_) => panic!(), + Self::Plugin(_) => panic!(), + } + } + /// Get a reference to a plugin function. + /// + /// # Panics + /// + /// Panics if the `CallableFunction` is not `Plugin`. + pub fn get_plugin_fn<'s>(&'s self) -> SharedPluginFunction { + match self { + Self::Plugin(f) => f.clone(), + Self::Pure(_) | Self::Method(_) | Self::Script(_) | Self::Iterator(_) => panic!(), } } /// Create a new `CallableFunction::Pure`. @@ -170,6 +204,18 @@ impl CallableFunction { pub fn from_method(func: Box) -> Self { Self::Method(func.into()) } + + #[cfg(feature = "sync")] + /// Create a new `CallableFunction::Plugin`. + pub fn from_plugin(plugin: impl PluginFunction + 'static + Send + Sync) -> Self { + Self::Plugin(Arc::new(plugin)) + } + + #[cfg(not(feature = "sync"))] + /// Create a new `CallableFunction::Plugin`. + pub fn from_plugin(plugin: impl PluginFunction + 'static) -> Self { + Self::Plugin(Rc::new(plugin)) + } } impl From for CallableFunction { diff --git a/src/fn_register.rs b/src/fn_register.rs index 1b07cbe8..cf83ae14 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -5,11 +5,91 @@ use crate::any::{Dynamic, Variant}; use crate::engine::Engine; use crate::fn_native::{CallableFunction, FnAny, FnCallArgs, SendSync}; use crate::parser::FnAccess; +use crate::plugin::Plugin; use crate::result::EvalAltResult; use crate::utils::ImmutableString; use crate::stdlib::{any::TypeId, boxed::Box, mem}; +/// A trait to register custom plugins with the `Engine`. +/// +/// A plugin consists of a number of functions. All functions will be registered with the engine. +pub trait RegisterPlugin { + /// Allow extensions of the engine's behavior. + /// + /// This can include importing modules, registering functions to the global name space, and + /// more. + /// + /// # Example + /// + /// ``` + /// use rhai::{FLOAT, INT, Module, ModuleResolver, RegisterFn, RegisterPlugin}; + /// use rhai::plugin::*; + /// use rhai::module_resolvers::*; + /// + /// // A function we want to expose to Rhai. + /// #[derive(Copy, Clone)] + /// struct DistanceFunction(); + /// + /// impl PluginFunction for DistanceFunction { + /// fn is_method_call(&self) -> bool { false } + /// fn is_varadic(&self) -> bool { false } + /// + /// fn call(&self, args: &[&mut Dynamic], pos: Position) -> Result> { + /// let x1: &FLOAT = args[0].downcast_ref::().unwrap(); + /// let y1: &FLOAT = args[1].downcast_ref::().unwrap(); + /// let x2: &FLOAT = args[2].downcast_ref::().unwrap(); + /// let y2: &FLOAT = args[3].downcast_ref::().unwrap(); + /// let square_sum = (y2 - y1).abs().powf(2.0) + (x2 -x1).abs().powf(2.0); + /// Ok(Dynamic::from(square_sum.sqrt())) + /// } + /// + /// fn clone_boxed(&self) -> Box { + /// Box::new(DistanceFunction()) + /// } + /// } + /// + /// // A simple custom plugin. This should not usually be done with hand-written code. + /// #[derive(Copy, Clone)] + /// pub struct AdvancedMathPlugin(); + /// + /// impl Plugin for AdvancedMathPlugin { + /// fn register_contents(self, engine: &mut Engine) { + /// // Plugins are allowed to have side-effects on the engine. + /// engine.register_fn("get_mystic_number", || { 42 as FLOAT }); + /// + /// // Main purpose: create a module to expose the functions to Rhai. + /// // + /// // This is currently a hack. There needs to be a better API here for "plugin" + /// // modules. + /// let mut m = Module::new(); + /// m.set_fn("euclidean_distance".to_string(), FnAccess::Public, + /// &[std::any::TypeId::of::(), + /// std::any::TypeId::of::(), + /// std::any::TypeId::of::(), + /// std::any::TypeId::of::()], + /// CallableFunction::from_plugin(DistanceFunction())); + /// let mut r = StaticModuleResolver::new(); + /// r.insert("Math::Advanced".to_string(), m); + /// engine.set_module_resolver(Some(r)); + /// } + /// } + /// + /// + /// # fn main() -> Result<(), Box> { + /// + /// let mut engine = Engine::new(); + /// engine.register_plugin(AdvancedMathPlugin()); + /// + /// assert_eq!(engine.eval::( + /// r#"import "Math::Advanced" as math; + /// let x = math::euclidean_distance(0.0, 1.0, 0.0, get_mystic_number()); x"#)?, 41.0); + /// # Ok(()) + /// # } + /// ``` + fn register_plugin(&mut self, plugin: PL); +} + /// Trait to register custom functions with the `Engine`. pub trait RegisterFn { /// Register a custom function with the `Engine`. @@ -111,6 +191,12 @@ pub fn by_value(data: &mut Dynamic) -> T { } } +impl RegisterPlugin for Engine { + fn register_plugin(&mut self, plugin: PL) { + plugin.register_contents(self); + } +} + /// This macro creates a closure wrapping a registered function. macro_rules! make_func { ($fn:ident : $map:expr ; $($par:ident => $convert:expr),*) => { diff --git a/src/lib.rs b/src/lib.rs index 106e83d4..e0f2245c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,10 +75,14 @@ mod engine; mod error; mod fn_call; mod fn_func; -mod fn_native; +pub mod fn_native; mod fn_register; mod module; mod optimize; +#[cfg(not(feature = "no_module"))] +pub mod plugin; +#[cfg(feature = "no_module")] +mod plugin; pub mod packages; mod parser; mod result; @@ -92,6 +96,7 @@ pub use any::Dynamic; pub use engine::Engine; pub use error::{ParseError, ParseErrorType}; pub use fn_register::{RegisterFn, RegisterResultFn}; +pub use fn_register::RegisterPlugin; pub use module::Module; pub use parser::{ImmutableString, AST, INT}; pub use result::EvalAltResult; diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 804af8ef..8f8d1731 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -15,6 +15,7 @@ use crate::stdlib::{ fmt::Display, format, string::{String, ToString}, + vec::Vec, }; fn prepend(x: T, y: ImmutableString) -> FuncReturn { @@ -293,4 +294,12 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str Ok(()) }, ); + + // Register string iterator + lib.set_iter( + TypeId::of::(), + |arr| Box::new( + arr.cast::().chars().collect::>().into_iter().map(Into::into) + ) as Box>, + ); }); diff --git a/src/plugin.rs b/src/plugin.rs new file mode 100644 index 00000000..8159d3c8 --- /dev/null +++ b/src/plugin.rs @@ -0,0 +1,42 @@ +//! Module defining plugins in Rhai. Is exported for use by plugin authors. + +pub use crate::Engine; +pub use crate::any::{Dynamic, Variant}; +pub use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn}; +pub use crate::parser::{ + FnAccess, + FnAccess::{Private, Public}, + AST, +}; +pub use crate::result::EvalAltResult; +pub use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; +pub use crate::token::{Position, Token}; +pub use crate::utils::StaticVec; + +#[cfg(features = "sync")] +/// Represents an externally-written plugin for the Rhai interpreter. +/// +/// This trait should not be used directly. Use the `#[plugin]` procedural attribute instead. +pub trait Plugin: Send { + fn register_contents(self, engine: &mut Engine); +} + +#[cfg(not(features = "sync"))] +/// Represents an externally-written plugin for the Rhai interpreter. +/// +/// This trait should not be used directly. Use the `#[plugin]` procedural attribute instead. +pub trait Plugin: Send + Sync { + fn register_contents(self, engine: &mut Engine); +} + +/// Represents a function that is statically defined within a plugin. +/// +/// This trait should not be used directly. Use the `#[plugin]` procedural attribute instead. +pub trait PluginFunction { + fn is_method_call(&self) -> bool; + fn is_varadic(&self) -> bool; + + fn call(&self, args: &[&mut Dynamic], pos: Position) -> Result>; + + fn clone_boxed(&self) -> Box; +} diff --git a/tests/for.rs b/tests/for.rs index cddff6ed..4f99c06f 100644 --- a/tests/for.rs +++ b/tests/for.rs @@ -30,6 +30,26 @@ fn test_for_array() -> Result<(), Box> { Ok(()) } +#[test] +fn test_for_string() -> Result<(), Box> { + let engine = Engine::new(); + + let script = r#" + let s = "hello"; + let sum = 0; + + for ch in s { + sum += ch.to_int(); + } + + sum + "#; + + assert_eq!(engine.eval::(script)?, 532); + + Ok(()) +} + #[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_index"))] #[test]