diff --git a/internal/commands/decr.go b/internal/commands/decr.go new file mode 100644 index 0000000..e43537a --- /dev/null +++ b/internal/commands/decr.go @@ -0,0 +1,147 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// DecrCommand handles DECREMENT operations +// Syntax: +// +// DEC # old syntax +// DECREMENT # old syntax +// -- # new syntax (literal, no space - variables only) +// +// can be a variable or absolute address (old syntax only) +type DecrCommand struct { + varName string + varKind compiler.VarKind + isAbsolute bool + absAddr uint16 +} + +func (c *DecrCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + + // Old syntax: DEC/DECREMENT + keyword := strings.ToUpper(params[0]) + if (keyword == "DEC" || keyword == "DECREMENT") && len(params) == 2 { + return true + } + + // New syntax: -- (literal, no space) + if len(params) == 1 && strings.HasSuffix(params[0], "--") { + return true + } + + return false +} + +func (c *DecrCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.varName = "" + c.isAbsolute = false + c.absAddr = 0 + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + scope := ctx.CurrentScope() + keyword := strings.ToUpper(params[0]) + + var targetParam string + var isNewSyntax bool + + if keyword == "DEC" || keyword == "DECREMENT" { + // Old syntax: DEC/DECREMENT + if len(params) != 2 { + return fmt.Errorf("DEC: wrong number of parameters") + } + targetParam = params[1] + isNewSyntax = false + } else if strings.HasSuffix(params[0], "--") { + // New syntax: -- (literal) + if len(params) != 1 { + return fmt.Errorf("DEC: wrong number of parameters") + } + targetParam = strings.TrimSuffix(params[0], "--") + isNewSyntax = true + } else { + return fmt.Errorf("DEC: unrecognized syntax") + } + + // Try variable lookup + sym := ctx.SymbolTable.Lookup(targetParam, scope) + if sym != nil { + if sym.IsConst() { + return fmt.Errorf("DEC: cannot decrement constant %q", targetParam) + } + c.varName = sym.FullName() + c.varKind = sym.GetVarKind() + c.isAbsolute = false + return nil + } + + // For new syntax (--), must be a variable + if isNewSyntax { + return fmt.Errorf("DEC: unknown variable %q", targetParam) + } + + // Old syntax allows absolute addresses + constLookup := func(name string) (int64, bool) { + s := ctx.SymbolTable.Lookup(name, scope) + if s != nil && s.IsConst() { + return int64(s.Value), true + } + return 0, false + } + + val, evalErr := utils.EvaluateExpression(targetParam, constLookup) + if evalErr != nil { + return fmt.Errorf("DEC: expected variable or absolute address, got %q: %w", targetParam, evalErr) + } + + if val < 0 || val > 65535 { + return fmt.Errorf("DEC: address %d out of range (0-65535)", val) + } + + c.isAbsolute = true + c.absAddr = uint16(val) + + return nil +} + +func (c *DecrCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { + var asm []string + + if c.isAbsolute { + // Absolute address + asm = append(asm, fmt.Sprintf("\tdec $%04x", c.absAddr)) + return asm, nil + } + + // Variable + if c.varKind == compiler.KindByte { + asm = append(asm, fmt.Sprintf("\tdec %s", c.varName)) + return asm, nil + } + + // Word variable - handle borrow from high byte + label := ctx.GeneralStack.Push() + asm = append(asm, fmt.Sprintf("\tlda %s", c.varName)) + asm = append(asm, fmt.Sprintf("\tbne %s", label)) + asm = append(asm, fmt.Sprintf("\tdec %s+1", c.varName)) + asm = append(asm, label) + asm = append(asm, fmt.Sprintf("\tdec %s", c.varName)) + + return asm, nil +} diff --git a/internal/commands/incr.go b/internal/commands/incr.go new file mode 100644 index 0000000..60573f2 --- /dev/null +++ b/internal/commands/incr.go @@ -0,0 +1,146 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// IncrCommand handles INCREMENT operations +// Syntax: +// +// INC # old syntax +// INCREMENT # old syntax +// ++ # new syntax (literal, no space - variables only) +// +// can be a variable or absolute address (old syntax only) +type IncrCommand struct { + varName string + varKind compiler.VarKind + isAbsolute bool + absAddr uint16 +} + +func (c *IncrCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + + // Old syntax: INC/INCREMENT + keyword := strings.ToUpper(params[0]) + if (keyword == "INC" || keyword == "INCREMENT") && len(params) == 2 { + return true + } + + // New syntax: ++ (literal, no space) + if len(params) == 1 && strings.HasSuffix(params[0], "++") { + return true + } + + return false +} + +func (c *IncrCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.varName = "" + c.isAbsolute = false + c.absAddr = 0 + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + scope := ctx.CurrentScope() + keyword := strings.ToUpper(params[0]) + + var targetParam string + var isNewSyntax bool + + if keyword == "INC" || keyword == "INCREMENT" { + // Old syntax: INC/INCREMENT + if len(params) != 2 { + return fmt.Errorf("INC: wrong number of parameters") + } + targetParam = params[1] + isNewSyntax = false + } else if strings.HasSuffix(params[0], "++") { + // New syntax: ++ (literal) + if len(params) != 1 { + return fmt.Errorf("INC: wrong number of parameters") + } + targetParam = strings.TrimSuffix(params[0], "++") + isNewSyntax = true + } else { + return fmt.Errorf("INC: unrecognized syntax") + } + + // Try variable lookup + sym := ctx.SymbolTable.Lookup(targetParam, scope) + if sym != nil { + if sym.IsConst() { + return fmt.Errorf("INC: cannot increment constant %q", targetParam) + } + c.varName = sym.FullName() + c.varKind = sym.GetVarKind() + c.isAbsolute = false + return nil + } + + // For new syntax (++), must be a variable + if isNewSyntax { + return fmt.Errorf("INC: unknown variable %q", targetParam) + } + + // Old syntax allows absolute addresses + constLookup := func(name string) (int64, bool) { + s := ctx.SymbolTable.Lookup(name, scope) + if s != nil && s.IsConst() { + return int64(s.Value), true + } + return 0, false + } + + val, evalErr := utils.EvaluateExpression(targetParam, constLookup) + if evalErr != nil { + return fmt.Errorf("INC: expected variable or absolute address, got %q: %w", targetParam, evalErr) + } + + if val < 0 || val > 65535 { + return fmt.Errorf("INC: address %d out of range (0-65535)", val) + } + + c.isAbsolute = true + c.absAddr = uint16(val) + + return nil +} + +func (c *IncrCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { + var asm []string + + if c.isAbsolute { + // Absolute address + asm = append(asm, fmt.Sprintf("\tinc $%04x", c.absAddr)) + return asm, nil + } + + // Variable + if c.varKind == compiler.KindByte { + asm = append(asm, fmt.Sprintf("\tinc %s", c.varName)) + return asm, nil + } + + // Word variable - handle carry to high byte + label := ctx.GeneralStack.Push() + asm = append(asm, fmt.Sprintf("\tinc %s", c.varName)) + asm = append(asm, fmt.Sprintf("\tbne %s", label)) + asm = append(asm, fmt.Sprintf("\tinc %s+1", c.varName)) + asm = append(asm, label) + + return asm, nil +} diff --git a/internal/commands/incr_decr_test.go b/internal/commands/incr_decr_test.go new file mode 100644 index 0000000..06e1290 --- /dev/null +++ b/internal/commands/incr_decr_test.go @@ -0,0 +1,402 @@ +package commands + +import ( + "strings" + "testing" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" +) + +func TestIncrCommand_WillHandle(t *testing.T) { + tests := []struct { + name string + line string + expected bool + }{ + {"INC keyword", "INC myvar", true}, + {"INCREMENT keyword", "INCREMENT myvar", true}, + {"inc lowercase", "inc myvar", true}, + {"New syntax ++ literal", "myvar++", true}, + {"Invalid - space before ++", "myvar ++", false}, + {"Invalid - no params", "INC", false}, + {"Invalid - too many params old", "INC a b c", false}, + {"Invalid - wrong suffix", "myvar+-", false}, + {"Invalid - ADD command", "ADD x TO y", false}, + } + + cmd := &IncrCommand{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + line := preproc.Line{Text: tt.line, LineNo: 1, Filename: "test.c65"} + result := cmd.WillHandle(line) + if result != tt.expected { + t.Errorf("WillHandle(%q) = %v, want %v", tt.line, result, tt.expected) + } + }) + } +} + +func TestDecrCommand_WillHandle(t *testing.T) { + tests := []struct { + name string + line string + expected bool + }{ + {"DEC keyword", "DEC myvar", true}, + {"DECREMENT keyword", "DECREMENT myvar", true}, + {"dec lowercase", "dec myvar", true}, + {"New syntax -- literal", "myvar--", true}, + {"Invalid - space before --", "myvar --", false}, + {"Invalid - no params", "DEC", false}, + {"Invalid - too many params old", "DEC a b c", false}, + {"Invalid - wrong suffix", "myvar-+", false}, + {"Invalid - SUB command", "SUB x FROM y", false}, + } + + cmd := &DecrCommand{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + line := preproc.Line{Text: tt.line, LineNo: 1, Filename: "test.c65"} + result := cmd.WillHandle(line) + if result != tt.expected { + t.Errorf("WillHandle(%q) = %v, want %v", tt.line, result, tt.expected) + } + }) + } +} + +func TestIncrCommand_InterpretAndGenerate(t *testing.T) { + tests := []struct { + name string + setup func(*compiler.CompilerContext) + line string + expectError bool + checkAsm func(*testing.T, []string) + }{ + { + name: "INC byte variable old syntax", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("counter", "", compiler.KindByte, 0) + }, + line: "INC counter", + expectError: false, + checkAsm: func(t *testing.T, asm []string) { + if len(asm) != 1 { + t.Errorf("Expected 1 asm line, got %d", len(asm)) + return + } + if !strings.Contains(asm[0], "inc counter") { + t.Errorf("Expected 'inc counter', got %q", asm[0]) + } + }, + }, + { + name: "INC byte variable new syntax", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("counter", "", compiler.KindByte, 0) + }, + line: "counter++", + expectError: false, + checkAsm: func(t *testing.T, asm []string) { + if len(asm) != 1 { + t.Errorf("Expected 1 asm line, got %d", len(asm)) + return + } + if !strings.Contains(asm[0], "inc counter") { + t.Errorf("Expected 'inc counter', got %q", asm[0]) + } + }, + }, + { + name: "INC word variable old syntax", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("pointer", "", compiler.KindWord, 0) + }, + line: "INCREMENT pointer", + expectError: false, + checkAsm: func(t *testing.T, asm []string) { + if len(asm) != 4 { + t.Errorf("Expected 4 asm lines for word inc, got %d", len(asm)) + return + } + if !strings.Contains(asm[0], "inc pointer") { + t.Errorf("Expected 'inc pointer' in line 0, got %q", asm[0]) + } + if !strings.Contains(asm[1], "bne") { + t.Errorf("Expected 'bne' in line 1, got %q", asm[1]) + } + if !strings.Contains(asm[2], "inc pointer+1") { + t.Errorf("Expected 'inc pointer+1' in line 2, got %q", asm[2]) + } + // Line 3 should be the label + }, + }, + { + name: "INC word variable new syntax", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("pointer", "", compiler.KindWord, 0) + }, + line: "pointer++", + expectError: false, + checkAsm: func(t *testing.T, asm []string) { + if len(asm) != 4 { + t.Errorf("Expected 4 asm lines for word inc, got %d", len(asm)) + } + }, + }, + { + name: "INC absolute address", + setup: func(ctx *compiler.CompilerContext) { + // No variables needed + }, + line: "INC $D020", + expectError: false, + checkAsm: func(t *testing.T, asm []string) { + if len(asm) != 1 { + t.Errorf("Expected 1 asm line, got %d", len(asm)) + return + } + if !strings.Contains(strings.ToLower(asm[0]), "inc $d020") { + t.Errorf("Expected 'inc $d020', got %q", asm[0]) + } + }, + }, + + { + name: "Error: INC unknown variable", + setup: func(ctx *compiler.CompilerContext) { + // No setup + }, + line: "INC unknown", + expectError: true, + }, + { + name: "Error: INC constant variable", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddConst("MAX", "", compiler.KindByte, 100) + }, + line: "INC MAX", + expectError: true, + }, + { + name: "Error: new syntax on unknown variable", + setup: func(ctx *compiler.CompilerContext) { + // No setup + }, + line: "unknown++", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(preproc.NewPragma()) + if tt.setup != nil { + tt.setup(ctx) + } + + cmd := &IncrCommand{} + line := preproc.Line{Text: tt.line, LineNo: 1, Filename: "test.c65"} + + err := cmd.Interpret(line, ctx) + if tt.expectError { + if err == nil { + t.Errorf("Expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate error: %v", err) + } + + if tt.checkAsm != nil { + tt.checkAsm(t, asm) + } + }) + } +} + +func TestDecrCommand_InterpretAndGenerate(t *testing.T) { + tests := []struct { + name string + setup func(*compiler.CompilerContext) + line string + expectError bool + checkAsm func(*testing.T, []string) + }{ + { + name: "DEC byte variable old syntax", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("counter", "", compiler.KindByte, 0) + }, + line: "DEC counter", + expectError: false, + checkAsm: func(t *testing.T, asm []string) { + if len(asm) != 1 { + t.Errorf("Expected 1 asm line, got %d", len(asm)) + return + } + if !strings.Contains(asm[0], "dec counter") { + t.Errorf("Expected 'dec counter', got %q", asm[0]) + } + }, + }, + { + name: "DEC byte variable new syntax", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("counter", "", compiler.KindByte, 0) + }, + line: "counter--", + expectError: false, + checkAsm: func(t *testing.T, asm []string) { + if len(asm) != 1 { + t.Errorf("Expected 1 asm line, got %d", len(asm)) + return + } + if !strings.Contains(asm[0], "dec counter") { + t.Errorf("Expected 'dec counter', got %q", asm[0]) + } + }, + }, + { + name: "DEC word variable old syntax", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("pointer", "", compiler.KindWord, 0) + }, + line: "DECREMENT pointer", + expectError: false, + checkAsm: func(t *testing.T, asm []string) { + if len(asm) != 5 { + t.Errorf("Expected 5 asm lines for word dec, got %d", len(asm)) + return + } + if !strings.Contains(asm[0], "lda pointer") { + t.Errorf("Expected 'lda pointer' in line 0, got %q", asm[0]) + } + if !strings.Contains(asm[1], "bne") { + t.Errorf("Expected 'bne' in line 1, got %q", asm[1]) + } + if !strings.Contains(asm[2], "dec pointer+1") { + t.Errorf("Expected 'dec pointer+1' in line 2, got %q", asm[2]) + } + // Line 3 is the label + if !strings.Contains(asm[4], "dec pointer") { + t.Errorf("Expected 'dec pointer' in line 4, got %q", asm[4]) + } + }, + }, + { + name: "DEC word variable new syntax", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("pointer", "", compiler.KindWord, 0) + }, + line: "pointer--", + expectError: false, + checkAsm: func(t *testing.T, asm []string) { + if len(asm) != 5 { + t.Errorf("Expected 5 asm lines for word dec, got %d", len(asm)) + } + }, + }, + { + name: "DEC absolute address", + setup: func(ctx *compiler.CompilerContext) { + // No variables needed + }, + line: "DEC $D020", + expectError: false, + checkAsm: func(t *testing.T, asm []string) { + if len(asm) != 1 { + t.Errorf("Expected 1 asm line, got %d", len(asm)) + return + } + if !strings.Contains(strings.ToLower(asm[0]), "dec $d020") { + t.Errorf("Expected 'dec $d020', got %q", asm[0]) + } + }, + }, + { + name: "Error: DEC unknown variable", + setup: func(ctx *compiler.CompilerContext) { + // No setup + }, + line: "DEC unknown", + expectError: true, + }, + { + name: "Error: DEC constant variable", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddConst("MAX", "", compiler.KindByte, 100) + }, + line: "DEC MAX", + expectError: true, + }, + { + name: "Error: new syntax on unknown variable", + setup: func(ctx *compiler.CompilerContext) { + // No setup + }, + line: "unknown--", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(preproc.NewPragma()) + if tt.setup != nil { + tt.setup(ctx) + } + + cmd := &DecrCommand{} + line := preproc.Line{Text: tt.line, LineNo: 1, Filename: "test.c65"} + + err := cmd.Interpret(line, ctx) + if tt.expectError { + if err == nil { + t.Errorf("Expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate error: %v", err) + } + + if tt.checkAsm != nil { + tt.checkAsm(t, asm) + } + }) + } +} + +func TestIncrDecrCommand_FullNameResolution(t *testing.T) { + // Test that variable name resolution works with full names + ctx := compiler.NewCompilerContext(preproc.NewPragma()) + + // Add a variable with scoped name directly + ctx.SymbolTable.AddVar("counter", "myfunc", compiler.KindWord, 0) + + // Note: CurrentScope will return nil (global) since we're not in a function context + // The symbol table lookup will try scoped search and fall back to global + + // Test that using the base name in global scope won't find the scoped var + incrCmd := &IncrCommand{} + line := preproc.Line{Text: "INC counter", LineNo: 1, Filename: "test.c65"} + err := incrCmd.Interpret(line, ctx) + + // Should fail - counter exists only in myfunc scope, not global + if err == nil { + t.Errorf("Expected error when accessing scoped variable from global scope") + } +} diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 08ac382..22e0c83 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -39,8 +39,24 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { // Skip non-source lines (assembler and script handled differently) if line.Kind != preproc.Source { if line.Kind == preproc.Assembler { - // Pass through assembler lines verbatim - codeOutput = append(codeOutput, line.Text) + // Expand |varname| -> scoped_varname for local variables in ASM blocks + text := line.Text + for { + start := strings.IndexByte(text, '|') + if start == -1 { + break + } + end := strings.IndexByte(text[start+1:], '|') + if end == -1 { + return nil, fmt.Errorf("%s:%d: unclosed | in assembler line", line.Filename, line.LineNo) + } + end += start + 1 + + varName := text[start+1 : end] + expandedName := c.ctx.SymbolTable.ExpandName(varName, c.ctx.CurrentScope()) + text = text[:start] + expandedName + text[end+1:] + } + codeOutput = append(codeOutput, text) } // Script lines ignored for now continue diff --git a/main.go b/main.go index dda09bb..2ba9f15 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,8 @@ func registerCommands(comp *compiler.Compiler) { comp.Registry().Register(&commands.FuncCommand{}) comp.Registry().Register(commands.NewCallCommand(comp.Context().FunctionHandler)) comp.Registry().Register(&commands.FendCommand{}) + comp.Registry().Register(&commands.IncrCommand{}) + comp.Registry().Register(&commands.DecrCommand{}) }