Added pragma _P_ASM_AFTER_VARS to control is ASM blocks should be generated after everything else (vars and string constants)

This commit is contained in:
Mattias Hansson 2026-04-17 23:01:42 +02:00
parent 4a7a766aa8
commit 411106ea36
4 changed files with 342 additions and 9 deletions

View file

@ -13,6 +13,7 @@ import (
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
@ -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 {
// 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
}

View file

@ -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")
}
}

View file

@ -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

View file

@ -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 <filename>, Line <line number>`
- 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:**