148 lines
3.5 KiB
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
|
|
}
|