Merge pull request #555 from schungx/master

New advanced API's.
This commit is contained in:
Stephen Chung 2022-04-23 14:02:19 +08:00 committed by GitHub
commit ba475a7ad4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 258 additions and 104 deletions

View File

@ -8,11 +8,12 @@ Bug fixes
--------- ---------
* Compound assignments now work properly with indexers. * Compound assignments now work properly with indexers.
* Cloning a `Scope` no longer turns all constants to mutable.
Script-breaking changes Script-breaking changes
----------------------- -----------------------
* _Strict Variables Mode_ no longer returns an error when an undeclared variable matches a constant in the provided external `Scope`. * _Strict Variables Mode_ no longer returns an error when an undeclared variable matches a variable/constant in the provided external `Scope`.
Enhancements Enhancements
------------ ------------
@ -22,6 +23,9 @@ Enhancements
* `Engine::parse_json` now natively handles nested JSON inputs (using a token remap filter) without needing to replace `{` with `#{`. * `Engine::parse_json` now natively handles nested JSON inputs (using a token remap filter) without needing to replace `{` with `#{`.
* `to_json` is added to object maps to cheaply convert it to JSON format (`()` is mapped to `null`, all other data types must be supported by JSON) * `to_json` is added to object maps to cheaply convert it to JSON format (`()` is mapped to `null`, all other data types must be supported by JSON)
* A global function `format_map_as_json` is provided which is the same as `to_json` for object maps. * A global function `format_map_as_json` is provided which is the same as `to_json` for object maps.
* `FileModuleResolver` now accepts a custom `Scope` to provide constants for optimization.
* A new low-level method `Engine::call_fn_raw_raw` is added to add speed to repeated function calls.
* A new low-level method `Engine::eval_statements_raw` is added to evaluate a sequence of statements.
Version 1.6.1 Version 1.6.1

View File

