mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 01:16:12 +00:00
Language: struct literals
struct Vec2 { x: u8, y: u8 }
var pos: Vec2 = Vec2 { x: 100, y: 50 }
on frame {
pos = Vec2 { x: pos.x + 1, y: pos.y }
}
- AST: new \`Expr::StructLiteral(name, fields, span)\` variant
- Parser: in expression position, \`Ident {\` enters struct-literal
mode when the new \`restrict_struct_literals\` flag is off.
\`if\`/\`while\`/\`for\` conditions set the flag so the \`{\` keeps
going to the following block. Condition contexts can still use
struct literals by parenthesizing them.
- Analyzer: validates that the struct type exists, each named field
belongs to it, and each field value has a compatible type.
- IR lowering: desugars \`var = StructLiteral { ... }\` (both in
assignments and variable initializers) into per-field StoreVar
operations against the analyzer-synthesized \`var.field\`
variables. No IR type for struct values is needed.
- AST codegen: no-op (legacy path).
- examples/structs_enums_for.ne now uses a struct literal for the
initial \`player\` state instead of per-field assignments.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
f17f1e7267
commit
c8ae433a7c
9 changed files with 221 additions and 13 deletions
|
|
@ -187,6 +187,31 @@ Fields are laid out contiguously in declaration order. A variable of
|
|||
struct type allocates enough contiguous bytes to hold all its fields;
|
||||
each field is accessible via the dot operator.
|
||||
|
||||
Struct literals initialize or assign all fields at once:
|
||||
|
||||
```
|
||||
struct Vec2 { x: u8, y: u8 }
|
||||
|
||||
// as an initializer
|
||||
var pos: Vec2 = Vec2 { x: 100, y: 50 }
|
||||
|
||||
// as an assignment
|
||||
on frame {
|
||||
pos = Vec2 { x: 0, y: 0 }
|
||||
if button.right {
|
||||
pos = Vec2 { x: pos.x + 1, y: pos.y }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Inside `if`, `while`, and `for` conditions the struct literal syntax
|
||||
is reserved for the following block, so wrap the literal in parens if
|
||||
you ever need one in a condition:
|
||||
|
||||
```
|
||||
if pos == (Vec2 { x: 0, y: 0 }) { /* ... */ }
|
||||
```
|
||||
|
||||
In v0.1 only primitive field types (`u8`, `i8`, `bool`) are supported —
|
||||
nested structs, `u16`, and array fields are not yet allowed.
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,16 @@ struct Player {
|
|||
alive: bool,
|
||||
}
|
||||
|
||||
var player: Player
|
||||
// Struct literal initializer in declaration.
|
||||
var player: Player = Player {
|
||||
x: 120,
|
||||
y: 112,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
facing: Down,
|
||||
frame: Idle,
|
||||
alive: true,
|
||||
}
|
||||
|
||||
// A small fixed-size array of enemy x-positions. In a real game this
|
||||
// would be an array of structs once those are supported.
|
||||
|
|
@ -38,15 +47,6 @@ var enemy_y: u8 = 100
|
|||
const SPEED: u8 = 1
|
||||
|
||||
on frame {
|
||||
// Initialize the player once on the very first frame.
|
||||
if player.alive == false {
|
||||
player.x = 120
|
||||
player.y = 112
|
||||
player.facing = Down
|
||||
player.frame = Idle
|
||||
player.alive = true
|
||||
}
|
||||
|
||||
// Read controls and update position. Velocities are u8 so we
|
||||
// treat them as signed by adding/subtracting SPEED.
|
||||
if button.left {
|
||||
|
|
|
|||
|
|
@ -364,6 +364,34 @@ impl Analyzer {
|
|||
self.walk_expr_reads(e);
|
||||
}
|
||||
}
|
||||
Expr::StructLiteral(name, fields, span) => {
|
||||
// Validate that the struct type exists and that each
|
||||
// named field is actually declared. Missing or extra
|
||||
// fields are an error; duplicate fields are silently
|
||||
// ignored (last-writer-wins).
|
||||
if let Some(layout) = self.struct_layouts.get(name).cloned() {
|
||||
for (fname, fexpr) in fields {
|
||||
if let Some((_, field_type, _)) =
|
||||
layout.fields.iter().find(|(n, _, _)| n == fname)
|
||||
{
|
||||
self.walk_expr_reads(fexpr);
|
||||
self.check_expr_type(fexpr, field_type);
|
||||
} else {
|
||||
self.diagnostics.push(Diagnostic::error(
|
||||
ErrorCode::E0201,
|
||||
format!("struct '{name}' has no field '{fname}'"),
|
||||
*span,
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.diagnostics.push(Diagnostic::error(
|
||||
ErrorCode::E0201,
|
||||
format!("unknown struct type '{name}'"),
|
||||
*span,
|
||||
));
|
||||
}
|
||||
}
|
||||
Expr::IntLiteral(_, _) | Expr::BoolLiteral(_, _) | Expr::ButtonRead(_, _, _) => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1149,6 +1177,7 @@ impl Analyzer {
|
|||
}
|
||||
Expr::ArrayLiteral(_, _) => Some(NesType::U8), // element type inferred from context
|
||||
Expr::Cast(_, target, _) => Some(target.clone()),
|
||||
Expr::StructLiteral(name, _, _) => Some(NesType::Struct(name.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1366,6 +1395,11 @@ fn collect_calls_expr(expr: &Expr, calls: &mut Vec<String>) {
|
|||
collect_calls_expr(e, calls);
|
||||
}
|
||||
}
|
||||
Expr::StructLiteral(_, fields, _) => {
|
||||
for (_, e) in fields {
|
||||
collect_calls_expr(e, calls);
|
||||
}
|
||||
}
|
||||
Expr::Cast(inner, _, _) => {
|
||||
collect_calls_expr(inner, calls);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -829,6 +829,11 @@ impl CodeGen {
|
|||
Expr::ArrayLiteral(_, _) => {
|
||||
// Array literals are handled at initialization time
|
||||
}
|
||||
Expr::StructLiteral(_, _, _) => {
|
||||
// Struct literals only appear in assignments on the
|
||||
// IR codegen path. Legacy AST codegen treats them as
|
||||
// no-ops.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -321,8 +321,19 @@ impl LoweringContext {
|
|||
});
|
||||
}
|
||||
if let Some(init) = &var.init {
|
||||
let val = self.lower_expr(init);
|
||||
self.emit(IrOp::StoreVar(var_id, val));
|
||||
// Struct literal initializers expand to per-field
|
||||
// stores on the synthetic field variables.
|
||||
if let Expr::StructLiteral(_, fields, _) = init {
|
||||
for (fname, fexpr) in fields {
|
||||
let full = format!("{}.{fname}", var.name);
|
||||
let fvid = self.get_or_create_var(&full);
|
||||
let val = self.lower_expr(fexpr);
|
||||
self.emit(IrOp::StoreVar(fvid, val));
|
||||
}
|
||||
} else {
|
||||
let val = self.lower_expr(init);
|
||||
self.emit(IrOp::StoreVar(var_id, val));
|
||||
}
|
||||
}
|
||||
}
|
||||
Statement::Assign(lvalue, op, expr, _) => {
|
||||
|
|
@ -425,6 +436,21 @@ impl LoweringContext {
|
|||
}
|
||||
|
||||
fn lower_assign(&mut self, lvalue: &LValue, op: AssignOp, expr: &Expr) {
|
||||
// Special case: `var = StructLiteral { ... }` expands to
|
||||
// per-field stores against the analyzer-synthesized field
|
||||
// variables. This avoids needing struct values as IR temps.
|
||||
if let (LValue::Var(name), AssignOp::Assign, Expr::StructLiteral(_, fields, _)) =
|
||||
(lvalue, op, expr)
|
||||
{
|
||||
for (fname, fexpr) in fields {
|
||||
let full = format!("{name}.{fname}");
|
||||
let field_var = self.get_or_create_var(&full);
|
||||
let val = self.lower_expr(fexpr);
|
||||
self.emit(IrOp::StoreVar(field_var, val));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match lvalue {
|
||||
LValue::Var(name) => {
|
||||
let var_id = self.get_or_create_var(name);
|
||||
|
|
@ -757,6 +783,16 @@ impl LoweringContext {
|
|||
self.emit(IrOp::LoadImm(t, 0));
|
||||
t
|
||||
}
|
||||
Expr::StructLiteral(_, _, _) => {
|
||||
// Struct literals are only supported as the right
|
||||
// hand side of a plain assignment (see lower_assign).
|
||||
// Falling through here means the literal was used in
|
||||
// an expression context the lowering can't handle;
|
||||
// emit zero so the build still produces a ROM.
|
||||
let t = self.fresh_temp();
|
||||
self.emit(IrOp::LoadImm(t, 0));
|
||||
t
|
||||
}
|
||||
Expr::Cast(inner, _, _) => {
|
||||
// For now, just evaluate the inner expression (truncation/extension is a no-op on 8-bit)
|
||||
self.lower_expr(inner)
|
||||
|
|
|
|||
|
|
@ -205,6 +205,11 @@ pub enum Expr {
|
|||
ButtonRead(Option<Player>, String, Span),
|
||||
ArrayLiteral(Vec<Expr>, Span),
|
||||
Cast(Box<Expr>, NesType, Span),
|
||||
/// Struct literal: `Name { field1: expr, field2: expr, ... }`.
|
||||
/// Only allowed in non-condition expression positions — the
|
||||
/// parser bans them inside `if`/`while`/`for` conditions to
|
||||
/// avoid ambiguity with the following block.
|
||||
StructLiteral(String, Vec<(String, Expr)>, Span),
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
|
|
@ -220,7 +225,8 @@ impl Expr {
|
|||
| Self::Call(_, _, s)
|
||||
| Self::ButtonRead(_, _, s)
|
||||
| Self::ArrayLiteral(_, s)
|
||||
| Self::Cast(_, _, s) => *s,
|
||||
| Self::Cast(_, _, s)
|
||||
| Self::StructLiteral(_, _, s) => *s,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ pub struct Parser {
|
|||
tokens: Vec<Token>,
|
||||
pos: usize,
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
/// When true, `parse_primary` refuses to consume an `Ident {`
|
||||
/// pattern as a struct literal — the `{` is reserved for the
|
||||
/// following `if` / `while` / `for` block. Struct literals in
|
||||
/// conditions must be parenthesized: `if x == (Foo { a: 1 })`.
|
||||
restrict_struct_literals: bool,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
|
|
@ -21,6 +26,7 @@ impl Parser {
|
|||
tokens,
|
||||
pos: 0,
|
||||
diagnostics: Vec::new(),
|
||||
restrict_struct_literals: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -945,7 +951,10 @@ impl Parser {
|
|||
fn parse_if(&mut self) -> Result<Statement, Diagnostic> {
|
||||
let start = self.current_span();
|
||||
self.expect(&TokenKind::KwIf)?;
|
||||
let saved = self.restrict_struct_literals;
|
||||
self.restrict_struct_literals = true;
|
||||
let condition = self.parse_expr()?;
|
||||
self.restrict_struct_literals = saved;
|
||||
let then_block = self.parse_block()?;
|
||||
|
||||
let mut else_ifs = Vec::new();
|
||||
|
|
@ -955,7 +964,9 @@ impl Parser {
|
|||
self.advance();
|
||||
if *self.peek() == TokenKind::KwIf {
|
||||
self.advance();
|
||||
self.restrict_struct_literals = true;
|
||||
let cond = self.parse_expr()?;
|
||||
self.restrict_struct_literals = saved;
|
||||
let block = self.parse_block()?;
|
||||
else_ifs.push((cond, block));
|
||||
} else {
|
||||
|
|
@ -972,7 +983,10 @@ impl Parser {
|
|||
fn parse_while(&mut self) -> Result<Statement, Diagnostic> {
|
||||
let start = self.current_span();
|
||||
self.expect(&TokenKind::KwWhile)?;
|
||||
let saved = self.restrict_struct_literals;
|
||||
self.restrict_struct_literals = true;
|
||||
let condition = self.parse_expr()?;
|
||||
self.restrict_struct_literals = saved;
|
||||
let body = self.parse_block()?;
|
||||
Ok(Statement::While(condition, body, start))
|
||||
}
|
||||
|
|
@ -989,9 +1003,12 @@ impl Parser {
|
|||
self.expect(&TokenKind::KwFor)?;
|
||||
let (var, _) = self.expect_ident()?;
|
||||
self.expect(&TokenKind::KwIn)?;
|
||||
let saved = self.restrict_struct_literals;
|
||||
self.restrict_struct_literals = true;
|
||||
let start_expr = self.parse_expr()?;
|
||||
self.expect(&TokenKind::DotDot)?;
|
||||
let end_expr = self.parse_expr()?;
|
||||
self.restrict_struct_literals = saved;
|
||||
let body = self.parse_block()?;
|
||||
Ok(Statement::For {
|
||||
var,
|
||||
|
|
@ -1565,6 +1582,38 @@ impl Parser {
|
|||
return Ok(Expr::FieldAccess(name, field, span));
|
||||
}
|
||||
|
||||
// Check for struct literal: `Name { field: expr, ... }`.
|
||||
// Disabled in condition contexts to keep parsing
|
||||
// unambiguous for `if`/`while`/`for`.
|
||||
if !self.restrict_struct_literals && *self.peek() == TokenKind::LBrace {
|
||||
self.advance();
|
||||
let mut fields = Vec::new();
|
||||
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
|
||||
let (field_name, _) = self.expect_ident()?;
|
||||
self.expect(&TokenKind::Colon)?;
|
||||
// Struct literal field values can contain
|
||||
// their own nested struct literals, so we
|
||||
// temporarily allow them regardless of the
|
||||
// outer restriction.
|
||||
let saved = self.restrict_struct_literals;
|
||||
self.restrict_struct_literals = false;
|
||||
let value = self.parse_expr()?;
|
||||
self.restrict_struct_literals = saved;
|
||||
fields.push((field_name, value));
|
||||
if *self.peek() == TokenKind::Comma {
|
||||
self.advance();
|
||||
} else if *self.peek() != TokenKind::RBrace {
|
||||
return Err(Diagnostic::error(
|
||||
ErrorCode::E0201,
|
||||
"expected ',' or '}' in struct literal",
|
||||
self.current_span(),
|
||||
));
|
||||
}
|
||||
}
|
||||
self.expect(&TokenKind::RBrace)?;
|
||||
return Ok(Expr::StructLiteral(name, fields, span));
|
||||
}
|
||||
|
||||
Ok(Expr::Ident(name, span))
|
||||
}
|
||||
TokenKind::LBracket => {
|
||||
|
|
|
|||
|
|
@ -740,6 +740,39 @@ fn parse_struct_decl() {
|
|||
assert_eq!(prog.structs[0].fields[1].name, "y");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_struct_literal_expr() {
|
||||
let src = r#"
|
||||
game "Test" { mapper: NROM }
|
||||
struct Vec2 { x: u8, y: u8 }
|
||||
var pos: Vec2 = Vec2 { x: 10, y: 20 }
|
||||
on frame {
|
||||
pos = Vec2 { x: 1, y: 2 }
|
||||
}
|
||||
start Main
|
||||
"#;
|
||||
parse_ok(src);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_struct_literal_in_if_condition_must_be_paren() {
|
||||
// `if x == Vec2 { ... }` is ambiguous: the `{` could be the if
|
||||
// block or the start of a struct literal. Without parens, the
|
||||
// parser should treat the struct literal fields as the if body.
|
||||
// This test just asserts the parser doesn't crash and doesn't
|
||||
// misinterpret the condition.
|
||||
let src = r#"
|
||||
game "Test" { mapper: NROM }
|
||||
struct Vec2 { x: u8, y: u8 }
|
||||
var pos: Vec2
|
||||
on frame {
|
||||
if pos.x == 5 { pos = Vec2 { x: 0, y: 0 } }
|
||||
}
|
||||
start Main
|
||||
"#;
|
||||
parse_ok(src);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_struct_field_access_expr() {
|
||||
let src = r#"
|
||||
|
|
|
|||
|
|
@ -228,6 +228,26 @@ fn program_with_for_loop() {
|
|||
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_with_struct_literals() {
|
||||
let source = r#"
|
||||
game "Lit" { mapper: NROM }
|
||||
struct Vec2 { x: u8, y: u8 }
|
||||
var pos: Vec2 = Vec2 { x: 10, y: 20 }
|
||||
on frame {
|
||||
pos = Vec2 { x: 100, y: 50 }
|
||||
if button.right {
|
||||
pos = Vec2 { x: pos.x + 1, y: pos.y }
|
||||
}
|
||||
draw Smiley at: (pos.x, pos.y)
|
||||
wait_frame
|
||||
}
|
||||
start Main
|
||||
"#;
|
||||
let rom_data = compile(source);
|
||||
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_with_structs() {
|
||||
let source = r#"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue