c65gm/internal/optimizer/scanner.go

148 lines
3.5 KiB
Go

package optimizer
import (
"strconv"
"strings"
)
// parseHexOrDec parses an ACME-style number: $ff or 255.
// Returns -1 if s is not a parseable simple number (e.g. label references, <label, >label).
func parseHexOrDec(s string) int {
if strings.HasPrefix(s, "$") {
v, err := strconv.ParseUint(s[1:], 16, 16)
if err != nil {
return -1
}
return int(v)
}
// Strip trailing characters after the number (e.g., ",y" in "5,y")
if idx := strings.IndexAny(s, ",)"); idx >= 0 {
s = s[:idx]
}
v, err := strconv.ParseUint(s, 10, 16)
if err != nil {
return -1
}
return int(v)
}
// isSimpleNumber returns true if s is a plain hex/dec number (no label refs, <, >, etc.)
func isSimpleNumber(s string) bool {
return parseHexOrDec(s) >= 0
}
type asmLine struct {
text string
isLabel bool
isCode bool
isComment bool
optMarker bool
opcode string
operand string
}
// Origin identifies the source of an output line. The optimizer must know
// whether a line was produced by the compiler itself (and is therefore safe
// to optimize) or emitted verbatim from an ASM block or SCRIPT output.
type Origin int
const (
OriginGenerated Origin = iota // compiler-generated code (optimizable)
OriginAsm // handwritten ASM block content (verbatim)
OriginScript // SCRIPT print() output (verbatim)
)
// SourceLine is a single output line together with its provenance.
type SourceLine struct {
Text string
Origin Origin
}
// SourceLineTexts extracts just the text from a slice of source lines.
func SourceLineTexts(lines []SourceLine) []string {
out := make([]string, len(lines))
for i, l := range lines {
out[i] = l.Text
}
return out
}
func parseLines(lines []SourceLine) []asmLine {
var result []asmLine
for _, sl := range lines {
l := sl.Text
al := asmLine{text: l}
if l == "" {
result = append(result, al)
continue
}
if strings.HasPrefix(l, "; @@OPT:") {
al.optMarker = true
al.isComment = true
result = append(result, al)
continue
}
if strings.HasPrefix(l, ";") {
al.isComment = true
result = append(result, al)
continue
}
// Only compiler-generated, indented lines are treated as optimizable
// instructions. ACME requires labels at column 0, so any leading
// whitespace means "not a label" — the exact character (tab or space)
// is irrelevant. Generated labels and any verbatim ASM/SCRIPT line
// become barriers.
if sl.Origin == OriginGenerated && isIndented(l) {
parts := strings.Fields(l)
if len(parts) > 0 {
al.isCode = true
al.opcode = strings.ToLower(parts[0])
if len(parts) > 1 {
al.operand = parts[1]
}
} else {
al.isLabel = true
}
} else {
al.isLabel = true
}
result = append(result, al)
}
return result
}
// isIndented reports whether a line has leading whitespace (an instruction),
// as opposed to a label which starts at column 0.
func isIndented(l string) bool {
return len(l) > 0 && (l[0] == ' ' || l[0] == '\t')
}
// stripOptMarkers removes @@OPT comment lines from the output
func stripOptMarkers(lines []asmLine) []asmLine {
var result []asmLine
for _, l := range lines {
if l.optMarker {
continue
}
result = append(result, l)
}
return result
}
func linesToString(lines []asmLine) []string {
var result []string
for _, l := range lines {
result = append(result, l.text)
}
return result
}
// skipJmpMarker returns true if the line is an @@OPT comment (to be skipped in pass logic)
func skipJmpMarker(line asmLine) bool {
return line.optMarker
}