package preproc import ( "errors" "fmt" "os" "path/filepath" "strings" ) // Line represents one post-processed source line and its provenance. type Line struct { Text string // post-preprocessor line text (after define replacement) Filename string // file the line came from (after resolving includes) LineNo int // 1-based line number in Filename Tokens []string // whitespace-split tokens from Text (space or tab; consecutive collapsed) 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) ([]Line, error) { pp := newPreproc() return pp.run(rootFilename) } // -------------------- 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 } func newPreproc() *preproc { return &preproc{ defs: NewDefineList(), pragma: NewPragma(), cond: []bool{}, inAsm: false, } } 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 } // cache of already-read files: fullpath -> []string cache := make(map[string][]string) newFrame := func(path string) (*frame, error) { abs, err := filepath.Abs(path) if err != nil { return nil, err } if lines, ok := cache[abs]; ok { return &frame{path: abs, lines: lines, idx: 0, line: 0, dir: filepath.Dir(abs)}, nil } data, err := os.ReadFile(abs) if err != nil { return nil, err } lines := strings.Split(string(data), "\n") cache[abs] = lines return &frame{path: abs, lines: lines, idx: 0, line: 0, dir: filepath.Dir(abs)}, nil } var frameStack []*frame absRoot, _ := filepath.Abs(root) frRoot, err := newFrame(absRoot) 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 { // Check for ASM entry if includeSource && len(tokens) > 0 && tokens[0] == "ASM" { p.inAsm = true out = append(out, Line{ Text: raw, Filename: currFrame.path, LineNo: currFrame.line, Tokens: []string{}, PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(), }) continue } } else { // We're in ASM mode // Check for ENDASM if len(tokens) > 0 && tokens[0] == "ENDASM" { p.inAsm = false out = append(out, Line{ Text: raw, Filename: currFrame.path, LineNo: currFrame.line, Tokens: []string{}, PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(), }) continue } // Otherwise emit line verbatim out = append(out, Line{ Text: raw, Filename: currFrame.path, LineNo: currFrame.line, Tokens: []string{}, 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 { val = strings.Join(parts[2:], " ") 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] nextPath, err := resolveInclude(incPathRaw, currFrame.dir) if err != nil { return nil, fmt.Errorf("%s at %s:%d", err, currFrame.path, currFrame.line) } next, err := newFrame(nextPath) if err != nil { return nil, fmt.Errorf("include open failed %q: %w", nextPath, 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 nil, 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: expand defines and emit. text := p.defs.ReplaceDefines(raw) out = append(out, Line{ Text: text, Filename: currFrame.path, LineNo: currFrame.line, Tokens: strings.Fields(text), PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(), }) } return out, nil } func (p *preproc) shouldIncludeSource() bool { for _, v := range p.cond { if !v { return false } } return true } func resolveInclude(spec string, curDir string) (string, error) { if len(spec) == 0 { return "", errors.New("empty include path") } // -> library include via C65LIBPATH if strings.HasPrefix(spec, "<") && strings.HasSuffix(spec, ">") { base := strings.TrimSpace(spec[1 : len(spec)-1]) lib := os.Getenv("C65LIBPATH") if lib == "" { return "", errors.New("C65LIBPATH not set for angle-bracket include") } path := filepath.Join(lib, base) if _, err := os.Stat(path); err != nil { return "", fmt.Errorf("library include not found: %s", path) } return path, nil } // quoted or bare -> relative to current file dir base := strings.Trim(spec, `"'`) path := filepath.Join(curDir, base) if _, err := os.Stat(path); err != nil { return "", fmt.Errorf("include not found: %s", path) } return path, nil }