Added and, or, xor and subtract commands

This commit is contained in:
Mattias Hansson 2025-11-02 18:07:14 +01:00
parent ac40f67ec0
commit 88f90fe5be
13 changed files with 2836 additions and 52 deletions

View file

@ -103,15 +103,20 @@ func (c *AddCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext)
return fmt.Errorf("ADD: cannot assign to constant %q", destName)
}
c.destVarName = destSym.FullName()
c.destVarKind = getVarKind(destSym)
c.destVarKind = destSym.GetVarKind()
// Parse param1
if err := c.parseParam(params[1], &c.param1VarName, &c.param1VarKind, &c.param1Value, &c.param1IsVar, ctx, scope, constLookup); err != nil {
var err error
c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam(
params[1], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("ADD: param1: %w", err)
}
// Parse param2
if err := c.parseParam(params[3], &c.param2VarName, &c.param2VarKind, &c.param2Value, &c.param2IsVar, ctx, scope, constLookup); err != nil {
c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam(
params[3], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("ADD: param2: %w", err)
}
@ -139,15 +144,20 @@ func (c *AddCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext)
return fmt.Errorf("ADD: cannot assign to constant %q", destName)
}
c.destVarName = destSym.FullName()
c.destVarKind = getVarKind(destSym)
c.destVarKind = destSym.GetVarKind()
// Parse param1
if err := c.parseParam(params[2], &c.param1VarName, &c.param1VarKind, &c.param1Value, &c.param1IsVar, ctx, scope, constLookup); err != nil {
var err error
c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam(
params[2], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("ADD: param1: %w", err)
}
// Parse param2
if err := c.parseParam(params[4], &c.param2VarName, &c.param2VarKind, &c.param2Value, &c.param2IsVar, ctx, scope, constLookup); err != nil {
c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam(
params[4], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("ADD: param2: %w", err)
}
}
@ -155,42 +165,6 @@ func (c *AddCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext)
return nil
}
func (c *AddCommand) parseParam(
param string,
varName *string,
varKind *compiler.VarKind,
value *uint16,
isVar *bool,
ctx *compiler.CompilerContext,
scope []string,
constLookup utils.ConstantLookup,
) error {
// Try variable lookup first
sym := ctx.SymbolTable.Lookup(param, scope)
if sym != nil {
// It's a variable or constant
*varName = sym.FullName()
*varKind = getVarKind(sym)
*value = sym.Value
*isVar = true
return nil
}
// Not a variable, must be an expression
val, err := utils.EvaluateExpression(param, constLookup)
if err != nil {
return fmt.Errorf("not a valid variable or expression: %w", err)
}
if val < 0 || val > 65535 {
return fmt.Errorf("value %d out of range (0-65535)", val)
}
*value = uint16(val)
*isVar = false
return nil
}
func (c *AddCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
var asm []string
@ -263,11 +237,3 @@ func (c *AddCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return asm, nil
}
// getVarKind extracts VarKind from Symbol
func getVarKind(sym *compiler.Symbol) compiler.VarKind {
if sym.IsByte() {
return compiler.KindByte
}
return compiler.KindWord
}

View file

@ -181,8 +181,11 @@ func TestAddCommand_Interpret_OldSyntax(t *testing.T) {
text: "ADD MAX TO a GIVING c",
wantErr: false,
check: func(t *testing.T, cmd *AddCommand) {
if !cmd.param1IsVar || cmd.param1VarName != "MAX" {
t.Errorf("param1 should be constant MAX")
if cmd.param1IsVar {
t.Errorf("param1 should be literal (constant inlined), got isVar=true")
}
if cmd.param1Value != 100 {
t.Errorf("param1 value = %d, want 100", cmd.param1Value)
}
},
},

237
internal/commands/and.go Normal file
View file

@ -0,0 +1,237 @@
package commands
import (
"fmt"
"strings"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// AndCommand handles bitwise AND operations
// Syntax:
//
// AND <param1> WITH <param2> GIVING <dest> # old syntax with WITH/GIVING
// AND <param1> WITH <param2> -> <dest> # old syntax with WITH/->
// <dest> = <param1> & <param2> # new syntax
type AndCommand struct {
param1VarName string
param1VarKind compiler.VarKind
param1Value uint16
param1IsVar bool
param2VarName string
param2VarKind compiler.VarKind
param2Value uint16
param2IsVar bool
destVarName string
destVarKind compiler.VarKind
}
func (c *AndCommand) WillHandle(line preproc.Line) bool {
params, err := utils.ParseParams(line.Text)
if err != nil || len(params) == 0 {
return false
}
// Old syntax: AND ... (must have exactly 6 params)
if strings.ToUpper(params[0]) == "AND" && len(params) == 6 {
return true
}
// New syntax: <dest> = <param1> & <param2>
if len(params) == 5 && params[1] == "=" && params[3] == "&" {
return true
}
return false
}
func (c *AndCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
// Clear state
c.param1VarName = ""
c.param1IsVar = false
c.param1Value = 0
c.param2VarName = ""
c.param2IsVar = false
c.param2Value = 0
c.destVarName = ""
params, err := utils.ParseParams(line.Text)
if err != nil {
return err
}
paramCount := len(params)
scope := ctx.CurrentScope()
// Create constant lookup function
constLookup := func(name string) (int64, bool) {
sym := ctx.SymbolTable.Lookup(name, scope)
if sym != nil && sym.IsConst() {
return int64(sym.Value), true
}
return 0, false
}
// Determine syntax and parse accordingly
if strings.ToUpper(params[0]) == "AND" {
// Old syntax: AND <param1> WITH <param2> GIVING/-> <dest>
if paramCount != 6 {
return fmt.Errorf("AND: wrong number of parameters (%d), expected 6", paramCount)
}
separator1 := strings.ToUpper(params[2])
if separator1 != "WITH" {
return fmt.Errorf("AND: parameter #3 must be 'WITH', got %q", params[2])
}
separator2 := strings.ToUpper(params[4])
if separator2 != "GIVING" && separator2 != "->" {
return fmt.Errorf("AND: parameter #5 must be 'GIVING' or '->', got %q", params[4])
}
// Parse destination
destName := params[5]
destSym := ctx.SymbolTable.Lookup(destName, scope)
if destSym == nil {
return fmt.Errorf("AND: unknown variable %q", destName)
}
if destSym.IsConst() {
return fmt.Errorf("AND: cannot assign to constant %q", destName)
}
c.destVarName = destSym.FullName()
c.destVarKind = destSym.GetVarKind()
// Parse param1
var err error
c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam(
params[1], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("AND: param1: %w", err)
}
// Parse param2
c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam(
params[3], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("AND: param2: %w", err)
}
} else {
// New syntax: <dest> = <param1> & <param2>
if paramCount != 5 {
return fmt.Errorf("AND: wrong number of parameters (%d), expected 5", paramCount)
}
if params[1] != "=" {
return fmt.Errorf("AND: expected '=' at position 2, got %q", params[1])
}
if params[3] != "&" {
return fmt.Errorf("AND: expected '&' at position 4, got %q", params[3])
}
// Parse destination
destName := params[0]
destSym := ctx.SymbolTable.Lookup(destName, scope)
if destSym == nil {
return fmt.Errorf("AND: unknown variable %q", destName)
}
if destSym.IsConst() {
return fmt.Errorf("AND: cannot assign to constant %q", destName)
}
c.destVarName = destSym.FullName()
c.destVarKind = destSym.GetVarKind()
// Parse param1
var err error
c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam(
params[2], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("AND: param1: %w", err)
}
// Parse param2
c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam(
params[4], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("AND: param2: %w", err)
}
}
return nil
}
func (c *AndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
var asm []string
// If both params are literals, fold at compile time
if !c.param1IsVar && !c.param2IsVar {
result := c.param1Value & c.param2Value
lo := uint8(result & 0xFF)
hi := uint8((result >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo))
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
if c.destVarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
return asm, nil
}
// At least one param is a variable - generate AND code
// Load param1
if c.param1IsVar {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
} else {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
}
// AND with param2
if c.param2IsVar {
asm = append(asm, fmt.Sprintf("\tand %s", c.param2VarName))
} else {
asm = append(asm, fmt.Sprintf("\tand #$%02x", uint8(c.param2Value&0xFF)))
}
// Store low byte
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
// If destination is word, handle high byte
if c.destVarKind == compiler.KindWord {
// Load high byte of param1
if c.param1IsVar {
if c.param1VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName))
} else {
asm = append(asm, "\tlda #0")
}
} else {
hi := uint8((c.param1Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
}
// AND with high byte of param2
if c.param2IsVar {
if c.param2VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tand %s+1", c.param2VarName))
} else {
asm = append(asm, "\tand #0")
}
} else {
hi := uint8((c.param2Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tand #$%02x", hi))
}
// Store high byte
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
return asm, nil
}

View file

@ -0,0 +1,426 @@
package commands
import (
"strings"
"testing"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
)
func TestAndCommand_WillHandle(t *testing.T) {
cmd := &AndCommand{}
tests := []struct {
name string
line string
want bool
}{
{"old syntax WITH/GIVING", "AND a WITH b GIVING c", true},
{"old syntax WITH/arrow", "AND a WITH b -> c", true},
{"new syntax", "result = x & y", true},
{"not AND", "ADD a TO b GIVING c", false},
{"wrong param count", "AND a b c", false},
{"empty", "", false},
{"new syntax wrong op", "result = x + y", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
got := cmd.WillHandle(line)
if got != tt.want {
t.Errorf("WillHandle() = %v, want %v", got, tt.want)
}
})
}
}
func TestAndCommand_OldSyntax(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
wantErr bool
}{
{
name: "byte AND byte -> byte (variables)",
line: "AND a WITH b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("b", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tand b",
"\tsta result",
},
},
{
name: "byte AND byte -> word",
line: "AND a WITH b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("b", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda a",
"\tand b",
"\tsta result",
"\tlda #0",
"\tand #0",
"\tsta result+1",
},
},
{
name: "word AND word -> word",
line: "AND x WITH y GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x1234)
st.AddVar("y", "", compiler.KindWord, 0x0F0F)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda x",
"\tand y",
"\tsta result",
"\tlda x+1",
"\tand y+1",
"\tsta result+1",
},
},
{
name: "byte AND literal -> byte",
line: "AND a WITH $F0 GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tand #$f0",
"\tsta result",
},
},
{
name: "literal AND byte -> byte",
line: "AND 255 WITH b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$ff",
"\tand b",
"\tsta result",
},
},
{
name: "constant folding: 255 AND 15 -> byte",
line: "AND 255 WITH 15 GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$0f",
"\tsta result",
},
},
{
name: "constant folding: $FFFF AND $0F0F -> word",
line: "AND $FFFF WITH $0F0F GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda #$0f",
"\tsta result",
"\tlda #$0f",
"\tsta result+1",
},
},
{
name: "arrow syntax",
line: "AND a WITH b -> result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tand b",
"\tsta result",
},
},
{
name: "word AND byte -> byte",
line: "AND wval WITH bval GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234)
st.AddVar("bval", "", compiler.KindByte, 0xFF)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda wval",
"\tand bval",
"\tsta result",
},
},
{
name: "word AND byte -> word",
line: "AND wval WITH bval GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234)
st.AddVar("bval", "", compiler.KindByte, 0xFF)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda wval",
"\tand bval",
"\tsta result",
"\tlda wval+1",
"\tand #0",
"\tsta result+1",
},
},
{
name: "error: unknown destination variable",
line: "AND a WITH b GIVING unknown",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: wrong separator",
line: "AND a TO b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: cannot assign to constant",
line: "AND a WITH b GIVING MAXVAL",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddConst("MAXVAL", "", compiler.KindByte, 255)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := compiler.NewCompilerContext(&preproc.Pragma{})
tt.setupVars(ctx.SymbolTable)
cmd := &AndCommand{}
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
err := cmd.Interpret(line, ctx)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
func TestAndCommand_NewSyntax(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
wantErr bool
}{
{
name: "byte & byte -> byte",
line: "result = a & b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("b", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tand b",
"\tsta result",
},
},
{
name: "byte & byte -> word",
line: "result = a & b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("b", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda a",
"\tand b",
"\tsta result",
"\tlda #0",
"\tand #0",
"\tsta result+1",
},
},
{
name: "word & word -> word",
line: "result = x & y",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x1234)
st.AddVar("y", "", compiler.KindWord, 0x0F0F)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda x",
"\tand y",
"\tsta result",
"\tlda x+1",
"\tand y+1",
"\tsta result+1",
},
},
{
name: "variable & literal",
line: "result = a & $F0",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tand #$f0",
"\tsta result",
},
},
{
name: "constant folding",
line: "result = 255 & 15",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$0f",
"\tsta result",
},
},
{
name: "constant folding word",
line: "result = $FFFF & $1234",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda #$34",
"\tsta result",
"\tlda #$12",
"\tsta result+1",
},
},
{
name: "using constant in expression",
line: "result = a & MASK",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddConst("MASK", "", compiler.KindByte, 0xF0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tand #$f0",
"\tsta result",
},
},
{
name: "error: unknown destination",
line: "unknown = a & b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: wrong operator",
line: "result = a + b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := compiler.NewCompilerContext(&preproc.Pragma{})
tt.setupVars(ctx.SymbolTable)
cmd := &AndCommand{}
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
err := cmd.Interpret(line, ctx)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsm(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// equalAsm compares two assembly slices for equality
func equalAsm(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

237
internal/commands/or.go Normal file
View file

@ -0,0 +1,237 @@
package commands
import (
"fmt"
"strings"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// OrCommand handles bitwise OR operations
// Syntax:
//
// OR <param1> WITH <param2> GIVING <dest> # old syntax with WITH/GIVING
// OR <param1> WITH <param2> -> <dest> # old syntax with WITH/->
// <dest> = <param1> | <param2> # new syntax
type OrCommand struct {
param1VarName string
param1VarKind compiler.VarKind
param1Value uint16
param1IsVar bool
param2VarName string
param2VarKind compiler.VarKind
param2Value uint16
param2IsVar bool
destVarName string
destVarKind compiler.VarKind
}
func (c *OrCommand) WillHandle(line preproc.Line) bool {
params, err := utils.ParseParams(line.Text)
if err != nil || len(params) == 0 {
return false
}
// Old syntax: OR ... (must have exactly 6 params)
if strings.ToUpper(params[0]) == "OR" && len(params) == 6 {
return true
}
// New syntax: <dest> = <param1> | <param2>
if len(params) == 5 && params[1] == "=" && params[3] == "|" {
return true
}
return false
}
func (c *OrCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
// Clear state
c.param1VarName = ""
c.param1IsVar = false
c.param1Value = 0
c.param2VarName = ""
c.param2IsVar = false
c.param2Value = 0
c.destVarName = ""
params, err := utils.ParseParams(line.Text)
if err != nil {
return err
}
paramCount := len(params)
scope := ctx.CurrentScope()
// Create constant lookup function
constLookup := func(name string) (int64, bool) {
sym := ctx.SymbolTable.Lookup(name, scope)
if sym != nil && sym.IsConst() {
return int64(sym.Value), true
}
return 0, false
}
// Determine syntax and parse accordingly
if strings.ToUpper(params[0]) == "OR" {
// Old syntax: OR <param1> WITH <param2> GIVING/-> <dest>
if paramCount != 6 {
return fmt.Errorf("OR: wrong number of parameters (%d), expected 6", paramCount)
}
separator1 := strings.ToUpper(params[2])
if separator1 != "WITH" {
return fmt.Errorf("OR: parameter #3 must be 'WITH', got %q", params[2])
}
separator2 := strings.ToUpper(params[4])
if separator2 != "GIVING" && separator2 != "->" {
return fmt.Errorf("OR: parameter #5 must be 'GIVING' or '->', got %q", params[4])
}
// Parse destination
destName := params[5]
destSym := ctx.SymbolTable.Lookup(destName, scope)
if destSym == nil {
return fmt.Errorf("OR: unknown variable %q", destName)
}
if destSym.IsConst() {
return fmt.Errorf("OR: cannot assign to constant %q", destName)
}
c.destVarName = destSym.FullName()
c.destVarKind = destSym.GetVarKind()
// Parse param1
var err error
c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam(
params[1], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("OR: param1: %w", err)
}
// Parse param2
c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam(
params[3], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("OR: param2: %w", err)
}
} else {
// New syntax: <dest> = <param1> | <param2>
if paramCount != 5 {
return fmt.Errorf("OR: wrong number of parameters (%d), expected 5", paramCount)
}
if params[1] != "=" {
return fmt.Errorf("OR: expected '=' at position 2, got %q", params[1])
}
if params[3] != "|" {
return fmt.Errorf("OR: expected '|' at position 4, got %q", params[3])
}
// Parse destination
destName := params[0]
destSym := ctx.SymbolTable.Lookup(destName, scope)
if destSym == nil {
return fmt.Errorf("OR: unknown variable %q", destName)
}
if destSym.IsConst() {
return fmt.Errorf("OR: cannot assign to constant %q", destName)
}
c.destVarName = destSym.FullName()
c.destVarKind = destSym.GetVarKind()
// Parse param1
var err error
c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam(
params[2], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("OR: param1: %w", err)
}
// Parse param2
c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam(
params[4], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("OR: param2: %w", err)
}
}
return nil
}
func (c *OrCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
var asm []string
// If both params are literals, fold at compile time
if !c.param1IsVar && !c.param2IsVar {
result := c.param1Value | c.param2Value
lo := uint8(result & 0xFF)
hi := uint8((result >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo))
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
if c.destVarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
return asm, nil
}
// At least one param is a variable - generate OR code
// Load param1
if c.param1IsVar {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
} else {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
}
// OR with param2
if c.param2IsVar {
asm = append(asm, fmt.Sprintf("\tora %s", c.param2VarName))
} else {
asm = append(asm, fmt.Sprintf("\tora #$%02x", uint8(c.param2Value&0xFF)))
}
// Store low byte
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
// If destination is word, handle high byte
if c.destVarKind == compiler.KindWord {
// Load high byte of param1
if c.param1IsVar {
if c.param1VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName))
} else {
asm = append(asm, "\tlda #0")
}
} else {
hi := uint8((c.param1Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
}
// OR with high byte of param2
if c.param2IsVar {
if c.param2VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tora %s+1", c.param2VarName))
} else {
asm = append(asm, "\tora #0")
}
} else {
hi := uint8((c.param2Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tora #$%02x", hi))
}
// Store high byte
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
return asm, nil
}

View file

@ -0,0 +1,426 @@
package commands
import (
"strings"
"testing"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
)
func TestOrCommand_WillHandle(t *testing.T) {
cmd := &OrCommand{}
tests := []struct {
name string
line string
want bool
}{
{"old syntax WITH/GIVING", "OR a WITH b GIVING c", true},
{"old syntax WITH/arrow", "OR a WITH b -> c", true},
{"new syntax", "result = x | y", true},
{"not OR", "ADD a TO b GIVING c", false},
{"wrong param count", "OR a b c", false},
{"empty", "", false},
{"new syntax wrong op", "result = x + y", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
got := cmd.WillHandle(line)
if got != tt.want {
t.Errorf("WillHandle() = %v, want %v", got, tt.want)
}
})
}
}
func TestOrCommand_OldSyntax(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
wantErr bool
}{
{
name: "byte OR byte -> byte (variables)",
line: "OR a WITH b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0)
st.AddVar("b", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tora b",
"\tsta result",
},
},
{
name: "byte OR byte -> word",
line: "OR a WITH b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0)
st.AddVar("b", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda a",
"\tora b",
"\tsta result",
"\tlda #0",
"\tora #0",
"\tsta result+1",
},
},
{
name: "word OR word -> word",
line: "OR x WITH y GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x1234)
st.AddVar("y", "", compiler.KindWord, 0x0F0F)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda x",
"\tora y",
"\tsta result",
"\tlda x+1",
"\tora y+1",
"\tsta result+1",
},
},
{
name: "byte OR literal -> byte",
line: "OR a WITH $0F GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tora #$0f",
"\tsta result",
},
},
{
name: "literal OR byte -> byte",
line: "OR 15 WITH b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xF0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$0f",
"\tora b",
"\tsta result",
},
},
{
name: "constant folding: 15 OR 240 -> byte",
line: "OR 15 WITH 240 GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$ff",
"\tsta result",
},
},
{
name: "constant folding: $00F0 OR $0F00 -> word",
line: "OR $00F0 WITH $0F00 GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda #$f0",
"\tsta result",
"\tlda #$0f",
"\tsta result+1",
},
},
{
name: "arrow syntax",
line: "OR a WITH b -> result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tora b",
"\tsta result",
},
},
{
name: "word OR byte -> byte",
line: "OR wval WITH bval GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234)
st.AddVar("bval", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda wval",
"\tora bval",
"\tsta result",
},
},
{
name: "word OR byte -> word",
line: "OR wval WITH bval GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234)
st.AddVar("bval", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda wval",
"\tora bval",
"\tsta result",
"\tlda wval+1",
"\tora #0",
"\tsta result+1",
},
},
{
name: "error: unknown destination variable",
line: "OR a WITH b GIVING unknown",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: wrong separator",
line: "OR a TO b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: cannot assign to constant",
line: "OR a WITH b GIVING MAXVAL",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddConst("MAXVAL", "", compiler.KindByte, 255)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := compiler.NewCompilerContext(&preproc.Pragma{})
tt.setupVars(ctx.SymbolTable)
cmd := &OrCommand{}
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
err := cmd.Interpret(line, ctx)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsmOr(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
func TestOrCommand_NewSyntax(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
wantErr bool
}{
{
name: "byte | byte -> byte",
line: "result = a | b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0)
st.AddVar("b", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tora b",
"\tsta result",
},
},
{
name: "byte | byte -> word",
line: "result = a | b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0)
st.AddVar("b", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda a",
"\tora b",
"\tsta result",
"\tlda #0",
"\tora #0",
"\tsta result+1",
},
},
{
name: "word | word -> word",
line: "result = x | y",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x1234)
st.AddVar("y", "", compiler.KindWord, 0x0F0F)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda x",
"\tora y",
"\tsta result",
"\tlda x+1",
"\tora y+1",
"\tsta result+1",
},
},
{
name: "variable | literal",
line: "result = a | $0F",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tora #$0f",
"\tsta result",
},
},
{
name: "constant folding",
line: "result = 15 | 240",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$ff",
"\tsta result",
},
},
{
name: "constant folding word",
line: "result = $00F0 | $0F00",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda #$f0",
"\tsta result",
"\tlda #$0f",
"\tsta result+1",
},
},
{
name: "using constant in expression",
line: "result = a | BITS",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xF0)
st.AddConst("BITS", "", compiler.KindByte, 0x0F)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\tora #$0f",
"\tsta result",
},
},
{
name: "error: unknown destination",
line: "unknown = a | b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: wrong operator",
line: "result = a + b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := compiler.NewCompilerContext(&preproc.Pragma{})
tt.setupVars(ctx.SymbolTable)
cmd := &OrCommand{}
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
err := cmd.Interpret(line, ctx)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsmOr(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// equalAsmOr compares two assembly slices for equality
func equalAsmOr(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

259
internal/commands/subtr.go Normal file
View file

@ -0,0 +1,259 @@
package commands
import (
"fmt"
"strings"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// SubtractCommand handles SUBTRACT operations
// Syntax:
//
// SUBTRACT <param2> FROM <param1> GIVING <dest> # old syntax: param1 - param2
// SUBT <param1> - <param2> -> <dest> # old syntax: param1 - param2
// <dest> = <param1> - <param2> # new syntax: param1 - param2
//
// Note: FROM syntax swaps parameter order (SUBTRACT a FROM b means b-a)
type SubtractCommand struct {
param1VarName string
param1VarKind compiler.VarKind
param1Value uint16
param1IsVar bool
param2VarName string
param2VarKind compiler.VarKind
param2Value uint16
param2IsVar bool
destVarName string
destVarKind compiler.VarKind
}
func (c *SubtractCommand) WillHandle(line preproc.Line) bool {
params, err := utils.ParseParams(line.Text)
if err != nil || len(params) == 0 {
return false
}
// Old syntax: SUBTRACT/SUBT ... (must have exactly 6 params)
kw := strings.ToUpper(params[0])
if (kw == "SUBTRACT" || kw == "SUBT") && len(params) == 6 {
return true
}
// New syntax: <dest> = <param1> - <param2>
if len(params) == 5 && params[1] == "=" && params[3] == "-" {
return true
}
return false
}
func (c *SubtractCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
// Clear state
c.param1VarName = ""
c.param1IsVar = false
c.param1Value = 0
c.param2VarName = ""
c.param2IsVar = false
c.param2Value = 0
c.destVarName = ""
params, err := utils.ParseParams(line.Text)
if err != nil {
return err
}
paramCount := len(params)
scope := ctx.CurrentScope()
// Create constant lookup function
constLookup := func(name string) (int64, bool) {
sym := ctx.SymbolTable.Lookup(name, scope)
if sym != nil && sym.IsConst() {
return int64(sym.Value), true
}
return 0, false
}
// Determine syntax and parse accordingly
kw := strings.ToUpper(params[0])
if kw == "SUBTRACT" || kw == "SUBT" {
// Old syntax: SUBTRACT/SUBT <p1> FROM/- <p2> GIVING/-> <dest>
if paramCount != 6 {
return fmt.Errorf("SUBTRACT: wrong number of parameters (%d), expected 6", paramCount)
}
separator1 := strings.ToUpper(params[2])
if separator1 != "FROM" && separator1 != "-" {
return fmt.Errorf("SUBTRACT: parameter #3 must be 'FROM' or '-', got %q", params[2])
}
separator2 := strings.ToUpper(params[4])
if separator2 != "GIVING" && separator2 != "->" {
return fmt.Errorf("SUBTRACT: parameter #5 must be 'GIVING' or '->', got %q", params[4])
}
// Parse destination
destName := params[5]
destSym := ctx.SymbolTable.Lookup(destName, scope)
if destSym == nil {
return fmt.Errorf("SUBTRACT: unknown variable %q", destName)
}
if destSym.IsConst() {
return fmt.Errorf("SUBTRACT: cannot assign to constant %q", destName)
}
c.destVarName = destSym.FullName()
c.destVarKind = destSym.GetVarKind()
// Parse parameters based on separator
// FROM syntax: SUBTRACT a FROM b means b - a (swap needed)
// Minus syntax: SUBT a - b means a - b (no swap)
var minuendParam, subtrahendParam string
if separator1 == "FROM" {
// Swap: position 4 becomes minuend, position 2 becomes subtrahend
minuendParam = params[3]
subtrahendParam = params[1]
} else {
// No swap: position 2 is minuend, position 4 is subtrahend
minuendParam = params[1]
subtrahendParam = params[3]
}
// Parse minuend (param1)
var err error
c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam(
minuendParam, ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("SUBTRACT: minuend: %w", err)
}
// Parse subtrahend (param2)
c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam(
subtrahendParam, ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("SUBTRACT: subtrahend: %w", err)
}
} else {
// New syntax: <dest> = <param1> - <param2>
if paramCount != 5 {
return fmt.Errorf("SUBTRACT: wrong number of parameters (%d), expected 5", paramCount)
}
if params[1] != "=" {
return fmt.Errorf("SUBTRACT: expected '=' at position 2, got %q", params[1])
}
if params[3] != "-" {
return fmt.Errorf("SUBTRACT: expected '-' at position 4, got %q", params[3])
}
// Parse destination
destName := params[0]
destSym := ctx.SymbolTable.Lookup(destName, scope)
if destSym == nil {
return fmt.Errorf("SUBTRACT: unknown variable %q", destName)
}
if destSym.IsConst() {
return fmt.Errorf("SUBTRACT: cannot assign to constant %q", destName)
}
c.destVarName = destSym.FullName()
c.destVarKind = destSym.GetVarKind()
// Parse param1 (minuend)
var err error
c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam(
params[2], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("SUBTRACT: param1: %w", err)
}
// Parse param2 (subtrahend)
c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam(
params[4], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("SUBTRACT: param2: %w", err)
}
}
return nil
}
func (c *SubtractCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
var asm []string
// If both params are literals, fold at compile time
if !c.param1IsVar && !c.param2IsVar {
// param1 - param2 (minuend - subtrahend)
result := uint16(int32(c.param1Value) - int32(c.param2Value))
lo := uint8(result & 0xFF)
hi := uint8((result >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo))
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
if c.destVarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
return asm, nil
}
// At least one param is a variable - generate subtract code
// SEC sets carry for borrow
asm = append(asm, "\tsec")
// Load minuend (param1)
if c.param1IsVar {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
} else {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
}
// Subtract subtrahend (param2)
if c.param2IsVar {
asm = append(asm, fmt.Sprintf("\tsbc %s", c.param2VarName))
} else {
asm = append(asm, fmt.Sprintf("\tsbc #$%02x", uint8(c.param2Value&0xFF)))
}
// Store low byte
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
// If destination is word, handle high byte
if c.destVarKind == compiler.KindWord {
// Load high byte of minuend (param1)
if c.param1IsVar {
if c.param1VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName))
} else {
asm = append(asm, "\tlda #0")
}
} else {
hi := uint8((c.param1Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
}
// Subtract high byte of subtrahend (param2)
if c.param2IsVar {
if c.param2VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tsbc %s+1", c.param2VarName))
} else {
asm = append(asm, "\tsbc #0")
}
} else {
hi := uint8((c.param2Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tsbc #$%02x", hi))
}
// Store high byte
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
return asm, nil
}

View file

@ -0,0 +1,510 @@
package commands
import (
"strings"
"testing"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
)
func TestSubtractCommand_WillHandle(t *testing.T) {
cmd := &SubtractCommand{}
tests := []struct {
name string
line string
want bool
}{
{"old syntax SUBTRACT FROM/GIVING", "SUBTRACT a FROM b GIVING c", true},
{"old syntax SUBT FROM/arrow", "SUBT a FROM b -> c", true},
{"old syntax SUBT minus/arrow", "SUBT a - b -> c", true},
{"old syntax SUBTRACT minus/GIVING", "SUBTRACT a - b GIVING c", true},
{"new syntax", "result = x - y", true},
{"not SUBTRACT", "ADD a TO b GIVING c", false},
{"wrong param count", "SUBTRACT a b c", false},
{"empty", "", false},
{"new syntax wrong op", "result = x + y", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
got := cmd.WillHandle(line)
if got != tt.want {
t.Errorf("WillHandle() = %v, want %v", got, tt.want)
}
})
}
}
func TestSubtractCommand_OldSyntax_FROM(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
wantErr bool
}{
{
name: "SUBTRACT a FROM b (means b-a)",
line: "SUBTRACT 5 FROM 10 GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$05",
"\tsta result",
},
},
{
name: "SUBTRACT byte FROM byte -> byte (variables)",
line: "SUBTRACT a FROM b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 10)
st.AddVar("b", "", compiler.KindByte, 20)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tsec",
"\tlda b",
"\tsbc a",
"\tsta result",
},
},
{
name: "SUBT a FROM b -> c with arrow",
line: "SUBT a FROM b -> result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 10)
st.AddVar("b", "", compiler.KindByte, 20)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tsec",
"\tlda b",
"\tsbc a",
"\tsta result",
},
},
{
name: "SUBTRACT literal FROM variable",
line: "SUBTRACT 10 FROM a GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 20)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tsec",
"\tlda a",
"\tsbc #$0a",
"\tsta result",
},
},
{
name: "word FROM word -> word",
line: "SUBTRACT x FROM y GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x1000)
st.AddVar("y", "", compiler.KindWord, 0x2000)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tsec",
"\tlda y",
"\tsbc x",
"\tsta result",
"\tlda y+1",
"\tsbc x+1",
"\tsta result+1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := compiler.NewCompilerContext(&preproc.Pragma{})
tt.setupVars(ctx.SymbolTable)
cmd := &SubtractCommand{}
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
err := cmd.Interpret(line, ctx)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsmSubtr(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
func TestSubtractCommand_OldSyntax_Minus(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
wantErr bool
}{
{
name: "SUBT a - b (means a-b, no swap)",
line: "SUBT 10 - 5 GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$05",
"\tsta result",
},
},
{
name: "SUBT byte - byte -> byte (variables)",
line: "SUBT a - b -> result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 20)
st.AddVar("b", "", compiler.KindByte, 10)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tsec",
"\tlda a",
"\tsbc b",
"\tsta result",
},
},
{
name: "SUBTRACT a - b GIVING c",
line: "SUBTRACT a - b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 20)
st.AddVar("b", "", compiler.KindByte, 10)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tsec",
"\tlda a",
"\tsbc b",
"\tsta result",
},
},
{
name: "SUBT variable - literal",
line: "SUBT a - 10 -> result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 20)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tsec",
"\tlda a",
"\tsbc #$0a",
"\tsta result",
},
},
{
name: "word - word -> word",
line: "SUBT x - y -> result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x2000)
st.AddVar("y", "", compiler.KindWord, 0x1000)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tsec",
"\tlda x",
"\tsbc y",
"\tsta result",
"\tlda x+1",
"\tsbc y+1",
"\tsta result+1",
},
},
{
name: "byte - byte -> word",
line: "SUBT a - b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 20)
st.AddVar("b", "", compiler.KindByte, 10)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tsec",
"\tlda a",
"\tsbc b",
"\tsta result",
"\tlda #0",
"\tsbc #0",
"\tsta result+1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := compiler.NewCompilerContext(&preproc.Pragma{})
tt.setupVars(ctx.SymbolTable)
cmd := &SubtractCommand{}
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
err := cmd.Interpret(line, ctx)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsmSubtr(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
func TestSubtractCommand_NewSyntax(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
wantErr bool
}{
{
name: "byte - byte -> byte",
line: "result = a - b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 20)
st.AddVar("b", "", compiler.KindByte, 10)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tsec",
"\tlda a",
"\tsbc b",
"\tsta result",
},
},
{
name: "byte - byte -> word",
line: "result = a - b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 20)
st.AddVar("b", "", compiler.KindByte, 10)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tsec",
"\tlda a",
"\tsbc b",
"\tsta result",
"\tlda #0",
"\tsbc #0",
"\tsta result+1",
},
},
{
name: "word - word -> word",
line: "result = x - y",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x2000)
st.AddVar("y", "", compiler.KindWord, 0x1000)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tsec",
"\tlda x",
"\tsbc y",
"\tsta result",
"\tlda x+1",
"\tsbc y+1",
"\tsta result+1",
},
},
{
name: "variable - literal",
line: "result = a - 10",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 20)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tsec",
"\tlda a",
"\tsbc #$0a",
"\tsta result",
},
},
{
name: "literal - variable",
line: "result = 20 - b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 10)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tsec",
"\tlda #$14",
"\tsbc b",
"\tsta result",
},
},
{
name: "constant folding",
line: "result = 100 - 25",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$4b",
"\tsta result",
},
},
{
name: "constant folding word",
line: "result = $2000 - $1000",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda #$00",
"\tsta result",
"\tlda #$10",
"\tsta result+1",
},
},
{
name: "using constant in expression",
line: "result = a - OFFSET",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 100)
st.AddConst("OFFSET", "", compiler.KindByte, 10)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tsec",
"\tlda a",
"\tsbc #$0a",
"\tsta result",
},
},
{
name: "word - byte -> word",
line: "result = wval - bval",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234)
st.AddVar("bval", "", compiler.KindByte, 0x10)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tsec",
"\tlda wval",
"\tsbc bval",
"\tsta result",
"\tlda wval+1",
"\tsbc #0",
"\tsta result+1",
},
},
{
name: "error: unknown destination",
line: "unknown = a - b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: wrong operator",
line: "result = a + b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: cannot assign to constant",
line: "MAXVAL = a - b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddConst("MAXVAL", "", compiler.KindByte, 255)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := compiler.NewCompilerContext(&preproc.Pragma{})
tt.setupVars(ctx.SymbolTable)
cmd := &SubtractCommand{}
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
err := cmd.Interpret(line, ctx)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsmSubtr(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// equalAsmSubtr compares two assembly slices for equality
func equalAsmSubtr(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

237
internal/commands/xor.go Normal file
View file

@ -0,0 +1,237 @@
package commands
import (
"fmt"
"strings"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// XorCommand handles bitwise XOR operations
// Syntax:
//
// XOR <param1> WITH <param2> GIVING <dest> # old syntax with WITH/GIVING
// XOR <param1> WITH <param2> -> <dest> # old syntax with WITH/->
// <dest> = <param1> ^ <param2> # new syntax
type XorCommand struct {
param1VarName string
param1VarKind compiler.VarKind
param1Value uint16
param1IsVar bool
param2VarName string
param2VarKind compiler.VarKind
param2Value uint16
param2IsVar bool
destVarName string
destVarKind compiler.VarKind
}
func (c *XorCommand) WillHandle(line preproc.Line) bool {
params, err := utils.ParseParams(line.Text)
if err != nil || len(params) == 0 {
return false
}
// Old syntax: XOR ... (must have exactly 6 params)
if strings.ToUpper(params[0]) == "XOR" && len(params) == 6 {
return true
}
// New syntax: <dest> = <param1> ^ <param2>
if len(params) == 5 && params[1] == "=" && params[3] == "^" {
return true
}
return false
}
func (c *XorCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
// Clear state
c.param1VarName = ""
c.param1IsVar = false
c.param1Value = 0
c.param2VarName = ""
c.param2IsVar = false
c.param2Value = 0
c.destVarName = ""
params, err := utils.ParseParams(line.Text)
if err != nil {
return err
}
paramCount := len(params)
scope := ctx.CurrentScope()
// Create constant lookup function
constLookup := func(name string) (int64, bool) {
sym := ctx.SymbolTable.Lookup(name, scope)
if sym != nil && sym.IsConst() {
return int64(sym.Value), true
}
return 0, false
}
// Determine syntax and parse accordingly
if strings.ToUpper(params[0]) == "XOR" {
// Old syntax: XOR <param1> WITH <param2> GIVING/-> <dest>
if paramCount != 6 {
return fmt.Errorf("XOR: wrong number of parameters (%d), expected 6", paramCount)
}
separator1 := strings.ToUpper(params[2])
if separator1 != "WITH" {
return fmt.Errorf("XOR: parameter #3 must be 'WITH', got %q", params[2])
}
separator2 := strings.ToUpper(params[4])
if separator2 != "GIVING" && separator2 != "->" {
return fmt.Errorf("XOR: parameter #5 must be 'GIVING' or '->', got %q", params[4])
}
// Parse destination
destName := params[5]
destSym := ctx.SymbolTable.Lookup(destName, scope)
if destSym == nil {
return fmt.Errorf("XOR: unknown variable %q", destName)
}
if destSym.IsConst() {
return fmt.Errorf("XOR: cannot assign to constant %q", destName)
}
c.destVarName = destSym.FullName()
c.destVarKind = destSym.GetVarKind()
// Parse param1
var err error
c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam(
params[1], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("XOR: param1: %w", err)
}
// Parse param2
c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam(
params[3], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("XOR: param2: %w", err)
}
} else {
// New syntax: <dest> = <param1> ^ <param2>
if paramCount != 5 {
return fmt.Errorf("XOR: wrong number of parameters (%d), expected 5", paramCount)
}
if params[1] != "=" {
return fmt.Errorf("XOR: expected '=' at position 2, got %q", params[1])
}
if params[3] != "^" {
return fmt.Errorf("XOR: expected '^' at position 4, got %q", params[3])
}
// Parse destination
destName := params[0]
destSym := ctx.SymbolTable.Lookup(destName, scope)
if destSym == nil {
return fmt.Errorf("XOR: unknown variable %q", destName)
}
if destSym.IsConst() {
return fmt.Errorf("XOR: cannot assign to constant %q", destName)
}
c.destVarName = destSym.FullName()
c.destVarKind = destSym.GetVarKind()
// Parse param1
var err error
c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam(
params[2], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("XOR: param1: %w", err)
}
// Parse param2
c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam(
params[4], ctx.SymbolTable, scope, constLookup)
if err != nil {
return fmt.Errorf("XOR: param2: %w", err)
}
}
return nil
}
func (c *XorCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
var asm []string
// If both params are literals, fold at compile time
if !c.param1IsVar && !c.param2IsVar {
result := c.param1Value ^ c.param2Value
lo := uint8(result & 0xFF)
hi := uint8((result >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo))
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
if c.destVarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
return asm, nil
}
// At least one param is a variable - generate XOR code
// Load param1
if c.param1IsVar {
asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName))
} else {
asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF)))
}
// XOR with param2
if c.param2IsVar {
asm = append(asm, fmt.Sprintf("\teor %s", c.param2VarName))
} else {
asm = append(asm, fmt.Sprintf("\teor #$%02x", uint8(c.param2Value&0xFF)))
}
// Store low byte
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
// If destination is word, handle high byte
if c.destVarKind == compiler.KindWord {
// Load high byte of param1
if c.param1IsVar {
if c.param1VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName))
} else {
asm = append(asm, "\tlda #0")
}
} else {
hi := uint8((c.param1Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
}
// XOR with high byte of param2
if c.param2IsVar {
if c.param2VarKind == compiler.KindWord {
asm = append(asm, fmt.Sprintf("\teor %s+1", c.param2VarName))
} else {
asm = append(asm, "\teor #0")
}
} else {
hi := uint8((c.param2Value >> 8) & 0xFF)
asm = append(asm, fmt.Sprintf("\teor #$%02x", hi))
}
// Store high byte
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
}
return asm, nil
}

