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