From 9d650cc4636cbf93cf011e7c66512635066f37c9 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Sat, 11 Oct 2025 08:20:41 +0200 Subject: [PATCH] First unfinished version of the preprocessor --- internal/preproc/preproc.go | 262 ++++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 internal/preproc/preproc.go diff --git a/internal/preproc/preproc.go b/internal/preproc/preproc.go new file mode 100644 index 0000000..91c200d --- /dev/null +++ b/internal/preproc/preproc.go @@ -0,0 +1,262 @@ +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 +} +*/