First unfinished version of the preprocessor
This commit is contained in:
parent
67703597b2
commit
9d650cc463
1 changed files with 262 additions and 0 deletions
262
internal/preproc/preproc.go
Normal file
262
internal/preproc/preproc.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
// <file> -> 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
|
||||
}
|
||||
*/
|
||||
Loading…
Reference in a new issue