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.
* Cloning a `Scope` no longer turns all constants to mutable.
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
------------
@ -22,6 +23,9 @@ Enhancements
* `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)
* 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

View File

@ -4,9 +4,10 @@
use crate::eval::{Caches, GlobalRuntimeState};
use crate::types::dynamic::Variant;
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")]
use std::prelude::v1::*;
@ -66,6 +67,15 @@ impl Engine {
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());
result.try_cast().ok_or_else(|| {
@ -73,8 +83,9 @@ impl Engine {
ERR::ErrorMismatchOutputType(t, typ.into(), Position::NONE).into()
})
}
/// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments and the
/// following options:
/// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments.
///
/// 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
@ -138,7 +149,7 @@ impl Engine {
/// # Ok(())
/// # }
/// ```
#[inline]
#[inline(always)]
pub fn call_fn_raw(
&self,
scope: &mut Scope,
@ -149,9 +160,85 @@ impl Engine {
this_ptr: Option<&mut Dynamic>,
arg_values: impl AsMut<[Dynamic]>,
) -> RhaiResult {
let caches = &mut Caches::new();
let global = &mut GlobalRuntimeState::new(self);
self.call_fn_internal(
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 orig_scope_len = scope.len();

View File

@ -6,9 +6,9 @@ use crate::types::dynamic::Variant;
use crate::{
Dynamic, Engine, Module, OptimizationLevel, Position, RhaiResult, RhaiResultOf, Scope, AST, ERR,
};
use std::any::type_name;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
use std::{any::type_name, mem};
impl Engine {
/// Evaluate a string.
@ -211,9 +211,10 @@ impl Engine {
global.source = ast.source_raw().clone();
#[cfg(not(feature = "no_module"))]
{
global.embedded_module_resolver = ast.resolver().cloned();
}
let orig_embedded_module_resolver = mem::replace(
&mut global.embedded_module_resolver,
ast.resolver().cloned(),
);
let statements = ast.statements();
@ -230,6 +231,36 @@ impl Engine {
} else {
&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!();
}
/// 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.
fn load_script(engine: &Engine) -> (rhai::AST, String) {
if let Some(filename) = env::args().skip(1).next() {
@ -365,7 +328,7 @@ fn debug_callback(
[] | ["step" | "s"] => break Ok(DebuggerCommand::StepInto),
["over" | "o"] => break Ok(DebuggerCommand::StepOver),
["next" | "n"] => break Ok(DebuggerCommand::Next),
["scope"] => print_scope(context.scope(), false),
["scope"] => println!("{}", context.scope()),
["print" | "p", "this"] => {
if let Some(value) = context.this_ptr() {
println!("=> {:?}", value);
@ -381,7 +344,7 @@ fn debug_callback(
}
}
["print" | "p"] => {
print_scope(context.scope(), true);
println!("{}", context.scope().clone_visible());
if let Some(value) = context.this_ptr() {
println!("this = {:?}", value);
}

View File

@ -108,27 +108,6 @@ fn print_keys() {
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.
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_std"))]
@ -458,7 +437,7 @@ fn main() {
continue;
}
"scope" => {
print_scope(&scope);
println!("{}", scope);
continue;
}
#[cfg(not(feature = "no_optimize"))]

View File

@ -50,6 +50,7 @@ pub struct FileModuleResolver {
base_path: Option<PathBuf>,
extension: Identifier,
cache_enabled: bool,
scope: Scope<'static>,
#[cfg(not(feature = "sync"))]
cache: std::cell::RefCell<BTreeMap<PathBuf, Shared<Module>>>,
@ -57,6 +58,13 @@ pub struct FileModuleResolver {
cache: std::sync::RwLock<BTreeMap<PathBuf, Shared<Module>>>,
}
impl Default for FileModuleResolver {
#[inline(always)]
fn default() -> Self {
Self::new()
}
}
impl FileModuleResolver {
/// Create a new [`FileModuleResolver`] with the current directory as base path.
///
@ -126,6 +134,7 @@ impl FileModuleResolver {
extension: extension.into(),
cache_enabled: true,
cache: BTreeMap::new().into(),
scope: Scope::new(),
}
}
@ -155,6 +164,7 @@ impl FileModuleResolver {
extension: extension.into(),
cache_enabled: true,
cache: BTreeMap::new().into(),
scope: Scope::new(),
}
}
@ -185,6 +195,32 @@ impl FileModuleResolver {
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.
#[inline(always)]
pub fn enable_cache(&mut self, enable: bool) -> &mut Self {
@ -281,10 +317,8 @@ impl FileModuleResolver {
}
}
let scope = Scope::new();
let mut ast = engine
.compile_file(file_path.clone())
.compile_file_with_scope(&self.scope, file_path.clone())
.map_err(|err| match *err {
ERR::ErrorSystem(.., err) if err.is::<IoError>() => {
Box::new(ERR::ErrorModuleNotFound(path.to_string(), pos))
@ -294,7 +328,9 @@ impl FileModuleResolver {
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)
} else {
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);
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct StaticModuleResolver(BTreeMap<Identifier, Shared<Module>>);
impl StaticModuleResolver {

View File

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

View File

@ -6,6 +6,7 @@ use smallvec::SmallVec;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
use std::{
fmt,
iter::{Extend, FromIterator},
marker::PhantomData,
};
@ -59,7 +60,7 @@ const SCOPE_ENTRIES_INLINED: usize = 8;
// direct indexing, by-passing the name altogether.
//
// [`Dynamic`] is reasonably small so packing it tightly improves cache performance.
#[derive(Debug, Clone, Hash, Default)]
#[derive(Debug, Hash, Default)]
pub struct Scope<'a> {
/// Current value of the entry.
values: SmallVec<[Dynamic; SCOPE_ENTRIES_INLINED]>,
@ -71,6 +72,51 @@ pub struct Scope<'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<'_> {
type Item = (String, Dynamic, Vec<Identifier>);
type IntoIter = Box<dyn Iterator<Item = Self::Item>>;
@ -551,24 +597,24 @@ impl Scope<'_> {
#[must_use]
pub fn clone_visible(&self) -> Self {
let len = self.len();
let mut scope = Self::new();
self.names
.iter()
.rev()
.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());
entries.values.push(value);
entries.aliases.push(alias.clone());
self.names.iter().rev().enumerate().for_each(|(i, name)| {
if scope.names.contains(name) {
return;
}
entries
})
let v1 = &self.values[len - 1 - i];
let alias = &self.aliases[len - 1 - i];
let mut v2 = v1.clone();
v2.set_access_mode(v1.access_mode());
scope.names.push(name.clone());
scope.values.push(v2);
scope.aliases.push(alias.clone());
});
scope
}
/// Get an iterator to entries in the [`Scope`].
#[inline]

View File

@ -57,7 +57,7 @@ fn test_options_strict_var() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::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);
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"))]
assert_eq!(
engine.eval_with_scope::<INT>(&mut scope, "fn foo(z) { x * y + z } foo(1)")?,
1
engine.eval_with_scope::<INT>(&mut scope, "fn foo(z) { y + z } foo(x)")?,
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(())
}