diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index 60aa56f0..f34ba59a 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -38,6 +38,12 @@ pub enum Stmt { cond: Box, body: Vec, }, + For { + init: Box, + cond: Box, + inc: Box, + body: Vec, + }, SetVar { name: Name, value: Box, @@ -101,6 +107,7 @@ impl Stmt { Stmt::Forever { span, .. } => span.clone(), Stmt::Branch { cond, .. } => cond.span(), Stmt::Until { cond, .. } => cond.span(), + Stmt::For { init, .. } => init.span(), Stmt::SetVar { name, .. } => name.span(), Stmt::ChangeVar { name, .. } => name.span(), Stmt::Show(name) => name.span(), diff --git a/src/codegen/sb3.rs b/src/codegen/sb3.rs index b552946c..8463e6c2 100644 --- a/src/codegen/sb3.rs +++ b/src/codegen/sb3.rs @@ -277,6 +277,7 @@ impl Stmt { } } Stmt::Until { .. } => "control_repeat_until", + Stmt::For { .. } => "control_repeat_until", Stmt::SetVar { .. } => "data_setvariableto", Stmt::ChangeVar { .. } => "data_changevariableby", Stmt::Show(name) => { @@ -1254,6 +1255,18 @@ where T: Write + Seek else_body, } => self.branch(s, d, this_id, cond, if_body, else_body), Stmt::Until { cond, body } => self.until(s, d, this_id, cond, body), + Stmt::For { + init, + cond, + inc, + body, + } => { + let init_id = self.id.new_id(); + self.stmt(s, d, init, init_id, None, Some(this_id))?; + let mut body = body.clone(); + body.push(*inc.clone()); + self.until(s, d, this_id, cond, &body) + } Stmt::SetVar { name, value, diff --git a/src/lexer/token.rs b/src/lexer/token.rs index cbea1a53..3e032901 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -82,6 +82,8 @@ pub enum Token { Forever, #[token("repeat")] Repeat, + #[token("for")] + For, #[token(",")] Comma, #[token("(")] @@ -276,6 +278,7 @@ impl Display for Token { Token::Until => write!(f, "until"), Token::Forever => write!(f, "forever"), Token::Repeat => write!(f, "repeat"), + Token::For => write!(f, "for"), Token::Comma => write!(f, ","), Token::LParen => write!(f, "("), Token::RParen => write!(f, ")"), diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop index 85a0a39d..365b1600 100644 --- a/src/parser/grammar.lalrpop +++ b/src/parser/grammar.lalrpop @@ -113,6 +113,29 @@ Sound: () = { Stmts: Vec = "{" <( ";"*)*> "}"; +ForInit: Stmt = { + "=" => { + Stmt::SetVar { + name: Name::Name { name, span: l..r }, + value, + type_, + is_local: false, + is_cloud: false, + } + }, + "++" => Stmt::increment(Name::Name { name, span: l..r }), + "--" => Stmt::decrement(Name::Name { name, span: l..r }), + "+=" => Stmt::augmented_assign(BinOp::Add, name, l..r, *value), + "-=" => Stmt::augmented_assign(BinOp::Sub, name, l..r, *value), + "*=" => Stmt::augmented_assign(BinOp::Mul, name, l..r, *value), + "/=" => Stmt::augmented_assign(BinOp::Div, name, l..r, *value), + "//=" => Stmt::augmented_assign(BinOp::FloorDiv, name, l..r, *value), + "%=" => Stmt::augmented_assign(BinOp::Mod, name, l..r, *value), + "&=" => Stmt::augmented_assign(BinOp::Join, name, l..r, *value), +}; + +ForInc: Stmt = ForInit; + Stmt: Stmt = { RETURN ";" => Stmt::Return { value, visited: false }, IF => { @@ -127,6 +150,14 @@ Stmt: Stmt = { REPEAT => Stmt::Repeat { times, body }, FOREVER => Stmt::Forever { body, span: l..r }, UNTIL => Stmt::Until { cond, body }, + FOR "(" ";" ";" ")" => { + Stmt::For { + init: Box::new(init), + cond, + inc: Box::new(inc), + body, + } + }, "=" ";" => { Stmt::SetVar { name: Name::Name { name, span: l..r }, @@ -618,6 +649,7 @@ extern { UNTIL => Token::Until, FOREVER => Token::Forever, REPEAT => Token::Repeat, + FOR => Token::For, "," => Token::Comma, "(" => Token::LParen, ")" => Token::RParen, diff --git a/src/visitor/pass0.rs b/src/visitor/pass0.rs index 2e7ce5af..7ff5849f 100644 --- a/src/visitor/pass0.rs +++ b/src/visitor/pass0.rs @@ -1,236 +1,243 @@ -use std::path::Path; - -use fxhash::FxHashMap; -use glob::glob; - -use crate::{ - ast::*, - misc::SmolStr, -}; - -struct V<'a> { - locals: Option<&'a mut FxHashMap>, - vars: &'a mut FxHashMap, - global_vars: Option<&'a mut FxHashMap>, -} - -pub fn visit_project(input: &Path, project: &mut Project) { - visit_sprite(input, &mut project.stage, None); - for sprite in project.sprites.values_mut() { - visit_sprite(input, sprite, Some(&mut project.stage)); - } -} - -fn visit_sprite(input: &Path, sprite: &mut Sprite, mut stage: Option<&mut Sprite>) { - visit_costumes(input, &mut sprite.costumes); - visit_sounds(input, &mut sprite.sounds); - for enum_ in sprite.enums.values_mut() { - visit_enum(enum_); - } - for proc in sprite.procs.values_mut() { - sprite - .proc_locals - .insert(proc.name.clone(), Default::default()); - let proc_definition = sprite.proc_definitions.get_mut(&proc.name).unwrap(); - visit_stmts( - proc_definition, - &mut V { - locals: sprite.proc_locals.get_mut(&proc.name), - vars: &mut sprite.vars, - global_vars: stage.as_mut().map(|stage| &mut stage.vars), - }, - ); - } - for func in sprite.funcs.values_mut() { - sprite - .func_locals - .insert(func.name.clone(), Default::default()); - let name: SmolStr = format!("{}:return", func.name).into(); - if !sprite.vars.contains_key(&name) { - sprite.vars.insert( - name.clone(), - Var { - name, - span: func.span.clone(), - type_: func.type_.clone(), - default: None, - is_cloud: false, - is_used: true, - }, - ); - } - let func_definition = sprite.func_definitions.get_mut(&func.name).unwrap(); - visit_stmts( - func_definition, - &mut V { - locals: sprite.func_locals.get_mut(&func.name), - vars: &mut sprite.vars, - global_vars: stage.as_mut().map(|stage| &mut stage.vars), - }, - ); - } - for event in &mut sprite.events { - visit_stmts( - &mut event.body, - &mut V { - locals: None, - vars: &mut sprite.vars, - global_vars: stage.as_mut().map(|stage| &mut stage.vars), - }, - ); - } -} - -fn visit_enum(enum_: &mut Enum) { - let mut index = 0.0; - for variant in &mut enum_.variants { - if let Some((value, _)) = &variant.value { - if let Value::Number(number) = value { - index = *number; - } - } else { - variant.value = Some((Value::Number(index), variant.span.clone())); - index += 1.0; - } - } -} - -fn visit_costumes(input: &Path, new: &mut Vec) { - let old: Vec = std::mem::take(new); - for costume in old { - if let Some(suffix) = costume.name.strip_prefix("@ascii/") { - new.extend((' '..='~').map(|ch| Costume { - name: format!("{suffix}{ch}").into(), - path: costume.path.clone(), - span: costume.span.clone(), - })); - } else if costume.path.contains('*') { - let mut costumes: Vec = glob(input.join(&*costume.path).to_str().unwrap()) - .unwrap() - .map(Result::unwrap) - .map(|path| Costume { - name: path.file_stem().unwrap().to_string_lossy().into(), - path: path.to_string_lossy().into(), - span: costume.span.clone(), - }) - .collect(); - costumes.sort_by(|a, b| a.name.cmp(&b.name)); - new.extend(costumes); - } else { - new.push(costume); - } - } -} - -fn visit_sounds(input: &Path, new: &mut Vec) { - let old: Vec = std::mem::take(new); - for sound in old { - if let Some(suffix) = sound.name.strip_prefix("@ascii/") { - new.extend((' '..='~').map(|ch| Sound { - name: format!("{suffix}{ch}").into(), - path: sound.path.clone(), - span: sound.span.clone(), - })); - } else if sound.path.contains('*') { - let mut sounds: Vec = glob(input.join(&*sound.path).to_str().unwrap()) - .unwrap() - .map(Result::unwrap) - .map(|path| Sound { - name: path.file_stem().unwrap().to_string_lossy().into(), - path: path.to_string_lossy().into(), - span: sound.span.clone(), - }) - .collect(); - sounds.sort_by(|a, b| a.name.cmp(&b.name)); - new.extend(sounds); - } else { - new.push(sound); - } - } -} - -fn visit_stmts(stmts: &mut Vec, v: &mut V) { - for stmt in stmts { - visit_stmt(stmt, v); - } -} - -fn visit_stmt(stmt: &mut Stmt, v: &mut V) { - match stmt { - Stmt::Repeat { body, .. } => visit_stmts(body, v), - Stmt::Forever { body, .. } => visit_stmts(body, v), - Stmt::Branch { - if_body, else_body, .. - } => { - visit_stmts(if_body, v); - visit_stmts(else_body, v) - } - Stmt::Until { body, .. } => visit_stmts(body, v), - Stmt::SetVar { - name, - type_, - is_local, - is_cloud, - .. - } => { - let basename = name.basename(); - let var = Var { - name: basename.clone(), - span: name.span(), - type_: type_.clone(), - default: v.vars.get(basename).and_then(|var| var.default.clone()), - is_cloud: *is_cloud, - is_used: false, - }; - if *is_local { - if let Some(locals) = &mut v.locals { - if let Some(existing_declaration) = locals.get(basename) { - if existing_declaration.type_.is_value() { - locals.insert(basename.clone(), var); - } - } else { - locals.insert(basename.clone(), var); - } - } - return; - } - if v.locals - .as_ref() - .is_some_and(|locals| locals.contains_key(basename)) - { - return; - } - if v.global_vars - .as_ref() - .is_some_and(|global_vars| global_vars.contains_key(basename)) - { - return; - } - //if let Some(existing_declaration) = v.vars.get(basename) { - // This condition ensures that variables with a specific type (e.g., a struct type) are not overwritten - // by a previous statement that didn't specify a type (which defaults to type `Value`). - // In this context, variables don't need to be explicitly declared if the type is `Value`. - // The syntax for setting variables is as follows: - // - For `Value` type: `variable_name = value;` - // - For a specific struct type: `typeName variable_name = value;` - // - // Since the visitor processes every variable assignment statement, this check ensures that if an - // existing variable has a specific type (not `Value`), it is preserved when a new statement tries to - // reassign it without a type (defaulting to `Value`). Only variables that are of type `Value` can be - // overwritten by the new assignment. - - // TODO: Make redeclaration of variables with different struct types an error. - // if existing_declaration.type_.is_value() { - // v.vars.insert(basename.clone(), var); - // } - if !v.vars.contains_key(basename) - && v.global_vars - .as_ref() - .is_some_and(|vars| !vars.contains_key(basename)) - { - v.vars.insert(basename.clone(), var); - } - } - _ => (), - } -} +use std::path::Path; + +use fxhash::FxHashMap; +use glob::glob; + +use crate::{ + ast::*, + misc::SmolStr, +}; + +struct V<'a> { + locals: Option<&'a mut FxHashMap>, + vars: &'a mut FxHashMap, + global_vars: Option<&'a mut FxHashMap>, +} + +pub fn visit_project(input: &Path, project: &mut Project) { + visit_sprite(input, &mut project.stage, None); + for sprite in project.sprites.values_mut() { + visit_sprite(input, sprite, Some(&mut project.stage)); + } +} + +fn visit_sprite(input: &Path, sprite: &mut Sprite, mut stage: Option<&mut Sprite>) { + visit_costumes(input, &mut sprite.costumes); + visit_sounds(input, &mut sprite.sounds); + for enum_ in sprite.enums.values_mut() { + visit_enum(enum_); + } + for proc in sprite.procs.values_mut() { + sprite + .proc_locals + .insert(proc.name.clone(), Default::default()); + let proc_definition = sprite.proc_definitions.get_mut(&proc.name).unwrap(); + visit_stmts( + proc_definition, + &mut V { + locals: sprite.proc_locals.get_mut(&proc.name), + vars: &mut sprite.vars, + global_vars: stage.as_mut().map(|stage| &mut stage.vars), + }, + ); + } + for func in sprite.funcs.values_mut() { + sprite + .func_locals + .insert(func.name.clone(), Default::default()); + let name: SmolStr = format!("{}:return", func.name).into(); + if !sprite.vars.contains_key(&name) { + sprite.vars.insert( + name.clone(), + Var { + name, + span: func.span.clone(), + type_: func.type_.clone(), + default: None, + is_cloud: false, + is_used: true, + }, + ); + } + let func_definition = sprite.func_definitions.get_mut(&func.name).unwrap(); + visit_stmts( + func_definition, + &mut V { + locals: sprite.func_locals.get_mut(&func.name), + vars: &mut sprite.vars, + global_vars: stage.as_mut().map(|stage| &mut stage.vars), + }, + ); + } + for event in &mut sprite.events { + visit_stmts( + &mut event.body, + &mut V { + locals: None, + vars: &mut sprite.vars, + global_vars: stage.as_mut().map(|stage| &mut stage.vars), + }, + ); + } +} + +fn visit_enum(enum_: &mut Enum) { + let mut index = 0.0; + for variant in &mut enum_.variants { + if let Some((value, _)) = &variant.value { + if let Value::Number(number) = value { + index = *number; + } + } else { + variant.value = Some((Value::Number(index), variant.span.clone())); + index += 1.0; + } + } +} + +fn visit_costumes(input: &Path, new: &mut Vec) { + let old: Vec = std::mem::take(new); + for costume in old { + if let Some(suffix) = costume.name.strip_prefix("@ascii/") { + new.extend((' '..='~').map(|ch| Costume { + name: format!("{suffix}{ch}").into(), + path: costume.path.clone(), + span: costume.span.clone(), + })); + } else if costume.path.contains('*') { + let mut costumes: Vec = glob(input.join(&*costume.path).to_str().unwrap()) + .unwrap() + .map(Result::unwrap) + .map(|path| Costume { + name: path.file_stem().unwrap().to_string_lossy().into(), + path: path.to_string_lossy().into(), + span: costume.span.clone(), + }) + .collect(); + costumes.sort_by(|a, b| a.name.cmp(&b.name)); + new.extend(costumes); + } else { + new.push(costume); + } + } +} + +fn visit_sounds(input: &Path, new: &mut Vec) { + let old: Vec = std::mem::take(new); + for sound in old { + if let Some(suffix) = sound.name.strip_prefix("@ascii/") { + new.extend((' '..='~').map(|ch| Sound { + name: format!("{suffix}{ch}").into(), + path: sound.path.clone(), + span: sound.span.clone(), + })); + } else if sound.path.contains('*') { + let mut sounds: Vec = glob(input.join(&*sound.path).to_str().unwrap()) + .unwrap() + .map(Result::unwrap) + .map(|path| Sound { + name: path.file_stem().unwrap().to_string_lossy().into(), + path: path.to_string_lossy().into(), + span: sound.span.clone(), + }) + .collect(); + sounds.sort_by(|a, b| a.name.cmp(&b.name)); + new.extend(sounds); + } else { + new.push(sound); + } + } +} + +fn visit_stmts(stmts: &mut Vec, v: &mut V) { + for stmt in stmts { + visit_stmt(stmt, v); + } +} + +fn visit_stmt(stmt: &mut Stmt, v: &mut V) { + match stmt { + Stmt::Repeat { body, .. } => visit_stmts(body, v), + Stmt::Forever { body, .. } => visit_stmts(body, v), + Stmt::Branch { + if_body, else_body, .. + } => { + visit_stmts(if_body, v); + visit_stmts(else_body, v) + } + Stmt::Until { body, .. } => visit_stmts(body, v), + Stmt::For { + init, inc, body, .. + } => { + visit_stmt(init, v); + visit_stmts(body, v); + visit_stmt(inc, v); + } + Stmt::SetVar { + name, + type_, + is_local, + is_cloud, + .. + } => { + let basename = name.basename(); + let var = Var { + name: basename.clone(), + span: name.span(), + type_: type_.clone(), + default: v.vars.get(basename).and_then(|var| var.default.clone()), + is_cloud: *is_cloud, + is_used: false, + }; + if *is_local { + if let Some(locals) = &mut v.locals { + if let Some(existing_declaration) = locals.get(basename) { + if existing_declaration.type_.is_value() { + locals.insert(basename.clone(), var); + } + } else { + locals.insert(basename.clone(), var); + } + } + return; + } + if v.locals + .as_ref() + .is_some_and(|locals| locals.contains_key(basename)) + { + return; + } + if v.global_vars + .as_ref() + .is_some_and(|global_vars| global_vars.contains_key(basename)) + { + return; + } + //if let Some(existing_declaration) = v.vars.get(basename) { + // This condition ensures that variables with a specific type (e.g., a struct type) are not overwritten + // by a previous statement that didn't specify a type (which defaults to type `Value`). + // In this context, variables don't need to be explicitly declared if the type is `Value`. + // The syntax for setting variables is as follows: + // - For `Value` type: `variable_name = value;` + // - For a specific struct type: `typeName variable_name = value;` + // + // Since the visitor processes every variable assignment statement, this check ensures that if an + // existing variable has a specific type (not `Value`), it is preserved when a new statement tries to + // reassign it without a type (defaulting to `Value`). Only variables that are of type `Value` can be + // overwritten by the new assignment. + + // TODO: Make redeclaration of variables with different struct types an error. + // if existing_declaration.type_.is_value() { + // v.vars.insert(basename.clone(), var); + // } + if !v.vars.contains_key(basename) + && v.global_vars + .as_ref() + .is_some_and(|vars| !vars.contains_key(basename)) + { + v.vars.insert(basename.clone(), var); + } + } + _ => (), + } +} diff --git a/src/visitor/pass1.rs b/src/visitor/pass1.rs index 42cc9983..cca9d755 100644 --- a/src/visitor/pass1.rs +++ b/src/visitor/pass1.rs @@ -99,6 +99,18 @@ fn visit_stmt(stmt: &mut Stmt, s: &mut S) -> Vec { before.extend(cond_callsites.iter().cloned()); body.extend(cond_callsites); } + Stmt::For { + init, + cond, + inc: _, + body, + } => { + visit_stmt(init, s); + let mut cond_callsites = vec![]; + visit_expr(cond, &mut cond_callsites, s); + visit_stmts(body, s); + before.extend(cond_callsites.iter().cloned()); + } Stmt::SetVar { name: _, value, diff --git a/src/visitor/pass2.rs b/src/visitor/pass2.rs index 468f681c..cc19306c 100644 --- a/src/visitor/pass2.rs +++ b/src/visitor/pass2.rs @@ -229,6 +229,16 @@ fn visit_stmt(stmt: &mut Stmt, s: S, d: D) { visit_expr(cond, s, d); visit_stmts(body, s, d, false); } + Stmt::For { + init, + cond, + inc: _, + body, + } => { + visit_stmt(init, s, d); + visit_expr(cond, s, d); + visit_stmts(body, s, d, false); + } Stmt::SetVar { name: _, value, diff --git a/src/visitor/pass3.rs b/src/visitor/pass3.rs index a436fc15..e622ea19 100644 --- a/src/visitor/pass3.rs +++ b/src/visitor/pass3.rs @@ -76,6 +76,16 @@ fn visit_stmt(stmt: &Stmt, s: &mut S) { visit_expr(cond, s); visit_stmts(body, s); } + Stmt::For { + init, + cond, + inc: _, + body, + } => { + visit_stmt(init, s); + visit_expr(cond, s); + visit_stmts(body, s); + } Stmt::SetVar { name: _, value,