Fixed bug in BYTE REGISTER. Made POINTER use one register

This commit is contained in:
Mattias Hansson 2026-08-01 02:01:19 +02:00
parent f0bf290bac
commit 471f33fcd0
4 changed files with 369 additions and 17 deletions

View file

@ -106,29 +106,29 @@ func (c *PointerCommand) Generate(ctx *compiler.CompilerContext) ([]string, erro
// Label reference
if c.isLabel {
asm = append(asm, fmt.Sprintf("\tldx #<%s", c.targetLabel))
asm = append(asm, fmt.Sprintf("\tlda #<%s", c.targetLabel))
asm = append(asm, fmt.Sprintf("\tsta %s", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tlda #>%s", c.targetLabel))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName))
return asm, nil
}
// Variable reference
if c.isVar {
asm = append(asm, fmt.Sprintf("\tldx #<%s", c.targetVarName))
asm = append(asm, fmt.Sprintf("\tlda #<%s", c.targetVarName))
asm = append(asm, fmt.Sprintf("\tsta %s", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tlda #>%s", c.targetVarName))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName))
return asm, nil
}
// Numeric address - create temp label
tempLabel := ctx.GeneralStack.Push()
asm = append(asm, fmt.Sprintf("%s = %d", tempLabel, c.targetAddress))
asm = append(asm, fmt.Sprintf("\tldx #<%s", tempLabel))
asm = append(asm, fmt.Sprintf("\tlda #<%s", tempLabel))
asm = append(asm, fmt.Sprintf("\tsta %s", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tlda #>%s", tempLabel))
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName))
return asm, nil
}

View file

@ -0,0 +1,131 @@
package commands
import (
"strings"
"testing"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
)
func TestPointerCommand_Generate(t *testing.T) {
tests := []struct {
name string
line string
setupVars func(*compiler.SymbolTable)
wantAsm []string
}{
{
name: "pointer to label — uses A only",
line: "POINTER ptr -> TARGET",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("ptr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #<TARGET",
"\tsta ptr",
"\tlda #>TARGET",
"\tsta ptr+1",
},
},
{
name: "pointer to variable — uses A only",
line: "POINTER ptr TO targetVar",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("ptr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("targetVar", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
wantAsm: []string{
"\tlda #<targetVar",
"\tsta ptr",
"\tlda #>targetVar",
"\tsta ptr+1",
},
},
{
name: "pointer to numeric address — uses A only",
line: "POINTER ptr -> 53280",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("ptr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
tt.setupVars(ctx.SymbolTable)
cmd := &PointerCommand{}
line := preproc.Line{
Text: tt.line,
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if tt.name == "pointer to numeric address — uses A only" {
foundLo := false
foundHi := false
foundSta := false
for _, a := range asm {
if strings.Contains(a, "lda #<") {
foundLo = true
}
if strings.Contains(a, "lda #>") {
foundHi = true
}
if strings.Contains(a, "sta ptr+1") {
foundSta = true
}
}
if !foundLo || !foundHi || !foundSta {
t.Errorf("expected A-only pattern (lda #< / sta ptr / lda #> / sta ptr+1), got:\n%s", strings.Join(asm, "\n"))
}
return
}
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 TestPointerCommand_NoXRegister(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("ptr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
cmd := &PointerCommand{}
line := preproc.Line{
Text: "POINTER ptr -> $0400",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := cmd.Interpret(line, ctx); err != nil {
t.Fatalf("Interpret() error = %v", err)
}
asm, err := cmd.Generate(ctx)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
joined := strings.Join(asm, "\n")
if strings.Contains(joined, "ldx") || strings.Contains(joined, "stx") {
t.Errorf("POINTER should not use X register, got:\n%s", joined)
}
}

View file

@ -866,3 +866,206 @@ func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) {
t.Errorf("expected sta spill_me_bg to be kept, got:\n%s", joined)
}
}
func TestPassRegDead_ReadModifyWrite(t *testing.T) {
tests := []struct {
name string
opcode string
lines []string
}{
{
name: "dec reads stored value",
opcode: "dec",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tdec rmw_var",
"\trts",
},
},
{
name: "inc reads stored value",
opcode: "inc",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tinc rmw_var",
"\trts",
},
},
{
name: "adc reads stored value",
opcode: "adc",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tclc",
"\tadc rmw_var",
"\trts",
},
},
{
name: "sbc reads stored value",
opcode: "sbc",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tsec",
"\tsbc rmw_var",
"\trts",
},
},
{
name: "and reads stored value",
opcode: "and",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tand rmw_var",
"\trts",
},
},
{
name: "ora reads stored value",
opcode: "ora",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tora rmw_var",
"\trts",
},
},
{
name: "eor reads stored value",
opcode: "eor",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\teor rmw_var",
"\trts",
},
},
{
name: "cmp reads stored value",
opcode: "cmp",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tcmp rmw_var",
"\trts",
},
},
{
name: "ldx reads stored value",
opcode: "ldx",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tldx rmw_var",
"\trts",
},
},
{
name: "ldy reads stored value",
opcode: "ldy",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tldy rmw_var",
"\trts",
},
},
{
name: "asl reads stored value",
opcode: "asl",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tasl rmw_var",
"\trts",
},
},
{
name: "lsr reads stored value",
opcode: "lsr",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tlsr rmw_var",
"\trts",
},
},
{
name: "rol reads stored value",
opcode: "rol",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\trol rmw_var",
"\trts",
},
},
{
name: "ror reads stored value",
opcode: "ror",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tror rmw_var",
"\trts",
},
},
{
name: "bit reads stored value",
opcode: "bit",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tbit rmw_var",
"\trts",
},
},
{
name: "cpx reads stored value",
opcode: "cpx",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tcpx rmw_var",
"\trts",
},
},
{
name: "cpy reads stored value",
opcode: "cpy",
lines: []string{
"\tlda #$05",
"\tsta rmw_var",
"\tcpy rmw_var",
"\trts",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
EnableRegisterVars: true,
RegisterVars: map[string]bool{
"rmw_var": true,
},
}
output, dissolved := Optimize(tt.lines, cfg)
if dissolved["rmw_var"] {
t.Errorf("rmw_var should NOT be dissolved — %s reads its value", tt.opcode)
}
joined := strings.Join(output, "\n")
if !strings.Contains(joined, "sta rmw_var") {
t.Errorf("expected sta rmw_var to be kept (followed by %s), got:\n%s", tt.opcode, joined)
}
})
}
}

