c65gm/internal/compiler/compiler_test.go

957 lines
24 KiB
Go

package compiler
import (
"fmt"
"strings"
"testing"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// TestBreakCommand is a simple command implementation for testing
type TestBreakCommand struct {
line preproc.Line
}
func (c *TestBreakCommand) WillHandle(line preproc.Line) bool {
params, err := utils.ParseParams(line.Text)
if err != nil || len(params) == 0 {
return false
}
return strings.ToUpper(params[0]) == "BREAK"
}
func (c *TestBreakCommand) Interpret(line preproc.Line, ctx *CompilerContext) error {
c.line = line
params, err := utils.ParseParams(line.Text)
if err != nil {
return err
}
if len(params) != 1 {
return fmt.Errorf("BREAK does not expect parameters")
}
return nil
}
func (c *TestBreakCommand) Generate(ctx *CompilerContext) ([]string, error) {
// BREAK jumps to end of WHILE loop
label, err := ctx.LoopEndStack.Peek()
if err != nil {
return nil, fmt.Errorf("BREAK outside of WHILE loop")
}
return []string{
fmt.Sprintf(" jmp %s_end", label),
}, nil
}
func TestCompilerArchitecture(t *testing.T) {
// Create pragma
pragma := preproc.NewPragma()
// Create compiler
comp := NewCompiler(pragma)
// Register BREAK command
comp.Registry().Register(&TestBreakCommand{})
// Create test input - BREAK inside a simulated WHILE
lines := []preproc.Line{
{
Text: "BREAK",
Filename: "test.c65",
LineNo: 1,
Kind: preproc.Source,
PragmaSetIndex: 0,
},
}
// Manually push a WHILE label so BREAK has something to reference
comp.Context().LoopEndStack.Push()
// Compile
output, err := comp.Compile(lines)
// Should fail because BREAK needs proper WHILE context
// But this tests the basic flow: WillHandle -> Interpret -> Generate
if err != nil {
t.Logf("Expected controlled error: %v", err)
}
// Check we got some output structure
if len(output) == 0 {
t.Logf("Got output lines: %d", len(output))
}
t.Logf("Output:\n%s", strings.Join(output, "\n"))
}
func TestCommandRegistry(t *testing.T) {
registry := NewCommandRegistry()
breakCmd := &TestBreakCommand{}
registry.Register(breakCmd)
line := preproc.Line{
Text: "BREAK",
Filename: "test.c65",
LineNo: 1,
Kind: preproc.Source,
}
cmd, found := registry.FindHandler(line)
if !found {
t.Fatal("Expected to find BREAK handler")
}
if cmd != breakCmd {
t.Fatal("Expected to get same command instance")
}
}
func TestCompilerContext(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// Test that all resources are initialized
if ctx.SymbolTable == nil {
t.Error("SymbolTable not initialized")
}
if ctx.FunctionHandler == nil {
t.Error("FunctionHandler not initialized")
}
if ctx.ConstStrHandler == nil {
t.Error("ConstStrHandler not initialized")
}
if ctx.LoopEndStack == nil {
t.Error("LoopEndStack not initialized")
}
if ctx.IfStack == nil {
t.Error("IfStack not initialized")
}
if ctx.GeneralStack == nil {
t.Error("GeneralStack not initialized")
}
if ctx.Pragma == nil {
t.Error("Pragma not initialized")
}
if ctx.ScriptLibraryGlobals == nil {
t.Error("ScriptLibraryGlobals not initialized")
}
if ctx.ScriptMacros == nil {
t.Error("ScriptMacros not initialized")
}
// Test CurrentScope
scope := ctx.CurrentScope()
if scope != nil {
t.Errorf("Expected nil scope in global context, got %v", scope)
}
}
func TestExecuteScript_BasicPrint(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
scriptLines := []string{
"for i in range(3):",
" print(' nop')",
}
output, err := executeScript(scriptLines, ctx, false)
if err != nil {
t.Fatalf("executeScript 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 TestExecuteScript_EmptyOutput(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
scriptLines := []string{
"x = 1 + 1",
}
output, err := executeScript(scriptLines, ctx, false)
if err != nil {
t.Fatalf("executeScript failed: %v", err)
}
if len(output) != 0 {
t.Errorf("expected 0 output lines, got %d: %v", len(output), output)
}
}
func TestExecuteScript_Library_DefinesFunction(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// Define a function in library mode
libraryLines := []string{
"def emit_nops(count):",
" for i in range(count):",
" print(' nop')",
}
_, err := executeScript(libraryLines, ctx, true)
if err != nil {
t.Fatalf("library executeScript failed: %v", err)
}
// Verify function is in globals
if _, ok := ctx.ScriptLibraryGlobals["emit_nops"]; !ok {
t.Fatal("expected emit_nops to be defined in ScriptLibraryGlobals")
}
}
func TestExecuteScript_Library_FunctionCallableFromScript(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// First: define function in library
libraryLines := []string{
"def emit_nops(count):",
" for i in range(count):",
" print(' nop')",
}
_, err := executeScript(libraryLines, ctx, true)
if err != nil {
t.Fatalf("library executeScript failed: %v", err)
}
// Second: call function from regular script
scriptLines := []string{
"emit_nops(2)",
}
output, err := executeScript(scriptLines, ctx, false)
if err != nil {
t.Fatalf("script executeScript failed: %v", err)
}
if len(output) != 2 {
t.Fatalf("expected 2 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 TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// First library: define func_a
lib1 := []string{
"def func_a():",
" print(' ; from a')",
}
_, err := executeScript(lib1, ctx, true)
if err != nil {
t.Fatalf("lib1 failed: %v", err)
}
// Second library: define func_b (should still have func_a)
lib2 := []string{
"def func_b():",
" print(' ; from b')",
}
_, err = executeScript(lib2, ctx, true)
if err != nil {
t.Fatalf("lib2 failed: %v", err)
}
// Both functions should be available
if _, ok := ctx.ScriptLibraryGlobals["func_a"]; !ok {
t.Error("func_a missing after second library")
}
if _, ok := ctx.ScriptLibraryGlobals["func_b"]; !ok {
t.Error("func_b missing after second library")
}
// Call both from a script
scriptLines := []string{
"func_a()",
"func_b()",
}
output, err := executeScript(scriptLines, ctx, false)
if err != nil {
t.Fatalf("script failed: %v", err)
}
if len(output) != 2 {
t.Fatalf("expected 2 lines, got %d: %v", len(output), output)
}
if output[0] != " ; from a" {
t.Errorf("expected ' ; from a', got %q", output[0])
}
if output[1] != " ; from b" {
t.Errorf("expected ' ; from b', got %q", output[1])
}
}
func TestExecuteScript_RegularScript_DoesNotPersist(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// Define function in regular script (not library)
scriptLines := []string{
"def local_func():",
" print('hello')",
"local_func()",
}
output, err := executeScript(scriptLines, ctx, false)
if err != nil {
t.Fatalf("script failed: %v", err)
}
if len(output) != 1 || output[0] != "hello" {
t.Errorf("unexpected output: %v", output)
}
// Function should NOT be in globals (it was in regular script)
if _, ok := ctx.ScriptLibraryGlobals["local_func"]; ok {
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 TestExecuteMacro_LocalVariableExpansion(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// Add a local variable in function scope "testfunc"
ctx.SymbolTable.AddVar("myvar", "testfunc", KindByte, 0)
// Enter the function scope by declaring the function
_, err := ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC testfunc"))
if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err)
}
// Register a macro that uses |%s| pattern - the ACTUAL use case
// The macro parameter 'varname' receives "myvar", then |%s| % varname
// produces |myvar| in the output, which should then be expanded
ctx.ScriptMacros["load_var"] = &ScriptMacro{
Name: "load_var",
Params: []string{"varname"},
Body: []string{
"print(' lda |%s|' % varname)",
},
}
// Execute macro with "myvar" as argument - should expand |myvar| to testfunc_myvar
output, err := ExecuteMacro("load_var", []string{"myvar"}, ctx)
if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err)
}
if len(output) != 1 {
t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
}
expected := " lda testfunc_myvar"
if output[0] != expected {
t.Errorf("expected %q, got %q", expected, output[0])
}
}
func TestExecuteMacro_LocalVariableExpansion_MultipleVars(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// Add local variables in function scope
ctx.SymbolTable.AddVar("color_index", "myfunc", KindByte, 0)
ctx.SymbolTable.AddVar("row_color", "myfunc", KindWord, 0)
// Add a global table (no function scope)
ctx.SymbolTable.AddVar("scroll_color_table", "", KindWord, 0)
// Enter the function scope
_, err := ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC myfunc"))
if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err)
}
// Register a macro using |%s| pattern - matches actual usage:
// SCRIPT MACRO table_lookup(table, index, dest)
// print(" ldx |%s|" % index)
// print(" lda %s,x" % table)
// print(" sta |%s|" % dest)
// ENDSCRIPT
ctx.ScriptMacros["table_lookup"] = &ScriptMacro{
Name: "table_lookup",
Params: []string{"table", "index", "dest"},
Body: []string{
"print(' ldx |%s|' % index)",
"print(' lda %s,x' % table)",
"print(' sta |%s|' % dest)",
},
}
// Execute macro with actual variable names as arguments
output, err := ExecuteMacro("table_lookup", []string{"scroll_color_table", "color_index", "row_color"}, ctx)
if err != nil {
t.Fatalf("ExecuteMacro failed: %v", err)
}
expectedLines := []string{
" ldx myfunc_color_index", // local var expanded with scope
" lda scroll_color_table,x", // global var (no pipes) stays as-is
" sta myfunc_row_color", // local var expanded with scope
}
if len(output) != len(expectedLines) {
t.Fatalf("expected %d output lines, got %d: %v", len(expectedLines), len(output), output)
}
for i, expected := range expectedLines {
if output[i] != expected {
t.Errorf("line %d: expected %q, got %q", i, expected, output[i])
}
}
}
func TestExecuteScript_LocalVariableExpansion(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// Add a local variable in function scope
ctx.SymbolTable.AddVar("counter", "loopfunc", KindByte, 0)
// Enter the function scope
_, err := ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC loopfunc"))
if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err)
}
// Script that uses |varname| syntax
scriptLines := []string{
"print(' inc |counter|')",
}
output, err := executeScript(scriptLines, ctx, false)
if err != nil {
t.Fatalf("executeScript failed: %v", err)
}
if len(output) != 1 {
t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
}
expected := " inc loopfunc_counter"
if output[0] != expected {
t.Errorf("expected %q, got %q", expected, output[0])
}
}
func TestExecuteScript_Library_GlobalVariableExpansion(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// Add a global variable
ctx.SymbolTable.AddVar("global_counter", "", KindByte, 0)
// Library script that uses |varname| syntax at global scope
// Should expand to the global name (unchanged since it's already global)
libraryLines := []string{
"def inc_global():",
" print(' inc |global_counter|')",
}
_, err := executeScript(libraryLines, ctx, true)
if err != nil {
t.Fatalf("library script failed: %v", err)
}
// Now call the library function from a regular script
scriptLines := []string{
"inc_global()",
}
output, err := executeScript(scriptLines, ctx, false)
if err != nil {
t.Fatalf("executeScript failed: %v", err)
}
if len(output) != 1 {
t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
}
// Global variable should stay as-is (no function prefix)
expected := " inc global_counter"
if output[0] != expected {
t.Errorf("expected %q, got %q", expected, output[0])
}
}
func TestExecuteScript_Library_VariableExpansionAtDefinitionTime(t *testing.T) {
pragma := preproc.NewPragma()
ctx := NewCompilerContext(pragma)
// Add a local variable in a function scope
ctx.SymbolTable.AddVar("local_var", "caller", KindByte, 0)
// Library is defined at GLOBAL scope (not inside any function)
// So |varname| expansion happens at global scope during library definition
libraryLines := []string{
"def use_local():",
" print(' lda |local_var|')", // This expands at library definition time
}
// Library defined at global scope - |local_var| won't find caller's local
_, err := executeScript(libraryLines, ctx, true)
if err != nil {
t.Fatalf("library script failed: %v", err)
}
// Now enter the function scope and call the library function
_, err = ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC caller"))
if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err)
}
scriptLines := []string{
"use_local()",
}
output, err := executeScript(scriptLines, ctx, false)
if err != nil {
t.Fatalf("executeScript failed: %v", err)
}
if len(output) != 1 {
t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
}
// Variable expansion happened at library definition time (global scope),
// so local_var was NOT found and stays as literal "local_var"
expected := " lda local_var"
if output[0] != expected {
t.Errorf("expected %q, got %q (library expands |vars| at definition time, not call time)", expected, output[0])
}
}
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)
}
}
}
func TestFindAsmCommentStart(t *testing.T) {
tests := []struct {
input string
want int
}{
// Basic cases
{"lda #$00", -1}, // no comment
{"; comment", 0}, // comment at start
{"lda #$00 ; comment", 9}, // comment after code
{" lda #$00 ; comment", 11}, // with leading whitespace
// Semicolon in double-quoted string
{`!text "hello; world"`, -1}, // no comment, ; inside string
{`!text "hello; world" ; comment`, 21}, // comment after string with ;
{`!text "a;b;c"`, -1}, // multiple ; in string
// Semicolon in single-quoted string
{`!byte ';'`, -1}, // ; as character literal
{`!byte ';' ; comment`, 10}, // comment after ; char literal
// Escape sequences in strings
{`!text "hello\"world"`, -1}, // escaped quote, no comment
{`!text "hello\"world" ; comment`, 21}, // comment after string with escaped quote
{`!text "path\\file"`, -1}, // escaped backslash
{`!text "a\\;b"`, -1}, // escaped backslash before ;
{`!text "a\;b"`, -1}, // escaped ; in string (stays in string)
// Mixed quotes
{`!text "it's"`, -1}, // single quote inside double
{`!byte '"'`, -1}, // double quote as char literal
{`!text "say \"hi\""`, -1}, // escaped quotes in string
// Edge cases
{"", -1}, // empty line
{`""`, -1}, // empty string
{`"" ; comment`, 3}, // empty string then comment
{`!text "unterminated`, -1}, // unterminated string (no ; found)
}
for _, tt := range tests {
got := findAsmCommentStart(tt.input)
if got != tt.want {
t.Errorf("findAsmCommentStart(%q) = %d, want %d", tt.input, got, tt.want)
}
}
}
func TestAsmBlock_VariableExpansion_IgnoresComments(t *testing.T) {
pragma := preproc.NewPragma()
comp := NewCompiler(pragma)
// Add a variable to the symbol table so expansion can work
comp.Context().SymbolTable.AddVar("myvar", "", KindByte, 0)
tests := []struct {
name string
input string
expected string
}{
{
name: "variable in code expands",
input: " lda |myvar|",
expected: " lda myvar",
},
{
name: "variable in comment stays unexpanded",
input: "; use |myvar| here",
expected: "; use |myvar| here",
},
{
name: "variable in code, different in comment",
input: " lda |myvar| ; load |myvar|",
expected: " lda myvar ; load |myvar|",
},
{
name: "only comment with variable",
input: " nop ; |myvar|",
expected: " nop ; |myvar|",
},
{
name: "variable in string not affected",
input: ` !text "|myvar|"`,
expected: ` !text "|myvar|"`,
},
{
name: "variable after string with semicolon",
input: ` !text "a;b" ; |myvar|`,
expected: ` !text "a;b" ; |myvar|`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create fresh compiler for each test
comp := NewCompiler(pragma)
comp.Context().SymbolTable.AddVar("myvar", "", KindByte, 0)
lines := []preproc.Line{
{
Text: tt.input,
Filename: "test.asm",
LineNo: 1,
Kind: preproc.Assembler,
},
{
// Empty source line to close the ASM block
Text: "",
Filename: "test.asm",
LineNo: 2,
Kind: preproc.Source,
},
}
output, err := comp.Compile(lines)
if err != nil {
t.Fatalf("Compile failed: %v", err)
}
// Find the ASM output line (between ; ASM and ; ENDASM markers)
var resultLine string
inAsmBlock := false
for _, line := range output {
if line == "; ASM" {
inAsmBlock = true
continue
}
if line == "; ENDASM" {
break
}
if inAsmBlock {
resultLine = line
break
}
}
if resultLine != tt.expected {
t.Errorf("got %q, want %q\nfull output: %v", resultLine, tt.expected, output)
}
})
}
}
func TestFindPipeOutsideStrings(t *testing.T) {
tests := []struct {
input string
startFrom int
want int
}{
// Basic cases
{"lda |var|", 0, 4},
{"lda |var|", 5, 8},
{"no pipes", 0, -1},
// Pipe in string should be skipped
{`"a|b"`, 0, -1},
{`"a|b" |var|`, 0, 6},
{`'|' |x|`, 0, 4},
// Escape sequences
{`"a\"|b"`, 0, -1}, // escaped quote, pipe still in string
{`"a\\"|b|`, 0, 5}, // escaped backslash, pipe outside
{`"a\|b"`, 0, -1}, // escaped pipe stays in string
// Start from different positions
{"|a| |b|", 0, 0},
{"|a| |b|", 1, 2},
{"|a| |b|", 3, 4},
}
for _, tt := range tests {
got := findPipeOutsideStrings(tt.input, tt.startFrom)
if got != tt.want {
t.Errorf("findPipeOutsideStrings(%q, %d) = %d, want %d", tt.input, tt.startFrom, got, tt.want)
}
}
}
func TestAsmBlock_MacroExpansion_IgnoresComments(t *testing.T) {
pragma := preproc.NewPragma()
comp := NewCompiler(pragma)
// Register a test macro
comp.Context().ScriptMacros["delay"] = &ScriptMacro{
Name: "delay",
Params: []string{"cycles"},
Body: []string{
"for i in range(cycles):",
" print(' nop')",
},
}
tests := []struct {
name string
input string
expectExpanded bool
expectedLines int // number of output lines (excluding comment wrappers)
}{
{
name: "macro in code expands",
input: " |@delay(3)|",
expectExpanded: true,
expectedLines: 3, // 3 nops
},
{
name: "macro in comment does not expand",
input: "; |@delay(3)|",
expectExpanded: false,
expectedLines: 1, // just the original line
},
{
name: "macro after semicolon comment does not expand",
input: " nop ; |@delay(3)|",
expectExpanded: false,
expectedLines: 1, // just the original line with nop
},
{
name: "macro in string does not expand",
input: ` !text "|@delay(3)|"`,
expectExpanded: false,
expectedLines: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create fresh compiler for each test to avoid state issues
comp := NewCompiler(pragma)
comp.Context().ScriptMacros["delay"] = &ScriptMacro{
Name: "delay",
Params: []string{"cycles"},
Body: []string{
"for i in range(cycles):",
" print(' nop')",
},
}
lines := []preproc.Line{
{
Text: tt.input,
Filename: "test.asm",
LineNo: 1,
Kind: preproc.Assembler,
},
{
// Empty source line to close the ASM block
Text: "",
Filename: "test.asm",
LineNo: 2,
Kind: preproc.Source,
},
}
output, err := comp.Compile(lines)
if err != nil {
t.Fatalf("Compile failed: %v", err)
}
// Count nop lines to determine if macro expanded
nopCount := 0
for _, line := range output {
if strings.TrimSpace(line) == "nop" {
nopCount++
}
}
if tt.expectExpanded {
if nopCount != tt.expectedLines {
t.Errorf("expected %d nop lines (macro expanded), got %d\nfull output: %v",
tt.expectedLines, nopCount, output)
}
} else {
if nopCount > 0 {
t.Errorf("expected no nop lines (macro should not expand in comment), got %d\nfull output: %v",
nopCount, output)
}
}
})
}
}