View file

@ -0,0 +1,426 @@
package commands
import (
"strings"
"testing"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
)
func TestXorCommand_WillHandle(t *testing.T) {
cmd := &XorCommand{}
tests := []struct {
name string
line string
want bool
}{
{"old syntax WITH/GIVING", "XOR a WITH b GIVING c", true},
{"old syntax WITH/arrow", "XOR a WITH b -> c", true},
{"new syntax", "result = x ^ y", true},
{"not XOR", "ADD a TO b GIVING c", false},
{"wrong param count", "XOR a b c", false},
{"empty", "", false},
{"new syntax wrong op", "result = x + y", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
got := cmd.WillHandle(line)
if got != tt.want {
t.Errorf("WillHandle() = %v, want %v", got, tt.want)
}
})
}
}
func TestXorCommand_OldSyntax(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
wantErr bool
}{
{
name: "byte XOR byte -> byte (variables)",
line: "XOR a WITH b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("b", "", compiler.KindByte, 0xAA)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\teor b",
"\tsta result",
},
},
{
name: "byte XOR byte -> word",
line: "XOR a WITH b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("b", "", compiler.KindByte, 0xAA)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda a",
"\teor b",
"\tsta result",
"\tlda #0",
"\teor #0",
"\tsta result+1",
},
},
{
name: "word XOR word -> word",
line: "XOR x WITH y GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x1234)
st.AddVar("y", "", compiler.KindWord, 0x5678)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda x",
"\teor y",
"\tsta result",
"\tlda x+1",
"\teor y+1",
"\tsta result+1",
},
},
{
name: "byte XOR literal -> byte",
line: "XOR a WITH $AA GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\teor #$aa",
"\tsta result",
},
},
{
name: "literal XOR byte -> byte",
line: "XOR 255 WITH b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("b", "", compiler.KindByte, 0xAA)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$ff",
"\teor b",
"\tsta result",
},
},
{
name: "constant folding: 255 XOR 170 -> byte",
line: "XOR 255 WITH 170 GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$55",
"\tsta result",
},
},
{
name: "constant folding: $FFFF XOR $AAAA -> word",
line: "XOR $FFFF WITH $AAAA GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda #$55",
"\tsta result",
"\tlda #$55",
"\tsta result+1",
},
},
{
name: "arrow syntax",
line: "XOR a WITH b -> result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\teor b",
"\tsta result",
},
},
{
name: "word XOR byte -> byte",
line: "XOR wval WITH bval GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234)
st.AddVar("bval", "", compiler.KindByte, 0xFF)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda wval",
"\teor bval",
"\tsta result",
},
},
{
name: "word XOR byte -> word",
line: "XOR wval WITH bval GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("wval", "", compiler.KindWord, 0x1234)
st.AddVar("bval", "", compiler.KindByte, 0xFF)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda wval",
"\teor bval",
"\tsta result",
"\tlda wval+1",
"\teor #0",
"\tsta result+1",
},
},
{
name: "error: unknown destination variable",
line: "XOR a WITH b GIVING unknown",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: wrong separator",
line: "XOR a TO b GIVING result",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: cannot assign to constant",
line: "XOR a WITH b GIVING MAXVAL",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddConst("MAXVAL", "", compiler.KindByte, 255)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := compiler.NewCompilerContext(&preproc.Pragma{})
tt.setupVars(ctx.SymbolTable)
cmd := &XorCommand{}
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
err := cmd.Interpret(line, ctx)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsmXor(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
func TestXorCommand_NewSyntax(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
wantErr bool
}{
{
name: "byte ^ byte -> byte",
line: "result = a ^ b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("b", "", compiler.KindByte, 0xAA)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\teor b",
"\tsta result",
},
},
{
name: "byte ^ byte -> word",
line: "result = a ^ b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("b", "", compiler.KindByte, 0xAA)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda a",
"\teor b",
"\tsta result",
"\tlda #0",
"\teor #0",
"\tsta result+1",
},
},
{
name: "word ^ word -> word",
line: "result = x ^ y",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("x", "", compiler.KindWord, 0x1234)
st.AddVar("y", "", compiler.KindWord, 0x5678)
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda x",
"\teor y",
"\tsta result",
"\tlda x+1",
"\teor y+1",
"\tsta result+1",
},
},
{
name: "variable ^ literal",
line: "result = a ^ $AA",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\teor #$aa",
"\tsta result",
},
},
{
name: "constant folding",
line: "result = 255 ^ 170",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda #$55",
"\tsta result",
},
},
{
name: "constant folding word",
line: "result = $FFFF ^ $AAAA",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("result", "", compiler.KindWord, 0)
},
wantAsm: []string{
"\tlda #$55",
"\tsta result",
"\tlda #$55",
"\tsta result+1",
},
},
{
name: "using constant in expression",
line: "result = a ^ MASK",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0xFF)
st.AddConst("MASK", "", compiler.KindByte, 0xAA)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantAsm: []string{
"\tlda a",
"\teor #$aa",
"\tsta result",
},
},
{
name: "error: unknown destination",
line: "unknown = a ^ b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
},
wantErr: true,
},
{
name: "error: wrong operator",
line: "result = a + b",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("a", "", compiler.KindByte, 0)
st.AddVar("b", "", compiler.KindByte, 0)
st.AddVar("result", "", compiler.KindByte, 0)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := compiler.NewCompilerContext(&preproc.Pragma{})
tt.setupVars(ctx.SymbolTable)
cmd := &XorCommand{}
line := preproc.Line{Text: tt.line, Kind: preproc.Source}
err := cmd.Interpret(line, ctx)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !equalAsmXor(asm, tt.wantAsm) {
t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(asm, "\n"),
strings.Join(tt.wantAsm, "\n"))
}
})
}
}
// equalAsmXor compares two assembly slices for equality
func equalAsmXor(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View file

@ -1,6 +1,7 @@
package compiler
import (
"c65gm/internal/utils"
"fmt"
"c65gm/internal/preproc"
@ -57,3 +58,47 @@ type UnhandledLineError struct {
func (e *UnhandledLineError) Error() string {
return fmt.Sprintf("%s:%d: unhandled line: %s", e.Line.Filename, e.Line.LineNo, e.Line.Text)
}
// ParseOperandParam parses a command parameter that can be a variable or expression
// Returns: varName, varKind, value, isVar, error
func ParseOperandParam(
param string,
symTable *SymbolTable,
scope []string,
constLookup utils.ConstantLookup,
) (varName string, varKind VarKind, value uint16, isVar bool, err error) {
// Try variable lookup first
sym := symTable.Lookup(param, scope)
if sym != nil {
varKind = sym.GetVarKind()
value = sym.Value
// Constants are treated as literals (inlined), not variables
if sym.IsConst() {
varName = sym.FullName() // Preserve name for documentation
isVar = false
return
}
// It's a variable
varName = sym.FullName()
isVar = true
return
}
// Not a variable, must be an expression
val, evalErr := utils.EvaluateExpression(param, constLookup)
if evalErr != nil {
err = fmt.Errorf("not a valid variable or expression: %w", evalErr)
return
}
if val < 0 || val > 65535 {
err = fmt.Errorf("value %d out of range (0-65535)", val)
return
}
value = uint16(val)
isVar = false
return
}

View file

@ -384,3 +384,11 @@ func GenerateVariables(st *SymbolTable) []string {
return nil
}
// GetVarKind extracts VarKind from Symbol
func (s *Symbol) GetVarKind() VarKind {
if s.IsByte() {
return KindByte
}
return KindWord
}

View file

@ -79,6 +79,10 @@ func registerCommands(comp *compiler.Compiler) {
comp.Registry().Register(&commands.ByteCommand{})
comp.Registry().Register(&commands.WordCommand{})
comp.Registry().Register(&commands.AddCommand{})
comp.Registry().Register(&commands.AndCommand{})
comp.Registry().Register(&commands.OrCommand{})
comp.Registry().Register(&commands.XorCommand{})
comp.Registry().Register(&commands.SubtractCommand{})
}
func writeOutput(filename string, lines []string) error {