diff --git a/docs/language-guide.md b/docs/language-guide.md index 2b9329c..ff30fbe 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -509,6 +509,37 @@ while i < 10 { } ``` +### Match Statement + +`match` matches a scrutinee against a sequence of patterns and +executes the body of the first matching arm. Each arm's pattern is +compared against the scrutinee with `==`. An underscore arm `_` acts +as the catch-all: + +``` +enum State { Title, Playing, GameOver } +var state: u8 = Title + +on frame { + match state { + Title => { + if button.start { state = Playing } + } + Playing => { + // ... game logic ... + } + GameOver => { + if button.a { state = Title } + } + _ => {} + } +} +``` + +`match` desugars to an `if` / `else if` chain at parse time, so +patterns can be any expression that produces a value comparable to +the scrutinee. + ### For Loop The `for` loop iterates over a half-open integer range `[start, end)`: diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 0d50024..38cb70b 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -179,6 +179,9 @@ impl<'a> Lexer<'a> { if self.peek() == Some(b'=') { self.advance(); Some(self.make_token(TokenKind::Eq, start)) + } else if self.peek() == Some(b'>') { + self.advance(); + Some(self.make_token(TokenKind::FatArrow, start)) } else { Some(self.make_token(TokenKind::Assign, start)) } @@ -443,6 +446,7 @@ impl<'a> Lexer<'a> { "while" => TokenKind::KwWhile, "for" => TokenKind::KwFor, "in" => TokenKind::KwIn, + "match" => TokenKind::KwMatch, "break" => TokenKind::KwBreak, "continue" => TokenKind::KwContinue, "return" => TokenKind::KwReturn, diff --git a/src/lexer/tests.rs b/src/lexer/tests.rs index 9f1d3b2..a436b1f 100644 --- a/src/lexer/tests.rs +++ b/src/lexer/tests.rs @@ -124,6 +124,9 @@ fn lex_all_keywords() { ("const", TokenKind::KwConst), ("enum", TokenKind::KwEnum), ("struct", TokenKind::KwStruct), + ("for", TokenKind::KwFor), + ("in", TokenKind::KwIn), + ("match", TokenKind::KwMatch), ("if", TokenKind::KwIf), ("else", TokenKind::KwElse), ("while", TokenKind::KwWhile), diff --git a/src/lexer/token.rs b/src/lexer/token.rs index 6ecc1a6..d76e82c 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -52,6 +52,7 @@ pub enum TokenKind { KwWhile, KwFor, KwIn, + KwMatch, KwBreak, KwContinue, KwReturn, @@ -99,6 +100,7 @@ pub enum TokenKind { Colon, Semicolon, Arrow, + FatArrow, Dot, DotDot, At, @@ -159,6 +161,7 @@ impl std::fmt::Display for TokenKind { Self::KwWhile => write!(f, "while"), Self::KwFor => write!(f, "for"), Self::KwIn => write!(f, "in"), + Self::KwMatch => write!(f, "match"), Self::KwBreak => write!(f, "break"), Self::KwContinue => write!(f, "continue"), Self::KwReturn => write!(f, "return"), @@ -204,6 +207,7 @@ impl std::fmt::Display for TokenKind { Self::Colon => write!(f, ":"), Self::Semicolon => write!(f, ";"), Self::Arrow => write!(f, "->"), + Self::FatArrow => write!(f, "=>"), Self::Dot => write!(f, "."), Self::DotDot => write!(f, ".."), Self::At => write!(f, "@"), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c0f2eb6..500483a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -850,6 +850,7 @@ impl Parser { TokenKind::KwIf => self.parse_if(), TokenKind::KwWhile => self.parse_while(), TokenKind::KwFor => self.parse_for(), + TokenKind::KwMatch => self.parse_match(), TokenKind::KwLoop => self.parse_loop(), TokenKind::KwBreak => { let span = self.current_span(); @@ -998,6 +999,83 @@ impl Parser { Ok(Statement::Loop(body, start)) } + /// Parse `match expr { pat => { body }, pat => { body }, _ => { body } }`. + /// Desugars to a chain of `if expr == pat { body } else if ...` + /// at parse time — no dedicated AST variant needed. + fn parse_match(&mut self) -> Result { + let start = self.current_span(); + self.expect(&TokenKind::KwMatch)?; + let saved = self.restrict_struct_literals; + self.restrict_struct_literals = true; + let scrutinee = self.parse_expr()?; + self.restrict_struct_literals = saved; + self.expect(&TokenKind::LBrace)?; + + let mut arms: Vec<(Expr, Block)> = Vec::new(); + let mut default: Option = None; + while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof { + // A default arm is `_ => { ... }`. + if let TokenKind::Ident(name) = self.peek().clone() { + if name == "_" { + self.advance(); + self.expect(&TokenKind::FatArrow)?; + let body = self.parse_block()?; + default = Some(body); + if *self.peek() == TokenKind::Comma { + self.advance(); + } + continue; + } + } + let pat_span = self.current_span(); + let pat = self.parse_expr()?; + self.expect(&TokenKind::FatArrow)?; + let body = self.parse_block()?; + // Build `scrutinee == pat` as the branch condition. + let cond = Expr::BinaryOp( + Box::new(scrutinee.clone()), + BinOp::Eq, + Box::new(pat), + pat_span, + ); + arms.push((cond, body)); + if *self.peek() == TokenKind::Comma { + self.advance(); + } + } + self.expect(&TokenKind::RBrace)?; + + if arms.is_empty() { + // `match x { _ => body }` or empty match — emit the + // default block directly, or an empty no-op. + if let Some(body) = default { + return Ok(Statement::If( + Expr::BoolLiteral(true, start), + body, + Vec::new(), + None, + start, + )); + } + return Ok(Statement::If( + Expr::BoolLiteral(false, start), + Block { + statements: Vec::new(), + span: start, + }, + Vec::new(), + None, + start, + )); + } + + // Build an if/else-if chain. The first arm becomes the + // `then` block; subsequent arms become `else if` entries; + // the default arm (if any) becomes the final `else`. + let (first_cond, first_body) = arms.remove(0); + Ok(Statement::If(first_cond, first_body, arms, default, start)) + } + fn parse_for(&mut self) -> Result { let start = self.current_span(); self.expect(&TokenKind::KwFor)?; diff --git a/src/parser/tests.rs b/src/parser/tests.rs index d82cd2a..850399a 100644 --- a/src/parser/tests.rs +++ b/src/parser/tests.rs @@ -665,6 +665,36 @@ fn parse_mmc3_mapper() { assert_eq!(prog.game.mapper, Mapper::MMC3); } +#[test] +fn parse_match_statement() { + let src = r#" + game "Test" { mapper: NROM } + enum State { Title, Playing, GameOver } + var s: u8 = Title + on frame { + match s { + Title => { s = Playing } + Playing => { s = GameOver } + _ => {} + } + wait_frame + } + start Main + "#; + // Match desugars to an If, so after parsing the first statement + // inside the frame handler should be an If with two elifs and an + // else. + let prog = parse_ok(src); + let frame = prog.states[0].on_frame.as_ref().unwrap(); + match &frame.statements[0] { + Statement::If(_, _, elifs, else_block, _) => { + assert_eq!(elifs.len(), 1, "expected 1 else-if, got {elifs:?}"); + assert!(else_block.is_some(), "expected an else block for `_`"); + } + _ => panic!("expected If, got {:?}", frame.statements[0]), + } +} + #[test] fn parse_for_loop() { let src = r#" diff --git a/tests/integration_test.rs b/tests/integration_test.rs index de1fa14..2b9afee 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -228,6 +228,36 @@ fn program_with_for_loop() { rom::validate_ines(&rom_data).expect("should be valid iNES"); } +#[test] +fn program_with_match_statement() { + // Note: the parser doesn't support `;` as a statement separator, + // so each arm body uses newlines between statements. + let source = r#" + game "Match" { mapper: NROM } + enum Mode { Idle, Run, Jump } + var mode: u8 = Idle + var x: u8 = 0 + on frame { + match mode { + Idle => { if button.a { mode = Run } } + Run => { + x += 1 + if button.b { mode = Jump } + } + Jump => { + x += 2 + if button.a { mode = Idle } + } + _ => {} + } + wait_frame + } + start Main + "#; + let rom_data = compile(source); + rom::validate_ines(&rom_data).expect("should be valid iNES"); +} + #[test] fn program_with_struct_literals() { let source = r#"