Added SCRIPT MACRO blocks

This commit is contained in:
Mattias Hansson 2026-02-11 13:56:21 +01:00
parent c6b67f8044
commit 83a0a20393
9 changed files with 597 additions and 47 deletions

View file

@ -1,7 +1,6 @@
//-----------------------------------------------------------
// SCRIPT LIBRARY Demo
// Demonstrates reusable Starlark functions defined in
// SCRIPT LIBRARY blocks and called from SCRIPT blocks
// SCRIPT LIBRARY and SCRIPT MACRO Demo
// Demonstrates reusable Starlark functions and macros
//-----------------------------------------------------------
#INCLUDE <c64start.c65>
@ -28,58 +27,75 @@ def emit_delay(cycles):
# 2 cycles per nop
for i in range(remainder // 2):
print(" nop")
ENDSCRIPT
//-----------------------------------------------------------
// Second SCRIPT LIBRARY: Functions accumulate
//-----------------------------------------------------------
SCRIPT LIBRARY
def emit_border_flash(color1, color2):
print(" lda #%d" % color1)
print(" sta $d020")
print(" lda #%d" % color2)
print(" sta $d020")
def emit_load_store(value, addr):
print(" lda #%d" % value)
print(" sta %d" % addr)
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
// Use emit_nops from first library
SCRIPT
print("; 5 nops from library")
emit_nops(5)
ENDSCRIPT
// Macro invocation outside ASM block
@delay(10)
@nops(3)
@border_flash(RED, BLUE)
// Use emit_delay from first library
SCRIPT
print("; 10 cycle delay")
emit_delay(10)
ENDSCRIPT
// Macro with expression argument
@delay(CYCLES_PER_LINE-20)
// Use emit_border_flash from second library
SCRIPT
print("; border flash red/blue")
emit_border_flash(2, 6)
ENDSCRIPT
// Macro with label argument
@set_irq(my_handler)
// Combine multiple library functions
SCRIPT
print("; combined: delay + flash + nops")
emit_delay(8)
emit_border_flash(0, 1)
emit_nops(3)
ENDSCRIPT
// Use emit_load_store
SCRIPT
print("; load/store to border")
emit_load_store(5, 0xd020)
ENDSCRIPT
// Macro invocation inside ASM block
ASM
lda #$00
|@delay(8)|
sta $d020
|@nops(2)|
lda #$01
ENDASM
SUBEND
LABEL my_handler
ASM
pha
inc $d020
pla
rti
ENDASM

View 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
}

View file

@ -38,6 +38,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
var lastKind = preproc.Source
var scriptBuffer []string
var scriptIsLibrary bool
var macroBuffer []string
var currentMacroName string
var currentMacroParams []string
for i, line := range lines {
// 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
if lastKind == preproc.Assembler {
codeOutput = append(codeOutput, "; ENDASM")
@ -71,6 +89,16 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
} 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
@ -79,8 +107,36 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
// Handle non-source lines
if line.Kind != preproc.Source {
if line.Kind == preproc.Assembler {
// Expand |varname| -> scoped_varname for local variables in ASM blocks
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 {
start := strings.IndexByte(text, '|')
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 {
// 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
}
@ -141,6 +204,8 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
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
@ -219,6 +284,51 @@ 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
}
// assembleOutput combines all generated sections into final assembly
func (c *Compiler) assembleOutput(codeLines []string) []string {
var output []string

View file

@ -142,6 +142,9 @@ func TestCompilerContext(t *testing.T) {
if ctx.ScriptLibraryGlobals == nil {
t.Error("ScriptLibraryGlobals not initialized")
}
if ctx.ScriptMacros == nil {
t.Error("ScriptMacros not initialized")
}
// Test 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")
}
}
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)
}
}
}

View file

@ -6,6 +6,13 @@ import (
"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
type CompilerContext struct {
// Symbol table for variables and constants
@ -31,6 +38,9 @@ type CompilerContext struct {
// ScriptLibraryGlobals holds persisted Starlark globals from SCRIPT LIBRARY blocks
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
@ -51,6 +61,7 @@ func NewCompilerContext(pragma *preproc.Pragma) *CompilerContext {
CaseSkipStack: NewLabelStack("_SKIPCASE"),
Pragma: pragma,
ScriptLibraryGlobals: make(starlark.StringDict),
ScriptMacros: make(map[string]*ScriptMacro),
}
// FunctionHandler needs references to other components

View file

@ -2,8 +2,11 @@ package compiler
import (
"bytes"
"fmt"
"strings"
"c65gm/internal/utils"
"go.starlark.net/lib/math"
"go.starlark.net/starlark"
)
@ -93,3 +96,179 @@ func expandVariables(text string, ctx *CompilerContext) string {
}
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
}

View file

@ -13,6 +13,7 @@ const (
Assembler
Script
ScriptLibrary
ScriptMacroDef
)
// 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
inScript bool // true when inside SCRIPT/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
}
@ -118,20 +120,31 @@ func (p *preproc) run(root string) ([]Line, error) {
tokens := strings.Fields(raw)
// ASM mode handling
if !p.inAsm && !p.inScript && !p.inScriptLibrary {
if !p.inAsm && !p.inScript && !p.inScriptLibrary && !p.inScriptMacro {
// Check for ASM entry
if includeSource && len(tokens) > 0 && tokens[0] == "ASM" {
p.inAsm = true
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 len(tokens) > 1 && tokens[1] == "LIBRARY" {
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 {
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 {
// We're in ASM mode
@ -150,18 +163,21 @@ func (p *preproc) run(root string) ([]Line, error) {
PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
})
continue
} else if p.inScript || p.inScriptLibrary {
// We're in SCRIPT or SCRIPT LIBRARY mode
} else if p.inScript || p.inScriptLibrary || p.inScriptMacro {
// We're in SCRIPT, SCRIPT LIBRARY, or SCRIPT MACRO mode
// Check for ENDSCRIPT
if len(tokens) > 0 && tokens[0] == "ENDSCRIPT" {
p.inScript = false
p.inScriptLibrary = false
p.inScriptMacro = false
continue // don't emit ENDSCRIPT marker
}
// Determine the kind based on which mode we're in
kind := Script
if p.inScriptLibrary {
kind = ScriptLibrary
} else if p.inScriptMacro {
kind = ScriptMacroDef
}
// Emit line verbatim with appropriate kind
out = append(out, Line{

View file

@ -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) {
files := map[string][]string{
"test.c65": {

View file

@ -111,6 +111,7 @@ func registerCommands(comp *compiler.Compiler) {
comp.Registry().Register(&commands.CaseCommand{})
comp.Registry().Register(&commands.DefaultCommand{})
comp.Registry().Register(&commands.EndSwitchCommand{})
comp.Registry().Register(&commands.MacroCommand{})
}
func writeOutput(filename string, lines []string) error {