Added incr and decr and also local variable expansion in asm blocks.

This commit is contained in:
Mattias Hansson 2025-11-05 18:49:54 +01:00
parent 7ac90260af
commit bacd4851ef
5 changed files with 715 additions and 2 deletions

147
internal/commands/decr.go Normal file
View file

@ -0,0 +1,147 @@
package commands
import (
"fmt"
"strings"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// DecrCommand handles DECREMENT operations
// Syntax:
//
// DEC <target> # old syntax
// DECREMENT <target> # old syntax
// <var>-- # new syntax (literal, no space - variables only)
//
// <target> 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: <var>-- (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 <target>
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: <var>-- (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
}

146
internal/commands/incr.go Normal file
View file

@ -0,0 +1,146 @@
package commands
import (
"fmt"
"strings"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// IncrCommand handles INCREMENT operations
// Syntax:
//
// INC <target> # old syntax
// INCREMENT <target> # old syntax
// <var>++ # new syntax (literal, no space - variables only)
//
// <target> 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: <var>++ (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 <target>
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: <var>++ (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
}

View file

@ -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")
}
}

View file

@ -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

View file

@ -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{})
}