Fix encapsulated environment in module functions.

This commit is contained in:
Stephen Chung 2022-01-30 17:27:13 +08:00
parent 8fc80ecd10
commit 7b92a80c32
13 changed files with 150 additions and 63 deletions

View File

@ -12,6 +12,7 @@ Bug fixes
* In `Scope::clone_visible`, constants are now properly cloned as constants.
* Variables introduced inside `try` blocks are now properly cleaned up upon an exception.
* Off-by-one error in character positions after a comment line is now fixed.
* Globally-defined constants are now encapsulated correctly inside a loaded module and no longer spill across call boundaries.
Script-breaking changes
-----------------------

View File

@ -11,6 +11,9 @@ pub use ast::{ASTNode, AST};
pub use expr::{BinaryExpr, CustomExpr, Expr, FnCallExpr, FnCallHashes};
pub use flags::{FnAccess, OptionFlags, AST_OPTION_FLAGS};
pub use ident::Ident;
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_function"))]
pub use script_fn::EncapsulatedEnviron;
#[cfg(not(feature = "no_function"))]
pub use script_fn::{ScriptFnDef, ScriptFnMetadata};
pub use stmt::{ConditionalStmtBlock, OpAssignment, Stmt, StmtBlock, SwitchCases, TryCatchBlock};

View File

