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, preproc.Line{Filename: "test.c65", LineNo: 1}) // 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, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("row_color", "myfunc", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) // Add a global table (no function scope) ctx.SymbolTable.AddVar("scroll_color_table", "", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) // 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, preproc.Line{Filename: "test.c65", LineNo: 1}) // 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, preproc.Line{Filename: "test.c65", LineNo: 1}) // 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, preproc.Line{Filename: "test.c65", LineNo: 1}) // 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, preproc.Line{Filename: "test.c65", LineNo: 1}) 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, preproc.Line{Filename: "test.c65", LineNo: 1}) 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) } } }) } } 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") } }