c65gm/internal/preproc/preproc.go

294 lines
7.3 KiB
Go

package preproc
import (
"fmt"
"path/filepath"
"strings"
)
type LineKind int
const (
Source LineKind = iota
Assembler
Script
)
// Line represents one post-processed source line and its provenance.
type Line struct {
RawText string // original line text before any processing
Text string // post-preprocessor line text (after define replacement, comment stripping)
Filename string // file the line came from (after resolving includes)
LineNo int // 1-based line number in Filename
Kind LineKind // Source, Assembler, or Script
PragmaSetIndex int // index into Pragma stack for this line
}
// HaltError is returned when a `#HALT` directive is encountered.
type HaltError struct{}
func (HaltError) Error() string { return "preprocessor HALT" }
// PreProcess processes the given root source file and returns the flattened, define-expanded
// lines. Directive lines are not emitted.
func PreProcess(rootFilename string, reader ...FileReader) ([]Line, *Pragma, error) {
var r FileReader
if len(reader) > 0 && reader[0] != nil {
r = reader[0]
} else {
r = NewDiskFileReader()
}
pp := newPreproc(r)
lines, err := pp.run(rootFilename)
return lines, pp.pragma, err
}
// -------------------- internal --------------------
type preproc struct {
defs *DefineList // from definelist.go
pragma *Pragma // pragma handler
cond []bool // conditional stack; a line is active if all are true
inAsm bool // true when inside ASM/ENDASM block
inScript bool // true when inside SCRIPT/ENDSCRIPT block
reader FileReader // file reader abstraction
}
func newPreproc(reader FileReader) *preproc {
return &preproc{
defs: NewDefineList(),
pragma: NewPragma(),
cond: []bool{},
inAsm: false,
inScript: false,
reader: reader,
}
}
func (p *preproc) run(root string) ([]Line, error) {
var out []Line
type frame struct {
path string
lines []string // file contents split into lines (no newline)
idx int // next line index (0-based)
line int // last emitted line number (1-based)
dir string
}
newFrame := func(spec string, curDir string) (*frame, error) {
lines, absPath, err := p.reader.ReadLines(spec, curDir)
if err != nil {
return nil, err
}
return &frame{
path: absPath,
lines: lines,
idx: 0,
line: 0,
dir: filepath.Dir(absPath),
}, nil
}
var frameStack []*frame
frRoot, err := newFrame(root, "")
if err != nil {
return nil, err
}
frameStack = append(frameStack, frRoot)
for len(frameStack) > 0 {
currFrame := frameStack[len(frameStack)-1]
// if we've exhausted lines in this frame, pop it
if currFrame.idx >= len(currFrame.lines) {
frameStack = frameStack[:len(frameStack)-1]
continue
}
// advance to next line
raw := currFrame.lines[currFrame.idx]
currFrame.idx++
currFrame.line = currFrame.idx
includeSource := p.shouldIncludeSource()
tokens := strings.Fields(raw)
// ASM mode handling
if !p.inAsm && !p.inScript {
// Check for ASM entry
if includeSource && len(tokens) > 0 && tokens[0] == "ASM" {
p.inAsm = true
continue // don't emit ASM marker
}
// Check for SCRIPT entry
if includeSource && len(tokens) > 0 && tokens[0] == "SCRIPT" {
p.inScript = true
continue // don't emit SCRIPT marker
}
} else if p.inAsm {
// We're in ASM mode
// Check for ENDASM
if len(tokens) > 0 && tokens[0] == "ENDASM" {
p.inAsm = false
continue // don't emit ENDASM marker
}
// Otherwise emit line verbatim as Assembler
out = append(out, Line{
RawText: raw,
Text: raw,
Filename: currFrame.path,
LineNo: currFrame.line,
Kind: Assembler,
PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
})
continue
} else if p.inScript {
// We're in SCRIPT mode
// Check for ENDSCRIPT
if len(tokens) > 0 && tokens[0] == "ENDSCRIPT" {
p.inScript = false
continue // don't emit ENDSCRIPT marker
}
// Otherwise emit line verbatim as Script
out = append(out, Line{
RawText: raw,
Text: raw,
Filename: currFrame.path,
LineNo: currFrame.line,
Kind: Script,
PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
})
continue
}
trim := strings.TrimSpace(raw)
isDirective := strings.HasPrefix(trim, "#")
if isDirective {
parts := strings.Fields(trim)
if len(parts) == 0 {
continue
}
switch strings.ToUpper(parts[0]) {
case "#DEFINE":
if includeSource && len(parts) >= 2 {
name := parts[1]
val := ""
if len(parts) > 2 {
start := 2
if parts[2] == "=" {
start = 3
}
if start < len(parts) {
val = strings.Join(parts[start:], " ")
val = p.defs.ReplaceDefines(val)
}
}
p.defs.Add(name, val)
}
continue
case "#UNDEF":
if includeSource && len(parts) >= 2 {
p.defs.Delete(parts[1])
}
continue
case "#IFDEF":
if len(parts) != 2 {
return nil, fmt.Errorf("#IFDEF requires exactly one argument at %s:%d", currFrame.path, currFrame.line)
}
p.cond = append(p.cond, p.defs.Defined(parts[1]))
continue
case "#IFNDEF":
if len(parts) != 2 {
return nil, fmt.Errorf("#IFNDEF requires exactly one argument at %s:%d", currFrame.path, currFrame.line)
}
p.cond = append(p.cond, !p.defs.Defined(parts[1]))
continue
case "#IFEND":
if len(p.cond) > 0 {
p.cond = p.cond[:len(p.cond)-1]
}
continue
case "#INCLUDE":
if includeSource {
if len(parts) < 2 {
return nil, fmt.Errorf("#INCLUDE without path at %s:%d", currFrame.path, currFrame.line)
}
incPathRaw := parts[1]
next, err := newFrame(incPathRaw, currFrame.dir)
if err != nil {
return nil, fmt.Errorf("include failed at %s:%d: %w", currFrame.path, currFrame.line, err)
}
frameStack = append(frameStack, next)
}
continue
case "#PRINT":
if includeSource {
msg := strings.TrimSpace(strings.TrimPrefix(trim, "#PRINT"))
msg = p.defs.ReplaceDefines(msg)
fmt.Println(msg)
}
continue
case "#HALT":
if includeSource {
return out, HaltError{}
}
continue
case "#PRAGMA":
if includeSource && len(parts) >= 2 {
name := strings.ToUpper(parts[1])
val := ""
if len(parts) > 2 {
val = strings.Join(parts[2:], " ")
val = p.defs.ReplaceDefines(val)
}
p.pragma.AddPragma(name, val)
}
continue
default:
// Unknown directive: drop (do not emit)
continue
}
}
if !includeSource {
continue
}
// Non-directive Source line: expand defines, strip comments, emit
text := p.defs.ReplaceDefines(raw)
// Strip comments (everything after //)
if idx := strings.Index(text, "//"); idx >= 0 {
text = text[:idx]
}
text = strings.TrimRight(text, " \t")
out = append(out, Line{
RawText: raw,
Text: text,
Filename: currFrame.path,
LineNo: currFrame.line,
Kind: Source,
PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
})
}
if len(p.cond) > 0 {
return nil, fmt.Errorf("unbalanced conditionals: %d unclosed #IFDEF/#IFNDEF directives", len(p.cond))
}
return out, nil
}
func (p *preproc) shouldIncludeSource() bool {
for _, v := range p.cond {
if !v {
return false
}
}
return true
}