View file

@ -2,19 +2,19 @@ package optimizer
// passRegDead eliminates dead stores to REGISTER variables.
// Two-phase approach:
// 1. Global pre-scan: if no lda regVar exists anywhere in the input,
// 1. Global pre-scan: if no instruction reads regVar anywhere in the input,
// all stores to that variable are provably dead (labels are irrelevant).
// 2. Per-store local scan: for variables that DO have at least one lda,
// 2. Per-store local scan: for variables that DO have at least one read,
// scan forward through the current basic block.
//
// This is safe because REGISTER variables have a contract: nothing external
// can observe their memory location (ASM/Script/Macro references are compile errors).
func passRegDead(lines []asmLine, registerVars map[string]bool) []asmLine {
// Phase 1 — global pre-scan: which REGISTER vars have at least one lda?
hasLda := make(map[string]bool, len(registerVars))
// Phase 1 — global pre-scan: which REGISTER vars have at least one read?
hasRead := make(map[string]bool, len(registerVars))
for _, l := range lines {
if l.isCode && l.opcode == "lda" && l.operand != "" && registerVars[l.operand] {
hasLda[l.operand] = true
if l.isCode && l.operand != "" && registerVars[l.operand] && readsFrom(l.opcode, l.operand, l.operand) {
hasRead[l.operand] = true
}
}
@ -24,8 +24,8 @@ func passRegDead(lines []asmLine, registerVars map[string]bool) []asmLine {
line := lines[i]
if line.isCode && line.opcode == "sta" && line.operand != "" && registerVars[line.operand] {
if !hasLda[line.operand] {
// Globally dead — no lda anywhere in the program
if !hasRead[line.operand] {
// Globally dead — no read anywhere in the program
continue
}
if isRegStoreDead(lines, i+1, line.operand) {
@ -41,8 +41,8 @@ func passRegDead(lines []asmLine, registerVars map[string]bool) []asmLine {
// isRegStoreDead scans forward from start to determine whether a store to a
// REGISTER variable operand is dead. Returns true if the store can be removed.
// A store is dead only if no matching lda is found anywhere forward before
// a label or control-flow-ending instruction.
// A store is dead only if no instruction that reads the operand is found
// anywhere forward before a label or control-flow-ending instruction.
func isRegStoreDead(lines []asmLine, start int, operand string) bool {
for i := start; i < len(lines); i++ {
l := lines[i]
@ -63,7 +63,7 @@ func isRegStoreDead(lines []asmLine, start int, operand string) bool {
return true // rts/jmp/brk/rti — execution ends here
}
if l.isCode && l.opcode == "lda" && l.operand == operand {
if l.isCode && readsFrom(l.opcode, l.operand, operand) {
return false // the value IS read from memory later
}
}
@ -71,6 +71,24 @@ func isRegStoreDead(lines []asmLine, start int, operand string) bool {
return true // end of block reached with no matching load
}
// readsFrom returns true if the instruction reads from the given memory operand.
// Covers loads (lda/ldx/ldy), compares (cmp/cpx/cpy), ALU ops (adc/sbc/and/ora/eor/bit),
// and read-modify-write instructions (dec/inc/asl/lsr/rol/ror).
func readsFrom(opcode, operand, varName string) bool {
if operand != varName {
return false
}
switch opcode {
case "lda", "ldx", "ldy":
return true
case "adc", "sbc", "and", "ora", "eor", "cmp", "cpx", "cpy", "bit":
return true
case "dec", "inc", "asl", "lsr", "rol", "ror":
return true
}
return false
}
// blocksFlow returns true for instructions that unconditionally end the current
// execution path: rts, jmp, brk, rti
func blocksFlow(line asmLine) bool {