diff --git a/docs/assets/for.png b/docs/assets/for.png new file mode 100644 index 00000000..de6ba1bb Binary files /dev/null and b/docs/assets/for.png differ diff --git a/docs/assets/foreach.png b/docs/assets/foreach.png new file mode 100644 index 00000000..07e7f12b Binary files /dev/null and b/docs/assets/foreach.png differ diff --git a/docs/language/control-flow.md b/docs/language/control-flow.md index 282a59ba..ad21ec6e 100644 --- a/docs/language/control-flow.md +++ b/docs/language/control-flow.md @@ -20,6 +20,26 @@ until condition == true { ![](../assets/until.png){width="200"} +## for each loop + +```goboscript +for x in n { + # code +} +``` + +![](../assets/foreach.png){width="200"} + +## for loop + +```goboscript +for x = 0; x > n; x++ { + # code +} +``` + +![](../assets/for.png){width="100"} + ## forever loop ```goboscript diff --git a/editors/code/syntaxes/goboscript.tmGrammar.yml b/editors/code/syntaxes/goboscript.tmGrammar.yml index 2f27b406..b8413361 100644 --- a/editors/code/syntaxes/goboscript.tmGrammar.yml +++ b/editors/code/syntaxes/goboscript.tmGrammar.yml @@ -31,7 +31,7 @@ patterns: - name: keyword match: "\\b(costumes|sounds|global|list|nowarp|onflag|onkey|onbackdrop|onloudness|ontimer|on|onclone)\\b" - name: keyword.control - match: "\\b(if|else|elif|until|forever|repeat|delete|at|add|to|insert|true|false|as|struct|enum|return)\\b" + match: "\\b(if|else|elif|until|for|forever|repeat|delete|at|add|to|insert|true|false|as|struct|enum|return)\\b" - name: keyword match: "\\b(error|warn|breakpoint|local|not|and|or|in|length|round|abs|floor|ceil|sqrt|sin|cos|tan|asin|acos|atan|ln|log|antiln|antilog)\\b" - name: support.function.builtin diff --git a/editors/notepad++/goboscript.udl.xml b/editors/notepad++/goboscript.udl.xml index 66b73c8e..16a0544a 100644 --- a/editors/notepad++/goboscript.udl.xml +++ b/editors/notepad++/goboscript.udl.xml @@ -24,7 +24,7 @@ - costumes sounds local proc func return nowarp if else elif until forever repeat list cloud struct enum + costumes sounds local proc func return nowarp if else elif until for forever repeat list cloud struct enum %define %if %else %endif %include %undef true false $ diff --git a/editors/sublime/goboscript.sublime-syntax b/editors/sublime/goboscript.sublime-syntax index b464e047..4734d37f 100644 --- a/editors/sublime/goboscript.sublime-syntax +++ b/editors/sublime/goboscript.sublime-syntax @@ -22,7 +22,7 @@ contexts: match: "\\b(costumes|sounds|global|variables|lists|nowarp|onflag|onkey|onbackdrop|onloudness|ontimer|on|onclone)\\b" - scope: keyword.control - match: "\\b(if|else|elif|until|forever|repeat|delete|at|add|to|insert)\\b" + match: "\\b(if|else|elif|until|for|forever|repeat|delete|at|add|to|insert)\\b" - scope: keyword match: "\\b(error|warn|breakpoint|local|not|and|or|in|length|round|abs|floor|ceil|sqrt|sin|cos|tan|asin|acos|atan|ln|log|antiln|antilog)\\b" diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index ccaf54c6..7b2380b6 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -20,6 +20,19 @@ pub enum Stmt { times: Box, body: Vec, }, + ForEach { + name: Name, + times: Box, + body: Vec, + }, + For { + name: Name, + value: Box, + type_: Type, + cond: Box, + incr: Box, + body: Vec, + }, Forever { body: Vec, span: Span, diff --git a/src/codegen/sb3.rs b/src/codegen/sb3.rs index 9dfc3dbd..d7296a24 100644 --- a/src/codegen/sb3.rs +++ b/src/codegen/sb3.rs @@ -224,6 +224,8 @@ impl Stmt { fn opcode(&self, s: S) -> &'static str { match self { Stmt::Repeat { .. } => "control_repeat", + Stmt::ForEach { .. } => "control_for_each", + Stmt::For { .. } => "control_repeat_until", Stmt::Forever { .. } => "control_forever", Stmt::Branch { else_body, .. } => { if else_body.is_empty() { @@ -965,18 +967,20 @@ where T: Write + Seek self.stmts(s, d, &event.body, next_id, Some(this_id)) } - pub fn stmts( + pub fn stmts_with_next( &mut self, s: S, d: D, stmts: &[Stmt], mut this_id: NodeID, + last_id: Option, mut parent_id: Option, ) -> io::Result<()> { for (i, stmt) in stmts.iter().enumerate() { let is_last = i == stmts.len() - 1; if is_last || stmt.is_terminator() { - self.stmt(s, d, stmt, this_id, None, parent_id)?; + let next_id = last_id; + self.stmt(s, d, stmt, this_id, next_id, parent_id)?; if !is_last { d.report(DiagnosticKind::FollowedByUnreachableCode, stmt.span()); } @@ -990,6 +994,17 @@ where T: Write + Seek Ok(()) } + pub fn stmts( + &mut self, + s: S, + d: D, + stmts: &[Stmt], + this_id: NodeID, + parent_id: Option, + ) -> io::Result<()> { + self.stmts_with_next(s, d, stmts, this_id, None, parent_id) + } + pub fn stmt( &mut self, s: S, @@ -1006,6 +1021,13 @@ where T: Write + Seek )?; match stmt { Stmt::Repeat { times, body } => self.repeat(s, d, this_id, times, body), + Stmt::ForEach { name, times, body } => self.foreach(s, d, this_id, name, times, body), + Stmt::For { + cond, + incr, + body, + .. + } => self.r#for(s, d, this_id, cond, incr, body), Stmt::Forever { body, span } => self.forever(s, d, this_id, body, span), Stmt::Branch { cond, diff --git a/src/codegen/stmt.rs b/src/codegen/stmt.rs index 0d56b68a..71df1df9 100644 --- a/src/codegen/stmt.rs +++ b/src/codegen/stmt.rs @@ -55,6 +55,60 @@ where T: Write + Seek self.stmts(s, d, body, body_id, Some(this_id)) } + pub fn foreach( + &mut self, + s: S, + d: D, + this_id: NodeID, + name: &Name, + times: &Expr, + body: &[Stmt], + ) -> io::Result<()> { + let times_id = self.id.new_id(); + let body_id = self.id.new_id(); + self.begin_inputs()?; + self.input(s, d, "VALUE", times, times_id)?; + self.substack("SUBSTACK", (!body.is_empty()).then_some(body_id))?; + self.end_obj()?; // inputs + match s.qualify_name(d, name) { + Some(QualifiedName::Var(qualified_name, _)) => { + self.single_field_id("VARIABLE", &qualified_name)? + } + Some(QualifiedName::List(..)) => { + d.report( + DiagnosticKind::UnrecognizedVariable(name.basename().clone()), + &name.span(), + ); + } + None => {} + } + self.end_obj()?; // node + self.expr(s, d, times, times_id, this_id)?; + self.stmts(s, d, body, body_id, Some(this_id)) + } + + pub fn r#for( + &mut self, + s: S, + d: D, + this_id: NodeID, + cond: &Expr, + incr: &Stmt, + body: &[Stmt], + ) -> io::Result<()> { + let cond_id = self.id.new_id(); + let incr_id = self.id.new_id(); + let body_id = self.id.new_id(); + self.begin_inputs()?; + self.input(s, d, "CONDITION", cond, cond_id)?; + self.substack("SUBSTACK", Some((!body.is_empty()).then_some(body_id).unwrap_or(incr_id)))?; + self.end_obj()?; // inputs + self.end_obj()?; // node + self.expr(s, d, cond, cond_id, this_id)?; + self.stmts_with_next(s, d, body, body_id, Some(incr_id), Some(this_id))?; + self.stmt(s, d, incr, incr_id, None, Some(body_id)) + } + pub fn forever( &mut self, s: S, diff --git a/src/lexer/token.rs b/src/lexer/token.rs index 274a6b6c..7a1bcfae 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -71,6 +71,8 @@ pub enum Token { Elif, #[token("until")] Until, + #[token("for")] + For, #[token("forever")] Forever, #[token("repeat")] diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop index 826cf316..783f9fd1 100644 --- a/src/parser/grammar.lalrpop +++ b/src/parser/grammar.lalrpop @@ -87,6 +87,23 @@ Stmt: Stmt = { Stmt::Branch { cond, if_body, else_body: vec![else_body] } }, REPEAT => Stmt::Repeat { times, body }, + FOR IN => { + Stmt::ForEach { + name: Name::Name { name, span: l..r }, + times, + body, + } + }, + FOR "=" ";" ";" => { + Stmt::For { + name: Name::Name { name, span: l..r }, + value, + type_, + cond, + incr, + body, + } + }, FOREVER => Stmt::Forever { body, span: l..r }, UNTIL => Stmt::Until { cond, body }, "=" ";" => { @@ -297,6 +314,9 @@ Elif: Stmt = { }, } +#[inline] +BoxedStmt: Box = => Box::new(stmt); + #[inline] BoxedExpr: Box = => Box::new(expr); @@ -593,6 +613,7 @@ extern { ELSE => Token::Else, ELIF => Token::Elif, UNTIL => Token::Until, + FOR => Token::For, FOREVER => Token::Forever, REPEAT => Token::Repeat, "," => Token::Comma, diff --git a/src/visitor/pass0.rs b/src/visitor/pass0.rs index 703d0654..a7a5d376 100644 --- a/src/visitor/pass0.rs +++ b/src/visitor/pass0.rs @@ -119,6 +119,8 @@ fn visit_stmts(stmts: &mut Vec, v: &mut V) { fn visit_stmt(stmt: &mut Stmt, v: &mut V) { match stmt { Stmt::Repeat { body, .. } => visit_stmts(body, v), + Stmt::ForEach { body, .. } => visit_stmts(body, v), + Stmt::For { body, .. } => visit_stmts(body, v), Stmt::Forever { body, .. } => visit_stmts(body, v), Stmt::Branch { if_body, else_body, .. diff --git a/src/visitor/pass1.rs b/src/visitor/pass1.rs index 3f80695d..2b6c6481 100644 --- a/src/visitor/pass1.rs +++ b/src/visitor/pass1.rs @@ -100,6 +100,30 @@ fn visit_stmt(stmt: &mut Stmt, s: &mut S) -> Vec { visit_expr(times, &mut before, s); visit_stmts(body, s); } + Stmt::ForEach { times, body, .. } => { + visit_expr(times, &mut before, s); + visit_stmts(body, s); + } + Stmt::For { + name, + value, + type_, + cond, + incr, + body, + } => { + before.push(Stmt::SetVar { + name: name.clone(), + value: value.clone(), + type_: type_.clone(), + is_local: false, + is_cloud: false, + }); + visit_expr(value, &mut before, s); + visit_expr(cond, &mut before, s); + visit_stmt(incr, s); + visit_stmts(body, s); + } Stmt::Forever { body, span: _ } => visit_stmts(body, s), Stmt::Branch { cond, diff --git a/src/visitor/pass2.rs b/src/visitor/pass2.rs index 80e00872..eab998df 100644 --- a/src/visitor/pass2.rs +++ b/src/visitor/pass2.rs @@ -197,6 +197,22 @@ fn visit_stmt(stmt: &mut Stmt, s: S, d: D) { visit_expr(times, s, d, false); visit_stmts(body, s, d, false); } + Stmt::ForEach { times, body, .. } => { + visit_expr(times, s, d, false); + visit_stmts(body, s, d, false); + } + Stmt::For { + value, + cond, + incr, + body, + .. + } => { + visit_expr(value, s, d, false); + visit_expr(cond, s, d, true); + visit_stmt(incr, s, d); + visit_stmts(body, s, d, false); + } Stmt::Forever { body, span: _ } => { visit_stmts(body, s, d, false); }