From 55d78ab5ce5ba6455efe57e969afdc599866c2b3 Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Tue, 3 Oct 2023 12:28:12 +0200 Subject: [PATCH] :sparkles: Loops --- GUIDE.md | 49 ++++++++- examples/collatz.mbas | 15 +++ src/parse/ast.rs | 181 +++++++++++++++++++++++++--------- src/parse/tokenize.rs | 15 ++- src/repr/basic.rs | 2 + src/translate/mod.rs | 64 +++++++++++- tests/examples/collatz.0.mlog | 42 ++++++++ tests/examples/collatz.1.mlog | 21 ++++ 8 files changed, 334 insertions(+), 55 deletions(-) create mode 100644 examples/collatz.mbas create mode 100644 tests/examples/collatz.0.mlog create mode 100644 tests/examples/collatz.1.mlog diff --git a/GUIDE.md b/GUIDE.md index 4adcdc1..738c915 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -157,4 +157,51 @@ END IF ## Loops - +MinBasic offers multiple ways to execute a block of code multiple times, on top of manually jumping to an earlier point in the code: + +### `FOR` loops + +`FOR` loops allow you to run a piece of code for a fixed amount of iterations, incrementing a variable when doing so. +The syntax is as follows: + +```basic +REM Prints the numbers from 1 to 10, with 10 included +FOR x = 1 TO 10 + PRINT x +NEXT x +``` + +The `FOR` keyword expects a variable name (here `x`), an initial value (here `1`), a maximal value (here `10`), and optionally an increment, which defaults to `1`. +To specify the increment, append `STEP n` to the `FOR` instruction: `FOR x = 1 TO 10 STEP 2`. + +The loop body is then executed, until the `NEXT` statement is reached, telling the loop to jump to the beginning, increment the variable, compare it and possibly execute the loop body. + +If the initial value is bigger than the maximal value, then the loop body will not be executed. + +### `WHILE` loops + +`WHILE` loops allow you to execute a piece of code any amount of time, until a condition turns false. +The syntax is as follows: + +```basic +REM Divides x until it is an odd number +WHILE x % 2 == 0 + x = x / 2 +WEND +``` + +If the condition (here `x % 2 == 0`) yields false on the first iteration, then the loop body will not be executed. + +`WEND` may be replaced with `END WHILE`, similar to VisualBasic. + +### `DO WHILE` loops + +Much like `WHILE` loops, `DO WHILE` loops execute a piece of code until a condition turns false. +The difference, however, is that the loop body will be executed at least once: + +```basic +REM Will repeatedly set x to 3*x+1, until it becomes an even number +DO WHILE x % 2 == 1 + x = 3*x + 1 +LOOP +``` diff --git a/examples/collatz.mbas b/examples/collatz.mbas new file mode 100644 index 0000000..88e44b5 --- /dev/null +++ b/examples/collatz.mbas @@ -0,0 +1,15 @@ +X = 7 + +WHILE X != 1 + WHILE X % 2 == 1 + X = 3*X + 1 + PRINT X, " " + END WHILE + + DO + X = X / 2 + PRINT X, " " + LOOP WHILE X % 2 == 0 +WEND + +PRINT_FLUSH(message1) diff --git a/src/parse/ast.rs b/src/parse/ast.rs index a4fce20..9fa2798 100644 --- a/src/parse/ast.rs +++ b/src/parse/ast.rs @@ -2,6 +2,37 @@ use super::*; use crate::cursor::Cursor; use crate::prelude::*; +/// Pops the last context, matches on it, and pushes the result of the match branch to the parent instruction list. +/// Errors can be returned early, and branches need to be exhaustive. +macro_rules! pop_context { + ( + $context_stack:expr, $instructions:ident, + { $( $match:pat => $instr:block $(,)? )* } + ) => { + match $context_stack.pop() { + Some(($instructions, context)) => { + match context { + $( + $match => { + let Some((ref mut parent_instructions, _)) = $context_stack.last_mut() + else { + unreachable!("{} not wrapped in another context", stringify!($match)); + }; + + #[allow(unreachable_code)] + parent_instructions.push($instr); + }, + )* + } + } + + None => { + unreachable!("Empty context stack"); + }, + } + } +} + pub fn build_ast(tokens: &[BasicToken], config: &Config) -> Result { enum Context { Main, @@ -13,6 +44,9 @@ pub fn build_ast(tokens: &[BasicToken], config: &Config) -> Result Result { tokens.take(1); - match context_stack.pop().unwrap() { - (instructions, Context::If(condition)) => { - let Some((ref mut parent_instructions, _)) = context_stack.last_mut() - else { - unreachable!("Context::If not wrapped in another context"); - }; - - parent_instructions.push(BasicAstInstruction::IfThenElse( + pop_context!(context_stack, instructions, { + Context::If(condition) => { + BasicAstInstruction::IfThenElse( condition, BasicAstBlock { instructions }, BasicAstBlock::default(), - )); - } - (instructions, Context::IfElse(condition, true_branch)) => { - let Some((ref mut parent_instructions, _)) = context_stack.last_mut() - else { - unreachable!("Context::IfElse not wrapped in another context"); - }; - - parent_instructions.push(BasicAstInstruction::IfThenElse( + ) + }, + Context::IfElse(condition, true_branch) => { + BasicAstInstruction::IfThenElse( condition, true_branch, BasicAstBlock { instructions }, - )); + ) } _ => { return Err(ParseError::UnexpectedToken(BasicToken::EndIf)); } - } + }); } // == For loops == [BasicToken::For, BasicToken::Name(variable), BasicToken::Assign, ..] => { @@ -118,37 +142,96 @@ pub fn build_ast(tokens: &[BasicToken], config: &Config) -> Result match context_stack.pop() { - Some((instructions, Context::For(expected_variable, start, end, step))) => { - tokens.take(2); + [BasicToken::Next, BasicToken::Name(variable), ..] => { + tokens.take(2); - let Some((ref mut parent_instructions, _)) = context_stack.last_mut() else { - unreachable!("Context::For not wrapped in another context"); - }; + pop_context!(context_stack, instructions, { + Context::For(expected_variable, start, end, step) => { + if *variable != expected_variable { + return Err(ParseError::WrongForVariable( + expected_variable, + variable.clone(), + )); + } - if *variable != expected_variable { - return Err(ParseError::WrongForVariable( - expected_variable, - variable.clone(), - )); + BasicAstInstruction::For { + variable: expected_variable, + start, + end, + step, + instructions: BasicAstBlock::new(instructions), + } + } + _ => { + return Err(ParseError::UnexpectedToken(BasicToken::Next)); } + }); + } + // == While loops == + [BasicToken::While, ..] => { + tokens.take(1); + let condition = parse_expression(&mut tokens)?; + expect_next_token(&tokens, &BasicToken::NewLine)?; - parent_instructions.push(BasicAstInstruction::For { - variable: expected_variable, - start, - end, - step, - instructions: BasicAstBlock::new(instructions), - }); - } - Some((_instructions, _context)) => { - eprintln!("NEXT outside of loop"); - return Err(ParseError::UnexpectedToken(BasicToken::Next)); - } - None => { - unreachable!("Empty context stack"); - } - }, + context_stack.push((Vec::new(), Context::While(condition))); + } + [BasicToken::Do, BasicToken::While, ..] => { + tokens.take(2); + let condition = parse_expression(&mut tokens)?; + expect_next_token(&tokens, &BasicToken::NewLine)?; + + context_stack.push((Vec::new(), Context::DoWhile(condition))); + } + [BasicToken::Do, ..] => { + tokens.take(1); + context_stack.push((Vec::new(), Context::Do)); + expect_next_token(&tokens, &BasicToken::NewLine)?; + } + + [BasicToken::Wend, ..] => { + tokens.take(1); + + pop_context!(context_stack, instructions, { + Context::While(condition) => { + BasicAstInstruction::While(condition, BasicAstBlock::new(instructions)) + }, + _ => { + return Err(ParseError::UnexpectedToken(BasicToken::Wend)); + } + }); + } + [BasicToken::Loop, BasicToken::While, ..] => { + tokens.take(2); + + let condition = parse_expression(&mut tokens)?; + + pop_context!(context_stack, instructions, { + Context::Do => { + BasicAstInstruction::DoWhile(condition, BasicAstBlock::new(instructions)) + }, + Context::DoWhile(_) => { + return Err(ParseError::UnexpectedToken(BasicToken::While)); + }, + _ => { + return Err(ParseError::UnexpectedToken(BasicToken::Loop)); + } + }); + } + [BasicToken::Loop, ..] => { + tokens.take(1); + + pop_context!(context_stack, instructions, { + Context::DoWhile(condition) => { + BasicAstInstruction::DoWhile(condition, BasicAstBlock::new(instructions)) + }, + Context::Do => { + return Err(ParseError::MissingToken(BasicToken::While)); + }, + _ => { + return Err(ParseError::UnexpectedToken(BasicToken::Wend)); + } + }); + } // == Goto == [BasicToken::Goto, BasicToken::Integer(label), ..] => { @@ -269,6 +352,12 @@ pub fn build_ast(tokens: &[BasicToken], config: &Config) -> Result { return Err(ParseError::MissingToken(BasicToken::Next)); } + Context::While(_) => { + return Err(ParseError::MissingToken(BasicToken::Wend)); + } + Context::Do | Context::DoWhile(_) => { + return Err(ParseError::MissingToken(BasicToken::Loop)); + } Context::Main => { unreachable!("There cannot be another context below the main context"); } diff --git a/src/parse/tokenize.rs b/src/parse/tokenize.rs index f9b5e41..13f7c18 100644 --- a/src/parse/tokenize.rs +++ b/src/parse/tokenize.rs @@ -15,6 +15,10 @@ pub enum BasicToken { To, Step, Next, + While, + Wend, + Do, + Loop, LabelEnd, OpenParen, CloseParen, @@ -75,14 +79,14 @@ pub fn tokenize(raw: &str) -> Result, ParseError> { let match_let = Regex::new(r"(?i)^let").unwrap(); let match_jump = Regex::new(r"(?i)^go\s*to").unwrap(); let match_word = - Regex::new(r"(?i)^(?:if|then|else|end\s?if|print|for|to|step|next)(?:\s|$)").unwrap(); + Regex::new(r"(?i)^(?:if|then|else|end\s?(?:if|while)|print|for|to|step|next|while|do|wend|loop)(?:\s|$)").unwrap(); let match_space = Regex::new(r"^\s+").unwrap(); let match_variable = Regex::new(r"^@?[a-zA-Z_][a-zA-Z_0-9]*").unwrap(); let match_float = Regex::new(r"^[0-9]*\.[0-9]+").unwrap(); let match_integer = Regex::new(r"^[0-9]+").unwrap(); let match_assign = Regex::new(r"^=").unwrap(); let match_comma = Regex::new(r"^,").unwrap(); - let match_operator = Regex::new(r"^(?:[+\-*/%]|[<>]=?|[!=]=|<<|>>)").unwrap(); + let match_operator = Regex::new(r"^(?:[+\-*/%]|[<>]=?|[!=]=|<>|<<|>>)").unwrap(); let match_label_end = Regex::new(r"^:").unwrap(); let match_paren = Regex::new(r"^(?:\(|\))").unwrap(); // TODO: handle escapes @@ -111,6 +115,11 @@ pub fn tokenize(raw: &str) -> Result, ParseError> { "to" => BasicToken::To, "step" => BasicToken::Step, "next" => BasicToken::Next, + "while" => BasicToken::While, + "do" => BasicToken::Do, + "wend" => BasicToken::Wend, + "end while" => BasicToken::Wend, + "loop" => BasicToken::Loop, _ => unreachable!("{}", word), }), match_variable(name) => (BasicToken::Name(name.to_string())), @@ -130,7 +139,7 @@ pub fn tokenize(raw: &str) -> Result, ParseError> { "<<" => Operator::LShift, ">>" => Operator::RShift, "==" => Operator::Eq, - "!=" => Operator::Neq, + "<>" | "!=" => Operator::Neq, _ => unreachable!(), })), match_assign => (BasicToken::Assign), diff --git a/src/repr/basic.rs b/src/repr/basic.rs index bf9d852..099c1cd 100644 --- a/src/repr/basic.rs +++ b/src/repr/basic.rs @@ -25,6 +25,8 @@ pub enum BasicAstInstruction { step: BasicAstExpression, instructions: BasicAstBlock, }, + While(BasicAstExpression, BasicAstBlock), + DoWhile(BasicAstExpression, BasicAstBlock), } #[derive(Clone, Debug, PartialEq, Default)] diff --git a/src/translate/mod.rs b/src/translate/mod.rs index fa3fe00..fda9f69 100644 --- a/src/translate/mod.rs +++ b/src/translate/mod.rs @@ -184,6 +184,7 @@ pub fn translate_ast( instructions, } => { let start_label = namer.label("start"); + let end_label = namer.label("end"); let end_name = namer.temporary(); let step_name = namer.temporary(); @@ -192,22 +193,75 @@ pub fn translate_ast( res.append(&mut translate_expression(end, namer, end_name.clone())); res.append(&mut translate_expression(step, namer, step_name.clone())); - // Body + // Condition res.push(MindustryOperation::JumpLabel(start_label.clone())); + res.push(MindustryOperation::JumpIf( + end_label.clone(), + Operator::Gt, + Operand::Variable(variable.clone()), + Operand::Variable(end_name), + )); + + // Body res.append(&mut translate_ast(instructions, namer, config)); - // Loop condition: increment variable and jump + // Increment variable and jump res.push(MindustryOperation::Operator( variable.clone(), Operator::Add, Operand::Variable(variable.clone()), Operand::Variable(step_name), )); + res.push(MindustryOperation::Jump(start_label)); + res.push(MindustryOperation::JumpLabel(end_label)); + } + Instr::While(condition, instructions) => { + let start_label = namer.label("start"); + let end_label = namer.label("end"); + let condition_name = namer.temporary(); + + // Loop condition + res.push(MindustryOperation::JumpLabel(start_label.clone())); + res.append(&mut translate_expression( + condition, + namer, + condition_name.clone(), + )); + res.push(MindustryOperation::JumpIf( + end_label.clone(), + Operator::Eq, + Operand::Variable(condition_name), + Operand::Variable(String::from("false")), + )); + + // Loop body + res.append(&mut translate_ast(instructions, namer, config)); + + // Loop end + res.push(MindustryOperation::Jump(start_label)); + res.push(MindustryOperation::JumpLabel(end_label)); + } + Instr::DoWhile(condition, instructions) => { + let start_label = namer.label("start"); + let condition_name = namer.temporary(); + + // Loop start + res.push(MindustryOperation::JumpLabel(start_label.clone())); + + // Loop body + res.append(&mut translate_ast(instructions, namer, config)); + + // Loop condition + res.append(&mut translate_expression( + condition, + namer, + condition_name.clone(), + )); res.push(MindustryOperation::JumpIf( start_label, - Operator::Lte, - Operand::Variable(variable.clone()), - Operand::Variable(end_name), + Operator::Eq, + Operand::Variable(condition_name), + Operand::Variable(String::from("true")), )); } Instr::Print(expressions) => { diff --git a/tests/examples/collatz.0.mlog b/tests/examples/collatz.0.mlog new file mode 100644 index 0000000..bea4710 --- /dev/null +++ b/tests/examples/collatz.0.mlog @@ -0,0 +1,42 @@ +set X 7 +main__label_0_start: +set main__tmp_1 X +set main__tmp_2 1 +op notEqual main__tmp_0 main__tmp_1 main__tmp_2 +jump main__label_1_end equal main__tmp_0 false +main__label_2_start: +set main__tmp_6 X +set main__tmp_7 2 +op mod main__tmp_4 main__tmp_6 main__tmp_7 +set main__tmp_5 1 +op equal main__tmp_3 main__tmp_4 main__tmp_5 +jump main__label_3_end equal main__tmp_3 false +set main__tmp_10 3 +set main__tmp_11 X +op mul main__tmp_8 main__tmp_10 main__tmp_11 +set main__tmp_9 1 +op add X main__tmp_8 main__tmp_9 +set main__tmp_12 X +print main__tmp_12 +set main__tmp_13 " " +print main__tmp_13 +jump main__label_2_start always 0 0 +main__label_3_end: +main__label_4_start: +set main__tmp_15 X +set main__tmp_16 2 +op div X main__tmp_15 main__tmp_16 +set main__tmp_17 X +print main__tmp_17 +set main__tmp_18 " " +print main__tmp_18 +set main__tmp_21 X +set main__tmp_22 2 +op mod main__tmp_19 main__tmp_21 main__tmp_22 +set main__tmp_20 0 +op equal main__tmp_14 main__tmp_19 main__tmp_20 +jump main__label_4_start equal main__tmp_14 true +jump main__label_0_start always 0 0 +main__label_1_end: +set main__tmp_23 message1 +printflush main__tmp_23 diff --git a/tests/examples/collatz.1.mlog b/tests/examples/collatz.1.mlog new file mode 100644 index 0000000..0be07bc --- /dev/null +++ b/tests/examples/collatz.1.mlog @@ -0,0 +1,21 @@ +set X 7 +main__label_0_start: +jump main__label_1_end equal X 1 +main__label_2_start: +op mod main__tmp_4 X 2 +jump main__label_3_end notEqual main__tmp_4 1 +op mul main__tmp_8 3 X +op add X main__tmp_8 1 +print X +print " " +jump main__label_2_start always 0 0 +main__label_3_end: +main__label_4_start: +op div X X 2 +print X +print " " +op mod main__tmp_19 X 2 +jump main__label_4_start equal main__tmp_19 0 +jump main__label_0_start always 0 0 +main__label_1_end: +printflush message1