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 // Find comment boundary - only process |...| patterns in the code portion commentPos := findAsmCommentStart(text) codePart := text commentPart := "" if commentPos != -1 { codePart = text[:commentPos] commentPart = text[commentPos:] } // Check for |@macro()| pattern first (only in code portion, outside strings) macroStart := findPipeOutsideStrings(codePart, 0) if macroStart != -1 && macroStart+1 < len(codePart) && codePart[macroStart+1] == '@' { macroEnd := findPipeOutsideStrings(codePart, macroStart+1) if macroEnd != -1 { invocation := codePart[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 (only in code portion, outside strings) searchFrom := 0 for { start := findPipeOutsideStrings(codePart, searchFrom) if start == -1 { break } end := findPipeOutsideStrings(codePart, start+1) if end == -1 { c.printErrorWithContext(lines, i, fmt.Errorf("unclosed | in assembler line")) return nil, fmt.Errorf("compilation failed") } varName := codePart[start+1 : end] expandedName := c.ctx.SymbolTable.ExpandName(varName, c.ctx.CurrentScope()) codePart = codePart[:start] + expandedName + codePart[end+1:] // Continue searching after the replacement searchFrom = start + len(expandedName) } codeOutput = append(codeOutput, codePart+commentPart) } 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 } // findAsmCommentStart finds the position of ';' that starts a comment in an ASM line, // taking into account that ';' inside strings (single or double quoted) should be ignored. // Supports ACME-style escape sequences (backslash escapes) inside strings. // Returns -1 if no comment is found. func findAsmCommentStart(line string) int { inString := false escaped := false var quoteChar rune for i, ch := range line { if escaped { escaped = false continue } if inString { if ch == '\\' { escaped = true continue } if ch == quoteChar { inString = false } continue } // Not in string if ch == '"' || ch == '\'' { inString = true quoteChar = ch continue } if ch == ';' { return i } } return -1 } // findPipeOutsideStrings finds the position of '|' that is not inside a string. // Supports ACME-style escape sequences (backslash escapes) inside strings. // Starts searching from 'startFrom' position. // Returns -1 if no such '|' is found. func findPipeOutsideStrings(line string, startFrom int) int { inString := false escaped := false var quoteChar rune for i, ch := range line { if i < startFrom { // Track string state even before startFrom if escaped { escaped = false continue } if inString { if ch == '\\' { escaped = true continue } if ch == quoteChar { inString = false } continue } if ch == '"' || ch == '\'' { inString = true quoteChar = ch } continue } if escaped { escaped = false continue } if inString { if ch == '\\' { escaped = true continue } if ch == quoteChar { inString = false } continue } // Not in string if ch == '"' || ch == '\'' { inString = true quoteChar = ch continue } if ch == '|' { return i } } return -1 } // 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 }