Fix encapsulated environment in module functions.
This commit is contained in:
parent
8fc80ecd10
commit
7b92a80c32
@ -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
|
||||||
-----------------------
|
-----------------------
|
||||||
|
@ -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};
|
||||||
|
@ -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.
|
||||||
|
@ -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| {
|
||||||
|
@ -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,
|
||||||
|
@ -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};
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
@ -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,28 +104,39 @@ 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"))]
|
||||||
if fn_lib.is_empty() {
|
let mut lib_merged = crate::StaticVec::with_capacity(lib.len() + 1);
|
||||||
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"))]
|
#[cfg(not(feature = "no_module"))]
|
||||||
if let Some(ref modules) = fn_def.global {
|
let (lib, constants) = if let Some(crate::ast::EncapsulatedEnviron {
|
||||||
for (n, m) in modules.iter().cloned() {
|
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)
|
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
|
// Evaluate the function
|
||||||
let result = self
|
let result = self
|
||||||
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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(())
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user