diff --git a/GUIDE.md b/GUIDE.md index 738c915..12418cc 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -205,3 +205,7 @@ DO WHILE x % 2 == 1 x = 3*x + 1 LOOP ``` + +## Subroutines + +Subroutines allow you to execute the same piece of code from diff --git a/examples/gosub-advanced.mbas b/examples/gosub-advanced.mbas new file mode 100644 index 0000000..6b7f674 --- /dev/null +++ b/examples/gosub-advanced.mbas @@ -0,0 +1,32 @@ +a = 0 +b = 0 +c = 0 + +a = 1 +GOSUB sub1 +c = c * 2 +a = 2 +GOTO end_loop + +sub1: + b = 1 + GOSUB sub2 + b = b * 2 + + RETURN + b = 5 + END + +end_loop: + IF a == 2 AND b == 2 AND c == 2 THEN + PRINT "success" + ELSE + PRINT "fail: ", a, ", ", b, ", ", c + END IF + PRINT_FLUSH(message1) + GOTO end_loop + +sub2: + c = 1 + RETURN + c = 5 diff --git a/examples/gosub.mbas b/examples/gosub.mbas new file mode 100644 index 0000000..3b107d1 --- /dev/null +++ b/examples/gosub.mbas @@ -0,0 +1,25 @@ +a = 0 +b = 0 +c = 0 + +GOSUB sub + +b = 1 +c = 1 + +GOTO trap + +END +sub: + a = 1 + RETURN + a = 2 + +trap: + IF a == 1 AND b == 1 AND c == 1 THEN + PRINT "success" + ELSE + PRINT "fail: ", a, ", ", b, ", ", c + END IF + PRINT_FLUSH(message1) + GOTO trap diff --git a/examples/procedural-waves.mbas b/examples/procedural-waves.mbas index f7a4251..9e08c10 100644 --- a/examples/procedural-waves.mbas +++ b/examples/procedural-waves.mbas @@ -1,23 +1,20 @@ -REM This is an example of a more complex program, -REM which spawns procedural waves +REM This is an example of a more complex program, which spawns procedural waves. +REM The waves become progressively harder, and the wave delay is controlled by `cell1[0]` LET wave = 0 LET timeout = @time + 120000 -initial_wait: +DO LET remaining = timeout - @time - IF remaining <= 0 THEN - GOTO main - END IF PRINT "[red]Enemies[white] approaching: " PRINT ceil(remaining / 1000), " s" PRINT_FLUSH_GLOBAL(mission) wait(0.5) +LOOP WHILE remaining > 0 - GOTO initial_wait -main: +WHILE true wave = wave + 1 REM TODO: difficult control wave multiplier LET progression = POW(wave / 4, 0.75) @@ -32,33 +29,27 @@ main: LET air_units = units - tank_units - mech_units LET spawnx = 30 + LET spawnairx = 30 LET spawny = 50 - GOTO spawn_tank - spawn_tank_end: - GOTO spawn_mech - spawn_mech_end: - LET spawnx = 20 - GOTO spawn_air - spawn_air_end: + GOSUB spawn_tank + GOSUB spawn_mech + GOSUB spawn_air WRITE(wave, cell1, 1) READ(timeout, cell1, 0) timeout = @time + timeout * 1000 - main_wait: + DO LET remaining = timeout - @time - IF remaining <= 0 THEN - GOTO main - END IF PRINT "[yellow]Wave ", wave, "[white] - " PRINT "Next wave: ", ceil(remaining / 1000), " s" PRINT_FLUSH_GLOBAL(mission) wait(0.5) - - GOTO main_wait + LOOP WHILE remaining > 0 +WEND spawn_tank: FOR spawned = 1 TO tank_units @@ -92,7 +83,7 @@ spawn_tank: END IF NEXT spawned - GOTO spawn_tank_end + RETURN spawn_mech: FOR spawned = 1 TO mech_units @@ -125,28 +116,29 @@ spawn_mech: END IF NEXT spawned - GOTO spawn_mech_end + RETURN spawn_air: FOR spawned = 1 TO air_units LET roll = rand(progression) IF roll >= 3 THEN IF roll >= 4 THEN - SPAWN(@disrupt, spawnx, spawny, 0, @crux, _) - spawnx = spawnx - 5.75 + SPAWN(@disrupt, spawnairx, spawny, 0, @crux, _) + spawnairx = spawnairx - 5.75 ELSE - SPAWN(@quell, spawnx, spawny, 0, @crux, _) - spawnx = spawnx - 4.5 + SPAWN(@quell, spawnairx, spawny, 0, @crux, _) + spawnairx = spawnairx - 4.5 END IF ELSE IF roll >= 2 THEN - SPAWN(@obviate, spawnx, spawny, 0, @crux, _) - spawnx = spawnx - 3.12 + SPAWN(@obviate, spawnairx, spawny, 0, @crux, _) + spawnairx = spawnairx - 3.12 ELSE IF roll >= 1 THEN - SPAWN(@avert, spawnx, spawny, 0, @crux, _) - spawnx = spawnx - 1 + SPAWN(@avert, spawnairx, spawny, 0, @crux, _) + spawnairx = spawnairx - 1 ELSE + REM The elude is the only non-air unit SPAWN(@elude, spawnx, spawny, 0, @crux, _) spawnx = spawnx - 1 END IF @@ -156,6 +148,9 @@ spawn_air: IF spawnx < 10 THEN spawnx = 10 END IF + IF spawnairx < 5 THEN + spawnairx = 5 + END IF NEXT spawned - GOTO spawn_air_end + RETURN diff --git a/src/parse/ast.rs b/src/parse/ast.rs index eff39d2..d9925ed 100644 --- a/src/parse/ast.rs +++ b/src/parse/ast.rs @@ -246,6 +246,18 @@ pub fn build_ast(tokens: &[BasicToken], config: &Config) -> Result { + tokens.take(2); + instructions.push(BasicAstInstruction::GoSub(label.to_string())); + } + [BasicToken::GoSub, BasicToken::Name(label), ..] => { + tokens.take(2); + instructions.push(BasicAstInstruction::GoSub(label.clone())); + } + [BasicToken::Return, ..] => { + tokens.take(1); + instructions.push(BasicAstInstruction::Return); + } // == Misc == [BasicToken::Name(variable_name), BasicToken::Assign, ..] => { tokens.take(2); diff --git a/src/parse/tokenize.rs b/src/parse/tokenize.rs index 57e03a4..b2c77c3 100644 --- a/src/parse/tokenize.rs +++ b/src/parse/tokenize.rs @@ -20,6 +20,8 @@ pub enum BasicToken { End, Do, Loop, + GoSub, + Return, LabelEnd, OpenParen, CloseParen, @@ -80,7 +82,7 @@ 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|while)|print|for|to|step|next|while|do|wend|loop)(?:\s|$)").unwrap(); + Regex::new(r"(?i)^(?:if|then|else|end\s?(?:if|while)|print|for|to|step|next|while|do|wend|loop|gosub|return|and|or)(?:\s|$)").unwrap(); let match_end = Regex::new(r"(?i)^end(?:\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(); @@ -88,7 +90,7 @@ pub fn tokenize(raw: &str) -> Result, ParseError> { 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 @@ -122,6 +124,10 @@ pub fn tokenize(raw: &str) -> Result, ParseError> { "wend" => BasicToken::Wend, "end while" => BasicToken::Wend, "loop" => BasicToken::Loop, + "gosub" => BasicToken::GoSub, + "return" => BasicToken::Return, + "and" => BasicToken::Operator(Operator::And), + "or" => BasicToken::Operator(Operator::Or), _ => unreachable!("{}", word), }), match_end => (BasicToken::End), @@ -133,6 +139,7 @@ pub fn tokenize(raw: &str) -> Result, ParseError> { "+" => Operator::Add, "-" => Operator::Sub, "*" => Operator::Mul, + "//" => Operator::IDiv, "/" => Operator::Div, "%" => Operator::Mod, "<" => Operator::Lt, @@ -143,6 +150,8 @@ pub fn tokenize(raw: &str) -> Result, ParseError> { ">>" => Operator::RShift, "==" => Operator::Eq, "<>" | "!=" => Operator::Neq, + "&&" => Operator::And, + "||" => Operator::Or, _ => unreachable!(), })), match_assign => (BasicToken::Assign), diff --git a/src/repr/basic.rs b/src/repr/basic.rs index 766623f..e0c09c9 100644 --- a/src/repr/basic.rs +++ b/src/repr/basic.rs @@ -13,7 +13,13 @@ pub enum BasicAstExpression { #[derive(Clone, Debug, PartialEq)] pub enum BasicAstInstruction { JumpLabel(String), + /// Immediately and always jump to the label or line number stored Jump(String), + /// Stores the return address, and jumps to the given label + GoSub(String), + /// Reads the return address and jumps to it + Return, + /// Stops the program, returning to the starting point End, Assign(String, BasicAstExpression), IfThenElse(BasicAstExpression, BasicAstBlock, BasicAstBlock), diff --git a/src/repr/operator.rs b/src/repr/operator.rs index d9d8a60..41795b4 100644 --- a/src/repr/operator.rs +++ b/src/repr/operator.rs @@ -3,6 +3,7 @@ pub enum Operator { Add, Sub, Mul, + IDiv, Div, Mod, RShift, @@ -16,18 +17,19 @@ pub enum Operator { Max, Min, Pow, - // etc. + And, + Or, } impl Operator { pub(crate) fn precedence(self) -> u8 { - use Operator as O; match self { - O::Add | O::Sub => 3, - O::RShift | O::LShift => 4, - O::Mod => 5, - O::Mul | O::Div => 10, - O::Eq | O::Neq | O::Gt | O::Lt | O::Gte | O::Lte => 0, + Self::Add | Self::Sub => 3, + Self::RShift | Self::LShift => 4, + Self::Mod => 5, + Self::Mul | Self::Div | Self::IDiv => 10, + Self::Eq | Self::Neq | Self::Gt | Self::Lt | Self::Gte | Self::Lte => 1, + Self::And | Self::Or => 0, _ => 128, } } @@ -54,12 +56,15 @@ pub(crate) fn format_operator(operator: Operator) -> &'static str { Operator::Sub => "sub", Operator::Mul => "mul", Operator::Div => "div", + Operator::IDiv => "idiv", Operator::Mod => "mod", Operator::RShift => "shr", Operator::LShift => "shl", Operator::Max => "max", Operator::Min => "min", Operator::Pow => "pow", + Operator::And => "and", + Operator::Or => "or", } } @@ -70,6 +75,7 @@ pub enum UnaryOperator { Ceil, Rand, Sqrt, + // Not, } impl TryFrom<&str> for UnaryOperator { diff --git a/src/translate/display.rs b/src/translate/display.rs index dde38c2..6c406b7 100644 --- a/src/translate/display.rs +++ b/src/translate/display.rs @@ -132,9 +132,9 @@ fn format_condition(operator: Operator) -> &'static str { Operator::Eq => "equal", Operator::Neq => "notEqual", Operator::Lt => "lessThan", - Operator::Lte => "lessThanEqual", + Operator::Lte => "lessThanEq", Operator::Gt => "greaterThan", - Operator::Gte => "greaterThanEqual", + Operator::Gte => "greaterThanEq", x => { panic!("Operator {:?} is not a condition!", x); } diff --git a/src/translate/mod.rs b/src/translate/mod.rs index d409f9b..4310995 100644 --- a/src/translate/mod.rs +++ b/src/translate/mod.rs @@ -4,6 +4,8 @@ use crate::prelude::*; mod display; +const MAX_INSTRUCTION_COUNT: usize = 1000; + pub struct Namer { var_index: usize, label_index: usize, @@ -125,6 +127,7 @@ pub fn translate_ast( ) -> MindustryProgram { use crate::repr::basic::BasicAstInstruction as Instr; let mut res = MindustryProgram::new(); + let mut has_return = false; for instruction in basic_ast.instructions.iter() { match instruction { @@ -134,6 +137,45 @@ pub fn translate_ast( Instr::Jump(to) => { res.push(MindustryOperation::Jump(to.clone())); } + Instr::GoSub(to) => { + let return_label = namer.label("return__phantom"); + res.push(MindustryOperation::Operator( + String::from("__gosub_retaddr"), + Operator::Mul, + Operand::Variable(String::from("__gosub_retaddr")), + Operand::Integer(MAX_INSTRUCTION_COUNT as i64)) + ); + res.push(MindustryOperation::Operator( + String::from("__gosub_retaddr"), + Operator::Add, + Operand::Variable(String::from("__gosub_retaddr")), + Operand::Variable(String::from("@counter")), + )); + res.push(MindustryOperation::Jump(to.clone())); + res.push(MindustryOperation::JumpLabel(return_label)); + } + Instr::Return => { + res.push(MindustryOperation::Operator( + String::from("__return"), + Operator::Mod, + Operand::Variable(String::from("__gosub_retaddr")), + Operand::Integer(MAX_INSTRUCTION_COUNT as i64)) + ); + res.push(MindustryOperation::Operator( + String::from("__gosub_retaddr"), + Operator::IDiv, + Operand::Variable(String::from("__gosub_retaddr")), + Operand::Integer(MAX_INSTRUCTION_COUNT as i64)) + ); + res.push(MindustryOperation::Operator( + String::from("@counter"), + Operator::Add, + Operand::Variable(String::from("__return")), + Operand::Integer(1)) + ); + // Add a guard at the beginning of the program, to clear out the return address + has_return = true; + } Instr::End => { res.push(MindustryOperation::End); } @@ -371,5 +413,9 @@ pub fn translate_ast( } } + if has_return { + res.0.insert(0, MindustryOperation::Set(String::from("__gosub_retaddr"), Operand::Integer(0))); + } + res } diff --git a/tests/examples/gosub.0.mlog b/tests/examples/gosub.0.mlog new file mode 100644 index 0000000..7c48536 --- /dev/null +++ b/tests/examples/gosub.0.mlog @@ -0,0 +1,51 @@ +set __gosub_retaddr 0 +set a 0 +set b 0 +set c 0 +op mul __gosub_retaddr __gosub_retaddr 1000 +op add __gosub_retaddr __gosub_retaddr @counter +jump sub always 0 0 +main__label_0_return__phantom: +set b 1 +set c 1 +jump trap always 0 0 +end +sub: +set a 1 +op mod __return __gosub_retaddr 1000 +op idiv __gosub_retaddr __gosub_retaddr 1000 +op add @counter __return 1 +set a 2 +trap: +set main__tmp_5 a +set main__tmp_6 1 +op equal main__tmp_3 main__tmp_5 main__tmp_6 +set main__tmp_7 b +set main__tmp_8 1 +op equal main__tmp_4 main__tmp_7 main__tmp_8 +op and main__tmp_1 main__tmp_3 main__tmp_4 +set main__tmp_9 c +set main__tmp_10 1 +op equal main__tmp_2 main__tmp_9 main__tmp_10 +op and main__tmp_0 main__tmp_1 main__tmp_2 +jump main__label_1_else notEqual main__tmp_0 true +set main__tmp_11 "success" +print main__tmp_11 +jump main__label_2_endif always 0 0 +main__label_1_else: +set main__tmp_12 "fail: " +print main__tmp_12 +set main__tmp_13 a +print main__tmp_13 +set main__tmp_14 ", " +print main__tmp_14 +set main__tmp_15 b +print main__tmp_15 +set main__tmp_16 ", " +print main__tmp_16 +set main__tmp_17 c +print main__tmp_17 +main__label_2_endif: +set main__tmp_18 message1 +printflush main__tmp_18 +jump trap always 0 0 diff --git a/tests/examples/gosub.1.mlog b/tests/examples/gosub.1.mlog new file mode 100644 index 0000000..99723dc --- /dev/null +++ b/tests/examples/gosub.1.mlog @@ -0,0 +1,37 @@ +set __gosub_retaddr 0 +set a 0 +set b 0 +set c 0 +op mul __gosub_retaddr __gosub_retaddr 1000 +op add __gosub_retaddr __gosub_retaddr @counter +jump sub always 0 0 +main__label_0_return__phantom: +set b 1 +set c 1 +jump trap always 0 0 +end +sub: +set a 1 +op mod __return __gosub_retaddr 1000 +op idiv __gosub_retaddr __gosub_retaddr 1000 +op add @counter __return 1 +set a 2 +trap: +op equal main__tmp_3 a 1 +op equal main__tmp_4 b 1 +op and main__tmp_1 main__tmp_3 main__tmp_4 +op equal main__tmp_2 c 1 +op and main__tmp_0 main__tmp_1 main__tmp_2 +jump main__label_1_else notEqual main__tmp_0 true +print "success" +jump main__label_2_endif always 0 0 +main__label_1_else: +print "fail: " +print a +print ", " +print b +print ", " +print c +main__label_2_endif: +printflush message1 +jump trap always 0 0 diff --git a/tests/examples/world-print.1.mlog b/tests/examples/world-print.1.mlog index 3f13b90..90bb624 100644 --- a/tests/examples/world-print.1.mlog +++ b/tests/examples/world-print.1.mlog @@ -1,5 +1,5 @@ print "Hello, world" -jump main__label_0_else greaterThanEqual method 2 +jump main__label_0_else greaterThanEq method 2 jump main__label_2_else notEqual method 0 message mission jump main__label_1_endif always 0 0