1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-09 09:18:01 +00:00

Language: enum declarations

Adds \`enum Name { V1, V2, ... }\` as a top-level declaration. Each
variant is registered as a \`u8\` constant equal to its index in the
declaration. Variant names are global and must be unique across all
enums and other symbols (E0501 on collision).

- Lexer: \`enum\` keyword
- AST: \`EnumDecl { name, variants, span }\` field on \`Program\`
- Parser: top-level \`enum\` syntax with optional trailing commas
- Analyzer: \`register_enum\` flattens variants into the symbol table
- IR lowering and AST codegen: variants resolve through the existing
  \`const_values\` path
- Tests cover parsing, duplicate detection, and usage

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 11:36:44 +00:00
parent ae051f5909
commit dbfcb313bc
No known key found for this signature in database
12 changed files with 181 additions and 0 deletions

View file

@ -107,6 +107,11 @@ impl Analyzer {
self.register_const(c);
}
// Register enum variants as constants with values 0, 1, 2, ...
for e in &program.enums {
self.register_enum(e);
}
// Register and allocate globals
for var in &program.globals {
self.register_var(var);
@ -411,6 +416,39 @@ impl Analyzer {
);
}
/// Register each variant of an enum declaration as a `u8` constant
/// with a value equal to its declaration order. Variant names must
/// be globally unique; a duplicate name emits E0501.
fn register_enum(&mut self, e: &EnumDecl) {
if self.symbols.contains_key(&e.name) {
self.diagnostics.push(Diagnostic::error(
ErrorCode::E0501,
format!("duplicate declaration of '{}'", e.name),
e.span,
));
// Don't return — still register the variants.
}
for (variant_name, variant_span) in &e.variants {
if self.symbols.contains_key(variant_name) {
self.diagnostics.push(Diagnostic::error(
ErrorCode::E0501,
format!("duplicate declaration of '{variant_name}'"),
*variant_span,
));
continue;
}
self.symbols.insert(
variant_name.clone(),
Symbol {
name: variant_name.clone(),
sym_type: NesType::U8,
is_const: true,
span: *variant_span,
},
);
}
}
fn register_var(&mut self, var: &VarDecl) {
if self.symbols.contains_key(&var.name) {
self.diagnostics.push(Diagnostic::error(

View file

@ -298,6 +298,42 @@ fn analyze_return_wrong_type() {
);
}
#[test]
fn analyze_enum_variants_as_constants() {
let result = analyze_ok(
r#"
game "Test" { mapper: NROM }
enum Color { Red, Green, Blue }
var c: u8 = Red
on frame {
if c == Blue { c = Green }
}
start Main
"#,
);
// Variants should be registered as constant symbols.
assert!(result.symbols.get("Red").is_some_and(|s| s.is_const));
assert!(result.symbols.get("Green").is_some_and(|s| s.is_const));
assert!(result.symbols.get("Blue").is_some_and(|s| s.is_const));
}
#[test]
fn analyze_duplicate_enum_variant_errors() {
let errors = analyze_errors(
r#"
game "Test" { mapper: NROM }
enum A { Foo, Bar }
enum B { Baz, Bar }
on frame { wait_frame }
start Main
"#,
);
assert!(
errors.contains(&ErrorCode::E0501),
"duplicate variant should emit E0501, got: {errors:?}"
);
}
#[test]
fn analyze_dead_code_after_break() {
let src = r#"

View file

@ -83,6 +83,7 @@ mod tests {
},
globals: Vec::new(),
constants: Vec::new(),
enums: Vec::new(),
functions: Vec::new(),
states: Vec::new(),
sprites: vec![sprite],

View file

@ -58,6 +58,8 @@ impl CodeGen {
const_values.insert(c.name.clone(), *v);
}
}
// Enum variants get wired in via `with_enums` below so that the
// main.rs call sites stay concise.
Self {
instructions: Vec::new(),
@ -93,6 +95,18 @@ impl CodeGen {
self
}
/// Register enum variants as constants (each variant gets a u8
/// equal to its declaration order within the enum).
#[must_use]
pub fn with_enums(mut self, enums: &[EnumDecl]) -> Self {
for e in enums {
for (i, (variant, _)) in e.variants.iter().enumerate() {
self.const_values.insert(variant.clone(), i as u16);
}
}
self
}
fn fresh_label(&mut self, prefix: &str) -> String {
self.label_counter += 1;
format!("__{prefix}_{}", self.label_counter)

View file

@ -126,6 +126,13 @@ impl LoweringContext {
}
}
// Register enum variants as constants (0, 1, 2, ... per enum)
for e in &program.enums {
for (i, (variant, _)) in e.variants.iter().enumerate() {
self.const_values.insert(variant.clone(), i as u16);
}
}
// Lower globals
for var in &program.globals {
let var_id = self.get_or_create_var(&var.name);

View file

@ -429,6 +429,7 @@ impl<'a> Lexer<'a> {
"fun" => TokenKind::KwFun,
"var" => TokenKind::KwVar,
"const" => TokenKind::KwConst,
"enum" => TokenKind::KwEnum,
"if" => TokenKind::KwIf,
"else" => TokenKind::KwElse,
"while" => TokenKind::KwWhile,

View file

@ -122,6 +122,7 @@ fn lex_all_keywords() {
("fun", TokenKind::KwFun),
("var", TokenKind::KwVar),
("const", TokenKind::KwConst),
("enum", TokenKind::KwEnum),
("if", TokenKind::KwIf),
("else", TokenKind::KwElse),
("while", TokenKind::KwWhile),

View file

@ -45,6 +45,7 @@ pub enum TokenKind {
KwFun,
KwVar,
KwConst,
KwEnum,
KwIf,
KwElse,
KwWhile,
@ -147,6 +148,7 @@ impl std::fmt::Display for TokenKind {
Self::KwFun => write!(f, "fun"),
Self::KwVar => write!(f, "var"),
Self::KwConst => write!(f, "const"),
Self::KwEnum => write!(f, "enum"),
Self::KwIf => write!(f, "if"),
Self::KwElse => write!(f, "else"),
Self::KwWhile => write!(f, "while"),

View file

@ -169,6 +169,7 @@ fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
let mut instructions = if use_ast {
CodeGen::new(&analysis.var_allocations, &program.constants)
.with_sprites(&sprites)
.with_enums(&program.enums)
.with_debug(debug)
.generate(&program)
} else {

View file

@ -5,6 +5,7 @@ pub struct Program {
pub game: GameDecl,
pub globals: Vec<VarDecl>,
pub constants: Vec<ConstDecl>,
pub enums: Vec<EnumDecl>,
pub functions: Vec<FunDecl>,
pub states: Vec<StateDecl>,
pub sprites: Vec<SpriteDecl>,
@ -15,6 +16,17 @@ pub struct Program {
pub span: Span,
}
/// `enum Name { V1, V2, V3 }` — variants become u8 constants with
/// values equal to their declaration order (0, 1, 2, ...). Variant
/// names are global: they're flattened into the constants table so
/// they can be referenced directly without namespacing.
#[derive(Debug, Clone)]
pub struct EnumDecl {
pub name: String,
pub variants: Vec<(String, Span)>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct SpriteDecl {
pub name: String,

View file

@ -116,6 +116,7 @@ impl Parser {
let mut game = None;
let mut globals = Vec::new();
let mut constants = Vec::new();
let mut enums: Vec<EnumDecl> = Vec::new();
let mut functions = Vec::new();
let mut states = Vec::new();
let mut sprites = Vec::new();
@ -143,6 +144,9 @@ impl Parser {
TokenKind::KwConst => {
constants.push(self.parse_const_decl()?);
}
TokenKind::KwEnum => {
enums.push(self.parse_enum_decl()?);
}
TokenKind::KwState => {
states.push(self.parse_state_decl()?);
}
@ -216,6 +220,7 @@ impl Parser {
game,
globals,
constants,
enums,
functions,
states,
sprites,
@ -227,6 +232,41 @@ impl Parser {
})
}
fn parse_enum_decl(&mut self) -> Result<EnumDecl, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwEnum)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LBrace)?;
let mut variants = Vec::new();
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
let span = self.current_span();
let (vname, _) = self.expect_ident()?;
variants.push((vname, span));
if *self.peek() == TokenKind::Comma {
self.advance();
} else if *self.peek() != TokenKind::RBrace {
return Err(Diagnostic::error(
ErrorCode::E0201,
"expected ',' or '}' in enum body",
self.current_span(),
));
}
}
self.expect(&TokenKind::RBrace)?;
if variants.len() > 256 {
return Err(Diagnostic::error(
ErrorCode::E0201,
"enum has more than 256 variants (u8 overflow)",
start,
));
}
Ok(EnumDecl {
name,
variants,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
// ── Game declaration ──
fn parse_game_decl(&mut self) -> Result<GameDecl, Diagnostic> {

View file

@ -665,6 +665,34 @@ fn parse_mmc3_mapper() {
assert_eq!(prog.game.mapper, Mapper::MMC3);
}
#[test]
fn parse_enum_decl() {
let src = r#"
game "Test" { mapper: NROM }
enum Direction { Up, Down, Left, Right }
on frame { wait_frame }
start Main
"#;
let prog = parse_ok(src);
assert_eq!(prog.enums.len(), 1);
assert_eq!(prog.enums[0].name, "Direction");
assert_eq!(prog.enums[0].variants.len(), 4);
assert_eq!(prog.enums[0].variants[0].0, "Up");
assert_eq!(prog.enums[0].variants[3].0, "Right");
}
#[test]
fn parse_enum_trailing_comma_optional() {
let src = r#"
game "Test" { mapper: NROM }
enum Flag { On, Off, }
on frame { wait_frame }
start Main
"#;
let prog = parse_ok(src);
assert_eq!(prog.enums[0].variants.len(), 2);
}
#[test]
fn parse_multiple_start_declarations_error() {
let src = r#"