package preproc import ( "bufio" "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) } // 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 map[string]string // #PRAGMA NAME VALUE (stored, not interpreted here) cond []bool // conditional stack; a line is active if all are true } func newPreproc() *preproc { return &preproc{ defs: NewDefineList(), pragma: make(map[string]string), cond: []bool{}, } } func (p *preproc) run(root string) ([]Line, error) { var out []Line type frame struct { path string f *os.File s *bufio.Scanner line int dir string } newFrame := func(path string) (*frame, error) { f, err := os.Open(path) if err != nil { return nil, err } sc := bufio.NewScanner(f) // Allow long lines (Pascal source and macro expansion can be large) sc.Buffer(make([]byte, 0, 64*1024), 10*1024*1024) return &frame{path: path, f: f, s: sc, line: 0, dir: filepath.Dir(path)}, nil } var stack []*frame absRoot, _ := filepath.Abs(root) fr, err := newFrame(absRoot) if err != nil { return nil, err } stack = append(stack, fr) for len(stack) > 0 { fr := stack[len(stack)-1] if !fr.s.Scan() { if err := fr.s.Err(); err != nil { _ = fr.f.Close() return nil, err } _ = fr.f.Close() stack = stack[:len(stack)-1] continue } fr.line++ raw := fr.s.Text() trim := strings.TrimSpace(raw) isDirective := strings.HasPrefix(trim, "#") active := p.isActive() if isDirective { parts := strings.Fields(trim) if len(parts) == 0 { continue } switch strings.ToUpper(parts[0]) { case "#DEFINE": if active && len(parts) >= 2 { name := parts[1] val := "" if len(parts) > 2 { val = strings.Join(parts[2:], " ") val = p.defs.ReplaceDefines(val) // allow nested defines in values } p.defs.Add(name, val) } continue case "#UNDEF": if active && 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", fr.path, fr.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", fr.path, fr.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 active { if len(parts) < 2 { return nil, fmt.Errorf("#INCLUDE without path at %s:%d", fr.path, fr.line) } incPathRaw := parts[1] nextPath, err := resolveInclude(incPathRaw, fr.dir) if err != nil { return nil, fmt.Errorf("%s at %s:%d", err, fr.path, fr.line) } next, err := newFrame(nextPath) if err != nil { return nil, fmt.Errorf("include open failed %q: %w", nextPath, err) } stack = append(stack, next) } continue case "#PRINT": if active { msg := strings.TrimSpace(strings.TrimPrefix(trim, "#PRINT")) msg = p.defs.ReplaceDefines(msg) fmt.Println(msg) } continue case "#HALT": if active { return nil, HaltError{} } continue case "#PRAGMA": if active && 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[name] = val } continue default: // Unknown directive: drop (do not emit) continue } } if !active { continue } // Non-directive: expand defines and emit. text := p.defs.ReplaceDefines(raw) out = append(out, Line{ Text: text, Filename: fr.path, LineNo: fr.line, Tokens: strings.Fields(text), }) } // Check any scanner errors on remaining frames (if any) for _, fr := range stack { if err := fr.s.Err(); err != nil { return nil, err } } return out, nil } func (p *preproc) isActive() 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 } // fieldsCollapsed splits on spaces and tabs, ignoring empties (i.e., collapses runs). /* func fieldsCollapsed(s string) []string { out := make([]string, 0, 8) field := strings.Builder{} flush := func() { if field.Len() > 0 { out = append(out, field.String()) field.Reset() } } for _, r := range s { if r == ' ' || r == '\t' { flush() } else { field.WriteRune(r) } } flush() return out } */