From 411106ea360f3338c23dcc265d1b43c23269fb73 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Fri, 17 Apr 2026 23:01:42 +0200 Subject: [PATCH] Added pragma _P_ASM_AFTER_VARS to control is ASM blocks should be generated after everything else (vars and string constants) --- internal/compiler/compiler.go | 57 ++++++- internal/compiler/compiler_test.go | 261 +++++++++++++++++++++++++++++ language.md | 17 ++ syntax.md | 16 ++ 4 files changed, 342 insertions(+), 9 deletions(-) diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 773af5a..30ab6d6 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -11,8 +11,9 @@ import ( // Compiler orchestrates the compilation process type Compiler struct { - ctx *CompilerContext - registry *CommandRegistry + ctx *CompilerContext + registry *CommandRegistry + deferredAsm []string // ASM blocks with _P_ASM_AFTER_VARS pragma } // NewCompiler creates a new compiler with initialized context and registry @@ -42,6 +43,10 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { 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 @@ -77,13 +82,29 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { } // Close previous Assembler block - if lastKind == preproc.Assembler { - codeOutput = append(codeOutput, "; ENDASM") + if lastKind == preproc.Assembler && currentAsmTarget != nil { + *currentAsmTarget = append(*currentAsmTarget, "; ENDASM") + currentAsmTarget = nil } // Open new block if line.Kind == preproc.Assembler { - codeOutput = append(codeOutput, "; ASM") + // 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 @@ -108,6 +129,12 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { // 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 @@ -139,9 +166,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { } // Emit with comments showing invocation - codeOutput = append(codeOutput, fmt.Sprintf("; %s", text)) - codeOutput = append(codeOutput, macroOutput...) - codeOutput = append(codeOutput, fmt.Sprintf("; end @%s", macroName)) + *currentAsmTarget = append(*currentAsmTarget, fmt.Sprintf("; %s", text)) + *currentAsmTarget = append(*currentAsmTarget, macroOutput...) + *currentAsmTarget = append(*currentAsmTarget, fmt.Sprintf("; end @%s", macroName)) continue } } @@ -165,7 +192,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { // Continue searching after the replacement searchFrom = start + len(expandedName) } - codeOutput = append(codeOutput, codePart+commentPart) + *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) @@ -211,6 +238,10 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { // 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.") @@ -559,5 +590,13 @@ func (c *Compiler) assembleOutput(codeLines []string, removedFuncs map[string]bo 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 } diff --git a/internal/compiler/compiler_test.go b/internal/compiler/compiler_test.go index b7d7d0d..af43c1b 100644 --- a/internal/compiler/compiler_test.go +++ b/internal/compiler/compiler_test.go @@ -955,3 +955,264 @@ func TestAsmBlock_MacroExpansion_IgnoresComments(t *testing.T) { }) } } + +func TestAsmAfterVarsPragma(t *testing.T) { + tests := []struct { + name string + pragmaEnabled bool + asmLines []string + expectDeferred bool + }{ + { + name: "pragma enabled with 1", + pragmaEnabled: true, + asmLines: []string{" lda #$00", " sta $d020"}, + expectDeferred: true, + }, + { + name: "pragma disabled with 0", + pragmaEnabled: false, + asmLines: []string{" lda #$01", " sta $d021"}, + expectDeferred: false, + }, + { + name: "pragma not set", + pragmaEnabled: false, + asmLines: []string{" nop", " rts"}, + expectDeferred: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create pragma and optionally enable _P_ASM_AFTER_VARS + pragma := preproc.NewPragma() + if tt.pragmaEnabled { + pragma.AddPragma("_P_ASM_AFTER_VARS", "1") + } else if tt.name == "pragma disabled with 0" { + pragma.AddPragma("_P_ASM_AFTER_VARS", "0") + } + + comp := NewCompiler(pragma) + + // Build test lines - simulate preprocessor output + // The preprocessor strips ASM/ENDASM and marks lines as Assembler + lines := []preproc.Line{} + + // Add ASM lines (simulating what preprocessor produces) + for i, asmLine := range tt.asmLines { + lines = append(lines, preproc.Line{ + Text: asmLine, + Filename: "test.c65", + LineNo: 1 + i, + Kind: preproc.Assembler, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + }) + } + + // Add an empty source line to trigger kind transition and close ASM block + lines = append(lines, preproc.Line{ + Text: "", + Filename: "test.c65", + LineNo: 1 + len(tt.asmLines), + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + }) + + output, err := comp.Compile(lines) + if err != nil { + t.Fatalf("Compile failed: %v", err) + } + + // Check for deferred comment in main code section + foundDeferredComment := false + foundDeferredBlock := false + inDeferredSection := false + + for _, line := range output { + if line == "; ASM block deferred to end of source" { + foundDeferredComment = true + } + if line == "; Deferred ASM blocks (after variables)" { + inDeferredSection = true + } + if inDeferredSection && strings.Contains(line, "; ASM Block from test.c65, Line 1") { + foundDeferredBlock = true + } + } + + if tt.expectDeferred { + if !foundDeferredComment { + t.Errorf("expected '; ASM block deferred to end of source' comment in main code") + } + if !foundDeferredBlock { + t.Errorf("expected deferred ASM block with source location") + } + } else { + if foundDeferredComment { + t.Errorf("unexpected '; ASM block deferred to end of source' comment when pragma disabled") + } + if foundDeferredBlock { + t.Errorf("unexpected deferred ASM block when pragma disabled") + } + } + + // Verify ASM lines appear somewhere in output + asmFound := false + for _, asmLine := range tt.asmLines { + for _, outputLine := range output { + if strings.TrimSpace(outputLine) == strings.TrimSpace(asmLine) { + asmFound = true + break + } + } + if asmFound { + break + } + } + if !asmFound { + t.Errorf("ASM lines not found in output") + } + }) + } +} + +func TestUnterminatedAsmBlock(t *testing.T) { + tests := []struct { + name string + source []preproc.Line + wantErr bool + errMsg string + }{ + { + name: "unterminated ASM block", + source: []preproc.Line{ + { + Text: " lda #$00", + Kind: preproc.Assembler, + }, + { + Text: " sta $d020", + Kind: preproc.Assembler, + }, + }, + wantErr: true, + errMsg: "Unclosed ASM block.", + }, + { + name: "properly terminated ASM block", + source: []preproc.Line{ + { + Text: " lda #$00", + Kind: preproc.Assembler, + }, + { + Text: " sta $d020", + Kind: preproc.Assembler, + }, + { + Text: "", + Kind: preproc.Source, + }, + }, + wantErr: false, + }, + { + name: "ASM block with pragma but no ENDASM", + source: []preproc.Line{ + { + Text: " lda #$00", + Kind: preproc.Assembler, + PragmaSetIndex: 1, // Simulate pragma active + }, + { + Text: " sta $d020", + Kind: preproc.Assembler, + PragmaSetIndex: 1, + }, + }, + wantErr: true, + errMsg: "Unclosed ASM block.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pragma := preproc.NewPragma() + // Set up pragma for test case 3 + if tt.name == "ASM block with pragma but no ENDASM" { + pragma.AddPragma("_P_ASM_AFTER_VARS", "1") + } + + compiler := NewCompiler(pragma) + _, err := compiler.Compile(tt.source) + + if tt.wantErr { + if err == nil { + t.Errorf("TestUnterminatedAsmBlock(%s): expected error but got none", tt.name) + } else if !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("TestUnterminatedAsmBlock(%s): expected error containing %q, got %q", tt.name, tt.errMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("TestUnterminatedAsmBlock(%s): unexpected error: %v", tt.name, err) + } + } + }) + } +} + +func TestAsmAfterVarsWithVariables(t *testing.T) { + // Test that deferred ASM blocks appear in the deferred section + pragma := preproc.NewPragma() + pragma.AddPragma("_P_ASM_AFTER_VARS", "1") + + comp := NewCompiler(pragma) + + // Simple test with just an ASM block + lines := []preproc.Line{ + // ASM line (simulating preprocessor output) + { + Text: " data: !8 1,2,3,4", + Filename: "test.c65", + LineNo: 1, + Kind: preproc.Assembler, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + }, + // Empty source line to close ASM block + { + Text: "", + Filename: "test.c65", + LineNo: 2, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + }, + } + + output, err := comp.Compile(lines) + if err != nil { + t.Fatalf("Compile failed: %v", err) + } + + // Check that deferred section exists and contains our ASM + foundDeferredSection := false + foundDeferredAsm := false + inDeferredSection := false + + for _, line := range output { + if line == "; Deferred ASM blocks (after variables)" { + foundDeferredSection = true + inDeferredSection = true + } + if inDeferredSection && strings.Contains(line, "data: !8 1,2,3,4") { + foundDeferredAsm = true + } + } + + if !foundDeferredSection { + t.Errorf("expected '; Deferred ASM blocks (after variables)' section") + } + if !foundDeferredAsm { + t.Errorf("expected ASM block in deferred section") + } +} diff --git a/language.md b/language.md index c74878d..0fca296 100644 --- a/language.md +++ b/language.md @@ -449,8 +449,22 @@ FUNC calculate({BYTE value}) sta |local| ENDASM FEND + +#### Placing ASM Blocks After Variables + +Use the `_P_ASM_AFTER_VARS` pragma to place ASM blocks after all variable storage and constant strings: + +```c65 +#PRAGMA _P_ASM_AFTER_VARS 1 +ASM +sprite_data: !binary "sprite.bin" + !8 1,2,3,4 +ENDASM +#PRAGMA _P_ASM_AFTER_VARS 0 ``` +This is useful for embedding binary data (graphics, music) with labels that should appear after the variable area. + ### SCRIPT Blocks Generate assembly code at compile time using Starlark (Python-like): @@ -676,6 +690,7 @@ Control compiler behavior: - `_P_USE_CBM_STRINGS`: Use PETSCII encoding for strings - `_P_IGNORE_UNUSED`: Suppress warnings for unused variables - `_P_REMOVE_UNUSED`: Remove unused functions from assembly output (requires explicit pragma on each function) +- `_P_ASM_AFTER_VARS`: Place ASM blocks after all variable storage and constant strings ```c65 #PRAGMA _P_USE_LONG_JUMP 1 // Use JMP instead of branches @@ -685,6 +700,8 @@ Control compiler behavior: #PRAGMA _P_IGNORE_UNUSED 0 // Enable unused variable warnings #PRAGMA _P_REMOVE_UNUSED 1 // Remove function if unused #PRAGMA _P_REMOVE_UNUSED 0 // Keep function even if unused (default) +#PRAGMA _P_ASM_AFTER_VARS 1 // Place ASM blocks after variables +#PRAGMA _P_ASM_AFTER_VARS 0 // Normal ASM block placement (default) ``` ### Debug Directives diff --git a/syntax.md b/syntax.md index 3b2bd52..4d752e6 100644 --- a/syntax.md +++ b/syntax.md @@ -260,6 +260,13 @@ Sets compiler pragmas (options). - Functions are only removed if they are never called in the program - The compiler will output an info message to stdout when a function is removed +**_P_ASM_AFTER_VARS** +- When enabled (value ≠ "" and ≠ "0"), ASM blocks are placed after all variable storage and constant strings +- Useful for embedding binary data (graphics, music) with labels after the variable area +- The compiler adds a comment `; ASM block deferred to end of source` at the original location +- The deferred block includes source location: `; ASM Block from , Line ` +- Value: any non-zero value enables + **Examples:** ``` #PRAGMA _P_USE_LONG_JUMP 1 @@ -270,6 +277,8 @@ Sets compiler pragmas (options). #PRAGMA _P_IGNORE_UNUSED 0 //enable unused variable warnings #PRAGMA _P_REMOVE_UNUSED 1 //remove function if unused #PRAGMA _P_REMOVE_UNUSED 0 //keep function even if unused (default) +#PRAGMA _P_ASM_AFTER_VARS 1 //place ASM blocks after variables +#PRAGMA _P_ASM_AFTER_VARS 0 //normal ASM block placement (default) ``` --- @@ -312,6 +321,13 @@ FUNC calculate({BYTE value}) sta |local| ENDASM FEND + +#PRAGMA _P_ASM_AFTER_VARS 1 +ASM +sprite_data: !binary "sprite.bin" + !8 1,2,3,4 +ENDASM +#PRAGMA _P_ASM_AFTER_VARS 0 ``` **Notes:**