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 }