@ -4,9 +4,10 @@
use crate::eval::{Caches, GlobalRuntimeState}; use crate::eval::{Caches, GlobalRuntimeState};
use crate::types::dynamic::Variant; use crate::types::dynamic::Variant;
use crate::{ use crate::{
Dynamic, Engine, FuncArgs, Position, RhaiResult, RhaiResultOf, Scope, StaticVec, AST, ERR, reify, Dynamic, Engine, FuncArgs, Position, RhaiResult, RhaiResultOf, Scope, StaticVec, AST,
ERR,
}; };
use std::any::type_name; use std::any::{type_name, TypeId};
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
@ -66,6 +67,15 @@ impl Engine {
let result = self.call_fn_raw(scope, ast, true, true, name, None, arg_values)?; let result = self.call_fn_raw(scope, ast, true, true, name, None, arg_values)?;
// Bail out early if the return type needs no cast
if TypeId::of::<T>() == TypeId::of::<Dynamic>() {
return Ok(reify!(result => T));
}
if TypeId::of::<T>() == TypeId::of::<()>() {
return Ok(reify!(() => T));
}
// Cast return type
let typ = self.map_type_name(result.type_name()); let typ = self.map_type_name(result.type_name());
result.try_cast().ok_or_else(|| { result.try_cast().ok_or_else(|| {
@ -73,8 +83,9 @@ impl Engine {
ERR::ErrorMismatchOutputType(t, typ.into(), Position::NONE).into() ERR::ErrorMismatchOutputType(t, typ.into(), Position::NONE).into()
}) })
} }
/// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments and the /// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments.
/// following options: ///
/// The following options are available:
/// ///
/// * whether to evaluate the [`AST`] to load necessary modules before calling the function /// * whether to evaluate the [`AST`] to load necessary modules before calling the function
/// * whether to rewind the [`Scope`] after the function call /// * whether to rewind the [`Scope`] after the function call
@ -138,7 +149,7 @@ impl Engine {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[inline] #[inline(always)]
pub fn call_fn_raw( pub fn call_fn_raw(
&self, &self,
scope: &mut Scope, scope: &mut Scope,
@ -149,9 +160,85 @@ impl Engine {
this_ptr: Option<&mut Dynamic>, this_ptr: Option<&mut Dynamic>,
arg_values: impl AsMut<[Dynamic]>, arg_values: impl AsMut<[Dynamic]>,
) -> RhaiResult { ) -> RhaiResult {
let caches = &mut Caches::new(); self.call_fn_internal(
let global = &mut GlobalRuntimeState::new(self); scope,
&mut GlobalRuntimeState::new(self),
&mut Caches::new(),
ast,
eval_ast,
rewind_scope,
name,
this_ptr,
arg_values,
)
}
/// _(internals)_ Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments.
/// Exported under the `internals` feature only.
///
/// The following options are available:
///
/// * whether to evaluate the [`AST`] to load necessary modules before calling the function
/// * whether to rewind the [`Scope`] after the function call
/// * a value for binding to the `this` pointer (if any)
///
/// Not available under `no_function`.
///
/// # WARNING - Low Level API
///
/// This function is very low level.
///
/// A [`GlobalRuntimeState`] and [`Caches`] need to be passed into the function, which can be
/// created via [`GlobalRuntimeState::new`] and [`Caches::new`].
/// This makes repeatedly calling particular functions more efficient as the functions resolution cache
/// is kept intact.
///
/// # Arguments
///
/// All the arguments are _consumed_, meaning that they're replaced by `()`.
/// This is to avoid unnecessarily cloning the arguments.
///
/// Do not use the arguments after this call. If they are needed afterwards, clone them _before_
/// calling this function.
#[cfg(feature = "internals")]
#[inline(always)]
pub fn call_fn_raw_raw(
&self,
scope: &mut Scope,
global: &mut GlobalRuntimeState,
caches: &mut Caches,
ast: &AST,
eval_ast: bool,
rewind_scope: bool,
name: impl AsRef<str>,
this_ptr: Option<&mut Dynamic>,
arg_values: impl AsMut<[Dynamic]>,
) -> RhaiResult {
self.call_fn_internal(
scope,
global,
caches,
ast,
eval_ast,
rewind_scope,
name,
this_ptr,
arg_values,
)
}
/// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments.
fn call_fn_internal(
&self,
scope: &mut Scope,
global: &mut GlobalRuntimeState,
caches: &mut Caches,
ast: &AST,
eval_ast: bool,
rewind_scope: bool,
name: impl AsRef<str>,
this_ptr: Option<&mut Dynamic>,
arg_values: impl AsMut<[Dynamic]>,
) -> RhaiResult {
let statements = ast.statements(); let statements = ast.statements();
let orig_scope_len = scope.len(); let orig_scope_len = scope.len();

View File

@ -6,9 +6,9 @@ use crate::types::dynamic::Variant;
use crate::{ use crate::{
Dynamic, Engine, Module, OptimizationLevel, Position, RhaiResult, RhaiResultOf, Scope, AST, ERR, Dynamic, Engine, Module, OptimizationLevel, Position, RhaiResult, RhaiResultOf, Scope, AST, ERR,
}; };
use std::any::type_name;
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
use std::{any::type_name, mem};
impl Engine { impl Engine {
/// Evaluate a string. /// Evaluate a string.
@ -211,9 +211,10 @@ impl Engine {
global.source = ast.source_raw().clone(); global.source = ast.source_raw().clone();
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
{ let orig_embedded_module_resolver = mem::replace(
global.embedded_module_resolver = ast.resolver().cloned(); &mut global.embedded_module_resolver,
} ast.resolver().cloned(),
);
let statements = ast.statements(); let statements = ast.statements();
@ -230,6 +231,36 @@ impl Engine {
} else { } else {
&lib[..] &lib[..]
}; };
self.eval_global_statements(scope, global, &mut caches, statements, lib, level)
let result =
self.eval_global_statements(scope, global, &mut caches, statements, lib, level);
#[cfg(not(feature = "no_module"))]
{
global.embedded_module_resolver = orig_embedded_module_resolver;
}
result
}
/// _(internals)_ Evaluate a list of statements with no `this` pointer.
/// Exported under the `internals` feature only.
///
/// This is commonly used to evaluate a list of statements in an [`AST`] or a script function body.
///
/// # WARNING - Low Level API
///
/// This function is very low level.
#[cfg(feature = "internals")]
#[inline(always)]
pub fn eval_statements_raw(
&self,
scope: &mut Scope,
global: &mut GlobalRuntimeState,
caches: &mut Caches,
statements: &[crate::ast::Stmt],
lib: &[&Module],
level: usize,
) -> RhaiResult {
self.eval_global_statements(scope, global, caches, statements, lib, level)
} }
} }

View File

@ -154,43 +154,6 @@ fn print_debug_help() {
println!(); println!();
} }
/// Display the current scope.
fn print_scope(scope: &Scope, dedup: bool) {
let flattened_clone;
let scope = if dedup {
flattened_clone = scope.clone_visible();
&flattened_clone
} else {
scope
};
for (i, (name, constant, value)) in scope.iter_raw().enumerate() {
#[cfg(not(feature = "no_closure"))]
let value_is_shared = if value.is_shared() { " (shared)" } else { "" };
#[cfg(feature = "no_closure")]
let value_is_shared = "";
if dedup {
println!(
"{}{}{} = {:?}",
if constant { "const " } else { "" },
name,
value_is_shared,
*value.read_lock::<Dynamic>().unwrap(),
);
} else {
println!(
"[{}] {}{}{} = {:?}",
i + 1,
if constant { "const " } else { "" },
name,
value_is_shared,
*value.read_lock::<Dynamic>().unwrap(),
);
}
}
}
// Load script to debug. // Load script to debug.
fn load_script(engine: &Engine) -> (rhai::AST, String) { fn load_script(engine: &Engine) -> (rhai::AST, String) {
if let Some(filename) = env::args().skip(1).next() { if let Some(filename) = env::args().skip(1).next() {
@ -365,7 +328,7 @@ fn debug_callback(
[] | ["step" | "s"] => break Ok(DebuggerCommand::StepInto), [] | ["step" | "s"] => break Ok(DebuggerCommand::StepInto),
["over" | "o"] => break Ok(DebuggerCommand::StepOver), ["over" | "o"] => break Ok(DebuggerCommand::StepOver),
["next" | "n"] => break Ok(DebuggerCommand::Next), ["next" | "n"] => break Ok(DebuggerCommand::Next),
["scope"] => print_scope(context.scope(), false), ["scope"] => println!("{}", context.scope()),
["print" | "p", "this"] => { ["print" | "p", "this"] => {
if let Some(value) = context.this_ptr() { if let Some(value) = context.this_ptr() {
println!("=> {:?}", value); println!("=> {:?}", value);
@ -381,7 +344,7 @@ fn debug_callback(
} }
} }
["print" | "p"] => { ["print" | "p"] => {
print_scope(context.scope(), true); println!("{}", context.scope().clone_visible());
if let Some(value) = context.this_ptr() { if let Some(value) = context.this_ptr() {
println!("this = {:?}", value); println!("this = {:?}", value);
} }

View File

@ -108,27 +108,6 @@ fn print_keys() {
println!(); println!();
} }
/// Display the scope.
fn print_scope(scope: &Scope) {
for (i, (name, constant, value)) in scope.iter_raw().enumerate() {
#[cfg(not(feature = "no_closure"))]
let value_is_shared = if value.is_shared() { " (shared)" } else { "" };
#[cfg(feature = "no_closure")]
let value_is_shared = "";
println!(
"[{}] {}{}{} = {:?}",
i + 1,
if constant { "const " } else { "" },
name,
value_is_shared,
*value.read_lock::<Dynamic>().unwrap(),
)
}
println!();
}
// Load script files specified in the command line. // Load script files specified in the command line.
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_std"))] #[cfg(not(feature = "no_std"))]
@ -458,7 +437,7 @@ fn main() {
continue; continue;
} }
"scope" => { "scope" => {
print_scope(&scope); println!("{}", scope);
continue; continue;
} }
#[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "no_optimize"))]

View File

@ -50,6 +50,7 @@ pub struct FileModuleResolver {
base_path: Option<PathBuf>, base_path: Option<PathBuf>,
extension: Identifier, extension: Identifier,
cache_enabled: bool, cache_enabled: bool,
scope: Scope<'static>,
#[cfg(not(feature = "sync"))] #[cfg(not(feature = "sync"))]
cache: std::cell::RefCell<BTreeMap<PathBuf, Shared<Module>>>, cache: std::cell::RefCell<BTreeMap<PathBuf, Shared<Module>>>,
@ -57,6 +58,13 @@ pub struct FileModuleResolver {
cache: std::sync::RwLock<BTreeMap<PathBuf, Shared<Module>>>, cache: std::sync::RwLock<BTreeMap<PathBuf, Shared<Module>>>,
} }
impl Default for FileModuleResolver {
#[inline(always)]
fn default() -> Self {
Self::new()
}
}
impl FileModuleResolver { impl FileModuleResolver {
/// Create a new [`FileModuleResolver`] with the current directory as base path. /// Create a new [`FileModuleResolver`] with the current directory as base path.
/// ///
@ -126,6 +134,7 @@ impl FileModuleResolver {
extension: extension.into(), extension: extension.into(),
cache_enabled: true, cache_enabled: true,
cache: BTreeMap::new().into(), cache: BTreeMap::new().into(),
scope: Scope::new(),
} }
} }
@ -155,6 +164,7 @@ impl FileModuleResolver {
extension: extension.into(), extension: extension.into(),
cache_enabled: true, cache_enabled: true,
cache: BTreeMap::new().into(), cache: BTreeMap::new().into(),
scope: Scope::new(),
} }
} }
@ -185,6 +195,32 @@ impl FileModuleResolver {
self self
} }
/// Get a reference to the file module resolver's [scope][Scope].
///
/// The [scope][Scope] is used for compiling module scripts.
#[must_use]
#[inline(always)]
pub const fn scope(&self) -> &Scope {
&self.scope
}
/// Set the file module resolver's [scope][Scope].
///
/// The [scope][Scope] is used for compiling module scripts.
#[inline(always)]
pub fn set_scope(&mut self, scope: Scope<'static>) {
self.scope = scope;
}
/// Get a mutable reference to the file module resolver's [scope][Scope].
///
/// The [scope][Scope] is used for compiling module scripts.
#[must_use]
#[inline(always)]
pub fn scope_mut(&mut self) -> &mut Scope<'static> {
&mut self.scope
}
/// Enable/disable the cache. /// Enable/disable the cache.
#[inline(always)] #[inline(always)]
pub fn enable_cache(&mut self, enable: bool) -> &mut Self { pub fn enable_cache(&mut self, enable: bool) -> &mut Self {
@ -281,10 +317,8 @@ impl FileModuleResolver {
} }
} }
let scope = Scope::new();
let mut ast = engine let mut ast = engine
.compile_file(file_path.clone()) .compile_file_with_scope(&self.scope, file_path.clone())
.map_err(|err| match *err { .map_err(|err| match *err {
ERR::ErrorSystem(.., err) if err.is::<IoError>() => { ERR::ErrorSystem(.., err) if err.is::<IoError>() => {
Box::new(ERR::ErrorModuleNotFound(path.to_string(), pos)) Box::new(ERR::ErrorModuleNotFound(path.to_string(), pos))
@ -294,7 +328,9 @@ impl FileModuleResolver {
ast.set_source(path); ast.set_source(path);
let m: Shared<Module> = if let Some(global) = global { let scope = Scope::new();
let m: Shared<_> = if let Some(global) = global {
Module::eval_ast_as_new_raw(engine, scope, global, &ast) Module::eval_ast_as_new_raw(engine, scope, global, &ast)
} else { } else {
Module::eval_ast_as_new(scope, &ast, engine) Module::eval_ast_as_new(scope, &ast, engine)

View File

@ -22,7 +22,7 @@ use std::{collections::btree_map::IntoIter, collections::BTreeMap, ops::AddAssig
/// ///
/// engine.set_module_resolver(resolver); /// engine.set_module_resolver(resolver);
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct StaticModuleResolver(BTreeMap<Identifier, Shared<Module>>); pub struct StaticModuleResolver(BTreeMap<Identifier, Shared<Module>>);
impl StaticModuleResolver { impl StaticModuleResolver {

View File

@ -1301,10 +1301,7 @@ impl Engine {
if settings.options.strict_var if settings.options.strict_var
&& !settings.is_closure_scope && !settings.is_closure_scope
&& index.is_none() && index.is_none()
&& !matches!( && !state.scope.contains(name)
state.scope.get_index(name),
Some((_, AccessMode::ReadOnly))
)
{ {
// If the parent scope is not inside another capturing closure // If the parent scope is not inside another capturing closure
// then we can conclude that the captured variable doesn't exist. // then we can conclude that the captured variable doesn't exist.
@ -1450,7 +1447,7 @@ impl Engine {
if settings.options.strict_var if settings.options.strict_var
&& index.is_none() && index.is_none()
&& !matches!(state.scope.get_index(&s), Some((_, AccessMode::ReadOnly))) && !state.scope.contains(&s)
{ {
return Err( return Err(
PERR::VariableUndefined(s.to_string()).into_err(settings.pos) PERR::VariableUndefined(s.to_string()).into_err(settings.pos)

View File

@ -6,6 +6,7 @@ use smallvec::SmallVec;
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
use std::{ use std::{
fmt,
iter::{Extend, FromIterator}, iter::{Extend, FromIterator},
marker::PhantomData, marker::PhantomData,
}; };
@ -59,7 +60,7 @@ const SCOPE_ENTRIES_INLINED: usize = 8;
// direct indexing, by-passing the name altogether. // direct indexing, by-passing the name altogether.
// //
// [`Dynamic`] is reasonably small so packing it tightly improves cache performance. // [`Dynamic`] is reasonably small so packing it tightly improves cache performance.
#[derive(Debug, Clone, Hash, Default)] #[derive(Debug, Hash, Default)]
pub struct Scope<'a> { pub struct Scope<'a> {
/// Current value of the entry. /// Current value of the entry.
values: SmallVec<[Dynamic; SCOPE_ENTRIES_INLINED]>, values: SmallVec<[Dynamic; SCOPE_ENTRIES_INLINED]>,
@ -71,6 +72,51 @@ pub struct Scope<'a> {
phantom: PhantomData<&'a ()>, phantom: PhantomData<&'a ()>,
} }
impl fmt::Display for Scope<'_> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, (name, constant, value)) in self.iter_raw().enumerate() {
#[cfg(not(feature = "no_closure"))]
let value_is_shared = if value.is_shared() { " (shared)" } else { "" };
#[cfg(feature = "no_closure")]
let value_is_shared = "";
write!(
f,
"[{}] {}{}{} = {:?}\n",
i + 1,
if constant { "const " } else { "" },
name,
value_is_shared,
*value.read_lock::<Dynamic>().unwrap(),
)?;
}
Ok(())
}
}
impl Clone for Scope<'_> {
#[inline]
fn clone(&self) -> Self {
Self {
values: self
.values
.iter()
.map(|v| {
// Also copy the value's access mode (otherwise will turn to read-write)
let mut v2 = v.clone();
v2.set_access_mode(v.access_mode());
v2
})
.collect(),
names: self.names.clone(),
aliases: self.aliases.clone(),
phantom: self.phantom.clone(),
}
}
}
impl IntoIterator for Scope<'_> { impl IntoIterator for Scope<'_> {
type Item = (String, Dynamic, Vec<Identifier>); type Item = (String, Dynamic, Vec<Identifier>);
type IntoIter = Box<dyn Iterator<Item = Self::Item>>; type IntoIter = Box<dyn Iterator<Item = Self::Item>>;
@ -551,24 +597,24 @@ impl Scope<'_> {
#[must_use] #[must_use]
pub fn clone_visible(&self) -> Self { pub fn clone_visible(&self) -> Self {
let len = self.len(); let len = self.len();
let mut scope = Self::new();
self.names self.names.iter().rev().enumerate().for_each(|(i, name)| {
.iter() if scope.names.contains(name) {
.rev() return;
.enumerate() }
.fold(Self::new(), |mut entries, (index, name)| {
if entries.names.is_empty() || !entries.names.contains(name) {
let orig_value = &self.values[len - 1 - index];
let alias = &self.aliases[len - 1 - index];
let mut value = orig_value.clone();
value.set_access_mode(orig_value.access_mode());
entries.names.push(name.clone()); let v1 = &self.values[len - 1 - i];
entries.values.push(value); let alias = &self.aliases[len - 1 - i];
entries.aliases.push(alias.clone()); let mut v2 = v1.clone();
} v2.set_access_mode(v1.access_mode());
entries
}) scope.names.push(name.clone());
scope.values.push(v2);
scope.aliases.push(alias.clone());
});
scope
} }
/// Get an iterator to entries in the [`Scope`]. /// Get an iterator to entries in the [`Scope`].
#[inline] #[inline]

View File

@ -57,7 +57,7 @@ fn test_options_strict_var() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new(); let mut engine = Engine::new();
let mut scope = Scope::new(); let mut scope = Scope::new();
scope.push_constant("x", 42 as INT); scope.push("x", 42 as INT);
scope.push_constant("y", 0 as INT); scope.push_constant("y", 0 as INT);
engine.compile("let x = if y { z } else { w };")?; engine.compile("let x = if y { z } else { w };")?;
@ -114,8 +114,8 @@ fn test_options_strict_var() -> Result<(), Box<EvalAltResult>> {
} }
#[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "no_optimize"))]
assert_eq!( assert_eq!(
engine.eval_with_scope::<INT>(&mut scope, "fn foo(z) { x * y + z } foo(1)")?, engine.eval_with_scope::<INT>(&mut scope, "fn foo(z) { y + z } foo(x)")?,
1 42
); );
} }

View File

@ -79,6 +79,17 @@ fn test_var_scope() -> Result<(), Box<EvalAltResult>> {
); );
} }
scope.clear();
scope.push("x", 42 as INT);
scope.push_constant("x", 42 as INT);
let scope2 = scope.clone();
let scope3 = scope.clone_visible();
assert_eq!(scope2.is_constant("x"), Some(true));
assert_eq!(scope3.is_constant("x"), Some(true));
Ok(()) Ok(())
} }