370 lines
11 KiB
Go
370 lines
11 KiB
Go
package compiler
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"c65gm/internal/preproc"
|
|
)
|
|
|
|
// Compiler orchestrates the compilation process
|
|
type Compiler struct {
|
|
ctx *CompilerContext
|
|
registry *CommandRegistry
|
|
}
|
|
|
|
// NewCompiler creates a new compiler with initialized context and registry
|
|
func NewCompiler(pragma *preproc.Pragma) *Compiler {
|
|
return &Compiler{
|
|
ctx: NewCompilerContext(pragma),
|
|
registry: NewCommandRegistry(),
|
|
}
|
|
}
|
|
|
|
// Context returns the compiler context (for registering commands that need it)
|
|
func (c *Compiler) Context() *CompilerContext {
|
|
return c.ctx
|
|
}
|
|
|
|
// Registry returns the command registry (for registering commands)
|
|
func (c *Compiler) Registry() *CommandRegistry {
|
|
return c.registry
|
|
}
|
|
|
|
// Compile processes preprocessed lines and generates assembly output
|
|
func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
|
var codeOutput []string
|
|
var lastKind = preproc.Source
|
|
var scriptBuffer []string
|
|
var scriptIsLibrary bool
|
|
var macroBuffer []string
|
|
var currentMacroName string
|
|
var currentMacroParams []string
|
|
|
|
for i, line := range lines {
|
|
// Detect kind transitions and emit markers
|
|
if line.Kind != lastKind {
|
|
// Execute and close previous Script or ScriptLibrary block
|
|
if lastKind == preproc.Script || lastKind == preproc.ScriptLibrary {
|
|
scriptOutput, err := executeScript(scriptBuffer, c.ctx, scriptIsLibrary)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("script execution failed: %w", err)
|
|
}
|
|
codeOutput = append(codeOutput, scriptOutput...)
|
|
scriptBuffer = nil
|
|
if scriptIsLibrary {
|
|
codeOutput = append(codeOutput, "; ENDSCRIPT LIBRARY")
|
|
} else {
|
|
codeOutput = append(codeOutput, "; ENDSCRIPT")
|
|
}
|
|
}
|
|
|
|
// Store previous ScriptMacroDef block
|
|
if lastKind == preproc.ScriptMacroDef {
|
|
if currentMacroName != "" {
|
|
c.ctx.ScriptMacros[currentMacroName] = &ScriptMacro{
|
|
Name: currentMacroName,
|
|
Params: currentMacroParams,
|
|
Body: macroBuffer,
|
|
}
|
|
codeOutput = append(codeOutput, fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName))
|
|
}
|
|
macroBuffer = nil
|
|
currentMacroName = ""
|
|
currentMacroParams = nil
|
|
}
|
|
|
|
// Close previous Assembler block
|
|
if lastKind == preproc.Assembler {
|
|
codeOutput = append(codeOutput, "; ENDASM")
|
|
}
|
|
|
|
// Open new block
|
|
if line.Kind == preproc.Assembler {
|
|
codeOutput = append(codeOutput, "; ASM")
|
|
} else if line.Kind == preproc.Script {
|
|
codeOutput = append(codeOutput, "; SCRIPT")
|
|
scriptIsLibrary = false
|
|
} else if line.Kind == preproc.ScriptLibrary {
|
|
codeOutput = append(codeOutput, "; SCRIPT LIBRARY")
|
|
scriptIsLibrary = true
|
|
} else if line.Kind == preproc.ScriptMacroDef {
|
|
// First line is the header - parse it
|
|
name, params, err := parseMacroHeader(line.Text)
|
|
if err != nil {
|
|
c.printErrorWithContext(lines, i, err)
|
|
return nil, fmt.Errorf("compilation failed")
|
|
}
|
|
currentMacroName = name
|
|
currentMacroParams = params
|
|
codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text))
|
|
}
|
|
|
|
lastKind = line.Kind
|
|
}
|
|
|
|
// Handle non-source lines
|
|
if line.Kind != preproc.Source {
|
|
if line.Kind == preproc.Assembler {
|
|
text := line.Text
|
|
|
|
// Check for |@macro()| pattern first
|
|
if macroStart := strings.Index(text, "|@"); macroStart != -1 {
|
|
macroEnd := strings.Index(text[macroStart+1:], "|")
|
|
if macroEnd != -1 {
|
|
macroEnd += macroStart + 1
|
|
invocation := text[macroStart+1 : macroEnd] // @name(args)
|
|
|
|
macroName, args, err := ParseMacroInvocation(invocation)
|
|
if err != nil {
|
|
c.printErrorWithContext(lines, i, err)
|
|
return nil, fmt.Errorf("compilation failed")
|
|
}
|
|
|
|
macroOutput, err := ExecuteMacro(macroName, args, c.ctx)
|
|
if err != nil {
|
|
c.printErrorWithContext(lines, i, fmt.Errorf("macro %s: %w", macroName, err))
|
|
return nil, fmt.Errorf("compilation failed")
|
|
}
|
|
|
|
// Emit with comments showing invocation
|
|
codeOutput = append(codeOutput, fmt.Sprintf("; %s", text))
|
|
codeOutput = append(codeOutput, macroOutput...)
|
|
codeOutput = append(codeOutput, fmt.Sprintf("; end @%s", macroName))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Expand |varname| -> scoped_varname for local variables in ASM blocks
|
|
for {
|
|
start := strings.IndexByte(text, '|')
|
|
if start == -1 {
|
|
break
|
|
}
|
|
end := strings.IndexByte(text[start+1:], '|')
|
|
if end == -1 {
|
|
c.printErrorWithContext(lines, i, fmt.Errorf("unclosed | in assembler line"))
|
|
return nil, fmt.Errorf("compilation failed")
|
|
}
|
|
end += start + 1
|
|
|
|
varName := text[start+1 : end]
|
|
expandedName := c.ctx.SymbolTable.ExpandName(varName, c.ctx.CurrentScope())
|
|
text = text[:start] + expandedName + text[end+1:]
|
|
}
|
|
codeOutput = append(codeOutput, text)
|
|
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
|
|
// Collect script lines for execution
|
|
scriptBuffer = append(scriptBuffer, line.Text)
|
|
} else if line.Kind == preproc.ScriptMacroDef {
|
|
// Skip the header line (already parsed in transition)
|
|
if strings.HasPrefix(strings.TrimSpace(line.Text), "SCRIPT MACRO ") {
|
|
continue
|
|
}
|
|
// Collect macro body lines
|
|
macroBuffer = append(macroBuffer, line.Text)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Skip empty/whitespace-only lines
|
|
if strings.TrimSpace(line.Text) == "" {
|
|
continue
|
|
}
|
|
|
|
// Find handler for this line
|
|
cmd, found := c.registry.FindHandler(line)
|
|
if !found {
|
|
c.printErrorWithContext(lines, i, fmt.Errorf("unhandled line (no matching command)"))
|
|
return nil, fmt.Errorf("compilation failed")
|
|
}
|
|
|
|
// Interpret the line
|
|
if err := cmd.Interpret(line, c.ctx); err != nil {
|
|
c.printErrorWithContext(lines, i, err)
|
|
return nil, fmt.Errorf("compilation failed")
|
|
}
|
|
|
|
// Generate assembly
|
|
asmLines, err := cmd.Generate(c.ctx)
|
|
if err != nil {
|
|
c.printErrorWithContext(lines, i, err)
|
|
return nil, fmt.Errorf("compilation failed")
|
|
}
|
|
|
|
codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text))
|
|
codeOutput = append(codeOutput, asmLines...)
|
|
}
|
|
|
|
// Close any open block
|
|
if lastKind == preproc.Assembler {
|
|
return nil, fmt.Errorf("Unclosed ASM block.")
|
|
} else if lastKind == preproc.Script {
|
|
return nil, fmt.Errorf("Unclosed SCRIPT block.")
|
|
} else if lastKind == preproc.ScriptLibrary {
|
|
return nil, fmt.Errorf("Unclosed SCRIPT LIBRARY block.")
|
|
} else if lastKind == preproc.ScriptMacroDef {
|
|
return nil, fmt.Errorf("Unclosed SCRIPT MACRO block.")
|
|
}
|
|
|
|
// Analyze for overlapping absolute addresses in function call chains
|
|
c.checkAbsoluteOverlaps()
|
|
|
|
// Assemble final output with headers and footers
|
|
return c.assembleOutput(codeOutput), nil
|
|
}
|
|
|
|
// printErrorWithContext prints an error with source code context to stderr
|
|
func (c *Compiler) printErrorWithContext(lines []preproc.Line, lineIndex int, err error) {
|
|
if lineIndex < 0 || lineIndex >= len(lines) {
|
|
// Shouldn't happen, but be safe
|
|
_, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
return
|
|
}
|
|
|
|
line := lines[lineIndex]
|
|
const contextLines = 3
|
|
|
|
// Print error header
|
|
_, _ = fmt.Fprintf(os.Stderr, "\nError: %v\n", err)
|
|
_, _ = fmt.Fprintf(os.Stderr, " --> %s:%d\n\n", line.Filename, line.LineNo)
|
|
|
|
// Find all lines from the same file for context
|
|
// Group consecutive lines from the same file
|
|
fileLines := make(map[string][]int)
|
|
for i, l := range lines {
|
|
if l.Filename == line.Filename {
|
|
fileLines[l.Filename] = append(fileLines[l.Filename], i)
|
|
}
|
|
}
|
|
|
|
// Get context range (lineIndex within our lines array)
|
|
startIdx := lineIndex - contextLines
|
|
if startIdx < 0 {
|
|
startIdx = 0
|
|
}
|
|
endIdx := lineIndex + contextLines
|
|
if endIdx >= len(lines) {
|
|
endIdx = len(lines) - 1
|
|
}
|
|
|
|
// Calculate width for line numbers based on actual source line numbers
|
|
maxLineNo := 0
|
|
for i := startIdx; i <= endIdx; i++ {
|
|
if lines[i].LineNo > maxLineNo {
|
|
maxLineNo = lines[i].LineNo
|
|
}
|
|
}
|
|
maxLineNumWidth := len(fmt.Sprintf("%d", maxLineNo))
|
|
|
|
// Print context
|
|
for i := startIdx; i <= endIdx; i++ {
|
|
l := lines[i]
|
|
|
|
// Skip lines from different files
|
|
if l.Filename != line.Filename {
|
|
continue
|
|
}
|
|
|
|
if i == lineIndex {
|
|
// Error line - highlight it
|
|
_, _ = fmt.Fprintf(os.Stderr, ">> %*d | %s\n", maxLineNumWidth, l.LineNo, l.Text)
|
|
} else {
|
|
// Context line
|
|
_, _ = fmt.Fprintf(os.Stderr, " %*d | %s\n", maxLineNumWidth, l.LineNo, l.Text)
|
|
}
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stderr, "\n")
|
|
}
|
|
|
|
// checkAbsoluteOverlaps analyzes and warns about overlapping absolute addresses
|
|
func (c *Compiler) checkAbsoluteOverlaps() {
|
|
c.ctx.FunctionHandler.ReportAbsoluteOverlaps()
|
|
}
|
|
|
|
// parseMacroHeader parses "SCRIPT MACRO name(param1, param2, ...)" and returns name and params
|
|
func parseMacroHeader(header string) (string, []string, error) {
|
|
// Expected format: "SCRIPT MACRO name(param1, param2, ...)"
|
|
header = strings.TrimSpace(header)
|
|
|
|
// Remove "SCRIPT MACRO " prefix
|
|
if !strings.HasPrefix(header, "SCRIPT MACRO ") {
|
|
return "", nil, fmt.Errorf("invalid macro header: %s", header)
|
|
}
|
|
rest := strings.TrimSpace(header[len("SCRIPT MACRO "):])
|
|
|
|
// Find opening paren
|
|
parenStart := strings.IndexByte(rest, '(')
|
|
if parenStart == -1 {
|
|
return "", nil, fmt.Errorf("macro header missing '(': %s", header)
|
|
}
|
|
|
|
// Find closing paren
|
|
parenEnd := strings.LastIndexByte(rest, ')')
|
|
if parenEnd == -1 || parenEnd < parenStart {
|
|
return "", nil, fmt.Errorf("macro header missing ')': %s", header)
|
|
}
|
|
|
|
name := strings.TrimSpace(rest[:parenStart])
|
|
if name == "" {
|
|
return "", nil, fmt.Errorf("macro name is empty")
|
|
}
|
|
|
|
// Parse parameters
|
|
paramStr := strings.TrimSpace(rest[parenStart+1 : parenEnd])
|
|
var params []string
|
|
if paramStr != "" {
|
|
parts := strings.Split(paramStr, ",")
|
|
for _, p := range parts {
|
|
param := strings.TrimSpace(p)
|
|
if param == "" {
|
|
return "", nil, fmt.Errorf("empty parameter in macro definition")
|
|
}
|
|
params = append(params, param)
|
|
}
|
|
}
|
|
|
|
return name, params, nil
|
|
}
|
|
|
|
// assembleOutput combines all generated sections into final assembly
|
|
func (c *Compiler) assembleOutput(codeLines []string) []string {
|
|
var output []string
|
|
|
|
// Header comment
|
|
output = append(output, ";Generated by c65gm")
|
|
output = append(output, "")
|
|
|
|
// Constants section
|
|
if constLines := GenerateConstants(c.ctx.SymbolTable); len(constLines) > 0 {
|
|
output = append(output, constLines...)
|
|
}
|
|
|
|
// Absolute addresses section
|
|
if absLines := GenerateAbsolutes(c.ctx.SymbolTable); len(absLines) > 0 {
|
|
output = append(output, absLines...)
|
|
}
|
|
|
|
// Main code section
|
|
output = append(output, ";Main code")
|
|
output = append(output, "")
|
|
output = append(output, codeLines...)
|
|
output = append(output, "")
|
|
|
|
// Variables section
|
|
if varLines := GenerateVariables(c.ctx.SymbolTable); len(varLines) > 0 {
|
|
output = append(output, varLines...)
|
|
}
|
|
|
|
// Constant strings section
|
|
if strLines := c.ctx.ConstStrHandler.GenerateConstStrDecls(); len(strLines) > 0 {
|
|
output = append(output, ";Constant strings (from c65gm)")
|
|
output = append(output, "")
|
|
output = append(output, strLines...)
|
|
output = append(output, "")
|
|
}
|
|
|
|
return output
|
|
}
|