1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 00:45:38 +00:00

Language: match statement

match state {
        Title => { if button.start { state = Playing } }
        Playing => { /* ... */ }
        GameOver => { if button.a { state = Title } }
        _ => {}
    }

- Lexer: \`match\` keyword and \`=>\` (FatArrow) token
- Parser: \`parse_match\` after the existing loop constructs. Each
  arm is \`pattern => { body }\`, with \`_\` as the catch-all. The
  match scrutinee is parsed with struct-literal restriction enabled
  so the following \`{\` is unambiguously the match body, not a
  struct literal.
- The parser desugars match directly into an if/else-if chain so
  the analyzer, IR lowering, and codegen don't need new AST variants
  — each arm becomes \`scrutinee == pattern\` as the condition, and
  the default arm (if any) becomes the final \`else\` block.

Tests cover parse + full pipeline integration for state-style
dispatch using an enum.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 17:21:00 +00:00
parent c8ae433a7c
commit f0264b124a
No known key found for this signature in database
7 changed files with 180 additions and 0 deletions

View file

@ -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)`:

View file

@ -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,

View file

@ -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),

View file

@ -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, "@"),

View file

@ -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<Statement, Diagnostic> {
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<Block> = 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<Statement, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwFor)?;

View file

@ -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#"

View file

@ -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#"