diff --git a/forge_core/src/ast.rs b/forge_core/src/ast.rs index bea9c54..49c8396 100644 --- a/forge_core/src/ast.rs +++ b/forge_core/src/ast.rs @@ -58,6 +58,7 @@ pub struct FunctionPrototype { #[derive(PartialEq, Clone, Debug)] pub enum Statement { + Break, Return(Return), Assignment(Assignment), Expr(Expr), @@ -231,6 +232,7 @@ impl Statement { pub fn description(&self) -> String { String::from( match self { + Statement::Break => "break", Statement::Return(_) => "return", Statement::Assignment(_) => "assignment", Statement::Expr(_) => "expr", diff --git a/forge_core/src/compiler/ast_nodes/block.rs b/forge_core/src/compiler/ast_nodes/block.rs index d2ce1a3..b609f4e 100644 --- a/forge_core/src/compiler/ast_nodes/block.rs +++ b/forge_core/src/compiler/ast_nodes/block.rs @@ -5,7 +5,7 @@ use crate::compiler::CompileError; use crate::compiler::state::State; impl Compilable for Block { - fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { + fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { let sig = sig.expect("Block outside function"); let frame_size_before_block = sig.frame_size(); @@ -15,6 +15,13 @@ impl Compilable for Block { let loc = stmt.location; sig.emit_comment(format!(";; {}:{} :: {}", loc.line, loc.col, stmt.ast.description())); match stmt.ast { + Statement::Break => { + if sig.in_loop() { + sig.emit("#break") + } else { + return Err(CompileError(loc.line, loc.col, "Break outside loop".to_string())) + } + } Statement::Return(ret) => { ret.process(state, Some(sig), loc)?; } @@ -59,6 +66,7 @@ impl Compilable for Block { #[cfg(test)] mod test { + use crate::compiler::CompileError; use crate::compiler::test_utils::*; #[test] @@ -74,6 +82,35 @@ mod test { ) } + #[test] + fn test_break_statements() { + assert_eq!( + test_body(state_for("fn test() { repeat(10) { break; } }")), + vec![ + "push 10", "#while", "dup", "agt 0", "#do", // standard repeat loop header + "#break", // the break + "sub 1", "#end", "pop" // repeat loop footer + ].join("\n") + ); + + assert_eq!( + test_body(state_for("fn test() { while(1) { break; } }")), + vec![ + "#while", "push 1", "#do", // standard repeat loop header + "#break", // the break + "#end" // repeat loop footer + ].join("\n") + ) + } + + #[test] + fn test_malformed_break_statements() { + assert_eq!( + test_err("fn test() { break; }"), + Some(CompileError(1, 13, "Break outside loop".to_string())) + ) + } + #[test] fn test_block_scoping() { assert_eq!( diff --git a/forge_core/src/compiler/ast_nodes/repeat_loop.rs b/forge_core/src/compiler/ast_nodes/repeat_loop.rs index 34ca485..f21e286 100644 --- a/forge_core/src/compiler/ast_nodes/repeat_loop.rs +++ b/forge_core/src/compiler/ast_nodes/repeat_loop.rs @@ -41,7 +41,9 @@ impl Compilable for RepeatLoop { // Loop body: sig.emit("#do"); + sig.enter_loop(); body.process(state, Some(sig), loc)?; + sig.exit_loop(); // After the body we need to increment the counter if there is one if named_counter { diff --git a/forge_core/src/compiler/ast_nodes/while_loop.rs b/forge_core/src/compiler/ast_nodes/while_loop.rs index 4e3a2fc..bb2ab35 100644 --- a/forge_core/src/compiler/ast_nodes/while_loop.rs +++ b/forge_core/src/compiler/ast_nodes/while_loop.rs @@ -11,7 +11,9 @@ impl Compilable for WhileLoop { sig.emit("#while"); condition.process(state, Some(sig), loc)?; sig.emit("#do"); + sig.enter_loop(); body.process(state, Some(sig), loc)?; + sig.exit_loop(); sig.emit("#end"); Ok(()) diff --git a/forge_core/src/compiler/compiled_fn.rs b/forge_core/src/compiler/compiled_fn.rs index b9a18ee..03a70ff 100644 --- a/forge_core/src/compiler/compiled_fn.rs +++ b/forge_core/src/compiler/compiled_fn.rs @@ -1,6 +1,7 @@ use std::cmp::max; use std::collections::btree_map::Entry::Vacant; use std::fmt::Display; +use crate::ast::Location; use crate::compiler::compile_error::CompileError; use crate::compiler::text::Text; use crate::compiler::utils::{Label, Scope, Variable}; @@ -56,6 +57,7 @@ pub struct CompiledFn { pub max_frame_size: usize, pub local_scope: Scope, pub arity: usize, + pub loop_level: u32, /// The preamble is the area of the function capturing the args, etc pub preamble: Text, @@ -83,6 +85,22 @@ impl CompiledFn { } } + pub fn enter_loop(&mut self) { + self.loop_level += 1 + } + pub fn exit_loop(&mut self) { + if self.loop_level > 0 { + self.loop_level -= 1; + } else { + // I doubt it's possible for this to happen, since it would be + // caught at the parse stage + unreachable!() + } + } + pub fn in_loop(&self) -> bool { + self.loop_level > 0 + } + /// Return an iterator over all the pub fn text(&self) -> impl Iterator { self.preamble.0.iter().chain(self.body.0.iter()).chain(self.outro.0.iter()).map(String::as_str) diff --git a/forge_core/src/compiler/test_utils.rs b/forge_core/src/compiler/test_utils.rs index 403082d..66ad3c1 100644 --- a/forge_core/src/compiler/test_utils.rs +++ b/forge_core/src/compiler/test_utils.rs @@ -1,4 +1,5 @@ use crate::compiler::compilable::Compilable; +use crate::compiler::CompileError; use crate::compiler::state::State; use crate::parser::parse; @@ -15,6 +16,11 @@ pub(crate) fn test_body(state: State) -> String { state.functions.get("test").unwrap().body.0.join("\n") } +pub(crate) fn test_err(src: &str) -> Option { + let mut state = State::default(); + parse(src).unwrap().process(&mut state, None, (0, 0).into()).err() +} + pub(crate) fn test_preamble(state: State) -> String { state.functions.get("test").unwrap().preamble.0.join("\n") } diff --git a/forge_core/src/parser/ast_nodes/statement.rs b/forge_core/src/parser/ast_nodes/statement.rs index 0c5a8ea..f04ec59 100644 --- a/forge_core/src/parser/ast_nodes/statement.rs +++ b/forge_core/src/parser/ast_nodes/statement.rs @@ -6,6 +6,7 @@ impl AstNode for Statement { fn from_pair(pair: Pair) -> Self { let pair = pair.first(); match pair.as_rule() { + PestRule::break_stmt => Self::Break, PestRule::return_stmt => Self::Return(Return::from_pair(pair)), PestRule::assignment => Self::Assignment(Assignment::from_pair(pair)), PestRule::expr => Self::Expr(Expr::from_pair(pair)), diff --git a/forge_core/src/parser/forge.pest b/forge_core/src/parser/forge.pest index ebf189c..5ae7698 100644 --- a/forge_core/src/parser/forge.pest +++ b/forge_core/src/parser/forge.pest @@ -73,7 +73,7 @@ suffix = _{ subscript | arglist } subscript = { "[" ~ expr ~ "]" } arglist = { "(" ~ (expr ~ ("," ~ expr)*)? ~ ")" } -statement = { asm | ((return_stmt | var_decl | assignment | expr) ~ ";") | conditional | while_loop | repeat_loop } +statement = { asm | ((break_stmt | return_stmt | var_decl | assignment | expr) ~ ";") | conditional | while_loop | repeat_loop } asm = { "asm" ~ asm_args? ~ "{" ~ asm_body ~ "}" } asm_args = _{ "(" ~ expr ~ ("," ~ expr)* ~ ")" } @@ -89,6 +89,7 @@ function_prototype = { "fn" ~ name ~ argnames ~ ";" } declaration = { function | function_prototype | global | const_decl } program = { (COMMENT | WHITESPACE)? ~ declaration* ~ EOI } +break_stmt = { "break" } return_stmt = { "return" ~ expr? } conditional = { "if" ~ "(" ~ expr ~ ")" ~ block ~ ("else" ~ block)? } while_loop = { "while" ~ "(" ~ expr ~ ")" ~ block } diff --git a/vasm_core/src/ast.rs b/vasm_core/src/ast.rs index a1f8b21..fed9657 100644 --- a/vasm_core/src/ast.rs +++ b/vasm_core/src/ast.rs @@ -62,6 +62,7 @@ pub enum Macro { Until, Do, End, + Break, } #[derive(Debug, PartialEq, Clone)] diff --git a/vasm_core/src/vasm.pest b/vasm_core/src/vasm.pest index f311863..f02e4e9 100644 --- a/vasm_core/src/vasm.pest +++ b/vasm_core/src/vasm.pest @@ -69,7 +69,7 @@ org_directive = { label_def? ~ ".org" ~ expr } equ_directive = { label_def ~ ".equ" ~ expr } include = { "include" ~ string } -control = { "if" | "unless" | "while" | "until" | "do" | "else" | "end" } +control = { "if" | "unless" | "while" | "until" | "do" | "else" | "end" | "break" } preprocessor = {"#" ~ (control | include) } blank = { WHITESPACE? ~ COMMENT? } diff --git a/vasm_core/src/vasm_parser.rs b/vasm_core/src/vasm_parser.rs index 5f6aedd..37bec12 100644 --- a/vasm_core/src/vasm_parser.rs +++ b/vasm_core/src/vasm_parser.rs @@ -208,6 +208,7 @@ fn parse_macro(line: Pair) -> Macro { "until" => Macro::Until, "do" => Macro::Do, "end" => Macro::End, + "break" => Macro::Break, _ => unreachable!(), }, Rule::include => Macro::Include(create_string(pre.only())), diff --git a/vasm_core/src/vasm_preprocessor.rs b/vasm_core/src/vasm_preprocessor.rs index 951c252..0aa97bc 100644 --- a/vasm_core/src/vasm_preprocessor.rs +++ b/vasm_core/src/vasm_preprocessor.rs @@ -81,7 +81,10 @@ impl, F: Fn(String) -> Result> // It's a macro, so do it and then try again Ok(VASMLine::Macro(mac)) => { - self.handle_macro(mac); + match self.handle_macro(mac) { + None => {} + Some(err) => return Some(Err(err)) + } self.next() } @@ -214,6 +217,20 @@ impl, F: Fn(String) -> Result> } _ => return Some(AssembleError::MacroError(self.current_location())), }, + + Macro::Break => { + let innermost_loop = self.control_stack.iter().rev().find(|cs| { + match cs { + ControlStructure::LoopDo(_, _) => true, + _ => false + } + }); + if let Some(ControlStructure::LoopDo(_, after)) = innermost_loop { + self.emit(format!("jmpr @{}", after)); + } else { + return Some(AssembleError::MacroError(self.current_location())) + } + } } None } @@ -227,10 +244,18 @@ mod tests { fn lines_for(source: Vec) -> Vec { let include = |_name: String| panic!(); - let src = LineSource::new("blah", source, include); + let src = LineSource::new("", source, include); src.map(|line| line.unwrap().line).collect() } + fn error_for(source: Vec) -> Option { + let include = |_name: String| panic!(); + for line in LineSource::new("", source, include) { + if line.is_err() { return line.err() } + } + None + } + fn stringify(a: Vec<&str>) -> Vec { a.into_iter().map(String::from).collect() } @@ -325,4 +350,40 @@ mod tests { ] ) } + + #[test] + fn test_preprocess_break() { + // First a working one + assert_eq!( + lines_for(stringify(vec!["#while", "#do", "#break", "#end"])), + vec![ + VASMLine::LabelDef(Label("__gensym_1".to_string())), + VASMLine::Instruction( + None, + Brz, + Some(Node::RelativeLabel("__gensym_2".to_string())) + ), + VASMLine::Instruction( + None, + Jmpr, + Some(Node::RelativeLabel("__gensym_2".to_string())) + ), + VASMLine::Instruction( + None, + Jmpr, + Some(Node::RelativeLabel("__gensym_1".to_string())) + ), + VASMLine::LabelDef(Label::from("__gensym_2")) + ] + ); + } + + #[test] + fn test_malformed_break() { + // Malformed one + assert_eq!( + error_for(stringify(vec!["#while", "#break"])), + Some(AssembleError::MacroError(Location::from(2))) + ) + } } diff --git a/vweb/src/forge_highlighter.js b/vweb/src/forge_highlighter.js index fc7338b..99e671e 100644 --- a/vweb/src/forge_highlighter.js +++ b/vweb/src/forge_highlighter.js @@ -16,7 +16,7 @@ export default { if (word = stream.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*/)) { // This is either a keyword or an identifier. // All the keywords are valid names, so, check that: - if (word[0].match(/^fn|var|new|static|asm|return|if|while|repeat|global|const|peek|poke$/)) { + if (word[0].match(/^fn|var|new|static|asm|return|if|while|repeat|global|const|peek|poke|break$/)) { if (word[0] === 'asm') { state.asm = true } // This is the start of an asm control structure