Fixed bug in BYTE REGISTER. Made POINTER use one register
This commit is contained in:
parent
f0bf290bac
commit
471f33fcd0
4 changed files with 369 additions and 17 deletions
|
|
@ -106,29 +106,29 @@ func (c *PointerCommand) Generate(ctx *compiler.CompilerContext) ([]string, erro
|
||||||
|
|
||||||
// Label reference
|
// Label reference
|
||||||
if c.isLabel {
|
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("\tlda #>%s", c.targetLabel))
|
||||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
|
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
|
||||||
asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName))
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variable reference
|
// Variable reference
|
||||||
if c.isVar {
|
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("\tlda #>%s", c.targetVarName))
|
||||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
|
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
|
||||||
asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName))
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Numeric address - create temp label
|
// Numeric address - create temp label
|
||||||
tempLabel := ctx.GeneralStack.Push()
|
tempLabel := ctx.GeneralStack.Push()
|
||||||
asm = append(asm, fmt.Sprintf("%s = %d", tempLabel, c.targetAddress))
|
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("\tlda #>%s", tempLabel))
|
||||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
|
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName))
|
||||||
asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName))
|
|
||||||
|
|
||||||
return asm, nil
|
return asm, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
131
internal/commands/point_test.go
Normal file
131
internal/commands/point_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -866,3 +866,206 @@ func TestPassRegDead_NotGloballyDeadWithLda(t *testing.T) {
|
||||||
t.Errorf("expected sta spill_me_bg to be kept, got:\n%s", joined)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,19 @@ package optimizer
|
||||||
|
|
||||||
// passRegDead eliminates dead stores to REGISTER variables.
|
// passRegDead eliminates dead stores to REGISTER variables.
|
||||||
// Two-phase approach:
|
// 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).
|
// 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.
|
// scan forward through the current basic block.
|
||||||
//
|
//
|
||||||
// This is safe because REGISTER variables have a contract: nothing external
|
// This is safe because REGISTER variables have a contract: nothing external
|
||||||
// can observe their memory location (ASM/Script/Macro references are compile errors).
|
// can observe their memory location (ASM/Script/Macro references are compile errors).
|
||||||
func passRegDead(lines []asmLine, registerVars map[string]bool) []asmLine {
|
func passRegDead(lines []asmLine, registerVars map[string]bool) []asmLine {
|
||||||
// Phase 1 — global pre-scan: which REGISTER vars have at least one lda?
|
// Phase 1 — global pre-scan: which REGISTER vars have at least one read?
|
||||||
hasLda := make(map[string]bool, len(registerVars))
|
hasRead := make(map[string]bool, len(registerVars))
|
||||||
for _, l := range lines {
|
for _, l := range lines {
|
||||||
if l.isCode && l.opcode == "lda" && l.operand != "" && registerVars[l.operand] {
|
if l.isCode && l.operand != "" && registerVars[l.operand] && readsFrom(l.opcode, l.operand, l.operand) {
|
||||||
hasLda[l.operand] = true
|
hasRead[l.operand] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,8 +24,8 @@ func passRegDead(lines []asmLine, registerVars map[string]bool) []asmLine {
|
||||||
line := lines[i]
|
line := lines[i]
|
||||||
|
|
||||||
if line.isCode && line.opcode == "sta" && line.operand != "" && registerVars[line.operand] {
|
if line.isCode && line.opcode == "sta" && line.operand != "" && registerVars[line.operand] {
|
||||||
if !hasLda[line.operand] {
|
if !hasRead[line.operand] {
|
||||||
// Globally dead — no lda anywhere in the program
|
// Globally dead — no read anywhere in the program
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isRegStoreDead(lines, i+1, line.operand) {
|
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
|
// 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.
|
// 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 store is dead only if no instruction that reads the operand is found
|
||||||
// a label or control-flow-ending instruction.
|
// anywhere forward before a label or control-flow-ending instruction.
|
||||||
func isRegStoreDead(lines []asmLine, start int, operand string) bool {
|
func isRegStoreDead(lines []asmLine, start int, operand string) bool {
|
||||||
for i := start; i < len(lines); i++ {
|
for i := start; i < len(lines); i++ {
|
||||||
l := 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
|
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
|
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
|
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
|
// blocksFlow returns true for instructions that unconditionally end the current
|
||||||
// execution path: rts, jmp, brk, rti
|
// execution path: rts, jmp, brk, rti
|
||||||
func blocksFlow(line asmLine) bool {
|
func blocksFlow(line asmLine) bool {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue