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:
parent
ae051f5909
commit
dbfcb313bc
12 changed files with 181 additions and 0 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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#"
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ mod tests {
|
|||
},
|
||||
globals: Vec::new(),
|
||||
constants: Vec::new(),
|
||||
enums: Vec::new(),
|
||||
functions: Vec::new(),
|
||||
states: Vec::new(),
|
||||
sprites: vec![sprite],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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#"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue