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:
parent
4a7a766aa8
commit
411106ea36
4 changed files with 342 additions and 9 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
language.md
17
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
|
||||
|
|
|
|||
16
syntax.md
16
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 <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:**
|
||||
|
|
|
|||
Loading…
Reference in a new issue