mirror of
https://github.com/imjasonh/nescript
synced 2026-07-16 12:12:04 +00:00
Language: struct types
Adds composite \`struct\` types with field access:
struct Vec2 { x: u8, y: u8 }
var pos: Vec2
pos.x = 100
pos.y = pos.x + 5
- Lexer: \`struct\` keyword
- AST: \`StructDecl\` with \`StructField\` list; \`NesType::Struct(name)\`
for struct-typed variable declarations; \`Expr::FieldAccess\` and
\`LValue::Field\` for reads/writes
- Parser: top-level \`struct Name { field: type, ... }\` (optional
trailing comma) and \`ident.field\` syntax in both expression and
lvalue position
- Analyzer: \`register_struct\` computes contiguous field offsets
(no padding) and stores them in \`struct_layouts\`. Struct variables
synthesize a \`VarAllocation\` per field under the name
\`"struct_var.field"\`, and \`Expr::FieldAccess\` / \`LValue::Field\`
resolve against those. Unknown struct types and unknown fields
emit E0201.
- IR lowering + AST codegen: treat struct field access as ordinary
variable access against the synthetic per-field symbols. No new IR
ops are needed.
v1 structs only support primitive fields (u8/i8/bool). Nested structs,
u16 fields, and array fields are not yet allowed.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
9c102cb68e
commit
225d40cea5
12 changed files with 464 additions and 3 deletions
|
|
@ -61,6 +61,7 @@ pub fn analyze(program: &Program) -> AnalysisResult {
|
||||||
function_signatures: HashMap::new(),
|
function_signatures: HashMap::new(),
|
||||||
current_return_type: None,
|
current_return_type: None,
|
||||||
in_function_body: false,
|
in_function_body: false,
|
||||||
|
struct_layouts: HashMap::new(),
|
||||||
};
|
};
|
||||||
analyzer.analyze_program(program);
|
analyzer.analyze_program(program);
|
||||||
|
|
||||||
|
|
@ -98,10 +99,26 @@ struct Analyzer {
|
||||||
/// distinguish "void function" from "state handler" when checking
|
/// distinguish "void function" from "state handler" when checking
|
||||||
/// `return value` statements.
|
/// `return value` statements.
|
||||||
in_function_body: bool,
|
in_function_body: bool,
|
||||||
|
/// Struct name to layout. Each field has an offset in bytes from
|
||||||
|
/// the base address of the struct.
|
||||||
|
struct_layouts: HashMap<String, StructLayout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout info for a struct type.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StructLayout {
|
||||||
|
pub size: u16,
|
||||||
|
pub fields: Vec<(String, NesType, u16)>, // (name, type, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Analyzer {
|
impl Analyzer {
|
||||||
fn analyze_program(&mut self, program: &Program) {
|
fn analyze_program(&mut self, program: &Program) {
|
||||||
|
// Register struct layouts first so later declarations can
|
||||||
|
// reference them (for variable sizing, etc.).
|
||||||
|
for s in &program.structs {
|
||||||
|
self.register_struct(s);
|
||||||
|
}
|
||||||
|
|
||||||
// Register constants
|
// Register constants
|
||||||
for c in &program.constants {
|
for c in &program.constants {
|
||||||
self.register_const(c);
|
self.register_const(c);
|
||||||
|
|
@ -279,6 +296,24 @@ impl Analyzer {
|
||||||
}
|
}
|
||||||
self.walk_expr_reads(idx);
|
self.walk_expr_reads(idx);
|
||||||
}
|
}
|
||||||
|
Expr::FieldAccess(name, field, span) => {
|
||||||
|
// Resolve the struct variable and verify the field
|
||||||
|
// exists. Mark the synthetic `name.field` variable as
|
||||||
|
// used so W0103 doesn't fire.
|
||||||
|
let full_name = format!("{name}.{field}");
|
||||||
|
if self.symbols.contains_key(&full_name) {
|
||||||
|
self.mark_var_used(name);
|
||||||
|
self.mark_var_used(&full_name);
|
||||||
|
} else if !self.symbols.contains_key(name) {
|
||||||
|
self.emit_undefined_var(name, *span);
|
||||||
|
} else {
|
||||||
|
self.diagnostics.push(Diagnostic::error(
|
||||||
|
ErrorCode::E0201,
|
||||||
|
format!("'{name}' has no field '{field}'"),
|
||||||
|
*span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
Expr::BinaryOp(lhs, op, rhs, span) => {
|
Expr::BinaryOp(lhs, op, rhs, span) => {
|
||||||
// W0101: warn about multiply/divide/modulo with a non-
|
// W0101: warn about multiply/divide/modulo with a non-
|
||||||
// constant operand. These lower to calls into the
|
// constant operand. These lower to calls into the
|
||||||
|
|
@ -416,6 +451,49 @@ impl Analyzer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a struct declaration. Computes each field's byte
|
||||||
|
/// offset from the base address (fields are laid out contiguously
|
||||||
|
/// in declaration order with no padding), and records the total
|
||||||
|
/// size. v1 structs only support primitive fields (u8/i8/bool).
|
||||||
|
fn register_struct(&mut self, s: &StructDecl) {
|
||||||
|
if self.struct_layouts.contains_key(&s.name) {
|
||||||
|
self.diagnostics.push(Diagnostic::error(
|
||||||
|
ErrorCode::E0501,
|
||||||
|
format!("duplicate struct declaration of '{}'", s.name),
|
||||||
|
s.span,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut fields = Vec::new();
|
||||||
|
let mut offset: u16 = 0;
|
||||||
|
for field in &s.fields {
|
||||||
|
// Reject non-primitive field types for now.
|
||||||
|
let size = match &field.field_type {
|
||||||
|
NesType::U8 | NesType::I8 | NesType::Bool => 1,
|
||||||
|
_ => {
|
||||||
|
self.diagnostics.push(Diagnostic::error(
|
||||||
|
ErrorCode::E0201,
|
||||||
|
format!(
|
||||||
|
"struct field '{}' has unsupported type '{}' (only u8/i8/bool allowed)",
|
||||||
|
field.name, field.field_type
|
||||||
|
),
|
||||||
|
field.span,
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fields.push((field.name.clone(), field.field_type.clone(), offset));
|
||||||
|
offset += size;
|
||||||
|
}
|
||||||
|
self.struct_layouts.insert(
|
||||||
|
s.name.clone(),
|
||||||
|
StructLayout {
|
||||||
|
size: offset,
|
||||||
|
fields,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Register each variant of an enum declaration as a `u8` constant
|
/// Register each variant of an enum declaration as a `u8` constant
|
||||||
/// with a value equal to its declaration order. Variant names must
|
/// with a value equal to its declaration order. Variant names must
|
||||||
/// be globally unique; a duplicate name emits E0501.
|
/// be globally unique; a duplicate name emits E0501.
|
||||||
|
|
@ -459,7 +537,24 @@ impl Analyzer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let size = type_size(&var.var_type);
|
// Validate struct type exists before sizing.
|
||||||
|
if let NesType::Struct(sname) = &var.var_type {
|
||||||
|
if !self.struct_layouts.contains_key(sname) {
|
||||||
|
self.diagnostics.push(Diagnostic::error(
|
||||||
|
ErrorCode::E0201,
|
||||||
|
format!("unknown struct type '{sname}'"),
|
||||||
|
var.span,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let struct_sizes: HashMap<String, u16> = self
|
||||||
|
.struct_layouts
|
||||||
|
.iter()
|
||||||
|
.map(|(n, l)| (n.clone(), l.size))
|
||||||
|
.collect();
|
||||||
|
let size = type_size_with(&var.var_type, &struct_sizes);
|
||||||
let Some(address) = self.allocate_ram(size, var.span) else {
|
let Some(address) = self.allocate_ram(size, var.span) else {
|
||||||
// Allocation failed (E0301 already emitted) — still add the
|
// Allocation failed (E0301 already emitted) — still add the
|
||||||
// symbol so that later references don't cascade into E0502,
|
// symbol so that later references don't cascade into E0502,
|
||||||
|
|
@ -476,6 +571,43 @@ impl Analyzer {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For struct-typed variables, synthesize per-field entries in
|
||||||
|
// the symbol table and var_allocations. This lets the rest of
|
||||||
|
// the compiler treat `pos.x` and `pos.y` as ordinary variables
|
||||||
|
// at known addresses, without special-casing struct layout.
|
||||||
|
if let NesType::Struct(sname) = &var.var_type {
|
||||||
|
let layout = self.struct_layouts[sname].clone();
|
||||||
|
for (field_name, field_type, offset) in &layout.fields {
|
||||||
|
let full_name = format!("{}.{field_name}", var.name);
|
||||||
|
self.symbols.insert(
|
||||||
|
full_name.clone(),
|
||||||
|
Symbol {
|
||||||
|
name: full_name.clone(),
|
||||||
|
sym_type: field_type.clone(),
|
||||||
|
is_const: false,
|
||||||
|
span: var.span,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.var_allocations.push(VarAllocation {
|
||||||
|
name: full_name,
|
||||||
|
address: address + offset,
|
||||||
|
size: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Also register the struct variable itself (as a symbol
|
||||||
|
// only — it doesn't have a single VarAllocation entry).
|
||||||
|
self.symbols.insert(
|
||||||
|
var.name.clone(),
|
||||||
|
Symbol {
|
||||||
|
name: var.name.clone(),
|
||||||
|
sym_type: var.var_type.clone(),
|
||||||
|
is_const: false,
|
||||||
|
span: var.span,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.symbols.insert(
|
self.symbols.insert(
|
||||||
var.name.clone(),
|
var.name.clone(),
|
||||||
Symbol {
|
Symbol {
|
||||||
|
|
@ -682,6 +814,23 @@ impl Analyzer {
|
||||||
self.mark_var_used(name);
|
self.mark_var_used(name);
|
||||||
self.walk_expr_reads(idx);
|
self.walk_expr_reads(idx);
|
||||||
}
|
}
|
||||||
|
LValue::Field(name, field) => {
|
||||||
|
let full_name = format!("{name}.{field}");
|
||||||
|
if self.symbols.contains_key(&full_name) {
|
||||||
|
// Assigning to a field is a mutation; don't
|
||||||
|
// mark the struct variable as "read" just
|
||||||
|
// because we wrote to one of its fields.
|
||||||
|
self.mark_var_used(&full_name);
|
||||||
|
} else if self.symbols.contains_key(name) {
|
||||||
|
self.diagnostics.push(Diagnostic::error(
|
||||||
|
ErrorCode::E0201,
|
||||||
|
format!("'{name}' has no field '{field}'"),
|
||||||
|
*span,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.emit_undefined_var(name, *span);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.walk_expr_reads(expr);
|
self.walk_expr_reads(expr);
|
||||||
let ltype = self.lvalue_type(lvalue, *span);
|
let ltype = self.lvalue_type(lvalue, *span);
|
||||||
|
|
@ -830,6 +979,10 @@ impl Analyzer {
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
LValue::Field(name, field) => {
|
||||||
|
let full_name = format!("{name}.{field}");
|
||||||
|
self.symbols.get(&full_name).map(|s| s.sym_type.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -934,6 +1087,10 @@ impl Analyzer {
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Expr::FieldAccess(name, field, _) => {
|
||||||
|
let full_name = format!("{name}.{field}");
|
||||||
|
self.symbols.get(&full_name).map(|s| s.sym_type.clone())
|
||||||
|
}
|
||||||
Expr::ArrayLiteral(_, _) => Some(NesType::U8), // element type inferred from context
|
Expr::ArrayLiteral(_, _) => Some(NesType::U8), // element type inferred from context
|
||||||
Expr::Cast(_, target, _) => Some(target.clone()),
|
Expr::Cast(_, target, _) => Some(target.clone()),
|
||||||
}
|
}
|
||||||
|
|
@ -1145,6 +1302,7 @@ fn collect_calls_expr(expr: &Expr, calls: &mut Vec<String>) {
|
||||||
Expr::IntLiteral(_, _)
|
Expr::IntLiteral(_, _)
|
||||||
| Expr::BoolLiteral(_, _)
|
| Expr::BoolLiteral(_, _)
|
||||||
| Expr::Ident(_, _)
|
| Expr::Ident(_, _)
|
||||||
|
| Expr::FieldAccess(_, _, _)
|
||||||
| Expr::ButtonRead(_, _, _) => {}
|
| Expr::ButtonRead(_, _, _) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1225,11 +1383,15 @@ fn compute_depth(
|
||||||
depth
|
depth
|
||||||
}
|
}
|
||||||
|
|
||||||
fn type_size(t: &NesType) -> u16 {
|
/// Compute the byte size of a type. Struct types are looked up in
|
||||||
|
/// `struct_sizes`; if absent, returns 0 (the analyzer will have
|
||||||
|
/// reported an error already).
|
||||||
|
fn type_size_with(t: &NesType, struct_sizes: &HashMap<String, u16>) -> u16 {
|
||||||
match t {
|
match t {
|
||||||
NesType::U8 | NesType::I8 | NesType::Bool => 1,
|
NesType::U8 | NesType::I8 | NesType::Bool => 1,
|
||||||
NesType::U16 => 2,
|
NesType::U16 => 2,
|
||||||
NesType::Array(elem, count) => type_size(elem) * count,
|
NesType::Array(elem, count) => type_size_with(elem, struct_sizes) * count,
|
||||||
|
NesType::Struct(name) => struct_sizes.get(name).copied().unwrap_or(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,68 @@ fn analyze_return_wrong_type() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn analyze_struct_variable_allocates_fields() {
|
||||||
|
let result = analyze_ok(
|
||||||
|
r#"
|
||||||
|
game "Test" { mapper: NROM }
|
||||||
|
struct Vec2 { x: u8, y: u8 }
|
||||||
|
var pos: Vec2
|
||||||
|
on frame {
|
||||||
|
pos.x = 10
|
||||||
|
pos.y = pos.x
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
// The analyzer should synthesize pos.x and pos.y as separate
|
||||||
|
// variables with consecutive addresses.
|
||||||
|
let px = result
|
||||||
|
.var_allocations
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name == "pos.x")
|
||||||
|
.expect("pos.x should be allocated");
|
||||||
|
let py = result
|
||||||
|
.var_allocations
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name == "pos.y")
|
||||||
|
.expect("pos.y should be allocated");
|
||||||
|
assert_eq!(py.address, px.address + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn analyze_struct_unknown_field_errors() {
|
||||||
|
let errors = analyze_errors(
|
||||||
|
r#"
|
||||||
|
game "Test" { mapper: NROM }
|
||||||
|
struct Vec2 { x: u8, y: u8 }
|
||||||
|
var pos: Vec2
|
||||||
|
on frame { pos.z = 5 }
|
||||||
|
start Main
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
errors.contains(&ErrorCode::E0201),
|
||||||
|
"unknown field should emit E0201: {errors:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn analyze_unknown_struct_type_errors() {
|
||||||
|
let errors = analyze_errors(
|
||||||
|
r#"
|
||||||
|
game "Test" { mapper: NROM }
|
||||||
|
var pos: NoSuchStruct
|
||||||
|
on frame { wait_frame }
|
||||||
|
start Main
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
errors.contains(&ErrorCode::E0201),
|
||||||
|
"unknown struct type should emit E0201: {errors:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn analyze_enum_variants_as_constants() {
|
fn analyze_enum_variants_as_constants() {
|
||||||
let result = analyze_ok(
|
let result = analyze_ok(
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ mod tests {
|
||||||
globals: Vec::new(),
|
globals: Vec::new(),
|
||||||
constants: Vec::new(),
|
constants: Vec::new(),
|
||||||
enums: Vec::new(),
|
enums: Vec::new(),
|
||||||
|
structs: Vec::new(),
|
||||||
functions: Vec::new(),
|
functions: Vec::new(),
|
||||||
states: Vec::new(),
|
states: Vec::new(),
|
||||||
sprites: vec![sprite],
|
sprites: vec![sprite],
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,38 @@ impl CodeGen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LValue::Field(name, field) => {
|
||||||
|
// Treat `name.field` as a regular variable. The
|
||||||
|
// analyzer has already synthesized a VarAllocation
|
||||||
|
// entry under the name `"struct.field"`.
|
||||||
|
let full_name = format!("{name}.{field}");
|
||||||
|
if let Some(&addr) = self.var_addrs.get(&full_name) {
|
||||||
|
match op {
|
||||||
|
AssignOp::Assign => {
|
||||||
|
self.gen_expr(expr);
|
||||||
|
self.emit_store(addr);
|
||||||
|
}
|
||||||
|
AssignOp::PlusAssign => {
|
||||||
|
self.emit_load(addr);
|
||||||
|
self.emit(CLC, AM::Implied);
|
||||||
|
self.gen_adc_expr(expr);
|
||||||
|
self.emit_store(addr);
|
||||||
|
}
|
||||||
|
AssignOp::MinusAssign => {
|
||||||
|
self.emit_load(addr);
|
||||||
|
self.emit(SEC, AM::Implied);
|
||||||
|
self.gen_sbc_expr(expr);
|
||||||
|
self.emit_store(addr);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Other compound ops: read, compute, store
|
||||||
|
self.emit_load(addr);
|
||||||
|
self.gen_expr(expr);
|
||||||
|
self.emit_store(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
LValue::ArrayIndex(name, index) => {
|
LValue::ArrayIndex(name, index) => {
|
||||||
if let Some(&base_addr) = self.var_addrs.get(name) {
|
if let Some(&base_addr) = self.var_addrs.get(name) {
|
||||||
// Evaluate index into X register
|
// Evaluate index into X register
|
||||||
|
|
@ -774,6 +806,12 @@ impl CodeGen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Expr::FieldAccess(name, field, _) => {
|
||||||
|
let full_name = format!("{name}.{field}");
|
||||||
|
if let Some(&addr) = self.var_addrs.get(&full_name) {
|
||||||
|
self.emit_load(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
Expr::Call(_, _, _) => {
|
Expr::Call(_, _, _) => {
|
||||||
// Function calls as expressions need JSR — handled elsewhere
|
// Function calls as expressions need JSR — handled elsewhere
|
||||||
// For now, result is 0
|
// For now, result is 0
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,38 @@ impl LoweringContext {
|
||||||
self.emit(IrOp::ArrayStore(var_id, idx, result));
|
self.emit(IrOp::ArrayStore(var_id, idx, result));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LValue::Field(name, field) => {
|
||||||
|
// The analyzer synthesizes a variable named
|
||||||
|
// `"struct.field"` for each struct field, so we can
|
||||||
|
// treat field assignment as a regular variable
|
||||||
|
// assignment to that synthetic name.
|
||||||
|
let full_name = format!("{name}.{field}");
|
||||||
|
let var_id = self.get_or_create_var(&full_name);
|
||||||
|
match op {
|
||||||
|
AssignOp::Assign => {
|
||||||
|
let val = self.lower_expr(expr);
|
||||||
|
self.emit(IrOp::StoreVar(var_id, val));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let current = self.fresh_temp();
|
||||||
|
self.emit(IrOp::LoadVar(current, var_id));
|
||||||
|
let rhs = self.lower_expr(expr);
|
||||||
|
let result = self.fresh_temp();
|
||||||
|
let ir_op = match op {
|
||||||
|
AssignOp::PlusAssign => IrOp::Add(result, current, rhs),
|
||||||
|
AssignOp::MinusAssign => IrOp::Sub(result, current, rhs),
|
||||||
|
AssignOp::AmpAssign => IrOp::And(result, current, rhs),
|
||||||
|
AssignOp::PipeAssign => IrOp::Or(result, current, rhs),
|
||||||
|
AssignOp::CaretAssign => IrOp::Xor(result, current, rhs),
|
||||||
|
AssignOp::ShiftLeftAssign => IrOp::ShiftLeft(result, current, 1),
|
||||||
|
AssignOp::ShiftRightAssign => IrOp::ShiftRight(result, current, 1),
|
||||||
|
AssignOp::Assign => unreachable!(),
|
||||||
|
};
|
||||||
|
self.emit(ir_op);
|
||||||
|
self.emit(IrOp::StoreVar(var_id, result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -537,6 +569,16 @@ impl LoweringContext {
|
||||||
self.emit(IrOp::ArrayLoad(t, var_id, idx));
|
self.emit(IrOp::ArrayLoad(t, var_id, idx));
|
||||||
t
|
t
|
||||||
}
|
}
|
||||||
|
Expr::FieldAccess(name, field, _) => {
|
||||||
|
// Field access lowers to a plain load of the
|
||||||
|
// synthetic `"struct.field"` variable produced by the
|
||||||
|
// analyzer.
|
||||||
|
let full_name = format!("{name}.{field}");
|
||||||
|
let var_id = self.get_or_create_var(&full_name);
|
||||||
|
let t = self.fresh_temp();
|
||||||
|
self.emit(IrOp::LoadVar(t, var_id));
|
||||||
|
t
|
||||||
|
}
|
||||||
Expr::BinaryOp(left, op, right, _) => self.lower_binop(left, *op, right),
|
Expr::BinaryOp(left, op, right, _) => self.lower_binop(left, *op, right),
|
||||||
Expr::UnaryOp(op, inner, _) => {
|
Expr::UnaryOp(op, inner, _) => {
|
||||||
let val = self.lower_expr(inner);
|
let val = self.lower_expr(inner);
|
||||||
|
|
@ -697,6 +739,10 @@ fn type_size(t: &NesType) -> u16 {
|
||||||
NesType::U8 | NesType::I8 | NesType::Bool => 1,
|
NesType::U8 | NesType::I8 | NesType::Bool => 1,
|
||||||
NesType::U16 => 2,
|
NesType::U16 => 2,
|
||||||
NesType::Array(elem, count) => type_size(elem) * count,
|
NesType::Array(elem, count) => type_size(elem) * count,
|
||||||
|
// Struct sizes are resolved in the analyzer. IR lowering only
|
||||||
|
// sees struct types on `var` declarations, which are skipped
|
||||||
|
// below via the analyzer's synthetic field allocations.
|
||||||
|
NesType::Struct(_) => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,7 @@ impl<'a> Lexer<'a> {
|
||||||
"var" => TokenKind::KwVar,
|
"var" => TokenKind::KwVar,
|
||||||
"const" => TokenKind::KwConst,
|
"const" => TokenKind::KwConst,
|
||||||
"enum" => TokenKind::KwEnum,
|
"enum" => TokenKind::KwEnum,
|
||||||
|
"struct" => TokenKind::KwStruct,
|
||||||
"if" => TokenKind::KwIf,
|
"if" => TokenKind::KwIf,
|
||||||
"else" => TokenKind::KwElse,
|
"else" => TokenKind::KwElse,
|
||||||
"while" => TokenKind::KwWhile,
|
"while" => TokenKind::KwWhile,
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ fn lex_all_keywords() {
|
||||||
("var", TokenKind::KwVar),
|
("var", TokenKind::KwVar),
|
||||||
("const", TokenKind::KwConst),
|
("const", TokenKind::KwConst),
|
||||||
("enum", TokenKind::KwEnum),
|
("enum", TokenKind::KwEnum),
|
||||||
|
("struct", TokenKind::KwStruct),
|
||||||
("if", TokenKind::KwIf),
|
("if", TokenKind::KwIf),
|
||||||
("else", TokenKind::KwElse),
|
("else", TokenKind::KwElse),
|
||||||
("while", TokenKind::KwWhile),
|
("while", TokenKind::KwWhile),
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ pub enum TokenKind {
|
||||||
KwVar,
|
KwVar,
|
||||||
KwConst,
|
KwConst,
|
||||||
KwEnum,
|
KwEnum,
|
||||||
|
KwStruct,
|
||||||
KwIf,
|
KwIf,
|
||||||
KwElse,
|
KwElse,
|
||||||
KwWhile,
|
KwWhile,
|
||||||
|
|
@ -149,6 +150,7 @@ impl std::fmt::Display for TokenKind {
|
||||||
Self::KwVar => write!(f, "var"),
|
Self::KwVar => write!(f, "var"),
|
||||||
Self::KwConst => write!(f, "const"),
|
Self::KwConst => write!(f, "const"),
|
||||||
Self::KwEnum => write!(f, "enum"),
|
Self::KwEnum => write!(f, "enum"),
|
||||||
|
Self::KwStruct => write!(f, "struct"),
|
||||||
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"),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ pub struct Program {
|
||||||
pub globals: Vec<VarDecl>,
|
pub globals: Vec<VarDecl>,
|
||||||
pub constants: Vec<ConstDecl>,
|
pub constants: Vec<ConstDecl>,
|
||||||
pub enums: Vec<EnumDecl>,
|
pub enums: Vec<EnumDecl>,
|
||||||
|
pub structs: Vec<StructDecl>,
|
||||||
pub functions: Vec<FunDecl>,
|
pub functions: Vec<FunDecl>,
|
||||||
pub states: Vec<StateDecl>,
|
pub states: Vec<StateDecl>,
|
||||||
pub sprites: Vec<SpriteDecl>,
|
pub sprites: Vec<SpriteDecl>,
|
||||||
|
|
@ -27,6 +28,24 @@ pub struct EnumDecl {
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `struct Name { field1: u8, field2: u8 }` — composite type with a
|
||||||
|
/// known layout. Fields are stored contiguously in memory in
|
||||||
|
/// declaration order (no padding). Only primitive-sized fields (u8,
|
||||||
|
/// i8, bool) are supported in the v1 layout.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StructDecl {
|
||||||
|
pub name: String,
|
||||||
|
pub fields: Vec<StructField>,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StructField {
|
||||||
|
pub name: String,
|
||||||
|
pub field_type: NesType,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SpriteDecl {
|
pub struct SpriteDecl {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
@ -148,6 +167,9 @@ pub enum NesType {
|
||||||
U16,
|
U16,
|
||||||
Bool,
|
Bool,
|
||||||
Array(Box<NesType>, u16),
|
Array(Box<NesType>, u16),
|
||||||
|
/// A user-declared struct, identified by its name. The analyzer
|
||||||
|
/// looks up field layouts in the `StructDecl` table.
|
||||||
|
Struct(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for NesType {
|
impl std::fmt::Display for NesType {
|
||||||
|
|
@ -158,6 +180,7 @@ impl std::fmt::Display for NesType {
|
||||||
Self::U16 => write!(f, "u16"),
|
Self::U16 => write!(f, "u16"),
|
||||||
Self::Bool => write!(f, "bool"),
|
Self::Bool => write!(f, "bool"),
|
||||||
Self::Array(t, n) => write!(f, "{t}[{n}]"),
|
Self::Array(t, n) => write!(f, "{t}[{n}]"),
|
||||||
|
Self::Struct(name) => write!(f, "{name}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -174,6 +197,8 @@ pub enum Expr {
|
||||||
BoolLiteral(bool, Span),
|
BoolLiteral(bool, Span),
|
||||||
Ident(String, Span),
|
Ident(String, Span),
|
||||||
ArrayIndex(String, Box<Expr>, Span),
|
ArrayIndex(String, Box<Expr>, Span),
|
||||||
|
/// Field access on a struct variable: `pos.x`.
|
||||||
|
FieldAccess(String, String, Span),
|
||||||
BinaryOp(Box<Expr>, BinOp, Box<Expr>, Span),
|
BinaryOp(Box<Expr>, BinOp, Box<Expr>, Span),
|
||||||
UnaryOp(UnaryOp, Box<Expr>, Span),
|
UnaryOp(UnaryOp, Box<Expr>, Span),
|
||||||
Call(String, Vec<Expr>, Span),
|
Call(String, Vec<Expr>, Span),
|
||||||
|
|
@ -189,6 +214,7 @@ impl Expr {
|
||||||
| Self::BoolLiteral(_, s)
|
| Self::BoolLiteral(_, s)
|
||||||
| Self::Ident(_, s)
|
| Self::Ident(_, s)
|
||||||
| Self::ArrayIndex(_, _, s)
|
| Self::ArrayIndex(_, _, s)
|
||||||
|
| Self::FieldAccess(_, _, s)
|
||||||
| Self::BinaryOp(_, _, _, s)
|
| Self::BinaryOp(_, _, _, s)
|
||||||
| Self::UnaryOp(_, _, s)
|
| Self::UnaryOp(_, _, s)
|
||||||
| Self::Call(_, _, s)
|
| Self::Call(_, _, s)
|
||||||
|
|
@ -300,6 +326,9 @@ pub struct DrawStmt {
|
||||||
pub enum LValue {
|
pub enum LValue {
|
||||||
Var(String),
|
Var(String),
|
||||||
ArrayIndex(String, Box<Expr>),
|
ArrayIndex(String, Box<Expr>),
|
||||||
|
/// Struct field: `pos.x = 5`. First string is the struct variable
|
||||||
|
/// name, second is the field name.
|
||||||
|
Field(String, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ impl Parser {
|
||||||
let mut globals = Vec::new();
|
let mut globals = Vec::new();
|
||||||
let mut constants = Vec::new();
|
let mut constants = Vec::new();
|
||||||
let mut enums: Vec<EnumDecl> = Vec::new();
|
let mut enums: Vec<EnumDecl> = Vec::new();
|
||||||
|
let mut structs: Vec<StructDecl> = Vec::new();
|
||||||
let mut functions = Vec::new();
|
let mut functions = Vec::new();
|
||||||
let mut states = Vec::new();
|
let mut states = Vec::new();
|
||||||
let mut sprites = Vec::new();
|
let mut sprites = Vec::new();
|
||||||
|
|
@ -147,6 +148,9 @@ impl Parser {
|
||||||
TokenKind::KwEnum => {
|
TokenKind::KwEnum => {
|
||||||
enums.push(self.parse_enum_decl()?);
|
enums.push(self.parse_enum_decl()?);
|
||||||
}
|
}
|
||||||
|
TokenKind::KwStruct => {
|
||||||
|
structs.push(self.parse_struct_decl()?);
|
||||||
|
}
|
||||||
TokenKind::KwState => {
|
TokenKind::KwState => {
|
||||||
states.push(self.parse_state_decl()?);
|
states.push(self.parse_state_decl()?);
|
||||||
}
|
}
|
||||||
|
|
@ -221,6 +225,7 @@ impl Parser {
|
||||||
globals,
|
globals,
|
||||||
constants,
|
constants,
|
||||||
enums,
|
enums,
|
||||||
|
structs,
|
||||||
functions,
|
functions,
|
||||||
states,
|
states,
|
||||||
sprites,
|
sprites,
|
||||||
|
|
@ -232,6 +237,40 @@ impl Parser {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_struct_decl(&mut self) -> Result<StructDecl, Diagnostic> {
|
||||||
|
let start = self.current_span();
|
||||||
|
self.expect(&TokenKind::KwStruct)?;
|
||||||
|
let (name, _) = self.expect_ident()?;
|
||||||
|
self.expect(&TokenKind::LBrace)?;
|
||||||
|
let mut fields = Vec::new();
|
||||||
|
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
|
||||||
|
let field_span = self.current_span();
|
||||||
|
let (field_name, _) = self.expect_ident()?;
|
||||||
|
self.expect(&TokenKind::Colon)?;
|
||||||
|
let field_type = self.parse_type()?;
|
||||||
|
fields.push(StructField {
|
||||||
|
name: field_name,
|
||||||
|
field_type,
|
||||||
|
span: field_span,
|
||||||
|
});
|
||||||
|
if *self.peek() == TokenKind::Comma {
|
||||||
|
self.advance();
|
||||||
|
} else if *self.peek() != TokenKind::RBrace {
|
||||||
|
return Err(Diagnostic::error(
|
||||||
|
ErrorCode::E0201,
|
||||||
|
"expected ',' or '}' in struct body",
|
||||||
|
self.current_span(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.expect(&TokenKind::RBrace)?;
|
||||||
|
Ok(StructDecl {
|
||||||
|
name,
|
||||||
|
fields,
|
||||||
|
span: Span::new(start.file_id, start.start, self.current_span().end),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_enum_decl(&mut self) -> Result<EnumDecl, Diagnostic> {
|
fn parse_enum_decl(&mut self) -> Result<EnumDecl, Diagnostic> {
|
||||||
let start = self.current_span();
|
let start = self.current_span();
|
||||||
self.expect(&TokenKind::KwEnum)?;
|
self.expect(&TokenKind::KwEnum)?;
|
||||||
|
|
@ -1121,6 +1160,19 @@ impl Parser {
|
||||||
start,
|
start,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
TokenKind::Dot => {
|
||||||
|
// Field assignment: name.field = value
|
||||||
|
self.advance();
|
||||||
|
let (field, _) = self.expect_ident()?;
|
||||||
|
let op = self.parse_assign_op()?;
|
||||||
|
let value = self.parse_expr()?;
|
||||||
|
Ok(Statement::Assign(
|
||||||
|
LValue::Field(name, field),
|
||||||
|
op,
|
||||||
|
value,
|
||||||
|
start,
|
||||||
|
))
|
||||||
|
}
|
||||||
TokenKind::LParen => {
|
TokenKind::LParen => {
|
||||||
// Function call
|
// Function call
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|
@ -1207,6 +1259,12 @@ impl Parser {
|
||||||
self.advance();
|
self.advance();
|
||||||
NesType::Bool
|
NesType::Bool
|
||||||
}
|
}
|
||||||
|
TokenKind::Ident(name) => {
|
||||||
|
// User-declared struct types are referenced by name.
|
||||||
|
// The analyzer validates that the name exists.
|
||||||
|
self.advance();
|
||||||
|
NesType::Struct(name)
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Diagnostic::error(
|
return Err(Diagnostic::error(
|
||||||
ErrorCode::E0201,
|
ErrorCode::E0201,
|
||||||
|
|
@ -1464,6 +1522,13 @@ impl Parser {
|
||||||
return Ok(Expr::Call(name, args, span));
|
return Ok(Expr::Call(name, args, span));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for field access: `name.field`
|
||||||
|
if *self.peek() == TokenKind::Dot {
|
||||||
|
self.advance();
|
||||||
|
let (field, _) = self.expect_ident()?;
|
||||||
|
return Ok(Expr::FieldAccess(name, field, span));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Expr::Ident(name, span))
|
Ok(Expr::Ident(name, span))
|
||||||
}
|
}
|
||||||
TokenKind::LBracket => {
|
TokenKind::LBracket => {
|
||||||
|
|
|
||||||
|
|
@ -665,6 +665,37 @@ fn parse_mmc3_mapper() {
|
||||||
assert_eq!(prog.game.mapper, Mapper::MMC3);
|
assert_eq!(prog.game.mapper, Mapper::MMC3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_struct_decl() {
|
||||||
|
let src = r#"
|
||||||
|
game "Test" { mapper: NROM }
|
||||||
|
struct Vec2 { x: u8, y: u8 }
|
||||||
|
on frame { wait_frame }
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let prog = parse_ok(src);
|
||||||
|
assert_eq!(prog.structs.len(), 1);
|
||||||
|
assert_eq!(prog.structs[0].name, "Vec2");
|
||||||
|
assert_eq!(prog.structs[0].fields.len(), 2);
|
||||||
|
assert_eq!(prog.structs[0].fields[0].name, "x");
|
||||||
|
assert_eq!(prog.structs[0].fields[1].name, "y");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_struct_field_access_expr() {
|
||||||
|
let src = r#"
|
||||||
|
game "Test" { mapper: NROM }
|
||||||
|
struct Vec2 { x: u8, y: u8 }
|
||||||
|
var pos: Vec2
|
||||||
|
on frame {
|
||||||
|
pos.x = 10
|
||||||
|
pos.y = pos.x
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
parse_ok(src);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_enum_decl() {
|
fn parse_enum_decl() {
|
||||||
let src = r#"
|
let src = r#"
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,29 @@ fn program_with_functions() {
|
||||||
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn program_with_structs() {
|
||||||
|
let source = r#"
|
||||||
|
game "Structs" { mapper: NROM }
|
||||||
|
struct Vec2 { x: u8, y: u8 }
|
||||||
|
struct Player { health: u8, lives: u8 }
|
||||||
|
|
||||||
|
var pos: Vec2
|
||||||
|
var hero: Player
|
||||||
|
|
||||||
|
on frame {
|
||||||
|
pos.x = 100
|
||||||
|
pos.y = 50
|
||||||
|
hero.health = 3
|
||||||
|
hero.lives = 5
|
||||||
|
if button.right { pos.x += 1 }
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let rom_data = compile(source);
|
||||||
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn program_with_enums() {
|
fn program_with_enums() {
|
||||||
let source = r#"
|
let source = r#"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue