New while loop command #138

This commit is contained in:
sagie gur ari 2020-11-15 14:01:00 +00:00
parent f036028108
commit 482cd0680e
11 changed files with 791 additions and 3 deletions

View file

@ -2,6 +2,7 @@
### v0.6.9
* New while loop command #138
* New linter CLI command #139
### v0.6.8 (2020-10-01)

View file

@ -60,6 +60,7 @@ command = "cargo"
args = ["run", "--", "--eval", "test_directory ./test/"]
[tasks.workspace-docs]
workspace = false
dependencies = ["generate-sdk-docs", "generate-readme"]
[tasks.install_local]

View file

@ -79,6 +79,7 @@
* [std::flowcontrol::Function (function, fn)](#std__flowcontrol__Function)
* [std::flowcontrol::GoTo (goto)](#std__flowcontrol__GoTo)
* [std::flowcontrol::If (if)](#std__flowcontrol__If)
* [std::flowcontrol::While (while)](#std__flowcontrol__While)
* [std::fs::Append (appendfile)](#std__fs__Append)
* [std::fs::CopyPath (cp)](#std__fs__CopyPath)
* [std::fs::CreateDirectory (mkdir)](#std__fs__CreateDirectory)
@ -3085,6 +3086,74 @@ end
#### Aliases:
if
<a name="std__flowcontrol__While"></a>
## std::flowcontrol::While
```sh
while [command|value|condition]
# commands
end
```
This command provides the while loop language feature as a set of commands:
* while - Defines a while condition and start of loop
* end - Defines the end of the while block
The while command accept either:
* A command with optional arguments and invokes it
* A single value which doesn't match any known command
* A condition statement
If the result is one of the following:
* No output
* false (case insensitive)
* 0
* no (case insensitive)
* Empty value
It is considered falsy.<br>
In case of falsy value, it will skip to the next line after the while block.<br>
If a truthy (non falsy) output is found, it will invoke the commands of that code block and go back to the start of the while condition.<br>
while blocks can be nested in other while blocks (see examples).
A condition statement is made up of values, or/and keywords and '('/')' groups.<br>
Each must be separated with a space character.
#### Parameters
* while - A command and its arguments to invoke and evaluate its output, if a single value is provided an no such command exists, it is evaluated as a value.
* end - no parameters
#### Return Value
None
#### Examples
```sh
top_count = set 0
inner_count = set 0
counter = set 0
while not equals ${top_count} 10
top_count = calc ${top_count} + 1
inner_count = set 0
while not equals ${inner_count} 10
inner_count = calc ${inner_count} + 1
counter = calc ${counter} + 1
end
end
assert_eq ${counter} 100
```
#### Aliases:
while
<a name="std__fs__Append"></a>
## std::fs::Append
```sh

View file

@ -1,4 +1,4 @@
use crate::sdk::std::flowcontrol::{end, function, get_line_key, ifelse};
use crate::sdk::std::flowcontrol::{end, function, get_line_key, ifelse, while_mod};
use crate::utils::state::{
get_as_string, get_core_sub_state_for_command, get_handle, get_list, get_sub_state,
};
@ -126,6 +126,9 @@ fn create_forin_meta_info_for_line(
let function_command = function::FunctionCommand::new(&package);
start_blocks.append(&mut function_command.aliases());
start_blocks.push(function_command.name());
let while_command = while_mod::WhileCommand::new(&package);
start_blocks.append(&mut while_command.aliases());
start_blocks.push(while_command.name());
let end_if_command = ifelse::EndIfCommand::new(&package);
let mut end_blocks = end_if_command.aliases();
@ -133,6 +136,9 @@ fn create_forin_meta_info_for_line(
let end_function_command = function::EndFunctionCommand::new(&package);
end_blocks.append(&mut end_function_command.aliases());
end_blocks.push(end_function_command.name());
let end_while_command = while_mod::EndWhileCommand::new(&package);
end_blocks.append(&mut end_while_command.aliases());
end_blocks.push(end_while_command.name());
end_blocks.push(end::END_COMMAND_NAME.to_string());
let positions_options = instruction_query::find_commands(

View file

@ -1,4 +1,4 @@
use crate::sdk::std::flowcontrol::{end, forin, ifelse};
use crate::sdk::std::flowcontrol::{end, forin, ifelse, while_mod};
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::{annotation, instruction_query, pckg, scope};
@ -345,6 +345,9 @@ impl Command for FunctionCommand {
let forin_command = forin::ForInCommand::new(&self.package);
start_blocks.append(&mut forin_command.aliases());
start_blocks.push(forin_command.name());
let while_command = while_mod::WhileCommand::new(&self.package);
start_blocks.append(&mut while_command.aliases());
start_blocks.push(while_command.name());
let end_if_command = ifelse::EndIfCommand::new(&self.package);
let mut end_blocks = end_if_command.aliases();
@ -352,6 +355,9 @@ impl Command for FunctionCommand {
let end_forin_command = forin::EndForInCommand::new(&self.package);
end_blocks.append(&mut end_forin_command.aliases());
end_blocks.push(end_forin_command.name());
let end_while_command = while_mod::EndWhileCommand::new(&self.package);
end_blocks.append(&mut end_while_command.aliases());
end_blocks.push(end_while_command.name());
end_blocks.push(end::END_COMMAND_NAME.to_string());
match instruction_query::find_commands(

View file

@ -1,4 +1,4 @@
use crate::sdk::std::flowcontrol::{end, forin, function, get_line_key};
use crate::sdk::std::flowcontrol::{end, forin, function, get_line_key, while_mod};
use crate::utils::state::{get_core_sub_state_for_command, get_list, get_sub_state};
use crate::utils::{condition, instruction_query, pckg};
use duckscript::types::command::{Command, CommandResult, Commands, GoToValue};
@ -186,6 +186,9 @@ fn create_if_meta_info_for_line(
let forin_command = forin::ForInCommand::new(&package);
start_blocks.append(&mut forin_command.aliases());
start_blocks.push(forin_command.name());
let while_command = while_mod::WhileCommand::new(&package);
start_blocks.append(&mut while_command.aliases());
start_blocks.push(while_command.name());
let end_forin_command = forin::EndForInCommand::new(&package);
let mut end_blocks = end_forin_command.aliases();
@ -193,6 +196,9 @@ fn create_if_meta_info_for_line(
let end_function_command = function::EndFunctionCommand::new(&package);
end_blocks.append(&mut end_function_command.aliases());
end_blocks.push(end_function_command.name());
let end_while_command = while_mod::EndWhileCommand::new(&package);
end_blocks.append(&mut end_while_command.aliases());
end_blocks.push(end_while_command.name());
end_blocks.push(end::END_COMMAND_NAME.to_string());
let positions_options = instruction_query::find_commands(

View file

@ -3,6 +3,7 @@ mod forin;
mod function;
mod goto;
mod ifelse;
mod while_mod;
use crate::types::scope::get_line_context_name;
use crate::utils::pckg;
@ -30,6 +31,7 @@ pub(crate) fn load(commands: &mut Commands, parent: &str) -> Result<(), ScriptEr
forin::load(commands, &package)?;
function::load(commands, &package)?;
ifelse::load(commands, &package)?;
while_mod::load(commands, &package)?;
Ok(())
}

View file

@ -0,0 +1,61 @@
```sh
while [command|value|condition]
# commands
end
```
This command provides the while loop language feature as a set of commands:
* while - Defines a while condition and start of loop
* end - Defines the end of the while block
The while command accept either:
* A command with optional arguments and invokes it
* A single value which doesn't match any known command
* A condition statement
If the result is one of the following:
* No output
* false (case insensitive)
* 0
* no (case insensitive)
* Empty value
It is considered falsy.<br>
In case of falsy value, it will skip to the next line after the while block.<br>
If a truthy (non falsy) output is found, it will invoke the commands of that code block and go back to the start of the while condition.<br>
while blocks can be nested in other while blocks (see examples).
A condition statement is made up of values, or/and keywords and '('/')' groups.<br>
Each must be separated with a space character.
#### Parameters
* while - A command and its arguments to invoke and evaluate its output, if a single value is provided an no such command exists, it is evaluated as a value.
* end - no parameters
#### Return Value
None
#### Examples
```sh
top_count = set 0
inner_count = set 0
counter = set 0
while not equals ${top_count} 10
top_count = calc ${top_count} + 1
inner_count = set 0
while not equals ${inner_count} 10
inner_count = calc ${inner_count} + 1
counter = calc ${counter} + 1
end
end
assert_eq ${counter} 100
```

View file

@ -0,0 +1,382 @@
use crate::sdk::std::flowcontrol::{end, forin, function, get_line_key, ifelse};
use crate::utils::state::{get_core_sub_state_for_command, get_list, get_sub_state};
use crate::utils::{condition, instruction_query, pckg};
use duckscript::types::command::{Command, CommandResult, Commands, GoToValue};
use duckscript::types::error::ScriptError;
use duckscript::types::instruction::Instruction;
use duckscript::types::runtime::StateValue;
use std::collections::HashMap;
#[cfg(test)]
#[path = "./mod_test.rs"]
mod mod_test;
static WHILE_STATE_KEY: &str = "while";
static META_INFO_STATE_KEY: &str = "meta_info";
static CALL_STACK_STATE_KEY: &str = "call_stack";
#[derive(Debug, Clone)]
struct WhileMetaInfo {
pub(crate) start: usize,
pub(crate) end: usize,
}
#[derive(Debug)]
struct CallInfo {
pub(crate) meta_info: WhileMetaInfo,
}
fn serialize_while_meta_info(
meta_info: &WhileMetaInfo,
sub_state: &mut HashMap<String, StateValue>,
) {
sub_state.insert(
"start".to_string(),
StateValue::UnsignedNumber(meta_info.start),
);
sub_state.insert("end".to_string(), StateValue::UnsignedNumber(meta_info.end));
}
fn deserialize_while_meta_info(
sub_state: &mut HashMap<String, StateValue>,
) -> Option<WhileMetaInfo> {
let start = match sub_state.get("start") {
Some(state_value) => match state_value {
StateValue::UnsignedNumber(value) => *value,
_ => return None,
},
None => return None,
};
let end = match sub_state.get("end") {
Some(state_value) => match state_value {
StateValue::UnsignedNumber(value) => *value,
_ => return None,
},
None => return None,
};
Some(WhileMetaInfo { start, end })
}
fn serialize_call_info(call_info: &CallInfo, sub_state: &mut HashMap<String, StateValue>) {
let mut meta_info_state = HashMap::new();
serialize_while_meta_info(&call_info.meta_info, &mut meta_info_state);
sub_state.insert(
"meta_info".to_string(),
StateValue::SubState(meta_info_state),
);
}
fn deserialize_call_info(sub_state: &mut HashMap<String, StateValue>) -> Option<CallInfo> {
let meta_info = match sub_state.get("meta_info") {
Some(state_value) => match state_value.clone() {
StateValue::SubState(mut value) => match deserialize_while_meta_info(&mut value) {
Some(meta_info) => meta_info,
None => return None,
},
_ => return None,
},
None => return None,
};
Some(CallInfo { meta_info })
}
fn create_while_meta_info_for_line(
line: usize,
instructions: &Vec<Instruction>,
package: String,
) -> Result<WhileMetaInfo, String> {
// start names
let if_command = WhileCommand {
package: package.clone(),
};
let mut start_names = if_command.aliases();
start_names.push(if_command.name());
// end names
let end_while_command = EndWhileCommand {
package: package.clone(),
};
let mut end_names = end_while_command.aliases();
end_names.push(end_while_command.name());
end_names.push(end::END_COMMAND_NAME.to_string());
let function_command = function::FunctionCommand::new(&package);
let mut start_blocks = function_command.aliases();
start_blocks.push(function_command.name());
let forin_command = forin::ForInCommand::new(&package);
start_blocks.append(&mut forin_command.aliases());
start_blocks.push(forin_command.name());
let if_command = ifelse::IfCommand::new(&package);
start_blocks.append(&mut if_command.aliases());
start_blocks.push(if_command.name());
let end_forin_command = forin::EndForInCommand::new(&package);
let mut end_blocks = end_forin_command.aliases();
end_blocks.push(end_forin_command.name());
let end_function_command = function::EndFunctionCommand::new(&package);
end_blocks.append(&mut end_function_command.aliases());
end_blocks.push(end_function_command.name());
let end_if_command = ifelse::EndIfCommand::new(&package);
end_blocks.append(&mut end_if_command.aliases());
end_blocks.push(end_if_command.name());
end_blocks.push(end::END_COMMAND_NAME.to_string());
let positions_options = instruction_query::find_commands(
instructions,
&start_names,
&vec![],
&end_names,
Some(line + 1),
None,
true,
&start_blocks,
&end_blocks,
)?;
match positions_options {
Some(positions) => Ok(WhileMetaInfo {
start: line,
end: positions.end,
}),
None => Err("End of while block not found.".to_string()),
}
}
fn get_or_create_while_meta_info_for_line(
line: usize,
state: &mut HashMap<String, StateValue>,
instructions: &Vec<Instruction>,
package: String,
) -> Result<WhileMetaInfo, String> {
let key = get_line_key(line, state);
let while_state = get_core_sub_state_for_command(state, WHILE_STATE_KEY.to_string());
let while_meta_info_state = get_sub_state(META_INFO_STATE_KEY.to_string(), while_state);
let mut while_state_for_line = get_sub_state(key.clone(), while_meta_info_state);
let result = match deserialize_while_meta_info(&mut while_state_for_line) {
Some(while_info) => Ok(while_info),
None => match create_while_meta_info_for_line(line, instructions, package.clone()) {
Ok(while_info) => {
serialize_while_meta_info(&while_info, while_state_for_line);
Ok(while_info)
}
Err(error) => Err(error),
},
};
match result {
Ok(ref info) => {
let end_while_command = EndWhileCommand {
package: package.clone(),
};
end::set_command(info.end, state, end_while_command.name());
}
_ => (),
};
result
}
fn pop_call_info_for_line(
line: usize,
state: &mut HashMap<String, StateValue>,
) -> Option<CallInfo> {
let while_state = get_core_sub_state_for_command(state, WHILE_STATE_KEY.to_string());
let call_info_stack = get_list(CALL_STACK_STATE_KEY.to_string(), while_state);
match call_info_stack.pop() {
Some(state_value) => match state_value {
StateValue::SubState(mut call_info_state) => {
match deserialize_call_info(&mut call_info_state) {
Some(call_info) => {
if call_info.meta_info.end == line {
Some(call_info)
} else {
pop_call_info_for_line(line, state)
}
}
None => None,
}
}
_ => pop_call_info_for_line(line, state),
},
None => None,
}
}
fn store_call_info(call_info: &CallInfo, state: &mut HashMap<String, StateValue>) {
let while_state = get_core_sub_state_for_command(state, WHILE_STATE_KEY.to_string());
let call_info_stack = get_list(CALL_STACK_STATE_KEY.to_string(), while_state);
let mut call_info_state = HashMap::new();
serialize_call_info(call_info, &mut call_info_state);
call_info_stack.push(StateValue::SubState(call_info_state));
}
#[derive(Clone)]
pub(crate) struct WhileCommand {
package: String,
}
impl WhileCommand {
/// Creates and returns a new instance.
pub(crate) fn new(package: &str) -> WhileCommand {
WhileCommand {
package: package.to_string(),
}
}
}
impl Command for WhileCommand {
fn name(&self) -> String {
pckg::concat(&self.package, "While")
}
fn aliases(&self) -> Vec<String> {
vec!["while".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 {
if arguments.is_empty() {
CommandResult::Error("Missing condition".to_string())
} else {
match get_or_create_while_meta_info_for_line(
line,
state,
instructions,
self.package.clone(),
) {
Ok(while_info) => {
match condition::eval_condition(
arguments,
instructions,
state,
variables,
commands,
) {
Ok(passed) => {
if passed {
let call_info = CallInfo {
meta_info: while_info.clone(),
};
store_call_info(&call_info, state);
CommandResult::Continue(None)
} else {
let next_line = while_info.end + 1;
CommandResult::GoTo(None, GoToValue::Line(next_line))
}
}
Err(error) => CommandResult::Error(error.to_string()),
}
}
Err(error) => CommandResult::Crash(error.to_string()),
}
}
}
}
#[derive(Clone)]
pub(crate) struct EndWhileCommand {
package: String,
}
impl EndWhileCommand {
/// Creates and returns a new instance.
pub(crate) fn new(package: &str) -> EndWhileCommand {
EndWhileCommand {
package: package.to_string(),
}
}
}
impl Command for EndWhileCommand {
fn name(&self) -> String {
pckg::concat(&self.package, "EndWhile")
}
fn aliases(&self) -> Vec<String> {
vec!["end_while".to_string(), "endwhile".to_string()]
}
fn help(&self) -> String {
"".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 {
match pop_call_info_for_line(line, state) {
Some(call_info) => {
let next_line = call_info.meta_info.start;
store_call_info(&call_info, state);
CommandResult::GoTo(None, GoToValue::Line(next_line))
}
None => CommandResult::Error(
"Found an end while command but not currently running part of a while invocation flow."
.to_string(),
),
}
}
}
pub(crate) fn create(package: &str) -> Vec<Box<dyn Command>> {
vec![
Box::new(WhileCommand {
package: package.to_string(),
}),
Box::new(EndWhileCommand {
package: package.to_string(),
}),
]
}
pub(crate) fn load(commands: &mut Commands, package: &str) -> Result<(), ScriptError> {
let multi_commands = create(package);
for command in multi_commands {
commands.set(command)?;
}
Ok(())
}

View file

@ -0,0 +1,123 @@
use super::*;
use crate::test;
use crate::test::{CommandValidation, SetCommand};
#[test]
fn common_functions() {
let commands = create("");
for command in commands {
test::test_common_command_functions(command);
}
}
#[test]
fn run_while_no_end() {
let set_command = SetCommand {};
let mut commands = create("");
commands.push(Box::new(set_command));
test::run_script_and_crash(
commands,
r#"
while test_set true
# no ending
"#,
);
}
#[test]
fn run_while_no_condition() {
test::run_script_and_error(
create(""),
r#"
out = while
end_while
"#,
"out",
);
}
#[test]
fn run_sub_while_no_end() {
let set_command = SetCommand {};
let mut commands = create("");
commands.push(Box::new(set_command));
test::run_script_and_crash(
commands,
r#"
while test_set true
while test_set true
end_while
# no ending
"#,
);
}
#[test]
fn run_while_true() {
let set_command = SetCommand {};
let mut commands = create("");
commands.push(Box::new(set_command));
test::run_script_and_validate(
commands,
r#"
test = test_set true
while ${test}
out = test_set while
test = test_set false
end_while
"#,
CommandValidation::Match("out".to_string(), "while".to_string()),
);
}
#[test]
fn run_while_false() {
let set_command = SetCommand {};
let mut commands = create("");
commands.push(Box::new(set_command));
test::run_script_and_validate(
commands,
r#"
out = test_set test_false
while test_set false
badcommand
end_while
"#,
CommandValidation::Match("out".to_string(), "test_false".to_string()),
);
}
#[test]
fn run_nested_while() {
let set_command = SetCommand {};
let mut commands = create("");
commands.push(Box::new(set_command));
test::run_script_and_validate(
commands,
r#"
top = test_set true
while ${top}
while test_set false
badcommand
end_while
top = test_set false
inner = test_set true
while ${inner}
inner = test_set false
out = test_set win
end_while
end_while
"#,
CommandValidation::Match("out".to_string(), "win".to_string()),
);
}

View file

@ -0,0 +1,131 @@
fn test_while_hardcoded_true
valid = set false
while true
valid = set true
goto :test_while_hardcoded_true
end
:test_while_hardcoded_true
assert ${valid}
end
fn test_while_hardcoded_false
while false
assert_fail
end
end
fn test_while_hardcoded_not_false
valid = set false
while not false
valid = set true
goto :test_while_hardcoded_not_false
end
:test_while_hardcoded_not_false
assert ${valid}
end
fn test_while_command_returns_true
valid = set false
while set true
valid = set true
goto :test_while_command_returns_true
end
:test_while_command_returns_true
assert ${valid}
end
fn test_while_condition_true
valid = set false
condition = set true
while true and false or true and false or ( true and true or false ) and ${condition}
valid = set true
condition = set false
end
assert ${valid}
end
fn test_while_condition_false
while true and false or true and false or ( true and true or false ) and false
assert_fail
end
end
fn test_nested_while
top_count = set 0
inner_count = set 0
counter = set 0
while not equals ${top_count} 10
top_count = calc ${top_count} + 1
inner_count = set 0
while not equals ${inner_count} 10
inner_count = calc ${inner_count} + 1
counter = calc ${counter} + 1
end
end
assert_eq ${counter} 100
end
fn test_while_call_command
text = set test,
valid = set false
while ends_with ${text} ,
valid = set true
text = set false
end
assert ${valid}
end
fn test_while_call_to_functions
value = _test_return_true
valid = set false
while ${value}
valid = set true
value = set false
end
assert ${valid}
value = _test_return_false
valid = set true
while ${value}
valid = set false
end
assert ${valid}
valid = set true
while _test_return_false
valid = set false
end
assert ${valid}
valid = set true
while not _test_return_true
valid = set false
end
assert ${valid}
end
fn _test_return_true
return true
end
fn _test_return_false
return false
end