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:
parent
c8ae433a7c
commit
f0264b124a
7 changed files with 180 additions and 0 deletions
|
|
@ -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)`:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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, "@"),
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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#"
|
||||
|
|
|
|||
|
|
@ -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#"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue