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. * In `Scope::clone_visible`, constants are now properly cloned as constants.
* Variables introduced inside `try` blocks are now properly cleaned up upon an exception. * 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. * 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 Script-breaking changes
----------------------- -----------------------

View File

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

View File

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

View File

@ -295,6 +295,7 @@ fn main() {
engine.set_module_resolver(resolver); engine.set_module_resolver(resolver);
} }
// Register sample functions
engine engine
.register_fn("test", |x: INT, y: INT| format!("{} {}", x, y)) .register_fn("test", |x: INT, y: INT| format!("{} {}", x, y))
.register_fn("test", |x: &mut INT, y: INT, z: &str| { .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::prelude::v1::*;
use std::{fmt, marker::PhantomData}; 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. /// _(internals)_ Global runtime states.
/// Exported under the `internals` feature only. /// 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. /// Interior mutability is needed because it is shared in order to aid in cloning.
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
pub(crate) constants: Option< pub(crate) constants: Option<GlobalConstants>,
crate::Shared<crate::Locked<std::collections::BTreeMap<Identifier, crate::Dynamic>>>,
>,
/// Debugging interface. /// Debugging interface.
#[cfg(feature = "debugging")] #[cfg(feature = "debugging")]
pub debugger: super::Debugger, pub debugger: super::Debugger,

View File

@ -17,5 +17,8 @@ pub use debugger::CallStackFrame;
pub use debugger::{BreakPoint, Debugger, DebuggerCommand, OnDebuggerCallback, OnDebuggingInit}; pub use debugger::{BreakPoint, Debugger, DebuggerCommand, OnDebuggerCallback, OnDebuggingInit};
pub use eval_context::EvalContext; pub use eval_context::EvalContext;
pub use eval_state::EvalState; 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 global_state::GlobalRuntimeState;
pub use target::{calc_index, calc_offset_len, Target}; pub use target::{calc_index, calc_offset_len, Target};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -514,3 +514,47 @@ fn test_module_file() -> Result<(), Box<EvalAltResult>> {
Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
Ok(()) 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(())
}