package compiler import ( "fmt" "os" "path/filepath" "strings" "c65gm/internal/preproc" ) // Compiler orchestrates the compilation process type Compiler struct { ctx *CompilerContext registry *CommandRegistry deferredAsm []string // ASM blocks with _P_ASM_AFTER_VARS pragma } // 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 var currentAsmTarget *[]string // nil = no active ASM block, or points to target slice // Reset deferred ASM storage for this compilation c.deferredAsm = nil 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 && currentAsmTarget != nil { *currentAsmTarget = append(*currentAsmTarget, "; ENDASM") currentAsmTarget = nil } // Open new block if line.Kind == preproc.Assembler { // Check if ASM block should be deferred to end pragmaSet := c.ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) asmAfterVars := pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "" && pragmaSet.GetPragma("_P_ASM_AFTER_VARS") != "0" if asmAfterVars { // Add inline comment and defer ASM block codeOutput = append(codeOutput, "; ASM block deferred to end of source") c.deferredAsm = append(c.deferredAsm, fmt.Sprintf("; ASM Block from %s, Line %d", line.Filename, line.LineNo)) currentAsmTarget = &c.deferredAsm } else { // Normal ASM block codeOutput = append(codeOutput, "; ASM") currentAsmTarget = &codeOutput } } 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 { // Safety check: currentAsmTarget should be set when processing assembler lines if currentAsmTarget == nil { c.printErrorWithContext(lines, i, fmt.Errorf("internal error: ASM line without active ASM block")) return nil, fmt.Errorf("compilation failed") } 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 *currentAsmTarget = append(*currentAsmTarget, fmt.Sprintf("; %s", text)) *currentAsmTarget = append(*currentAsmTarget, macroOutput...) *currentAsmTarget = append(*currentAsmTarget, 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) } *currentAsmTarget = append(*currentAsmTarget, 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 { // Close the final ASM block if still open if currentAsmTarget != nil { *currentAsmTarget = append(*currentAsmTarget, "; ENDASM") } 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() // Get functions with _P_REMOVE_UNUSED pragma (for suppressing variable warnings) funcsWithRemovePragma := c.ctx.FunctionHandler.GetFunctionsWithRemovePragma() // Check for unused variables and print warnings (skip variables in functions with remove pragma) warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma) for _, warning := range warnings { _, _ = fmt.Fprintf(os.Stderr, "%s\n", warning) } // Check for unused functions and print warnings funcWarnings := c.ctx.FunctionHandler.CheckUnusedFunctions() for _, warning := range funcWarnings { _, _ = fmt.Fprintf(os.Stderr, "%s\n", warning) } // Remove unused functions with _P_REMOVE_UNUSED pragma codeOutput, removedFuncs := c.removeUnusedFunctions(codeOutput) // Assemble final output with headers and footers return c.assembleOutput(codeOutput, removedFuncs), 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 } // removeUnusedFunctions removes functions marked with _P_REMOVE_UNUSED pragma from assembly output // Returns the filtered code lines and a map of removed function names func (c *Compiler) removeUnusedFunctions(codeLines []string) ([]string, map[string]bool) { toRemove := c.ctx.FunctionHandler.GetFunctionsToRemove() if len(toRemove) == 0 { return codeLines, nil } var result []string i := 0 for i < len(codeLines) { line := codeLines[i] // Check for function start marker if strings.HasPrefix(line, "; @@FUNC_BEGIN ") { parts := strings.Fields(line) if len(parts) >= 3 && toRemove[parts[2]] { // Collect all consecutive @@FUNC_BEGIN markers in this group (all removable) groupNames := []string{parts[2]} j := i + 1 for j < len(codeLines) { if strings.HasPrefix(codeLines[j], "; @@FUNC_BEGIN ") { p := strings.Fields(codeLines[j]) if len(p) >= 3 && toRemove[p[2]] { groupNames = append(groupNames, p[2]) j++ } else { break } } else { break } } // Print info messages for all removed functions for _, funcName := range groupNames { var filename string var lineNo int for _, funcDecl := range c.ctx.FunctionHandler.functions { if funcDecl.Name == funcName { filename = funcDecl.Line.Filename lineNo = funcDecl.Line.LineNo break } } if filename != "" { baseFilename := filepath.Base(filename) fmt.Printf("info:%s:%d:FUNC %s removed.\n", baseFilename, lineNo, funcName) } else { fmt.Printf("info:unknown:0:FUNC %s removed.\n", funcName) } } // Build set of end markers to find endSet := make(map[string]bool) for _, name := range groupNames { endSet[name] = true } // Skip past all FUNC_BEGIN markers i = j // Skip body until all corresponding @@FUNC_END markers are found for i < len(codeLines) && len(endSet) > 0 { if strings.HasPrefix(codeLines[i], "; @@FUNC_END ") { p := strings.Fields(codeLines[i]) if len(p) >= 3 && endSet[p[2]] { delete(endSet, p[2]) } } i++ } continue } } result = append(result, line) i++ } return result, toRemove } // assembleOutput combines all generated sections into final assembly func (c *Compiler) assembleOutput(codeLines []string, removedFuncs map[string]bool) []string { var output []string // Header comment output = append(output, ";Generated by c65gm") output = append(output, "") // Constants section if constLines := GenerateConstants(c.ctx.SymbolTable, removedFuncs); len(constLines) > 0 { output = append(output, constLines...) } // Absolute addresses section if absLines := GenerateAbsolutes(c.ctx.SymbolTable, removedFuncs); 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, removedFuncs); 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, "") } // Deferred ASM blocks (with _P_ASM_AFTER_VARS pragma) if len(c.deferredAsm) > 0 { output = append(output, "; Deferred ASM blocks (after variables)") output = append(output, "") output = append(output, c.deferredAsm...) output = append(output, "") } return output }