@ -2,24 +2,43 @@
#![cfg(not(feature = "no_function"))]
use super::{FnAccess, StmtBlock};
use crate::{Identifier, Module, Shared, StaticVec};
use crate::{Identifier, StaticVec};
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
use std::{fmt, hash::Hash};
/// _(internals)_ Encapsulated AST environment.
/// Exported under the `internals` feature only.
///
/// 1) other functions defined within the same AST
/// 2) the stack of imported [modules][crate::Module]
/// 3) global constants
///
/// Not available under `no_module` or `no_function`.
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_function"))]
#[derive(Debug, Clone)]
pub struct EncapsulatedEnviron {
/// Functions defined within the same [`AST`][crate::AST].
pub lib: crate::Shared<crate::Module>,
/// Imported [modules][crate::Module].
pub imports: Box<[(Identifier, crate::Shared<crate::Module>)]>,
/// Globally-defined constants.
pub constants: Option<crate::eval::GlobalConstants>,
}
/// _(internals)_ A type containing information on a script-defined function.
/// Exported under the `internals` feature only.
#[derive(Debug, Clone)]
pub struct ScriptFnDef {
/// Function body.
pub body: StmtBlock,
/// Encapsulated running environment, if any.
pub lib: Option<Shared<Module>>,
/// Encapsulated stack of imported modules, if any.
/// Encapsulated AST environment, if any.
///
/// Not available under `no_module`.
/// Not available under `no_module` or `no_function`.
#[cfg(not(feature = "no_module"))]
pub global: Option<Box<[(Identifier, Shared<Module>)]>>,
#[cfg(not(feature = "no_function"))]
pub environ: Option<EncapsulatedEnviron>,
/// Function name.
pub name: Identifier,
/// Function access mode.

View File

@ -295,6 +295,7 @@ fn main() {
engine.set_module_resolver(resolver);
}
// Register sample functions
engine
.register_fn("test", |x: INT, y: INT| format!("{} {}", x, y))
.register_fn("test", |x: &mut INT, y: INT, z: &str| {

View File

@ -5,6 +5,12 @@ use crate::{Engine, Identifier};
use std::prelude::v1::*;
use std::{fmt, marker::PhantomData};
/// Collection of globally-defined constants.
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_function"))]
pub type GlobalConstants =
crate::Shared<crate::Locked<std::collections::BTreeMap<Identifier, crate::Dynamic>>>;
/// _(internals)_ Global runtime states.
/// Exported under the `internals` feature only.
//
@ -44,9 +50,7 @@ pub struct GlobalRuntimeState<'a> {
/// Interior mutability is needed because it is shared in order to aid in cloning.
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_function"))]
pub(crate) constants: Option<
crate::Shared<crate::Locked<std::collections::BTreeMap<Identifier, crate::Dynamic>>>,
>,
pub(crate) constants: Option<GlobalConstants>,
/// Debugging interface.
#[cfg(feature = "debugging")]
pub debugger: super::Debugger,

View File

@ -17,5 +17,8 @@ pub use debugger::CallStackFrame;
pub use debugger::{BreakPoint, Debugger, DebuggerCommand, OnDebuggerCallback, OnDebuggingInit};
pub use eval_context::EvalContext;
pub use eval_state::EvalState;
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_function"))]
pub use global_state::GlobalConstants;
pub use global_state::GlobalRuntimeState;
pub use target::{calc_index, calc_offset_len, Target};

View File

@ -646,13 +646,12 @@ impl Engine {
};
mem::swap(&mut global.source, &mut source);
let level = _level + 1;
let result = if _is_method_call {
// Method call of script function - map first argument to `this`
let (first_arg, rest_args) = args.split_first_mut().unwrap();
let level = _level + 1;
let result = self.call_script_fn(
scope,
global,
@ -679,8 +678,6 @@ impl Engine {
.change_first_arg_to_copy(args);
}
let level = _level + 1;
let result = self.call_script_fn(
scope, global, state, lib, &mut None, func, args, pos, true, level,
);

View File

@ -4,7 +4,7 @@
use super::call::FnCallArgs;
use crate::ast::ScriptFnDef;
use crate::eval::{EvalState, GlobalRuntimeState};
use crate::{Dynamic, Engine, Module, Position, RhaiError, RhaiResult, Scope, StaticVec, ERR};
use crate::{Dynamic, Engine, Module, Position, RhaiError, RhaiResult, Scope, ERR};
use std::mem;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
@ -41,13 +41,19 @@ impl Engine {
err: RhaiError,
pos: Position,
) -> RhaiResult {
let _fn_def = fn_def;
#[cfg(not(feature = "no_module"))]
let source = _fn_def
.environ
.as_ref()
.and_then(|environ| environ.lib.id().map(str::to_string));
#[cfg(feature = "no_module")]
let source = None;
Err(ERR::ErrorInFunctionCall(
name,
fn_def
.lib
.as_ref()
.and_then(|m| m.id().map(str::to_string))
.unwrap_or_else(|| global.source.to_string()),
source.unwrap_or_else(|| global.source.to_string()),
err,
pos,
)
@ -98,28 +104,39 @@ impl Engine {
);
// Merge in encapsulated environment, if any
let mut lib_merged = StaticVec::with_capacity(lib.len() + 1);
let orig_fn_resolution_caches_len = state.fn_resolution_caches_len();
let lib = if let Some(ref fn_lib) = fn_def.lib {
if fn_lib.is_empty() {
lib
} else {
state.push_fn_resolution_cache();
lib_merged.push(fn_lib.as_ref());
lib_merged.extend(lib.iter().cloned());
&lib_merged
}
} else {
lib
};
#[cfg(not(feature = "no_module"))]
let mut lib_merged = crate::StaticVec::with_capacity(lib.len() + 1);
#[cfg(not(feature = "no_module"))]
if let Some(ref modules) = fn_def.global {
for (n, m) in modules.iter().cloned() {
let (lib, constants) = if let Some(crate::ast::EncapsulatedEnviron {
lib: ref fn_lib,
ref imports,
#[cfg(not(feature = "no_function"))]
ref constants,
}) = fn_def.environ
{
for (n, m) in imports.iter().cloned() {
global.push_import(n, m)
}
}
(
if fn_lib.is_empty() {
lib
} else {
state.push_fn_resolution_cache();
lib_merged.push(fn_lib.as_ref());
lib_merged.extend(lib.iter().cloned());
&lib_merged
},
#[cfg(not(feature = "no_function"))]
Some(mem::replace(&mut global.constants, constants.clone())),
#[cfg(feature = "no_function")]
None,
)
} else {
(lib, None)
};
// Evaluate the function
let result = self
@ -165,6 +182,12 @@ impl Engine {
#[cfg(not(feature = "no_module"))]
global.truncate_imports(orig_imports_len);
// Restore constants
#[cfg(not(feature = "no_function"))]
if let Some(constants) = constants {
global.constants = constants;
}
// Restore state
state.rewind_fn_resolution_caches(orig_fn_resolution_caches_len);

View File

@ -259,6 +259,11 @@ pub use ast::{
AST_OPTION_FLAGS::*,
};
#[cfg(feature = "internals")]
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_function"))]
pub use ast::EncapsulatedEnviron;
#[cfg(feature = "internals")]
#[cfg(not(feature = "no_float"))]
pub use ast::FloatWrapper;

View File

@ -1696,33 +1696,21 @@ impl Module {
let mut module = Module::new();
// Extra modules left become sub-modules
#[cfg(not(feature = "no_function"))]
let mut func_global = None;
let mut imports = StaticVec::new_const();
if result.is_ok() {
global
.scan_imports_raw()
.skip(orig_imports_len)
.for_each(|(k, m)| {
#[cfg(not(feature = "no_function"))]
if func_global.is_none() {
func_global = Some(StaticVec::new());
}
#[cfg(not(feature = "no_function"))]
func_global
.as_mut()
.expect("`Some`")
.push((k.clone(), m.clone()));
imports.push((k.clone(), m.clone()));
module.set_sub_module(k.clone(), m.clone());
});
}
// Restore global state
#[cfg(not(feature = "no_function"))]
{
global.constants = orig_constants;
}
let constants = std::mem::replace(&mut global.constants, orig_constants);
global.truncate_imports(orig_imports_len);
global.source = orig_source;
@ -1747,12 +1735,9 @@ impl Module {
}
}
#[cfg(not(feature = "no_function"))]
let func_global = func_global.map(|v| v.into_boxed_slice());
// Non-private functions defined become module functions
#[cfg(not(feature = "no_function"))]
if ast.has_functions() {
{
ast.shared_lib()
.iter_fn()
.filter(|&f| match f.metadata.access {
@ -1761,15 +1746,20 @@ impl Module {
})
.filter(|&f| f.func.is_script())
.for_each(|f| {
// Encapsulate AST environment
let mut func = f
.func
.get_script_fn_def()
.expect("script-defined function")
.as_ref()
.clone();
func.lib = Some(ast.shared_lib().clone());
func.global = func_global.clone();
// Encapsulate AST environment
func.environ = Some(crate::ast::EncapsulatedEnviron {
lib: ast.shared_lib().clone(),
imports: imports.clone().into_boxed_slice(),
constants: constants.clone(),
});
module.set_script_fn(func);
});
}

View File

@ -1211,9 +1211,8 @@ pub fn optimize_into_ast(
access: fn_def.access,
body: crate::ast::StmtBlock::NONE,
params: fn_def.params.clone(),
lib: None,
#[cfg(not(feature = "no_module"))]
global: None,
environ: None,
#[cfg(not(feature = "no_function"))]
#[cfg(feature = "metadata")]
comments: None,

View File

@ -3185,9 +3185,8 @@ fn parse_fn(
access,
params,
body,
lib: None,
#[cfg(not(feature = "no_module"))]
global: None,
environ: None,
#[cfg(not(feature = "no_function"))]
#[cfg(feature = "metadata")]
comments: if comments.is_empty() {
@ -3341,9 +3340,8 @@ fn parse_anon_fn(
access: crate::FnAccess::Public,
params,
body: body.into(),
lib: None,
#[cfg(not(feature = "no_module"))]
global: None,
environ: None,
#[cfg(not(feature = "no_function"))]
#[cfg(feature = "metadata")]
comments: None,

View File

@ -514,3 +514,47 @@ fn test_module_file() -> Result<(), Box<EvalAltResult>> {
Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
Ok(())
}
#[cfg(not(feature = "no_function"))]
#[test]
fn test_module_environ() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
let ast = engine.compile(
r#"
const SECRET = 42;
fn foo(x) {
print(global::SECRET);
global::SECRET + x
}
"#,
)?;
let mut m = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
m.set_id("test");
m.build_index();
engine.register_static_module("test", m.into());
assert_eq!(
engine.eval::<String>(
r#"
const SECRET = "hello";
fn foo(x) {
print(global::SECRET);
global::SECRET + x
}
let t = test::foo(0);
foo(t)
"#
)?,
"hello42"
);
Ok(())
}