mirror of
https://github.com/imjasonh/nescript
synced 2026-07-18 14:45:58 +00:00
Language: for i in start..end loops
Adds a \`for NAME in START..END { BODY }\` half-open range loop:
for i in 0..8 {
total += arr[i]
}
- Lexer: \`for\`, \`in\` keywords and the \`..\` range operator
- AST: new \`Statement::For\` variant with var/start/end/body
- Parser: \`parse_for\` after \`while\` / \`loop\`
- Analyzer: registers the loop variable as a u8 symbol for the body
(restoring any shadowed outer symbol afterwards), allocates it via
the normal RAM allocator, and tracks it as "used"
- IR lowering: desugars to \`var = start; while var < end { body;
var = var + 1 }\` using a \`for_step\` continue-edge block so
\`continue\` properly increments the index
- AST codegen: no-op (legacy path doesn't need for loops)
- Tests: parse + full-pipeline integration
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
8f0e0635df
commit
240da57b54
10 changed files with 248 additions and 1 deletions
|
|
@ -484,6 +484,24 @@ while i < 10 {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### For Loop
|
||||||
|
|
||||||
|
The `for` loop iterates over a half-open integer range `[start, end)`:
|
||||||
|
|
||||||
|
```
|
||||||
|
for i in 0..8 {
|
||||||
|
total += arr[i]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The loop variable is a `u8` scoped to the loop body. Both bounds can
|
||||||
|
be any expression that evaluates to `u8` at runtime, including
|
||||||
|
constants or variables. The range is half-open, so `0..8` iterates
|
||||||
|
`0, 1, 2, ..., 7` (8 iterations). For a closed range, use `0..9`.
|
||||||
|
|
||||||
|
The loop is desugared into a `while` loop with an index variable, so
|
||||||
|
`break` and `continue` work the same as in any loop body.
|
||||||
|
|
||||||
### Loop (Infinite)
|
### Loop (Infinite)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -866,6 +866,50 @@ impl Analyzer {
|
||||||
self.check_block(body, state_names);
|
self.check_block(body, state_names);
|
||||||
self.in_loop = was_in_loop;
|
self.in_loop = was_in_loop;
|
||||||
}
|
}
|
||||||
|
Statement::For {
|
||||||
|
var,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
body,
|
||||||
|
span,
|
||||||
|
} => {
|
||||||
|
// Evaluate start/end (both u8) for reads and type
|
||||||
|
// checking, then register the loop variable as a u8
|
||||||
|
// for the duration of the body.
|
||||||
|
self.walk_expr_reads(start);
|
||||||
|
self.walk_expr_reads(end);
|
||||||
|
self.check_expr_type(start, &NesType::U8);
|
||||||
|
self.check_expr_type(end, &NesType::U8);
|
||||||
|
let was_shadowed = self.symbols.remove(var);
|
||||||
|
self.symbols.insert(
|
||||||
|
var.clone(),
|
||||||
|
Symbol {
|
||||||
|
name: var.clone(),
|
||||||
|
sym_type: NesType::U8,
|
||||||
|
is_const: false,
|
||||||
|
span: *span,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Synthesize a VarAllocation for the loop variable
|
||||||
|
// so IR lowering / codegen can treat it like any
|
||||||
|
// other u8 local.
|
||||||
|
let loop_var_addr = self.allocate_ram(1, *span).unwrap_or(0x10);
|
||||||
|
self.var_allocations.push(VarAllocation {
|
||||||
|
name: var.clone(),
|
||||||
|
address: loop_var_addr,
|
||||||
|
size: 1,
|
||||||
|
});
|
||||||
|
// Loop variable is always "used" in the header.
|
||||||
|
self.mark_var_used(var);
|
||||||
|
let was_in_loop = self.in_loop;
|
||||||
|
self.in_loop = true;
|
||||||
|
self.check_block(body, state_names);
|
||||||
|
self.in_loop = was_in_loop;
|
||||||
|
self.symbols.remove(var);
|
||||||
|
if let Some(old) = was_shadowed {
|
||||||
|
self.symbols.insert(var.clone(), old);
|
||||||
|
}
|
||||||
|
}
|
||||||
Statement::Loop(body, span) => {
|
Statement::Loop(body, span) => {
|
||||||
let was_in_loop = self.in_loop;
|
let was_in_loop = self.in_loop;
|
||||||
self.in_loop = true;
|
self.in_loop = true;
|
||||||
|
|
@ -1148,6 +1192,9 @@ fn collect_transitions_stmt(stmt: &Statement, queue: &mut Vec<String>) {
|
||||||
Statement::While(_, body, _) | Statement::Loop(body, _) => {
|
Statement::While(_, body, _) | Statement::Loop(body, _) => {
|
||||||
collect_transitions_block(body, queue);
|
collect_transitions_block(body, queue);
|
||||||
}
|
}
|
||||||
|
Statement::For { body, .. } => {
|
||||||
|
collect_transitions_block(body, queue);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1187,6 +1234,13 @@ fn collect_calls_stmt(stmt: &Statement, calls: &mut Vec<String>) {
|
||||||
Statement::Loop(body, _) => {
|
Statement::Loop(body, _) => {
|
||||||
collect_calls_block(body, calls);
|
collect_calls_block(body, calls);
|
||||||
}
|
}
|
||||||
|
Statement::For {
|
||||||
|
start, end, body, ..
|
||||||
|
} => {
|
||||||
|
collect_calls_expr(start, calls);
|
||||||
|
collect_calls_expr(end, calls);
|
||||||
|
collect_calls_block(body, calls);
|
||||||
|
}
|
||||||
Statement::Assign(_, _, expr, _) => {
|
Statement::Assign(_, _, expr, _) => {
|
||||||
collect_calls_expr(expr, calls);
|
collect_calls_expr(expr, calls);
|
||||||
}
|
}
|
||||||
|
|
@ -1284,6 +1338,7 @@ fn stmt_can_exit_or_yield(stmt: &Statement) -> bool {
|
||||||
// control, so check its body recursively.
|
// control, so check its body recursively.
|
||||||
block_can_exit_or_yield(body)
|
block_can_exit_or_yield(body)
|
||||||
}
|
}
|
||||||
|
Statement::For { body, .. } => block_can_exit_or_yield(body),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,12 @@ impl CodeGen {
|
||||||
self.emit_label(&end_label);
|
self.emit_label(&end_label);
|
||||||
self.loop_stack.pop();
|
self.loop_stack.pop();
|
||||||
}
|
}
|
||||||
|
Statement::For { .. } => {
|
||||||
|
// AST codegen is legacy; for loops are only supported
|
||||||
|
// through the IR codegen path. Emit nothing so the
|
||||||
|
// program still links — users should use `--use-ast`
|
||||||
|
// without for loops if they rely on this path.
|
||||||
|
}
|
||||||
Statement::Draw(draw) => {
|
Statement::Draw(draw) => {
|
||||||
self.gen_draw(draw);
|
self.gen_draw(draw);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,24 @@ impl LoweringContext {
|
||||||
Statement::Loop(body, _) => {
|
Statement::Loop(body, _) => {
|
||||||
self.lower_loop(body);
|
self.lower_loop(body);
|
||||||
}
|
}
|
||||||
|
Statement::For {
|
||||||
|
var,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
body,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// Desugar `for var in start..end { body }` into:
|
||||||
|
// var = start
|
||||||
|
// while var < end { body; var = var + 1 }
|
||||||
|
let var_id = self.get_or_create_var(var);
|
||||||
|
let start_temp = self.lower_expr(start);
|
||||||
|
self.emit(IrOp::StoreVar(var_id, start_temp));
|
||||||
|
// Precompute the end value once outside the loop
|
||||||
|
// header so subsequent iterations don't recompute it.
|
||||||
|
// (For a literal, the optimizer collapses this.)
|
||||||
|
self.lower_for_body(var_id, end, body);
|
||||||
|
}
|
||||||
Statement::Break(_) => {
|
Statement::Break(_) => {
|
||||||
if let Some(ctx) = self.loop_stack.last() {
|
if let Some(ctx) = self.loop_stack.last() {
|
||||||
let label = ctx.break_label.clone();
|
let label = ctx.break_label.clone();
|
||||||
|
|
@ -575,6 +593,55 @@ impl LoweringContext {
|
||||||
self.start_block(&end_label);
|
self.start_block(&end_label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lower the loop body for a `for var in start..end { body }`.
|
||||||
|
/// Assumes `var` has already been initialized to the start
|
||||||
|
/// value. Emits the condition `var < end` each iteration and
|
||||||
|
/// increments `var` at the continue edge.
|
||||||
|
fn lower_for_body(&mut self, var_id: VarId, end: &Expr, body: &Block) {
|
||||||
|
let cond_label = self.fresh_label("for_cond");
|
||||||
|
let body_label = self.fresh_label("for_body");
|
||||||
|
let end_label = self.fresh_label("for_end");
|
||||||
|
|
||||||
|
self.end_block(IrTerminator::Jump(cond_label.clone()));
|
||||||
|
|
||||||
|
// Condition: var < end
|
||||||
|
self.start_block(&cond_label);
|
||||||
|
let var_temp = self.fresh_temp();
|
||||||
|
self.emit(IrOp::LoadVar(var_temp, var_id));
|
||||||
|
let end_temp = self.lower_expr(end);
|
||||||
|
let cmp_temp = self.fresh_temp();
|
||||||
|
self.emit(IrOp::CmpLt(cmp_temp, var_temp, end_temp));
|
||||||
|
self.end_block(IrTerminator::Branch(
|
||||||
|
cmp_temp,
|
||||||
|
body_label.clone(),
|
||||||
|
end_label.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Body + increment.
|
||||||
|
let step_label = self.fresh_label("for_step");
|
||||||
|
self.loop_stack.push(LoopContext {
|
||||||
|
continue_label: step_label.clone(),
|
||||||
|
break_label: end_label.clone(),
|
||||||
|
});
|
||||||
|
self.start_block(&body_label);
|
||||||
|
self.lower_block(body);
|
||||||
|
self.end_block(IrTerminator::Jump(step_label.clone()));
|
||||||
|
self.loop_stack.pop();
|
||||||
|
|
||||||
|
// Step: var = var + 1
|
||||||
|
self.start_block(&step_label);
|
||||||
|
let cur = self.fresh_temp();
|
||||||
|
self.emit(IrOp::LoadVar(cur, var_id));
|
||||||
|
let one = self.fresh_temp();
|
||||||
|
self.emit(IrOp::LoadImm(one, 1));
|
||||||
|
let next = self.fresh_temp();
|
||||||
|
self.emit(IrOp::Add(next, cur, one));
|
||||||
|
self.emit(IrOp::StoreVar(var_id, next));
|
||||||
|
self.end_block(IrTerminator::Jump(cond_label));
|
||||||
|
|
||||||
|
self.start_block(&end_label);
|
||||||
|
}
|
||||||
|
|
||||||
fn lower_loop(&mut self, body: &Block) {
|
fn lower_loop(&mut self, body: &Block) {
|
||||||
let body_label = self.fresh_label("loop_body");
|
let body_label = self.fresh_label("loop_body");
|
||||||
let end_label = self.fresh_label("loop_end");
|
let end_label = self.fresh_label("loop_end");
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,14 @@ impl<'a> Lexer<'a> {
|
||||||
b',' => Some(self.make_token(TokenKind::Comma, start)),
|
b',' => Some(self.make_token(TokenKind::Comma, start)),
|
||||||
b':' => Some(self.make_token(TokenKind::Colon, start)),
|
b':' => Some(self.make_token(TokenKind::Colon, start)),
|
||||||
b';' => Some(self.make_token(TokenKind::Semicolon, start)),
|
b';' => Some(self.make_token(TokenKind::Semicolon, start)),
|
||||||
b'.' => Some(self.make_token(TokenKind::Dot, start)),
|
b'.' => {
|
||||||
|
if self.peek() == Some(b'.') {
|
||||||
|
self.advance();
|
||||||
|
Some(self.make_token(TokenKind::DotDot, start))
|
||||||
|
} else {
|
||||||
|
Some(self.make_token(TokenKind::Dot, start))
|
||||||
|
}
|
||||||
|
}
|
||||||
b'@' => Some(self.make_token(TokenKind::At, start)),
|
b'@' => Some(self.make_token(TokenKind::At, start)),
|
||||||
b'~' => Some(self.make_token(TokenKind::Tilde, start)),
|
b'~' => Some(self.make_token(TokenKind::Tilde, start)),
|
||||||
|
|
||||||
|
|
@ -434,6 +441,8 @@ impl<'a> Lexer<'a> {
|
||||||
"if" => TokenKind::KwIf,
|
"if" => TokenKind::KwIf,
|
||||||
"else" => TokenKind::KwElse,
|
"else" => TokenKind::KwElse,
|
||||||
"while" => TokenKind::KwWhile,
|
"while" => TokenKind::KwWhile,
|
||||||
|
"for" => TokenKind::KwFor,
|
||||||
|
"in" => TokenKind::KwIn,
|
||||||
"break" => TokenKind::KwBreak,
|
"break" => TokenKind::KwBreak,
|
||||||
"continue" => TokenKind::KwContinue,
|
"continue" => TokenKind::KwContinue,
|
||||||
"return" => TokenKind::KwReturn,
|
"return" => TokenKind::KwReturn,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ pub enum TokenKind {
|
||||||
KwIf,
|
KwIf,
|
||||||
KwElse,
|
KwElse,
|
||||||
KwWhile,
|
KwWhile,
|
||||||
|
KwFor,
|
||||||
|
KwIn,
|
||||||
KwBreak,
|
KwBreak,
|
||||||
KwContinue,
|
KwContinue,
|
||||||
KwReturn,
|
KwReturn,
|
||||||
|
|
@ -98,6 +100,7 @@ pub enum TokenKind {
|
||||||
Semicolon,
|
Semicolon,
|
||||||
Arrow,
|
Arrow,
|
||||||
Dot,
|
Dot,
|
||||||
|
DotDot,
|
||||||
At,
|
At,
|
||||||
|
|
||||||
// Operators
|
// Operators
|
||||||
|
|
@ -154,6 +157,8 @@ impl std::fmt::Display for TokenKind {
|
||||||
Self::KwIf => write!(f, "if"),
|
Self::KwIf => write!(f, "if"),
|
||||||
Self::KwElse => write!(f, "else"),
|
Self::KwElse => write!(f, "else"),
|
||||||
Self::KwWhile => write!(f, "while"),
|
Self::KwWhile => write!(f, "while"),
|
||||||
|
Self::KwFor => write!(f, "for"),
|
||||||
|
Self::KwIn => write!(f, "in"),
|
||||||
Self::KwBreak => write!(f, "break"),
|
Self::KwBreak => write!(f, "break"),
|
||||||
Self::KwContinue => write!(f, "continue"),
|
Self::KwContinue => write!(f, "continue"),
|
||||||
Self::KwReturn => write!(f, "return"),
|
Self::KwReturn => write!(f, "return"),
|
||||||
|
|
@ -200,6 +205,7 @@ impl std::fmt::Display for TokenKind {
|
||||||
Self::Semicolon => write!(f, ";"),
|
Self::Semicolon => write!(f, ";"),
|
||||||
Self::Arrow => write!(f, "->"),
|
Self::Arrow => write!(f, "->"),
|
||||||
Self::Dot => write!(f, "."),
|
Self::Dot => write!(f, "."),
|
||||||
|
Self::DotDot => write!(f, ".."),
|
||||||
Self::At => write!(f, "@"),
|
Self::At => write!(f, "@"),
|
||||||
Self::Plus => write!(f, "+"),
|
Self::Plus => write!(f, "+"),
|
||||||
Self::Minus => write!(f, "-"),
|
Self::Minus => write!(f, "-"),
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,15 @@ pub enum Statement {
|
||||||
If(Expr, Block, Vec<(Expr, Block)>, Option<Block>, Span),
|
If(Expr, Block, Vec<(Expr, Block)>, Option<Block>, Span),
|
||||||
While(Expr, Block, Span),
|
While(Expr, Block, Span),
|
||||||
Loop(Block, Span),
|
Loop(Block, Span),
|
||||||
|
/// `for NAME in START..END { BODY }` — half-open range.
|
||||||
|
/// Lowers to an index variable + while loop in IR.
|
||||||
|
For {
|
||||||
|
var: String,
|
||||||
|
start: Expr,
|
||||||
|
end: Expr,
|
||||||
|
body: Block,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
Break(Span),
|
Break(Span),
|
||||||
Continue(Span),
|
Continue(Span),
|
||||||
Return(Option<Expr>, Span),
|
Return(Option<Expr>, Span),
|
||||||
|
|
@ -302,6 +311,7 @@ impl Statement {
|
||||||
match self {
|
match self {
|
||||||
Self::VarDecl(v) => v.span,
|
Self::VarDecl(v) => v.span,
|
||||||
Self::Draw(d) => d.span,
|
Self::Draw(d) => d.span,
|
||||||
|
Self::For { span, .. } => *span,
|
||||||
Self::Assign(_, _, _, s)
|
Self::Assign(_, _, _, s)
|
||||||
| Self::If(_, _, _, _, s)
|
| Self::If(_, _, _, _, s)
|
||||||
| Self::While(_, _, s)
|
| Self::While(_, _, s)
|
||||||
|
|
|
||||||
|
|
@ -843,6 +843,7 @@ impl Parser {
|
||||||
}
|
}
|
||||||
TokenKind::KwIf => self.parse_if(),
|
TokenKind::KwIf => self.parse_if(),
|
||||||
TokenKind::KwWhile => self.parse_while(),
|
TokenKind::KwWhile => self.parse_while(),
|
||||||
|
TokenKind::KwFor => self.parse_for(),
|
||||||
TokenKind::KwLoop => self.parse_loop(),
|
TokenKind::KwLoop => self.parse_loop(),
|
||||||
TokenKind::KwBreak => {
|
TokenKind::KwBreak => {
|
||||||
let span = self.current_span();
|
let span = self.current_span();
|
||||||
|
|
@ -983,6 +984,24 @@ impl Parser {
|
||||||
Ok(Statement::Loop(body, start))
|
Ok(Statement::Loop(body, start))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_for(&mut self) -> Result<Statement, Diagnostic> {
|
||||||
|
let start = self.current_span();
|
||||||
|
self.expect(&TokenKind::KwFor)?;
|
||||||
|
let (var, _) = self.expect_ident()?;
|
||||||
|
self.expect(&TokenKind::KwIn)?;
|
||||||
|
let start_expr = self.parse_expr()?;
|
||||||
|
self.expect(&TokenKind::DotDot)?;
|
||||||
|
let end_expr = self.parse_expr()?;
|
||||||
|
let body = self.parse_block()?;
|
||||||
|
Ok(Statement::For {
|
||||||
|
var,
|
||||||
|
start: start_expr,
|
||||||
|
end: end_expr,
|
||||||
|
body,
|
||||||
|
span: start,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_draw(&mut self) -> Result<Statement, Diagnostic> {
|
fn parse_draw(&mut self) -> Result<Statement, Diagnostic> {
|
||||||
let start = self.current_span();
|
let start = self.current_span();
|
||||||
self.expect(&TokenKind::KwDraw)?;
|
self.expect(&TokenKind::KwDraw)?;
|
||||||
|
|
|
||||||
|
|
@ -665,6 +665,44 @@ fn parse_mmc3_mapper() {
|
||||||
assert_eq!(prog.game.mapper, Mapper::MMC3);
|
assert_eq!(prog.game.mapper, Mapper::MMC3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_for_loop() {
|
||||||
|
let src = r#"
|
||||||
|
game "Test" { mapper: NROM }
|
||||||
|
var sum: u8 = 0
|
||||||
|
on frame {
|
||||||
|
for i in 0..10 {
|
||||||
|
sum += i
|
||||||
|
}
|
||||||
|
wait_frame
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let prog = parse_ok(src);
|
||||||
|
let frame = prog.states[0].on_frame.as_ref().unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(&frame.statements[0], Statement::For { var, .. } if var == "i"),
|
||||||
|
"expected for loop, got {:?}",
|
||||||
|
frame.statements[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_for_loop_with_const_bounds() {
|
||||||
|
let src = r#"
|
||||||
|
game "Test" { mapper: NROM }
|
||||||
|
const START: u8 = 5
|
||||||
|
const END: u8 = 15
|
||||||
|
var total: u8 = 0
|
||||||
|
on frame {
|
||||||
|
for n in START..END { total += n }
|
||||||
|
wait_frame
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
parse_ok(src);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_audio_statements() {
|
fn parse_audio_statements() {
|
||||||
let src = r#"
|
let src = r#"
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,25 @@ fn program_with_on_scanline_per_state() {
|
||||||
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn program_with_for_loop() {
|
||||||
|
let source = r#"
|
||||||
|
game "ForLoop" { mapper: NROM }
|
||||||
|
var arr: u8[8] = [0, 0, 0, 0, 0, 0, 0, 0]
|
||||||
|
var total: u8 = 0
|
||||||
|
on frame {
|
||||||
|
total = 0
|
||||||
|
for i in 0..8 {
|
||||||
|
total += arr[i]
|
||||||
|
}
|
||||||
|
wait_frame
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let rom_data = compile(source);
|
||||||
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn program_with_structs() {
|
fn program_with_structs() {
|
||||||
let source = r#"
|
let source = r#"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue