Added SCRIPT MACRO blocks
This commit is contained in:
parent
c6b67f8044
commit
83a0a20393
9 changed files with 597 additions and 47 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
// SCRIPT LIBRARY Demo
|
// SCRIPT LIBRARY and SCRIPT MACRO Demo
|
||||||
// Demonstrates reusable Starlark functions defined in
|
// Demonstrates reusable Starlark functions and macros
|
||||||
// SCRIPT LIBRARY blocks and called from SCRIPT blocks
|
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
|
|
||||||
#INCLUDE <c64start.c65>
|
#INCLUDE <c64start.c65>
|
||||||
|
|
@ -28,58 +27,75 @@ def emit_delay(cycles):
|
||||||
# 2 cycles per nop
|
# 2 cycles per nop
|
||||||
for i in range(remainder // 2):
|
for i in range(remainder // 2):
|
||||||
print(" nop")
|
print(" nop")
|
||||||
ENDSCRIPT
|
|
||||||
|
|
||||||
//-----------------------------------------------------------
|
|
||||||
// Second SCRIPT LIBRARY: Functions accumulate
|
|
||||||
//-----------------------------------------------------------
|
|
||||||
SCRIPT LIBRARY
|
|
||||||
def emit_border_flash(color1, color2):
|
def emit_border_flash(color1, color2):
|
||||||
print(" lda #%d" % color1)
|
print(" lda #%d" % color1)
|
||||||
print(" sta $d020")
|
print(" sta $d020")
|
||||||
print(" lda #%d" % color2)
|
print(" lda #%d" % color2)
|
||||||
print(" sta $d020")
|
print(" sta $d020")
|
||||||
|
|
||||||
def emit_load_store(value, addr):
|
|
||||||
print(" lda #%d" % value)
|
|
||||||
print(" sta %d" % addr)
|
|
||||||
ENDSCRIPT
|
ENDSCRIPT
|
||||||
|
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
// Main code using library functions
|
// SCRIPT MACRO: Named macros with parameters
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
SCRIPT MACRO delay(cycles)
|
||||||
|
emit_delay(cycles)
|
||||||
|
ENDSCRIPT
|
||||||
|
|
||||||
|
SCRIPT MACRO nops(count)
|
||||||
|
emit_nops(count)
|
||||||
|
ENDSCRIPT
|
||||||
|
|
||||||
|
SCRIPT MACRO border_flash(c1, c2)
|
||||||
|
emit_border_flash(c1, c2)
|
||||||
|
ENDSCRIPT
|
||||||
|
|
||||||
|
// Macro with label parameter (passed as string)
|
||||||
|
SCRIPT MACRO set_irq(handler)
|
||||||
|
print(" lda #<%s" % handler)
|
||||||
|
print(" sta $fffe")
|
||||||
|
print(" lda #>%s" % handler)
|
||||||
|
print(" sta $ffff")
|
||||||
|
ENDSCRIPT
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Constants for testing expression evaluation
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
BYTE CONST CYCLES_PER_LINE = 63
|
||||||
|
BYTE CONST RED = 2
|
||||||
|
BYTE CONST BLUE = 6
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Main code demonstrating macros
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
LABEL start
|
LABEL start
|
||||||
|
|
||||||
// Use emit_nops from first library
|
// Macro invocation outside ASM block
|
||||||
SCRIPT
|
@delay(10)
|
||||||
print("; 5 nops from library")
|
@nops(3)
|
||||||
emit_nops(5)
|
@border_flash(RED, BLUE)
|
||||||
ENDSCRIPT
|
|
||||||
|
|
||||||
// Use emit_delay from first library
|
// Macro with expression argument
|
||||||
SCRIPT
|
@delay(CYCLES_PER_LINE-20)
|
||||||
print("; 10 cycle delay")
|
|
||||||
emit_delay(10)
|
|
||||||
ENDSCRIPT
|
|
||||||
|
|
||||||
// Use emit_border_flash from second library
|
// Macro with label argument
|
||||||
SCRIPT
|
@set_irq(my_handler)
|
||||||
print("; border flash red/blue")
|
|
||||||
emit_border_flash(2, 6)
|
|
||||||
ENDSCRIPT
|
|
||||||
|
|
||||||
// Combine multiple library functions
|
// Macro invocation inside ASM block
|
||||||
SCRIPT
|
ASM
|
||||||
print("; combined: delay + flash + nops")
|
lda #$00
|
||||||
emit_delay(8)
|
|@delay(8)|
|
||||||
emit_border_flash(0, 1)
|
sta $d020
|
||||||
emit_nops(3)
|
|@nops(2)|
|
||||||
ENDSCRIPT
|
lda #$01
|
||||||
|
ENDASM
|
||||||
// Use emit_load_store
|
|
||||||
SCRIPT
|
|
||||||
print("; load/store to border")
|
|
||||||
emit_load_store(5, 0xd020)
|
|
||||||
ENDSCRIPT
|
|
||||||
|
|
||||||
SUBEND
|
SUBEND
|
||||||
|
|
||||||
|
LABEL my_handler
|
||||||
|
ASM
|
||||||
|
pha
|
||||||
|
inc $d020
|
||||||
|
pla
|
||||||
|
rti
|
||||||
|
ENDASM
|
||||||
|
|
|
||||||
46
internal/commands/macro.go
Normal file
46
internal/commands/macro.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"c65gm/internal/compiler"
|
||||||
|
"c65gm/internal/preproc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MacroCommand handles macro invocations
|
||||||
|
// Syntax: @macroname(arg1, arg2, ...)
|
||||||
|
type MacroCommand struct {
|
||||||
|
macroName string
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MacroCommand) WillHandle(line preproc.Line) bool {
|
||||||
|
trimmed := strings.TrimSpace(line.Text)
|
||||||
|
return strings.HasPrefix(trimmed, "@")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MacroCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext) error {
|
||||||
|
trimmed := strings.TrimSpace(line.Text)
|
||||||
|
|
||||||
|
name, args, err := compiler.ParseMacroInvocation(trimmed)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.macroName = name
|
||||||
|
c.args = args
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MacroCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
|
||||||
|
macroOutput, err := compiler.ExecuteMacro(c.macroName, c.args, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("macro %s: %w", c.macroName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return output with end comment
|
||||||
|
result := macroOutput
|
||||||
|
result = append(result, fmt.Sprintf("; end @%s", c.macroName))
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
var lastKind = preproc.Source
|
var lastKind = preproc.Source
|
||||||
var scriptBuffer []string
|
var scriptBuffer []string
|
||||||
var scriptIsLibrary bool
|
var scriptIsLibrary bool
|
||||||
|
var macroBuffer []string
|
||||||
|
var currentMacroName string
|
||||||
|
var currentMacroParams []string
|
||||||
|
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
// Detect kind transitions and emit markers
|
// Detect kind transitions and emit markers
|
||||||
|
|
@ -57,6 +60,21 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Close previous Assembler block
|
||||||
if lastKind == preproc.Assembler {
|
if lastKind == preproc.Assembler {
|
||||||
codeOutput = append(codeOutput, "; ENDASM")
|
codeOutput = append(codeOutput, "; ENDASM")
|
||||||
|
|
@ -71,6 +89,16 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
} else if line.Kind == preproc.ScriptLibrary {
|
} else if line.Kind == preproc.ScriptLibrary {
|
||||||
codeOutput = append(codeOutput, "; SCRIPT LIBRARY")
|
codeOutput = append(codeOutput, "; SCRIPT LIBRARY")
|
||||||
scriptIsLibrary = true
|
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
|
lastKind = line.Kind
|
||||||
|
|
@ -79,8 +107,36 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
// Handle non-source lines
|
// Handle non-source lines
|
||||||
if line.Kind != preproc.Source {
|
if line.Kind != preproc.Source {
|
||||||
if line.Kind == preproc.Assembler {
|
if line.Kind == preproc.Assembler {
|
||||||
// Expand |varname| -> scoped_varname for local variables in ASM blocks
|
|
||||||
text := line.Text
|
text := line.Text
|
||||||
|
|
||||||
|
// Check for |@macro()| pattern first
|
||||||
|
if macroStart := strings.Index(text, "|@"); macroStart != -1 {
|
||||||
|
macroEnd := strings.Index(text[macroStart+1:], "|")
|
||||||
|
if macroEnd != -1 {
|
||||||
|
macroEnd += macroStart + 1
|
||||||
|
invocation := text[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
|
||||||
for {
|
for {
|
||||||
start := strings.IndexByte(text, '|')
|
start := strings.IndexByte(text, '|')
|
||||||
if start == -1 {
|
if start == -1 {
|
||||||
|
|
@ -101,6 +157,13 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
|
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
|
||||||
// Collect script lines for execution
|
// Collect script lines for execution
|
||||||
scriptBuffer = append(scriptBuffer, line.Text)
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +204,8 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
return nil, fmt.Errorf("Unclosed SCRIPT block.")
|
return nil, fmt.Errorf("Unclosed SCRIPT block.")
|
||||||
} else if lastKind == preproc.ScriptLibrary {
|
} else if lastKind == preproc.ScriptLibrary {
|
||||||
return nil, fmt.Errorf("Unclosed SCRIPT LIBRARY block.")
|
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
|
// Analyze for overlapping absolute addresses in function call chains
|
||||||
|
|
@ -219,6 +284,51 @@ func (c *Compiler) checkAbsoluteOverlaps() {
|
||||||
c.ctx.FunctionHandler.ReportAbsoluteOverlaps()
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// assembleOutput combines all generated sections into final assembly
|
// assembleOutput combines all generated sections into final assembly
|
||||||
func (c *Compiler) assembleOutput(codeLines []string) []string {
|
func (c *Compiler) assembleOutput(codeLines []string) []string {
|
||||||
var output []string
|
var output []string
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,9 @@ func TestCompilerContext(t *testing.T) {
|
||||||
if ctx.ScriptLibraryGlobals == nil {
|
if ctx.ScriptLibraryGlobals == nil {
|
||||||
t.Error("ScriptLibraryGlobals not initialized")
|
t.Error("ScriptLibraryGlobals not initialized")
|
||||||
}
|
}
|
||||||
|
if ctx.ScriptMacros == nil {
|
||||||
|
t.Error("ScriptMacros not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
// Test CurrentScope
|
// Test CurrentScope
|
||||||
scope := ctx.CurrentScope()
|
scope := ctx.CurrentScope()
|
||||||
|
|
@ -330,3 +333,129 @@ func TestExecuteScript_RegularScript_DoesNotPersist(t *testing.T) {
|
||||||
t.Error("local_func should not persist from regular script")
|
t.Error("local_func should not persist from regular script")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecuteMacro_Basic(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
// Register a macro
|
||||||
|
ctx.ScriptMacros["test_macro"] = &ScriptMacro{
|
||||||
|
Name: "test_macro",
|
||||||
|
Params: []string{"count"},
|
||||||
|
Body: []string{
|
||||||
|
"for i in range(count):",
|
||||||
|
" print(' nop')",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute macro
|
||||||
|
output, err := ExecuteMacro("test_macro", []string{"3"}, ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExecuteMacro failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) != 3 {
|
||||||
|
t.Fatalf("expected 3 output lines, got %d: %v", len(output), output)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, line := range output {
|
||||||
|
if line != " nop" {
|
||||||
|
t.Errorf("line %d: expected ' nop', got %q", i, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteMacro_WithLibraryFunction(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
// Define library function
|
||||||
|
lib := []string{
|
||||||
|
"def emit_nop():",
|
||||||
|
" print(' nop')",
|
||||||
|
}
|
||||||
|
_, err := executeScript(lib, ctx, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("library failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a macro that uses the library function
|
||||||
|
ctx.ScriptMacros["nop_macro"] = &ScriptMacro{
|
||||||
|
Name: "nop_macro",
|
||||||
|
Params: []string{},
|
||||||
|
Body: []string{
|
||||||
|
"emit_nop()",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute macro
|
||||||
|
output, err := ExecuteMacro("nop_macro", []string{}, ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExecuteMacro failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) != 1 || output[0] != " nop" {
|
||||||
|
t.Errorf("unexpected output: %v", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteMacro_StringParameter(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
// Register a macro with string parameter (label)
|
||||||
|
ctx.ScriptMacros["jump_to"] = &ScriptMacro{
|
||||||
|
Name: "jump_to",
|
||||||
|
Params: []string{"label"},
|
||||||
|
Body: []string{
|
||||||
|
"print(' jmp %s' % label)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute with identifier (should be passed as string)
|
||||||
|
output, err := ExecuteMacro("jump_to", []string{"my_label"}, ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExecuteMacro failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(output) != 1 || output[0] != " jmp my_label" {
|
||||||
|
t.Errorf("unexpected output: %v", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMacroInvocation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
wantName string
|
||||||
|
wantArgs []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"@delay(10)", "delay", []string{"10"}, false},
|
||||||
|
{"@nops(5)", "nops", []string{"5"}, false},
|
||||||
|
{"@setup(80, handler)", "setup", []string{"80", "handler"}, false},
|
||||||
|
{"@empty()", "empty", []string{}, false},
|
||||||
|
{"@expr(10+5)", "expr", []string{"10+5"}, false},
|
||||||
|
{"missing_at()", "", nil, true},
|
||||||
|
{"@no_parens", "", nil, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
name, args, err := ParseMacroInvocation(tt.input)
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ParseMacroInvocation(%q): expected error, got none", tt.input)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ParseMacroInvocation(%q): unexpected error: %v", tt.input, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if name != tt.wantName {
|
||||||
|
t.Errorf("ParseMacroInvocation(%q): name = %q, want %q", tt.input, name, tt.wantName)
|
||||||
|
}
|
||||||
|
if len(args) != len(tt.wantArgs) {
|
||||||
|
t.Errorf("ParseMacroInvocation(%q): args = %v, want %v", tt.input, args, tt.wantArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,13 @@ import (
|
||||||
"go.starlark.net/starlark"
|
"go.starlark.net/starlark"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ScriptMacro represents a named, parameterized script macro
|
||||||
|
type ScriptMacro struct {
|
||||||
|
Name string // macro name
|
||||||
|
Params []string // parameter names
|
||||||
|
Body []string // Starlark code lines (the macro body)
|
||||||
|
}
|
||||||
|
|
||||||
// CompilerContext holds all shared resources needed by commands during compilation
|
// CompilerContext holds all shared resources needed by commands during compilation
|
||||||
type CompilerContext struct {
|
type CompilerContext struct {
|
||||||
// Symbol table for variables and constants
|
// Symbol table for variables and constants
|
||||||
|
|
@ -31,6 +38,9 @@ type CompilerContext struct {
|
||||||
|
|
||||||
// ScriptLibraryGlobals holds persisted Starlark globals from SCRIPT LIBRARY blocks
|
// ScriptLibraryGlobals holds persisted Starlark globals from SCRIPT LIBRARY blocks
|
||||||
ScriptLibraryGlobals starlark.StringDict
|
ScriptLibraryGlobals starlark.StringDict
|
||||||
|
|
||||||
|
// ScriptMacros holds named macro definitions from SCRIPT MACRO blocks
|
||||||
|
ScriptMacros map[string]*ScriptMacro
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCompilerContext creates a new compiler context with initialized resources
|
// NewCompilerContext creates a new compiler context with initialized resources
|
||||||
|
|
@ -51,6 +61,7 @@ func NewCompilerContext(pragma *preproc.Pragma) *CompilerContext {
|
||||||
CaseSkipStack: NewLabelStack("_SKIPCASE"),
|
CaseSkipStack: NewLabelStack("_SKIPCASE"),
|
||||||
Pragma: pragma,
|
Pragma: pragma,
|
||||||
ScriptLibraryGlobals: make(starlark.StringDict),
|
ScriptLibraryGlobals: make(starlark.StringDict),
|
||||||
|
ScriptMacros: make(map[string]*ScriptMacro),
|
||||||
}
|
}
|
||||||
|
|
||||||
// FunctionHandler needs references to other components
|
// FunctionHandler needs references to other components
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@ package compiler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"c65gm/internal/utils"
|
||||||
|
|
||||||
"go.starlark.net/lib/math"
|
"go.starlark.net/lib/math"
|
||||||
"go.starlark.net/starlark"
|
"go.starlark.net/starlark"
|
||||||
)
|
)
|
||||||
|
|
@ -93,3 +96,179 @@ func expandVariables(text string, ctx *CompilerContext) string {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecuteMacro executes a named macro with the given arguments and returns output lines
|
||||||
|
func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]string, error) {
|
||||||
|
// Look up the macro
|
||||||
|
macro, ok := ctx.ScriptMacros[macroName]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("undefined macro: %s", macroName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check argument count
|
||||||
|
if len(args) != len(macro.Params) {
|
||||||
|
return nil, fmt.Errorf("macro %s expects %d arguments, got %d", macroName, len(macro.Params), len(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate arguments and build parameter bindings
|
||||||
|
paramBindings := make(starlark.StringDict)
|
||||||
|
for i, arg := range args {
|
||||||
|
val, err := evaluateMacroArg(arg, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error evaluating argument %d for macro %s: %w", i+1, macroName, err)
|
||||||
|
}
|
||||||
|
paramBindings[macro.Params[i]] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the script: wrap macro body in a function with parameters bound
|
||||||
|
scriptText := strings.Join(macro.Body, "\n")
|
||||||
|
|
||||||
|
// Wrap in function for control flow support
|
||||||
|
finalScript := "def _macro():\n"
|
||||||
|
for _, line := range strings.Split(scriptText, "\n") {
|
||||||
|
finalScript += " " + line + "\n"
|
||||||
|
}
|
||||||
|
finalScript += "_macro()\n"
|
||||||
|
|
||||||
|
// Capture print output
|
||||||
|
var output bytes.Buffer
|
||||||
|
thread := &starlark.Thread{
|
||||||
|
Print: func(_ *starlark.Thread, msg string) {
|
||||||
|
output.WriteString(msg)
|
||||||
|
output.WriteString("\n")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set execution limit
|
||||||
|
thread.SetMaxExecutionSteps(1000000)
|
||||||
|
|
||||||
|
// Build predeclared: math + library globals + parameter bindings
|
||||||
|
predeclared := starlark.StringDict{
|
||||||
|
"math": math.Module,
|
||||||
|
}
|
||||||
|
for k, v := range ctx.ScriptLibraryGlobals {
|
||||||
|
predeclared[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range paramBindings {
|
||||||
|
predeclared[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
_, err := starlark.ExecFile(thread, "macro.star", finalScript, predeclared)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split output into lines
|
||||||
|
outputStr := output.String()
|
||||||
|
if outputStr == "" {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
return strings.Split(strings.TrimRight(outputStr, "\n"), "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// evaluateMacroArg evaluates a macro argument, returning either an int or string Starlark value
|
||||||
|
func evaluateMacroArg(arg string, ctx *CompilerContext) (starlark.Value, error) {
|
||||||
|
arg = strings.TrimSpace(arg)
|
||||||
|
|
||||||
|
// Create lookup function for constants
|
||||||
|
lookup := func(name string) (int64, bool) {
|
||||||
|
sym := ctx.SymbolTable.Lookup(name, nil)
|
||||||
|
if sym != nil && sym.IsConst() {
|
||||||
|
return int64(sym.Value), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to evaluate as expression (number, constant, arithmetic)
|
||||||
|
val, err := utils.EvaluateExpression(arg, lookup)
|
||||||
|
if err == nil {
|
||||||
|
// Successfully evaluated as integer
|
||||||
|
return starlark.MakeInt64(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a valid identifier, treat as label (string)
|
||||||
|
if utils.ValidateIdentifier(arg) {
|
||||||
|
return starlark.String(arg), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, error
|
||||||
|
return nil, fmt.Errorf("invalid macro argument: %s (not a valid expression or identifier)", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMacroInvocation parses "@name(arg1, arg2, ...)" and returns name and args
|
||||||
|
func ParseMacroInvocation(invocation string) (string, []string, error) {
|
||||||
|
invocation = strings.TrimSpace(invocation)
|
||||||
|
|
||||||
|
// Must start with @
|
||||||
|
if !strings.HasPrefix(invocation, "@") {
|
||||||
|
return "", nil, fmt.Errorf("macro invocation must start with @")
|
||||||
|
}
|
||||||
|
rest := invocation[1:]
|
||||||
|
|
||||||
|
// Find opening paren
|
||||||
|
parenStart := strings.IndexByte(rest, '(')
|
||||||
|
if parenStart == -1 {
|
||||||
|
return "", nil, fmt.Errorf("macro invocation missing '(': %s", invocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find closing paren
|
||||||
|
parenEnd := strings.LastIndexByte(rest, ')')
|
||||||
|
if parenEnd == -1 || parenEnd < parenStart {
|
||||||
|
return "", nil, fmt.Errorf("macro invocation missing ')': %s", invocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSpace(rest[:parenStart])
|
||||||
|
if name == "" {
|
||||||
|
return "", nil, fmt.Errorf("macro name is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse arguments (handle nested parens for expressions)
|
||||||
|
argStr := strings.TrimSpace(rest[parenStart+1 : parenEnd])
|
||||||
|
args, err := splitMacroArgs(argStr)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitMacroArgs splits comma-separated arguments, respecting parentheses
|
||||||
|
func splitMacroArgs(argStr string) ([]string, error) {
|
||||||
|
if argStr == "" {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
var current strings.Builder
|
||||||
|
parenDepth := 0
|
||||||
|
|
||||||
|
for _, ch := range argStr {
|
||||||
|
if ch == '(' {
|
||||||
|
parenDepth++
|
||||||
|
current.WriteRune(ch)
|
||||||
|
} else if ch == ')' {
|
||||||
|
parenDepth--
|
||||||
|
if parenDepth < 0 {
|
||||||
|
return nil, fmt.Errorf("unbalanced parentheses in macro arguments")
|
||||||
|
}
|
||||||
|
current.WriteRune(ch)
|
||||||
|
} else if ch == ',' && parenDepth == 0 {
|
||||||
|
args = append(args, strings.TrimSpace(current.String()))
|
||||||
|
current.Reset()
|
||||||
|
} else {
|
||||||
|
current.WriteRune(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parenDepth != 0 {
|
||||||
|
return nil, fmt.Errorf("unbalanced parentheses in macro arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add final argument
|
||||||
|
if current.Len() > 0 {
|
||||||
|
args = append(args, strings.TrimSpace(current.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const (
|
||||||
Assembler
|
Assembler
|
||||||
Script
|
Script
|
||||||
ScriptLibrary
|
ScriptLibrary
|
||||||
|
ScriptMacroDef
|
||||||
)
|
)
|
||||||
|
|
||||||
// Line represents one post-processed source line and its provenance.
|
// Line represents one post-processed source line and its provenance.
|
||||||
|
|
@ -53,6 +54,7 @@ type preproc struct {
|
||||||
inAsm bool // true when inside ASM/ENDASM block
|
inAsm bool // true when inside ASM/ENDASM block
|
||||||
inScript bool // true when inside SCRIPT/ENDSCRIPT block
|
inScript bool // true when inside SCRIPT/ENDSCRIPT block
|
||||||
inScriptLibrary bool // true when inside SCRIPT LIBRARY/ENDSCRIPT block
|
inScriptLibrary bool // true when inside SCRIPT LIBRARY/ENDSCRIPT block
|
||||||
|
inScriptMacro bool // true when inside SCRIPT MACRO/ENDSCRIPT block
|
||||||
reader FileReader // file reader abstraction
|
reader FileReader // file reader abstraction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,20 +120,31 @@ func (p *preproc) run(root string) ([]Line, error) {
|
||||||
tokens := strings.Fields(raw)
|
tokens := strings.Fields(raw)
|
||||||
|
|
||||||
// ASM mode handling
|
// ASM mode handling
|
||||||
if !p.inAsm && !p.inScript && !p.inScriptLibrary {
|
if !p.inAsm && !p.inScript && !p.inScriptLibrary && !p.inScriptMacro {
|
||||||
// Check for ASM entry
|
// Check for ASM entry
|
||||||
if includeSource && len(tokens) > 0 && tokens[0] == "ASM" {
|
if includeSource && len(tokens) > 0 && tokens[0] == "ASM" {
|
||||||
p.inAsm = true
|
p.inAsm = true
|
||||||
continue // don't emit ASM marker
|
continue // don't emit ASM marker
|
||||||
}
|
}
|
||||||
// Check for SCRIPT entry (SCRIPT LIBRARY or plain SCRIPT)
|
// Check for SCRIPT entry (SCRIPT LIBRARY, SCRIPT MACRO, or plain SCRIPT)
|
||||||
if includeSource && len(tokens) > 0 && tokens[0] == "SCRIPT" {
|
if includeSource && len(tokens) > 0 && tokens[0] == "SCRIPT" {
|
||||||
if len(tokens) > 1 && tokens[1] == "LIBRARY" {
|
if len(tokens) > 1 && tokens[1] == "LIBRARY" {
|
||||||
p.inScriptLibrary = true
|
p.inScriptLibrary = true
|
||||||
|
} else if len(tokens) > 1 && tokens[1] == "MACRO" {
|
||||||
|
p.inScriptMacro = true
|
||||||
|
// Emit the header line (SCRIPT MACRO name(params)) for compiler to parse
|
||||||
|
out = append(out, Line{
|
||||||
|
RawText: raw,
|
||||||
|
Text: raw,
|
||||||
|
Filename: currFrame.path,
|
||||||
|
LineNo: currFrame.line,
|
||||||
|
Kind: ScriptMacroDef,
|
||||||
|
PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
p.inScript = true
|
p.inScript = true
|
||||||
}
|
}
|
||||||
continue // don't emit SCRIPT marker
|
continue // don't emit SCRIPT marker (except for MACRO which emits header)
|
||||||
}
|
}
|
||||||
} else if p.inAsm {
|
} else if p.inAsm {
|
||||||
// We're in ASM mode
|
// We're in ASM mode
|
||||||
|
|
@ -150,18 +163,21 @@ func (p *preproc) run(root string) ([]Line, error) {
|
||||||
PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
|
PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
} else if p.inScript || p.inScriptLibrary {
|
} else if p.inScript || p.inScriptLibrary || p.inScriptMacro {
|
||||||
// We're in SCRIPT or SCRIPT LIBRARY mode
|
// We're in SCRIPT, SCRIPT LIBRARY, or SCRIPT MACRO mode
|
||||||
// Check for ENDSCRIPT
|
// Check for ENDSCRIPT
|
||||||
if len(tokens) > 0 && tokens[0] == "ENDSCRIPT" {
|
if len(tokens) > 0 && tokens[0] == "ENDSCRIPT" {
|
||||||
p.inScript = false
|
p.inScript = false
|
||||||
p.inScriptLibrary = false
|
p.inScriptLibrary = false
|
||||||
|
p.inScriptMacro = false
|
||||||
continue // don't emit ENDSCRIPT marker
|
continue // don't emit ENDSCRIPT marker
|
||||||
}
|
}
|
||||||
// Determine the kind based on which mode we're in
|
// Determine the kind based on which mode we're in
|
||||||
kind := Script
|
kind := Script
|
||||||
if p.inScriptLibrary {
|
if p.inScriptLibrary {
|
||||||
kind = ScriptLibrary
|
kind = ScriptLibrary
|
||||||
|
} else if p.inScriptMacro {
|
||||||
|
kind = ScriptMacroDef
|
||||||
}
|
}
|
||||||
// Emit line verbatim with appropriate kind
|
// Emit line verbatim with appropriate kind
|
||||||
out = append(out, Line{
|
out = append(out, Line{
|
||||||
|
|
|
||||||
|
|
@ -1021,6 +1021,48 @@ func TestPreProcess_ScriptVsScriptLibrary(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreProcess_ScriptMacroBlock(t *testing.T) {
|
||||||
|
files := map[string][]string{
|
||||||
|
"test.c65": {
|
||||||
|
"SCRIPT MACRO delay(cycles)",
|
||||||
|
" emit_delay(cycles)",
|
||||||
|
"ENDSCRIPT",
|
||||||
|
"NOP",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
reader := NewMockFileReader(files)
|
||||||
|
lines, _, err := PreProcess("test.c65", reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PreProcess failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have header + body line + source line = 3 lines
|
||||||
|
if len(lines) != 3 {
|
||||||
|
t.Fatalf("expected 3 lines, got %d", len(lines))
|
||||||
|
}
|
||||||
|
|
||||||
|
// First line is the header (also ScriptMacroDef kind)
|
||||||
|
if lines[0].Kind != ScriptMacroDef {
|
||||||
|
t.Errorf("line 0: expected ScriptMacroDef, got %v", lines[0].Kind)
|
||||||
|
}
|
||||||
|
if lines[0].Text != "SCRIPT MACRO delay(cycles)" {
|
||||||
|
t.Errorf("line 0: expected header, got %q", lines[0].Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second line is macro body
|
||||||
|
if lines[1].Kind != ScriptMacroDef {
|
||||||
|
t.Errorf("line 1: expected ScriptMacroDef, got %v", lines[1].Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third line is source
|
||||||
|
if lines[2].Kind != Source {
|
||||||
|
t.Errorf("line 2: expected Source, got %v", lines[2].Kind)
|
||||||
|
}
|
||||||
|
if lines[2].Text != "NOP" {
|
||||||
|
t.Errorf("line 2: expected 'NOP', got %q", lines[2].Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPreProcess_DollarEscapeExpansion(t *testing.T) {
|
func TestPreProcess_DollarEscapeExpansion(t *testing.T) {
|
||||||
files := map[string][]string{
|
files := map[string][]string{
|
||||||
"test.c65": {
|
"test.c65": {
|
||||||
|
|
|
||||||
1
main.go
1
main.go
|
|
@ -111,6 +111,7 @@ func registerCommands(comp *compiler.Compiler) {
|
||||||
comp.Registry().Register(&commands.CaseCommand{})
|
comp.Registry().Register(&commands.CaseCommand{})
|
||||||
comp.Registry().Register(&commands.DefaultCommand{})
|
comp.Registry().Register(&commands.DefaultCommand{})
|
||||||
comp.Registry().Register(&commands.EndSwitchCommand{})
|
comp.Registry().Register(&commands.EndSwitchCommand{})
|
||||||
|
comp.Registry().Register(&commands.MacroCommand{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeOutput(filename string, lines []string) error {
|
func writeOutput(filename string, lines []string) error {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue