From 8c0056365ebb20ac8ad56bc55783f5493aff1290 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Sat, 1 Nov 2025 19:42:32 +0100 Subject: [PATCH] Added WORD command --- internal/commands/word.go | 184 +++++++++++++ internal/commands/word_test.go | 462 +++++++++++++++++++++++++++++++++ main.go | 2 +- 3 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 internal/commands/word.go create mode 100644 internal/commands/word_test.go diff --git a/internal/commands/word.go b/internal/commands/word.go new file mode 100644 index 0000000..c1e282a --- /dev/null +++ b/internal/commands/word.go @@ -0,0 +1,184 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// WordCommand handles WORD variable declarations +// Syntax: +// +// WORD varname # word with init = 0 +// WORD varname = value # word with init value +// WORD varname = "string" # word pointer to constant string +// WORD varname @ address # word at absolute address +// WORD CONST varname = value # constant word +type WordCommand struct { + varName string + value uint16 + isConst bool + isAbs bool +} + +func (c *WordCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + return strings.ToUpper(params[0]) == "WORD" +} + +func (c *WordCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.varName = "" + c.value = 0 + c.isConst = false + c.isAbs = false + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + paramCount := len(params) + + // Validate parameter count + if paramCount != 2 && paramCount != 4 && paramCount != 5 { + return fmt.Errorf("WORD: wrong number of parameters (%d)", paramCount) + } + + var varName string + var value int64 + scope := ctx.FunctionHandler.CurrentFunction() + + // Create constant lookup function + constLookup := func(name string) (int64, bool) { + sym := ctx.SymbolTable.Lookup(name, ctx.CurrentScope()) + if sym != nil && sym.IsConst() { + return int64(sym.Value), true + } + return 0, false + } + + switch paramCount { + case 2: + // WORD varname + varName = params[1] + value = 0 + + if !utils.ValidateIdentifier(varName) { + return fmt.Errorf("WORD: invalid identifier %q", varName) + } + + err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindWord, uint16(value)) + + case 4: + // WORD varname = value OR WORD varname @ address OR WORD varname = "string" + varName = params[1] + operator := params[2] + valueStr := params[3] + + if !utils.ValidateIdentifier(varName) { + return fmt.Errorf("WORD: invalid identifier %q", varName) + } + + // Check for string literal + if utils.IsStringLiteral(valueStr) { + if operator != "=" { + return fmt.Errorf("WORD: expected '=' when assigning string pointer, got %q", operator) + } + + // Generate label for the string + label := ctx.GeneralStack.Push() + + // Get pragma set for this line + pragmaSet := ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) + + // Add string to constant string handler + ctx.ConstStrHandler.AddConstStr(label, valueStr, false, pragmaSet) + + err = ctx.SymbolTable.AddLabel(varName, scope, label) + if err != nil { + return fmt.Errorf("WORD: %w", err) + } + + c.varName = varName + c.value = 0 // label refs don't have numeric values + return nil + } + + // Not a string, evaluate as expression + value, err = utils.EvaluateExpression(valueStr, constLookup) + if err != nil { + return fmt.Errorf("WORD: invalid value %q: %w", valueStr, err) + } + + if operator == "=" { + // WORD varname = value + if value < 0 || value > 65535 { + return fmt.Errorf("WORD: init value %d out of range (0-65535)", value) + } + err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindWord, uint16(value)) + + } else if operator == "@" { + // WORD varname @ address + if value < 0 || value > 0xFFFF { + return fmt.Errorf("WORD: absolute address $%X out of range", value) + } + c.isAbs = true + err = ctx.SymbolTable.AddAbsolute(varName, scope, compiler.KindWord, uint16(value)) + + } else { + return fmt.Errorf("WORD: expected '=' or '@', got %q", operator) + } + + case 5: + // WORD CONST varname = value + constKeyword := strings.ToUpper(params[1]) + varName = params[2] + operator := params[3] + valueStr := params[4] + + if constKeyword != "CONST" { + return fmt.Errorf("WORD: expected CONST keyword, got %q", params[1]) + } + + if operator != "=" { + return fmt.Errorf("WORD: expected '=', got %q", operator) + } + + if !utils.ValidateIdentifier(varName) { + return fmt.Errorf("WORD: invalid identifier %q", varName) + } + + value, err = utils.EvaluateExpression(valueStr, constLookup) + if err != nil { + return fmt.Errorf("WORD: invalid value %q: %w", valueStr, err) + } + + if value < 0 || value > 65535 { + return fmt.Errorf("WORD: const value %d out of range (0-65535)", value) + } + + c.isConst = true + err = ctx.SymbolTable.AddConst(varName, scope, compiler.KindWord, uint16(value)) + } + + if err != nil { + return fmt.Errorf("WORD: %w", err) + } + + c.varName = varName + c.value = uint16(value) + + return nil +} + +func (c *WordCommand) Generate(_ *compiler.CompilerContext) ([]string, error) { + // Variables are rendered by assembleOutput, not by individual commands + return nil, nil +} diff --git a/internal/commands/word_test.go b/internal/commands/word_test.go new file mode 100644 index 0000000..233ea88 --- /dev/null +++ b/internal/commands/word_test.go @@ -0,0 +1,462 @@ +package commands + +import ( + "strings" + "testing" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" +) + +func TestWordCommand_WillHandle(t *testing.T) { + tests := []struct { + name string + text string + want bool + }{ + { + name: "handles WORD", + text: "WORD x", + want: true, + }, + { + name: "handles word lowercase", + text: "word x", + want: true, + }, + { + name: "handles WORD with init", + text: "WORD x = 1000", + want: true, + }, + { + name: "handles WORD with string", + text: `WORD ptr = "hello"`, + want: true, + }, + { + name: "handles WORD at absolute", + text: "WORD x @ $C000", + want: true, + }, + { + name: "handles WORD CONST", + text: "WORD CONST x = 1000", + want: true, + }, + { + name: "does not handle BYTE", + text: "BYTE x", + want: false, + }, + { + name: "does not handle empty", + text: "", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &WordCommand{} + line := preproc.Line{Text: tt.text} + got := cmd.WillHandle(line) + if got != tt.want { + t.Errorf("WillHandle() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestWordCommand_Interpret(t *testing.T) { + tests := []struct { + name string + text string + wantErr bool + errContains string + checkVar func(*testing.T, *compiler.CompilerContext) + }{ + { + name: "simple word", + text: "WORD x", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("x", nil) + if sym == nil { + t.Fatal("Expected variable x to be declared") + } + if !sym.IsWord() { + t.Error("Expected word variable") + } + if sym.IsConst() { + t.Error("Expected regular variable, not const") + } + if sym.Value != 0 { + t.Errorf("Expected init value 0, got %d", sym.Value) + } + }, + }, + { + name: "word with decimal init", + text: "WORD counter = 1000", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("counter", nil) + if sym == nil { + t.Fatal("Expected variable counter to be declared") + } + if sym.Value != 1000 { + t.Errorf("Expected init value 1000, got %d", sym.Value) + } + }, + }, + { + name: "word with hex init", + text: "WORD status = $FFFF", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("status", nil) + if sym == nil { + t.Fatal("Expected variable status to be declared") + } + if sym.Value != 65535 { + t.Errorf("Expected init value 65535, got %d", sym.Value) + } + }, + }, + { + name: "word with string pointer", + text: `WORD msg = "hello world"`, + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("msg", nil) + if sym == nil { + t.Fatal("Expected variable msg to be declared") + } + if !sym.Has(compiler.FlagLabelRef) { + t.Error("Expected label reference") + } + if sym.LabelRef == "" { + t.Error("Expected non-empty label reference") + } + // Check that string was added to handler + strs := ctx.ConstStrHandler.GenerateConstStrDecls() + if len(strs) == 0 { + t.Error("Expected string to be added to ConstStrHandler") + } + }, + }, + { + name: "word at absolute address", + text: "WORD ptr @ $C000", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("ptr", nil) + if sym == nil { + t.Fatal("Expected variable ptr to be declared") + } + if !sym.IsAbsolute() { + t.Error("Expected absolute variable") + } + if sym.AbsAddr != 0xC000 { + t.Errorf("Expected address $C000, got $%04X", sym.AbsAddr) + } + }, + }, + { + name: "word at zero page", + text: "WORD zpvar @ $80", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("zpvar", nil) + if sym == nil { + t.Fatal("Expected variable zpvar to be declared") + } + if !sym.IsZeroPage() { + t.Error("Expected zero page variable") + } + }, + }, + { + name: "const word", + text: "WORD CONST maxval = 65535", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("maxval", nil) + if sym == nil { + t.Fatal("Expected constant maxval to be declared") + } + if !sym.IsConst() { + t.Error("Expected constant") + } + if sym.Value != 65535 { + t.Errorf("Expected value 65535, got %d", sym.Value) + } + }, + }, + { + name: "const word with hex", + text: "WORD CONST flag = $FFFF", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("flag", nil) + if sym == nil { + t.Fatal("Expected constant flag to be declared") + } + if !sym.IsConst() { + t.Error("Expected constant") + } + if sym.Value != 65535 { + t.Errorf("Expected value 65535, got %d", sym.Value) + } + }, + }, + { + name: "word with expression", + text: "WORD x = 100+200", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("x", nil) + if sym == nil { + t.Fatal("Expected variable x to be declared") + } + if sym.Value != 300 { + t.Errorf("Expected value 300, got %d", sym.Value) + } + }, + }, + { + name: "word with binary", + text: "WORD x = !1111111111111111", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("x", nil) + if sym == nil { + t.Fatal("Expected variable x to be declared") + } + if sym.Value != 65535 { + t.Errorf("Expected value 65535, got %d", sym.Value) + } + }, + }, + { + name: "word with bitwise OR", + text: "WORD x = $FF00|$00FF", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("x", nil) + if sym == nil { + t.Fatal("Expected variable x to be declared") + } + if sym.Value != 65535 { + t.Errorf("Expected value 65535, got %d", sym.Value) + } + }, + }, + { + name: "word with bitwise AND", + text: "WORD x = $FFFF&$00FF", + wantErr: false, + checkVar: func(t *testing.T, ctx *compiler.CompilerContext) { + sym := ctx.SymbolTable.Lookup("x", nil) + if sym == nil { + t.Fatal("Expected variable x to be declared") + } + if sym.Value != 255 { + t.Errorf("Expected value 255, got %d", sym.Value) + } + }, + }, + { + name: "word with out of range value", + text: "WORD x = 65536", + wantErr: true, + errContains: "out of range", + }, + { + name: "const word out of range", + text: "WORD CONST x = 65536", + wantErr: true, + errContains: "out of range", + }, + { + name: "word without name", + text: "WORD", + wantErr: true, + errContains: "wrong number of parameters", + }, + { + name: "word with invalid identifier", + text: "WORD 123invalid", + wantErr: true, + errContains: "invalid identifier", + }, + { + name: "word with wrong operator", + text: "WORD x + 10", + wantErr: true, + errContains: "expected '=' or '@'", + }, + { + name: "word string with wrong operator", + text: `WORD x @ "hello"`, + wantErr: true, + errContains: "expected '=' when assigning string pointer", + }, + { + name: "const without equals", + text: "WORD CONST x 10", + wantErr: true, + errContains: "expected '='", + }, + { + name: "wrong keyword instead of CONST", + text: "WORD VAR x = 10", + wantErr: true, + errContains: "expected CONST keyword", + }, + { + name: "duplicate declaration", + text: "WORD x", + wantErr: true, + errContains: "already declared", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pragma := preproc.NewPragma() + ctx := compiler.NewCompilerContext(pragma) + + // For duplicate test, pre-declare the variable + if tt.name == "duplicate declaration" { + ctx.SymbolTable.AddVar("x", "", compiler.KindWord, 0) + } + + cmd := &WordCommand{} + line := preproc.Line{ + Text: tt.text, + Filename: "test.c65", + LineNo: 1, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + + err := cmd.Interpret(line, ctx) + + if tt.wantErr { + if err == nil { + t.Fatal("Expected error, got nil") + } + if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("Error %q does not contain %q", err.Error(), tt.errContains) + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if tt.checkVar != nil { + tt.checkVar(t, ctx) + } + } + }) + } +} + +func TestWordCommand_Generate(t *testing.T) { + pragma := preproc.NewPragma() + ctx := compiler.NewCompilerContext(pragma) + + cmd := &WordCommand{} + line := preproc.Line{ + Text: "WORD x = 1000", + Filename: "test.c65", + LineNo: 1, + Kind: preproc.Source, + PragmaSetIndex: 0, + } + + // Interpret first + if err := cmd.Interpret(line, ctx); err != nil { + t.Fatalf("Interpret failed: %v", err) + } + + // Generate should return nil (variables handled by assembleOutput) + output, err := cmd.Generate(ctx) + if err != nil { + t.Errorf("Generate returned error: %v", err) + } + if output != nil { + t.Errorf("Generate should return nil, got %v", output) + } +} + +func TestWordCommand_WithConstantExpression(t *testing.T) { + pragma := preproc.NewPragma() + ctx := compiler.NewCompilerContext(pragma) + + // First, declare a constant + ctx.SymbolTable.AddConst("MAXVAL", "", compiler.KindWord, 1000) + ctx.SymbolTable.AddConst("OFFSET", "", compiler.KindWord, 500) + + // Now declare a word using the constant in an expression + cmd := &WordCommand{} + line := preproc.Line{ + Text: "WORD x = MAXVAL+OFFSET", + Filename: "test.c65", + LineNo: 1, + Kind: preproc.Source, + PragmaSetIndex: 0, + } + + if err := cmd.Interpret(line, ctx); err != nil { + t.Fatalf("Interpret failed: %v", err) + } + + // Check value + sym := ctx.SymbolTable.Lookup("x", nil) + if sym == nil { + t.Fatal("Expected variable x to be declared") + } + if sym.Value != 1500 { + t.Errorf("Expected value 1500 (1000+500), got %d", sym.Value) + } +} + +func TestWordCommand_MultipleStrings(t *testing.T) { + pragma := preproc.NewPragma() + ctx := compiler.NewCompilerContext(pragma) + + // Create multiple string pointers + stringsd := []string{ + `WORD msg1 = "hello"`, + `WORD msg2 = "world"`, + `WORD msg3 = "test"`, + } + + for i, text := range stringsd { + cmd := &WordCommand{} + line := preproc.Line{ + Text: text, + Filename: "test.c65", + LineNo: i + 1, + Kind: preproc.Source, + PragmaSetIndex: 0, + } + + if err := cmd.Interpret(line, ctx); err != nil { + t.Fatalf("Failed to interpret line %d: %v", i+1, err) + } + } + + // Check all were added + if ctx.SymbolTable.Count() != 3 { + t.Errorf("Expected 3 variables, got %d", ctx.SymbolTable.Count()) + } + + // Check string declarations were generated + strDecls := ctx.ConstStrHandler.GenerateConstStrDecls() + if len(strDecls) < 9 { // Each string needs at least 3 lines (label, data, terminator) + t.Errorf("Expected at least 9 lines of string declarations, got %d", len(strDecls)) + } +} diff --git a/main.go b/main.go index 533147a..cd444ca 100644 --- a/main.go +++ b/main.go @@ -77,9 +77,9 @@ func registerCommands(comp *compiler.Compiler) { // This is the single place where all commands are wired up comp.Registry().Register(&commands.ByteCommand{}) + comp.Registry().Register(&commands.WordCommand{}) // TODO: Add more commands as they're implemented: - // comp.Registry().Register(&commands.WordCommand{}) // comp.Registry().Register(&commands.LetCommand{}) // comp.Registry().Register(&commands.IfCommand{}) // comp.Registry().Register(&commands.WhileCommand{})