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 for i, line := range lines { // Detect kind transitions and emit markers if line.Kind != lastKind { // Execute and close previous Script block if lastKind == preproc.Script { scriptOutput, err := executeScript(scriptBuffer, c.ctx) if err != nil { return nil, fmt.Errorf("script execution failed: %w", err) } codeOutput = append(codeOutput, scriptOutput...) scriptBuffer = nil codeOutput = append(codeOutput, "; ENDSCRIPT") } // 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") } lastKind = line.Kind } // Handle non-source lines if line.Kind != preproc.Source { if line.Kind == preproc.Assembler { // Expand |varname| -> scoped_varname for local variables in ASM blocks text := line.Text 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 { // Collect script lines for execution scriptBuffer = append(scriptBuffer, 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 { //codeOutput = append(codeOutput, "; ENDASM") return nil, fmt.Errorf("Unclosed ASM block.") } else if lastKind == preproc.Script { //codeOutput = append(codeOutput, "; ENDSCRIPT") return nil, fmt.Errorf("Unclosed SCRIPT 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() } // 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 }