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 {
{width="200"}
+## for each loop
+
+```goboscript
+for x in n {
+ # code
+}
+```
+
+{width="200"}
+
+## for loop
+
+```goboscript
+for x = 0; x > n; x++ {
+ # code
+}
+```
+
+{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);
}