package preproc import ( "strings" "testing" ) func TestPreProcess_BasicDefine(t *testing.T) { files := map[string][]string{ "test.c65": { "#DEFINE FOO = 42", "LDA #FOO", "STA $D020", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 2 { t.Fatalf("expected 2 lines, got %d", len(lines)) } if lines[0].Text != "LDA #42" { t.Errorf("expected 'LDA #42', got %q", lines[0].Text) } if lines[0].Kind != Source { t.Errorf("expected Kind=Source, got %v", lines[0].Kind) } } func TestPreProcess_DefineExpansion(t *testing.T) { files := map[string][]string{ "test.c65": { "#DEFINE BASE = $D000", "#DEFINE OFFSET = 32", "#DEFINE ADDR = BASE+OFFSET", "STA ADDR", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 1 { t.Fatalf("expected 1 line, got %d", len(lines)) } if lines[0].Text != "STA $D000+32" { t.Errorf("expected 'STA $D000+32', got %q", lines[0].Text) } } func TestPreProcess_IncludeGuard(t *testing.T) { files := map[string][]string{ "lib.c65": { "#IFNDEF __LIB", "#DEFINE __LIB = 1", "LABEL lib_init", "#IFEND", }, "main.c65": { "#INCLUDE lib.c65", "#INCLUDE lib.c65", "GOTO main", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("main.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 2 { t.Fatalf("expected 2 lines, got %d", len(lines)) } if lines[0].Text != "LABEL lib_init" { t.Errorf("expected 'LABEL lib_init', got %q", lines[0].Text) } if lines[1].Text != "GOTO main" { t.Errorf("expected 'GOTO main', got %q", lines[1].Text) } } func TestPreProcess_ASMBlock(t *testing.T) { files := map[string][]string{ "test.c65": { "#DEFINE FOO = 42", "ASM", " lda #FOO", " sta $d020", "ENDASM", "LDA #FOO", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } // ASM and ENDASM markers are stripped, so only 2 asm lines + 1 source line if len(lines) != 3 { t.Fatalf("expected 3 lines, got %d", len(lines)) } // ASM content should NOT be processed if lines[0].Text != " lda #FOO" { t.Errorf("expected ' lda #FOO', got %q", lines[0].Text) } if lines[0].Kind != Assembler { t.Errorf("expected Kind=Assembler, got %v", lines[0].Kind) } if lines[1].Text != " sta $d020" { t.Errorf("expected ' sta $d020', got %q", lines[1].Text) } if lines[1].Kind != Assembler { t.Errorf("expected Kind=Assembler, got %v", lines[1].Kind) } // After ENDASM, defines work again if lines[2].Text != "LDA #42" { t.Errorf("expected 'LDA #42', got %q", lines[2].Text) } if lines[2].Kind != Source { t.Errorf("expected Kind=Source, got %v", lines[2].Kind) } } func TestPreProcess_ScriptBlock(t *testing.T) { files := map[string][]string{ "test.c65": { "#DEFINE VAR = 100", "SCRIPT", " x = VAR + 1", " print(x)", "ENDSCRIPT", "LDA #VAR", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } // SCRIPT and ENDSCRIPT markers are stripped if len(lines) != 3 { t.Fatalf("expected 3 lines, got %d", len(lines)) } // Script content should NOT be processed if lines[0].Text != " x = VAR + 1" { t.Errorf("expected ' x = VAR + 1', got %q", lines[0].Text) } if lines[0].Kind != Script { t.Errorf("expected Kind=Script, got %v", lines[0].Kind) } if lines[1].Text != " print(x)" { t.Errorf("expected ' print(x)', got %q", lines[1].Text) } if lines[1].Kind != Script { t.Errorf("expected Kind=Script, got %v", lines[1].Kind) } // After ENDSCRIPT, defines work again if lines[2].Text != "LDA #100" { t.Errorf("expected 'LDA #100', got %q", lines[2].Text) } if lines[2].Kind != Source { t.Errorf("expected Kind=Source, got %v", lines[2].Kind) } } func TestPreProcess_CommentStripping(t *testing.T) { files := map[string][]string{ "test.c65": { "LDA #42 // load accumulator", "STA $D020 // border color", "NOP // comment only", "JMP $0810", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 4 { t.Fatalf("expected 4 lines, got %d", len(lines)) } if lines[0].Text != "LDA #42" { t.Errorf("expected 'LDA #42', got %q", lines[0].Text) } if lines[0].RawText != "LDA #42 // load accumulator" { t.Errorf("expected RawText to preserve comment, got %q", lines[0].RawText) } if lines[1].Text != "STA $D020" { t.Errorf("expected 'STA $D020', got %q", lines[1].Text) } if lines[2].Text != "NOP" { t.Errorf("expected 'NOP', got %q", lines[2].Text) } if lines[3].Text != "JMP $0810" { t.Errorf("expected 'JMP $0810', got %q", lines[3].Text) } } func TestPreProcess_CommentInASMBlock(t *testing.T) { files := map[string][]string{ "test.c65": { "ASM", " lda #42 // this comment stays", " sta $d020 // this too", "ENDASM", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 2 { t.Fatalf("expected 2 lines, got %d", len(lines)) } // Comments should be preserved in ASM blocks if lines[0].Text != " lda #42 // this comment stays" { t.Errorf("expected comment preserved, got %q", lines[0].Text) } if lines[1].Text != " sta $d020 // this too" { t.Errorf("expected comment preserved, got %q", lines[1].Text) } } func TestPreProcess_CommentInScriptBlock(t *testing.T) { files := map[string][]string{ "test.c65": { "SCRIPT", " x = 1 // script comment", " y = 2 // another one", "ENDSCRIPT", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 2 { t.Fatalf("expected 2 lines, got %d", len(lines)) } // Comments should be preserved in Script blocks if lines[0].Text != " x = 1 // script comment" { t.Errorf("expected comment preserved, got %q", lines[0].Text) } if lines[1].Text != " y = 2 // another one" { t.Errorf("expected comment preserved, got %q", lines[1].Text) } } func TestPreProcess_RawTextPreservation(t *testing.T) { files := map[string][]string{ "test.c65": { "#DEFINE FOO = 42", "LDA #FOO // comment here", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 1 { t.Fatalf("expected 1 line, got %d", len(lines)) } // RawText should be original if lines[0].RawText != "LDA #FOO // comment here" { t.Errorf("expected RawText 'LDA #FOO // comment here', got %q", lines[0].RawText) } // Text should be processed if lines[0].Text != "LDA #42" { t.Errorf("expected Text 'LDA #42', got %q", lines[0].Text) } } func TestPreProcess_MismatchedBlockTerminators(t *testing.T) { tests := []struct { name string lines []string }{ { name: "ASM ended with ENDSCRIPT", lines: []string{ "ASM", " lda #42", "ENDSCRIPT", "NOP", }, }, { name: "SCRIPT ended with ENDASM", lines: []string{ "SCRIPT", " x = 1", "ENDASM", "NOP", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { files := map[string][]string{ "test.c65": tt.lines, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } // Wrong terminator won't close the block // All lines including the wrong terminator should be in the block if len(lines) < 2 { t.Errorf("expected at least 2 lines, got %d", len(lines)) } // The mismatched terminator should be treated as block content // and NOP should still be in the block too for _, line := range lines { if line.Kind == Source { t.Errorf("found Source line when all should be in block: %q", line.Text) } } }) } } func TestPreProcess_NestedConditionals(t *testing.T) { files := map[string][]string{ "test.c65": { "#DEFINE OUTER", "#IFDEF OUTER", " LINE1", " #IFDEF INNER", " LINE2", " #IFEND", " LINE3", "#IFEND", "ALWAYS", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 3 { t.Fatalf("expected 3 lines, got %d", len(lines)) } expected := []string{"LINE1", "LINE3", "ALWAYS"} for i, exp := range expected { if !strings.Contains(lines[i].Text, exp) { t.Errorf("line %d: expected %q, got %q", i, exp, lines[i].Text) } } } func TestPreProcess_ConditionalBlocking(t *testing.T) { files := map[string][]string{ "test.c65": { "#IFNDEF UNDEFINED", " VISIBLE", " #DEFINE FOO = 1", "#IFEND", "#IFDEF UNDEFINED", " INVISIBLE", " #DEFINE BAR = 2", "#IFEND", "LDA #FOO", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 2 { t.Fatalf("expected 2 lines, got %d", len(lines)) } // FOO should be defined, BAR should not if lines[1].Text != "LDA #1" { t.Errorf("expected 'LDA #1', got %q", lines[1].Text) } } func TestPreProcess_NestedIncludes(t *testing.T) { files := map[string][]string{ "a.c65": { "#INCLUDE b.c65", "LINE_A", }, "b.c65": { "#INCLUDE c.c65", "LINE_B", }, "c.c65": { "LINE_C", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("a.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 3 { t.Fatalf("expected 3 lines, got %d", len(lines)) } expected := []string{"LINE_C", "LINE_B", "LINE_A"} for i, exp := range expected { if lines[i].Text != exp { t.Errorf("line %d: expected %q, got %q", i, exp, lines[i].Text) } } } func TestPreProcess_Filename(t *testing.T) { files := map[string][]string{ "main.c65": { "#INCLUDE lib.c65", "MAIN_LINE", }, "lib.c65": { "LIB_LINE", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("main.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if lines[0].Filename != "lib.c65" { t.Errorf("expected filename 'lib.c65', got %q", lines[0].Filename) } if lines[1].Filename != "main.c65" { t.Errorf("expected filename 'main.c65', got %q", lines[1].Filename) } } func TestPreProcess_LineNumbers(t *testing.T) { files := map[string][]string{ "test.c65": { "LINE1", "LINE2", "#DEFINE FOO = 1", "LINE4", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } expectedLines := []int{1, 2, 4} for i, exp := range expectedLines { if lines[i].LineNo != exp { t.Errorf("line %d: expected line number %d, got %d", i, exp, lines[i].LineNo) } } } func TestPreProcess_Undef(t *testing.T) { files := map[string][]string{ "test.c65": { "#DEFINE FOO = 1", "LDA #FOO", "#UNDEF FOO", "LDA #FOO", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if lines[0].Text != "LDA #1" { t.Errorf("expected 'LDA #1', got %q", lines[0].Text) } if lines[1].Text != "LDA #FOO" { t.Errorf("expected 'LDA #FOO' (unexpanded), got %q", lines[1].Text) } } func TestPreProcess_PragmaWithDefines(t *testing.T) { files := map[string][]string{ "test.c65": { "#DEFINE SETTING = 1", "#PRAGMA USE_FEATURE SETTING", "CODE", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 1 { t.Fatalf("expected 1 line, got %d", len(lines)) } } func TestPreProcess_EmptyFile(t *testing.T) { files := map[string][]string{ "test.c65": {}, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 0 { t.Errorf("expected 0 lines, got %d", len(lines)) } } func TestPreProcess_MissingInclude(t *testing.T) { files := map[string][]string{ "test.c65": { "#INCLUDE notfound.c65", }, } reader := NewMockFileReader(files) _, _, err := PreProcess("test.c65", reader) if err == nil { t.Error("expected error for missing include") } } func TestPreProcess_HaltDirective(t *testing.T) { files := map[string][]string{ "test.c65": { "LINE1", "#HALT", "LINE2", }, } reader := NewMockFileReader(files) _, _, err := PreProcess("test.c65", reader) if err == nil { t.Error("expected HaltError") } if _, ok := err.(HaltError); !ok { t.Errorf("expected HaltError, got %T", err) } } func TestPreProcess_ConditionalHalt(t *testing.T) { files := map[string][]string{ "test.c65": { "LINE1", "#IFDEF UNDEFINED", "#HALT", "#IFEND", "LINE2", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } // HALT is inside false conditional, should not trigger if len(lines) != 2 { t.Errorf("expected 2 lines, got %d", len(lines)) } } func TestPreProcess_ComplexIncludeGuard(t *testing.T) { files := map[string][]string{ "c64scr.c65": { "#IFNDEF __C64_SCR", "#DEFINE __C64_SCR = 1", "GOTO lib_c64scr_skip", "LABEL lib_c64scr_blank", "ASM", " lda $d011", "ENDASM", "SUBEND", "LABEL lib_c64scr_skip", "#IFEND", }, "main.c65": { "#INCLUDE c64scr.c65", "#INCLUDE c64scr.c65", "MAIN", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("main.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } // First include should emit the library, second should be blocked count := 0 for _, line := range lines { if strings.Contains(line.Text, "lda $d011") { count++ } } if count != 1 { t.Errorf("expected 1 occurrence from multiple includes of same file, got %d", count) } } func TestPreProcess_UnbalancedConditionals(t *testing.T) { files := map[string][]string{ "test.c65": { "#DEFINE FOO", "#IFDEF FOO", " LINE1", " LINE2", // Missing #IFEND }, } reader := NewMockFileReader(files) _, _, err := PreProcess("test.c65", reader) if err == nil { t.Error("expected error for unbalanced conditionals") } if !strings.Contains(err.Error(), "unbalanced") { t.Errorf("expected 'unbalanced' error, got: %v", err) } } func TestPreProcess_FilenameAndLineNumberTracking(t *testing.T) { files := map[string][]string{ "main.c65": { "MAIN_LINE1", // line 1 "#INCLUDE lib.c65", // line 2 (directive, not emitted) "MAIN_LINE2", // line 3 "MAIN_LINE3", // line 4 }, "lib.c65": { "LIB_LINE1", // line 1 "LIB_LINE2", // line 2 "LIB_LINE3", // line 3 }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("main.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } expected := []struct { text string filename string lineNo int }{ {"MAIN_LINE1", "main.c65", 1}, {"LIB_LINE1", "lib.c65", 1}, {"LIB_LINE2", "lib.c65", 2}, {"LIB_LINE3", "lib.c65", 3}, {"MAIN_LINE2", "main.c65", 3}, {"MAIN_LINE3", "main.c65", 4}, } if len(lines) != len(expected) { t.Fatalf("expected %d lines, got %d", len(expected), len(lines)) } for i, exp := range expected { if lines[i].Text != exp.text { t.Errorf("line %d: expected text %q, got %q", i, exp.text, lines[i].Text) } if lines[i].Filename != exp.filename { t.Errorf("line %d: expected filename %q, got %q", i, exp.filename, lines[i].Filename) } if lines[i].LineNo != exp.lineNo { t.Errorf("line %d: expected line number %d, got %d", i, exp.lineNo, lines[i].LineNo) } } } func TestPreProcess_PragmaTracking(t *testing.T) { files := map[string][]string{ "test.c65": { "LINE1", // no pragmas yet (index 0) "LINE2", // no pragmas yet (index 0) "#PRAGMA FEATURE1 enabled", // directive, not emitted "LINE3", // FEATURE1=enabled (index 1) "LINE4", // FEATURE1=enabled (index 1) "#PRAGMA FEATURE2 123", // directive, not emitted "LINE5", // FEATURE1=enabled, FEATURE2=123 (index 2) "#PRAGMA FEATURE1 disabled", // directive, not emitted "LINE6", // FEATURE1=disabled, FEATURE2=123 (index 3) }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 6 { t.Fatalf("expected 6 lines, got %d", len(lines)) } // Create pragma instance to verify the indices pragma := NewPragma() pragma.AddPragma("FEATURE1", "enabled") pragma.AddPragma("FEATURE2", "123") pragma.AddPragma("FEATURE1", "disabled") tests := []struct { lineIdx int text string pragmaSetIdx int feature1 string feature2 string }{ {0, "LINE1", 0, "", ""}, {1, "LINE2", 0, "", ""}, {2, "LINE3", 1, "enabled", ""}, {3, "LINE4", 1, "enabled", ""}, {4, "LINE5", 2, "enabled", "123"}, {5, "LINE6", 3, "disabled", "123"}, } for _, tt := range tests { line := lines[tt.lineIdx] if line.Text != tt.text { t.Errorf("line %d: expected text %q, got %q", tt.lineIdx, tt.text, line.Text) } if line.PragmaSetIndex != tt.pragmaSetIdx { t.Errorf("line %d: expected pragma set index %d, got %d", tt.lineIdx, tt.pragmaSetIdx, line.PragmaSetIndex) } // Verify we can retrieve pragma values using the index pragmaSet := pragma.GetPragmaSetByIndex(line.PragmaSetIndex) if got := pragmaSet.GetPragma("FEATURE1"); got != tt.feature1 { t.Errorf("line %d: expected FEATURE1=%q, got %q", tt.lineIdx, tt.feature1, got) } if got := pragmaSet.GetPragma("FEATURE2"); got != tt.feature2 { t.Errorf("line %d: expected FEATURE2=%q, got %q", tt.lineIdx, tt.feature2, got) } } } func TestPreProcess_Halt(t *testing.T) { files := map[string][]string{ "test.c65": { "LINE1", "LINE2", "#HALT", "LINE3", "LINE4", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err == nil { t.Fatal("expected HaltError, got nil") } if _, ok := err.(HaltError); !ok { t.Fatalf("expected HaltError, got %T: %v", err, err) } if len(lines) != 2 { t.Errorf("expected 2 lines before halt, got %d", len(lines)) } if len(lines) >= 1 && lines[0].Text != "LINE1" { t.Errorf("expected LINE1, got %q", lines[0].Text) } if len(lines) >= 2 && lines[1].Text != "LINE2" { t.Errorf("expected LINE2, got %q", lines[1].Text) } } func TestPreProcess_HaltInInclude(t *testing.T) { files := map[string][]string{ "main.c65": { "MAIN_LINE1", "#INCLUDE lib.c65", "MAIN_LINE2", }, "lib.c65": { "LIB_LINE1", "#HALT", "LIB_LINE2", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("main.c65", reader) if err == nil { t.Fatal("expected HaltError, got nil") } if _, ok := err.(HaltError); !ok { t.Fatalf("expected HaltError, got %T: %v", err, err) } if len(lines) != 2 { t.Fatalf("expected 2 lines before halt, got %d", len(lines)) } if lines[0].Text != "MAIN_LINE1" { t.Errorf("expected MAIN_LINE1, got %q", lines[0].Text) } if lines[1].Text != "LIB_LINE1" { t.Errorf("expected LIB_LINE1, got %q", lines[1].Text) } } func TestPreProcess_MixedBlocksAndComments(t *testing.T) { files := map[string][]string{ "test.c65": { "#DEFINE X = 10", "LDA #X // source comment", "ASM", " lda #X // asm comment", "ENDASM", "SCRIPT", " y = X // script comment", "ENDSCRIPT", "STA $D020 // another source comment", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } expected := []struct { text string kind LineKind }{ {"LDA #10", Source}, {" lda #X // asm comment", Assembler}, {" y = X // script comment", Script}, {"STA $D020", Source}, } if len(lines) != len(expected) { t.Fatalf("expected %d lines, got %d", len(expected), len(lines)) } for i, exp := range expected { if lines[i].Text != exp.text { t.Errorf("line %d: expected text %q, got %q", i, exp.text, lines[i].Text) } if lines[i].Kind != exp.kind { t.Errorf("line %d: expected Kind=%v, got %v", i, exp.kind, lines[i].Kind) } } } func TestPreProcess_EmptyASMBlock(t *testing.T) { files := map[string][]string{ "test.c65": { "ASM", "ENDASM", "NOP", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } // Empty block produces no lines, just NOP if len(lines) != 1 { t.Fatalf("expected 1 line, got %d", len(lines)) } if lines[0].Text != "NOP" { t.Errorf("expected 'NOP', got %q", lines[0].Text) } } func TestPreProcess_EmptyScriptBlock(t *testing.T) { files := map[string][]string{ "test.c65": { "SCRIPT", "ENDSCRIPT", "NOP", }, } reader := NewMockFileReader(files) lines, _, err := PreProcess("test.c65", reader) if err != nil { t.Fatalf("PreProcess failed: %v", err) } if len(lines) != 1 { t.Fatalf("expected 1 line, got %d", len(lines)) } if lines[0].Text != "NOP" { t.Errorf("expected 'NOP', got %q", lines[0].Text) } }