Merge pull request #122 from sagiegurari/0.6.3

0.6.3
This commit is contained in:
Sagie Gur-Ari 2020-07-24 19:10:20 +03:00 committed by GitHub
commit 5eafcc4ae3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 765 additions and 16 deletions

View file

@ -1,5 +1,11 @@
## CHANGELOG
### v0.6.2
* New function <scope> annotation #121
* New scope_pop_stack command #121
* New scope_push_stack command #121
### v0.6.1 (2020-07-08)
* New is_path_newer command.

View file

@ -83,7 +83,7 @@ The following sections will teach you how to write and run duck scripts.
<a name="tutorial-hello-world"></a>
### Hello World Example
Let's take a really simple example (all examples are located in the [examples](https://github.com/sagiegurari/duckscript/tree/master/examples) directory):
Let's take a really simple example (all examples are located in the [examples](https://github.com/sagiegurari/duckscript/tree/master/examples) directory:
```sh
# print the text "Hello World"

View file

@ -126,6 +126,8 @@
* [std::process::Spawn (spawn)](#std__process__Spawn)
* [std::process::Watchdog (watchdog)](#std__process__Watchdog)
* [std::scope::Clear (clear_scope)](#std__scope__Clear)
* [std::scope::PopStack (scope_pop_stack)](#std__scope__PopStack)
* [std::scope::PushStack (scope_push_stack)](#std__scope__PushStack)
* [std::string::Base64 (base64)](#std__string__Base64)
* [std::string::Base64Decode (base64_decode)](#std__string__Base64Decode)
* [std::string::Base64Encode (base64_encode)](#std__string__Base64Encode)
@ -2813,17 +2815,22 @@ for
<a name="std__flowcontrol__Function"></a>
## std::flowcontrol::Function
```sh
function my_function
fn my_function
# function content
return output
end
fn <scope> another_function
# function content
end
```
This command provides the function language feature as a set of commands:
* function - Defines a function start block
* function/fn - Defines a function start block
* end - Defines the end of the function block
* return - Allows to exist a function at any point and return an output
* *&lt;scope&gt;* - Optional annotation which enables to use a new scope during the function invocation.
* *function name* - Dynamically created commands based on the function name which are used to invoke the function code.
When a function command is detected, it will search for the end command that comes after.<br>
@ -2836,13 +2843,19 @@ Since variables are global, it will overwrite any older values stored in those v
To exist a function and return a value, simply use the **return** command with the value you want to return.<br>
The variable that was used when the function was originally called, will now store that value.<br>
The return command can be used to exist early without any value.<br>
In case the code reached the **end** call, the function will exist but will return not value.
In case the code reached the **end** call, the function will exist but will return not value.<br>
The *&lt;scope&gt;* annotation enables to start a new scope when running the function.<br>
All variables defined will not be available except the variables provided to the function as arguments.<br>
All variables created during the function invocation will be deleted once the function ends, except the return value.<br>
This enables a clean function invocation without impacting the global variables.
#### Parameters
* function - The function name used later on to invoke the function
* end - no parameters
* return - optional single paramter to return as an output of the function call
* *&lt;scope&gt;* - Optional annotation which enables to use a new scope during the function invocation.
* *function name* - Any number of arguments which will automatically be set as global variables: ${1}, ${2}, ... as so on.
#### Return Value
@ -2874,7 +2887,9 @@ text = get_hello_world
echo ${text}
# Example of passing arguments
fn print_input
# Also the function is with scope annotation so it has no access
# to any variable except those provided during the function invocation.
fn <scope> print_input
# ${1} is set with the value 'hello'
# ${2} is set with the value 'world'
echo ${1} ${2}
@ -4651,6 +4666,92 @@ assert_false ${defined}
#### Aliases:
clear_scope
<a name="std__scope__PopStack"></a>
## std::scope::PopStack
```sh
scope_pop_stack [--copy name1 name2 ...]
```
Removes all known variables except for the variables provided by the optional --copy argument and than restores the
previously pushed stack.<br>
Functions with the **<scope>** annotation will automatically invoke this command when they end or return a value.
#### Parameters
Optional variable names to keep.
#### Return Value
None.
#### Examples
```sh
var1 = set 1
var2 = set 2
scope_push_stack --copy var2
defined = is_defined var1
echo ${defined}
defined = is_defined var2
echo ${defined}
var3 = set 3
var4 = set 4
scope_pop_stack --copy var4
defined = is_defined var1
echo ${defined}
defined = is_defined var2
echo ${defined}
defined = is_defined var3
echo ${defined}
defined = is_defined var4
echo ${defined}
```
#### Aliases:
scope_pop_stack
<a name="std__scope__PushStack"></a>
## std::scope::PushStack
```sh
scope_push_stack [--copy name1 name2 ...]
```
Removes all known variables except for the variables provided by the optional --copy argument.<br>
Functions with the **<scope>** annotation will automatically invoke this command and keep only the relevant
function arguments in the new scope.
#### Parameters
Optional variable names to keep.
#### Return Value
None.
#### Examples
```sh
var1 = set 1
var2 = set 2
scope_push_stack --copy var2
defined = is_defined var1
echo ${defined}
defined = is_defined var2
echo ${defined}
```
#### Aliases:
scope_push_stack
<a name="std__string__Base64"></a>
## std::string::Base64

View file

@ -1,15 +1,20 @@
```sh
function my_function
fn my_function
# function content
return output
end
fn <scope> another_function
# function content
end
```
This command provides the function language feature as a set of commands:
* function - Defines a function start block
* function/fn - Defines a function start block
* end - Defines the end of the function block
* return - Allows to exist a function at any point and return an output
* *&lt;scope&gt;* - Optional annotation which enables to use a new scope during the function invocation.
* *function name* - Dynamically created commands based on the function name which are used to invoke the function code.
When a function command is detected, it will search for the end command that comes after.<br>
@ -22,13 +27,19 @@ Since variables are global, it will overwrite any older values stored in those v
To exist a function and return a value, simply use the **return** command with the value you want to return.<br>
The variable that was used when the function was originally called, will now store that value.<br>
The return command can be used to exist early without any value.<br>
In case the code reached the **end** call, the function will exist but will return not value.
In case the code reached the **end** call, the function will exist but will return not value.<br>
The *&lt;scope&gt;* annotation enables to start a new scope when running the function.<br>
All variables defined will not be available except the variables provided to the function as arguments.<br>
All variables created during the function invocation will be deleted once the function ends, except the return value.<br>
This enables a clean function invocation without impacting the global variables.
#### Parameters
* function - The function name used later on to invoke the function
* end - no parameters
* return - optional single paramter to return as an output of the function call
* *&lt;scope&gt;* - Optional annotation which enables to use a new scope during the function invocation.
* *function name* - Any number of arguments which will automatically be set as global variables: ${1}, ${2}, ... as so on.
#### Return Value
@ -60,7 +71,9 @@ text = get_hello_world
echo ${text}
# Example of passing arguments
fn print_input
# Also the function is with scope annotation so it has no access
# to any variable except those provided during the function invocation.
fn <scope> print_input
# ${1} is set with the value 'hello'
# ${2} is set with the value 'world'
echo ${1} ${2}

View file

@ -1,7 +1,7 @@
use crate::sdk::std::flowcontrol::{end, forin, ifelse};
use crate::types::scope::get_line_context_name;
use crate::utils::state::{get_core_sub_state_for_command, get_list, get_sub_state};
use crate::utils::{instruction_query, pckg};
use crate::utils::{annotation, instruction_query, pckg, scope};
use duckscript::types::command::{Command, CommandResult, Commands, GoToValue};
use duckscript::types::error::ScriptError;
use duckscript::types::instruction::Instruction;
@ -21,6 +21,7 @@ struct FunctionMetaInfo {
pub(crate) name: String,
pub(crate) start: usize,
pub(crate) end: usize,
pub(crate) scoped: bool,
}
#[derive(Debug)]
@ -30,6 +31,7 @@ struct CallInfo {
pub(crate) end_line: usize,
pub(crate) line_context_name: String,
pub(crate) output_variable: Option<String>,
pub(crate) scoped: bool,
}
fn store_fn_info_in_state(
@ -57,6 +59,7 @@ fn store_fn_info_in_state(
StateValue::UnsignedNumber(meta_info.start),
);
fn_info_state.insert("end".to_string(), StateValue::UnsignedNumber(meta_info.end));
fn_info_state.insert("scoped".to_string(), StateValue::Boolean(meta_info.scoped));
Ok(())
}
@ -85,10 +88,19 @@ fn get_fn_info_from_state(
_ => return None,
};
let scoped = match fn_info_state.get("scoped") {
Some(state_value) => match state_value {
StateValue::Boolean(value) => *value,
_ => false,
},
_ => false,
};
Some(FunctionMetaInfo {
name: name.to_string(),
start,
end,
scoped,
})
} else {
None
@ -122,6 +134,7 @@ fn push_to_call_stack(state: &mut HashMap<String, StateValue>, call_info: &CallI
StateValue::String(output_variable.to_string()),
);
}
sub_state.insert("scoped".to_string(), StateValue::Boolean(call_info.scoped));
call_stack.push(StateValue::SubState(sub_state));
}
@ -173,12 +186,21 @@ fn pop_from_call_stack(state: &mut HashMap<String, StateValue>) -> Option<CallIn
None => None,
};
let scoped = match sub_state.get("scoped") {
Some(value) => match value {
StateValue::Boolean(scoped) => *scoped,
_ => false,
},
None => false,
};
Some(CallInfo {
call_line,
start_line,
end_line,
line_context_name,
output_variable,
scoped,
})
}
_ => return pop_from_call_stack(state),
@ -197,6 +219,13 @@ fn run_call(
) -> CommandResult {
match get_fn_info_from_state(state, &function_name) {
Some(fn_info) => {
if fn_info.scoped {
match scope::push(variables, state, &vec![]) {
Err(error) => return CommandResult::Error(error),
_ => (),
}
}
// define function arguments
let mut index = 0;
for argument in arguments {
@ -214,6 +243,7 @@ fn run_call(
end_line: fn_info.end,
line_context_name,
output_variable,
scoped: fn_info.scoped,
};
push_to_call_stack(state, &call_info);
@ -273,7 +303,18 @@ impl Command for FunctionCommand {
if arguments.is_empty() {
CommandResult::Error("Missing function name.".to_string())
} else {
let function_name = arguments[0].clone();
let (function_name, scoped) = if arguments.len() == 1 {
(arguments[0].clone(), false)
} else {
match annotation::parse(&arguments[0]) {
Some(annotations) => (
arguments[1].clone(),
annotations.contains(&"scope".to_string()),
),
None => (arguments[0].clone(), false),
}
};
match get_fn_info_from_state(state, &function_name) {
Some(fn_info) => {
if fn_info.start != line {
@ -332,6 +373,7 @@ impl Command for FunctionCommand {
name: function_name.clone(),
start: line,
end: fn_end_line,
scoped,
};
end::set_command(fn_end_line, state, end_command.name());
@ -446,7 +488,7 @@ impl Command for EndFunctionCommand {
&self,
_arguments: Vec<String>,
state: &mut HashMap<String, StateValue>,
_variables: &mut HashMap<String, String>,
variables: &mut HashMap<String, String>,
_output_variable: Option<String>,
_instructions: &Vec<Instruction>,
_commands: &mut Commands,
@ -458,6 +500,14 @@ impl Command for EndFunctionCommand {
Some(call_info) => {
if call_info.end_line == line && call_info.line_context_name == line_context_name {
let next_line = call_info.call_line + 1;
if call_info.scoped {
match scope::pop(variables, state, &vec![]) {
Err(error) => return CommandResult::Error(error),
_ => (),
}
}
CommandResult::GoTo(None, GoToValue::Line(next_line))
} else {
push_to_call_stack(state, &call_info);
@ -514,11 +564,11 @@ impl Command for ReturnCommand {
&& call_info.line_context_name == line_context_name
{
match call_info.output_variable {
Some(name) => {
Some(ref name) => {
if arguments.is_empty() {
variables.remove(&name);
variables.remove(name);
} else {
variables.insert(name, arguments[0].clone());
variables.insert(name.to_string(), arguments[0].clone());
}
}
None => (),
@ -530,6 +580,18 @@ impl Command for ReturnCommand {
Some(arguments[0].clone())
};
if call_info.scoped {
let copy = match call_info.output_variable {
Some(ref name) => vec![name.to_string()],
None => vec![],
};
match scope::pop(variables, state, &copy) {
Err(error) => return CommandResult::Error(error),
_ => (),
}
}
let next_line = call_info.call_line + 1;
CommandResult::GoTo(output, GoToValue::Line(next_line))
} else {

View file

@ -1,4 +1,6 @@
pub(crate) mod clear;
mod pop_stack;
mod push_stack;
use crate::utils::pckg;
use duckscript::types::command::Commands;
@ -10,6 +12,8 @@ pub(crate) fn load(commands: &mut Commands, parent: &str) -> Result<(), ScriptEr
let package = pckg::concat(parent, PACKAGE);
commands.set(clear::create(&package))?;
commands.set(pop_stack::create(&package))?;
commands.set(push_stack::create(&package))?;
Ok(())
}

View file

@ -0,0 +1,43 @@
```sh
scope_pop_stack [--copy name1 name2 ...]
```
Removes all known variables except for the variables provided by the optional --copy argument and than restores the
previously pushed stack.<br>
Functions with the **<scope>** annotation will automatically invoke this command when they end or return a value.
#### Parameters
Optional variable names to keep.
#### Return Value
None.
#### Examples
```sh
var1 = set 1
var2 = set 2
scope_push_stack --copy var2
defined = is_defined var1
echo ${defined}
defined = is_defined var2
echo ${defined}
var3 = set 3
var4 = set 4
scope_pop_stack --copy var4
defined = is_defined var1
echo ${defined}
defined = is_defined var2
echo ${defined}
defined = is_defined var3
echo ${defined}
defined = is_defined var4
echo ${defined}
```

View file

@ -0,0 +1,66 @@
use crate::utils::{pckg, scope};
use duckscript::types::command::{Command, CommandResult, Commands};
use duckscript::types::instruction::Instruction;
use duckscript::types::runtime::StateValue;
use std::collections::HashMap;
#[cfg(test)]
#[path = "./mod_test.rs"]
mod mod_test;
#[derive(Clone)]
pub(crate) struct CommandImpl {
package: String,
}
impl Command for CommandImpl {
fn name(&self) -> String {
pckg::concat(&self.package, "PopStack")
}
fn aliases(&self) -> Vec<String> {
vec!["scope_pop_stack".to_string()]
}
fn help(&self) -> String {
include_str!("help.md").to_string()
}
fn clone_and_box(&self) -> Box<dyn Command> {
Box::new((*self).clone())
}
fn requires_context(&self) -> bool {
true
}
fn run_with_context(
&self,
arguments: Vec<String>,
state: &mut HashMap<String, StateValue>,
variables: &mut HashMap<String, String>,
_output_variable: Option<String>,
_instructions: &Vec<Instruction>,
_commands: &mut Commands,
_line: usize,
) -> CommandResult {
let copy = if arguments.is_empty() {
&[]
} else if arguments[0] == "--copy" {
&arguments[1..]
} else {
&[]
};
match scope::pop(variables, state, &copy) {
Ok(_) => CommandResult::Continue(Some("true".to_string())),
Err(error) => CommandResult::Error(error),
}
}
}
pub(crate) fn create(package: &str) -> Box<dyn Command> {
Box::new(CommandImpl {
package: package.to_string(),
})
}

View file

@ -0,0 +1,55 @@
use super::*;
use crate::sdk::std::scope::push_stack;
use crate::test;
use crate::test::{CommandValidation, SetCommand};
#[test]
fn common_functions() {
test::test_common_command_functions(create(""));
}
#[test]
fn run_no_args() {
let context = test::run_script_and_validate(
vec![create(""), push_stack::create(""), Box::new(SetCommand {})],
r#"
a = test_set 1
b = test_set 2
scope_push_stack
c = test_set 3
scope_pop_stack
"#,
CommandValidation::Ignore,
);
let variables = context.variables;
assert_eq!(variables.len(), 2);
assert_eq!(variables.get("a").unwrap(), "1");
assert_eq!(variables.get("b").unwrap(), "2");
}
#[test]
fn run_keep_variables() {
let context = test::run_script_and_validate(
vec![create(""), push_stack::create(""), Box::new(SetCommand {})],
r#"
a = test_set 1
b = test_set 2
c = test_set 3
scope_push_stack --copy b c
d = test_set 4
e = test_set 5
scope_pop_stack --copy e
"#,
CommandValidation::Ignore,
);
let variables = context.variables;
assert_eq!(variables.len(), 4);
assert_eq!(variables.get("a").unwrap(), "1");
assert_eq!(variables.get("b").unwrap(), "2");
assert_eq!(variables.get("c").unwrap(), "3");
assert_eq!(variables.get("e").unwrap(), "5");
}

View file

@ -0,0 +1,29 @@
```sh
scope_push_stack [--copy name1 name2 ...]
```
Removes all known variables except for the variables provided by the optional --copy argument.<br>
Functions with the **<scope>** annotation will automatically invoke this command and keep only the relevant
function arguments in the new scope.
#### Parameters
Optional variable names to keep.
#### Return Value
None.
#### Examples
```sh
var1 = set 1
var2 = set 2
scope_push_stack --copy var2
defined = is_defined var1
echo ${defined}
defined = is_defined var2
echo ${defined}
```

View file

@ -0,0 +1,66 @@
use crate::utils::{pckg, scope};
use duckscript::types::command::{Command, CommandResult, Commands};
use duckscript::types::instruction::Instruction;
use duckscript::types::runtime::StateValue;
use std::collections::HashMap;
#[cfg(test)]
#[path = "./mod_test.rs"]
mod mod_test;
#[derive(Clone)]
pub(crate) struct CommandImpl {
package: String,
}
impl Command for CommandImpl {
fn name(&self) -> String {
pckg::concat(&self.package, "PushStack")
}
fn aliases(&self) -> Vec<String> {
vec!["scope_push_stack".to_string()]
}
fn help(&self) -> String {
include_str!("help.md").to_string()
}
fn clone_and_box(&self) -> Box<dyn Command> {
Box::new((*self).clone())
}
fn requires_context(&self) -> bool {
true
}
fn run_with_context(
&self,
arguments: Vec<String>,
state: &mut HashMap<String, StateValue>,
variables: &mut HashMap<String, String>,
_output_variable: Option<String>,
_instructions: &Vec<Instruction>,
_commands: &mut Commands,
_line: usize,
) -> CommandResult {
let copy = if arguments.is_empty() {
&[]
} else if arguments[0] == "--copy" {
&arguments[1..]
} else {
&[]
};
match scope::push(variables, state, &copy) {
Ok(_) => CommandResult::Continue(Some("true".to_string())),
Err(error) => CommandResult::Error(error),
}
}
}
pub(crate) fn create(package: &str) -> Box<dyn Command> {
Box::new(CommandImpl {
package: package.to_string(),
})
}

View file

@ -0,0 +1,43 @@
use super::*;
use crate::test;
use crate::test::{CommandValidation, SetCommand};
#[test]
fn common_functions() {
test::test_common_command_functions(create(""));
}
#[test]
fn run_no_args() {
let context = test::run_script_and_validate(
vec![create(""), Box::new(SetCommand {})],
r#"
a = test_set 1
b = test_set 2
scope_push_stack
"#,
CommandValidation::None,
);
let variables = context.variables;
assert!(variables.is_empty());
}
#[test]
fn run_keep_variables() {
let context = test::run_script_and_validate(
vec![create(""), Box::new(SetCommand {})],
r#"
a = test_set 1
b = test_set 2
c = test_set 3
scope_push_stack --copy b c
"#,
CommandValidation::Ignore,
);
let variables = context.variables;
assert_eq!(variables.len(), 2);
assert_eq!(variables.get("b").unwrap(), "2");
assert_eq!(variables.get("c").unwrap(), "3");
}

View file

@ -0,0 +1,19 @@
pub(crate) fn parse(string: &str) -> Option<Vec<String>> {
let mut modified_string = string.trim();
if modified_string.starts_with("<") && modified_string.ends_with(">") {
modified_string = modified_string
.strip_prefix("<")
.unwrap()
.strip_suffix(">")
.unwrap();
let values: Vec<String> = modified_string
.split(',')
.map(|item| item.to_string())
.collect();
Some(values)
} else {
None
}
}

View file

@ -1,3 +1,4 @@
pub(crate) mod annotation;
pub(crate) mod condition;
pub(crate) mod eval;
pub(crate) mod exec;
@ -5,4 +6,5 @@ pub(crate) mod flags;
pub(crate) mod instruction_query;
pub(crate) mod io;
pub(crate) mod pckg;
pub(crate) mod scope;
pub(crate) mod state;

View file

@ -0,0 +1,85 @@
use crate::utils::state::{ensure_list, mutate_list};
use duckscript::types::runtime::StateValue;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
static SCOPE_STACK_STATE_KEY: &str = "scope_stack";
pub(crate) fn push(
variables: &mut HashMap<String, String>,
state: &mut HashMap<String, StateValue>,
copy: &[String],
) -> Result<(), String> {
ensure_list(SCOPE_STACK_STATE_KEY, state);
match mutate_list(SCOPE_STACK_STATE_KEY.to_string(), state, |list| {
list.push(StateValue::Any(Rc::new(RefCell::new(variables.clone()))));
Ok(None)
}) {
Ok(_) => {
let mut new_variables = HashMap::new();
for key in copy {
let value = variables.remove(key);
new_variables.insert(key, value);
}
variables.clear();
for (key, value) in new_variables {
variables.insert(key.to_string(), value.unwrap());
}
Ok(())
}
Err(error) => Err(error),
}
}
pub(crate) fn pop(
variables: &mut HashMap<String, String>,
state: &mut HashMap<String, StateValue>,
copy: &[String],
) -> Result<(), String> {
ensure_list(SCOPE_STACK_STATE_KEY, state);
match mutate_list(
SCOPE_STACK_STATE_KEY.to_string(),
state,
|list| match list.pop() {
Some(state_value) => match state_value {
StateValue::Any(rc_value) => {
let value_any = rc_value.borrow();
match value_any.downcast_ref::<HashMap<String, String>>() {
Some(old_variables) => {
let mut new_variables = HashMap::new();
for key in copy {
let value = variables.remove(key);
new_variables.insert(key, value);
}
variables.clear();
for (key, value) in old_variables {
variables.insert(key.to_string(), value.clone());
}
for (key, value) in new_variables {
variables.insert(key.to_string(), value.unwrap());
}
Ok(None)
}
None => Err("Scope stack not available, invalid type.".to_string()),
}
}
_ => Err("Scope stack not available, invalid state value type.".to_string()),
},
None => Err("Reached end of scope stack.".to_string()),
},
) {
Ok(_) => Ok(()),
Err(error) => Err(error),
}
}

View file

@ -111,7 +111,7 @@ pub(crate) fn get_sub_state(
}
}
fn ensure_list(key: &str, state: &mut HashMap<String, StateValue>) {
pub(crate) fn ensure_list(key: &str, state: &mut HashMap<String, StateValue>) {
match state.get(key) {
Some(value) => match value {
StateValue::List(_) => (),

View file

@ -0,0 +1,70 @@
fn global_fn
defined = is_defined v1
assert ${defined}
defined = is_defined v2
assert ${defined}
assert_eq ${v1} 1
assert_eq ${v2} 2
assert_eq ${1} 2
assert_eq ${2} 1
global_var = set global
end
fn <scope> scoped_fn
defined = is_defined global_var
assert_false ${defined}
defined = is_defined scoped
assert_false ${defined}
defined = is_defined v1
assert_false ${defined}
defined = is_defined v2
assert_false ${defined}
assert_eq ${1} 1
defined = is_defined 2
assert_false ${defined}
scoped = set 1
end
fn <scope> scoped_return_fn
defined = is_defined global_var
assert_false ${defined}
defined = is_defined scoped
assert_false ${defined}
defined = is_defined v1
assert_false ${defined}
defined = is_defined v2
assert_false ${defined}
defined = is_defined 1
assert_false ${defined}
defined = is_defined 2
assert_false ${defined}
scoped = set 1
return scoped_return_value
end
fn test_scoped_functions
v1 = set 1
v2 = set 2
global_fn ${v2} ${v1}
scoped_fn ${v1}
output = scoped_return_fn
defined = is_defined scoped
assert_false ${defined}
assert_eq ${global_var} global
assert ${output} scoped_return_value
end

View file

@ -0,0 +1,85 @@
fn test_push_pop
root1 = set 1
root2 = set 2
root3 = set 3
root4 = set 4
result = scope_push_stack
assert ${result}
defined = is_defined root1
assert_false ${defined}
defined = is_defined root2
assert_false ${defined}
defined = is_defined root3
assert_false ${defined}
defined = is_defined root4
assert_false ${defined}
child1 = set 1
child2 = set 2
child3 = set 3
child4 = set 4
result = scope_pop_stack
assert ${result}
assert_eq ${root1} 1
assert_eq ${root2} 2
assert_eq ${root3} 3
assert_eq ${root4} 4
defined = is_defined child1
assert_false ${defined}
defined = is_defined child2
assert_false ${defined}
defined = is_defined child3
assert_false ${defined}
defined = is_defined child4
assert_false ${defined}
end
fn test_push_pop_with_copy
root1 = set 1
root2 = set 2
root3 = set 3
root4 = set 4
result = scope_push_stack --copy root2 root4
assert ${result}
assert_eq ${root2} 2
assert_eq ${root4} 4
defined = is_defined root2
assert ${defined}
defined = is_defined root1
assert_false ${defined}
defined = is_defined root3
assert_false ${defined}
child1 = set 1
child2 = set 2
child3 = set 3
child4 = set 4
result = scope_pop_stack --copy child2 child4
assert ${result}
assert_eq ${root1} 1
assert_eq ${root2} 2
assert_eq ${root3} 3
assert_eq ${root4} 4
assert_eq ${child2} 2
assert_eq ${child4} 4
defined = is_defined child2
assert ${defined}
defined = is_defined child1
assert_false ${defined}
defined = is_defined child3
assert_false ${defined}
end