c65gm/internal/preproc/preproc_test.go

725 lines
17 KiB
Go

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)
}
}
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)
}
// First include emits LABEL, second include is blocked by guard
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)
}
if len(lines) != 5 {
t.Fatalf("expected 5 lines, got %d", len(lines))
}
// ASM content should NOT be processed
if lines[1].Text != " lda #FOO" {
t.Errorf("expected ' lda #FOO', got %q", lines[1].Text)
}
// After ENDASM, defines work again
if lines[4].Text != "LDA #42" {
t.Errorf("expected 'LDA #42', got %q", lines[4].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)
}
// Pragma should have been processed with define expansion
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_Tokens(t *testing.T) {
files := map[string][]string{
"test.c65": {
"LDA #$42",
" STA $D020 ",
},
}
reader := NewMockFileReader(files)
lines, err := PreProcess("test.c65", reader)
if err != nil {
t.Fatalf("PreProcess failed: %v", err)
}
expectedTokens := [][]string{
{"LDA", "#$42"},
{"STA", "$D020"},
}
for i, expected := range expectedTokens {
if len(lines[i].Tokens) != len(expected) {
t.Errorf("line %d: expected %d tokens, got %d", i, len(expected), len(lines[i].Tokens))
continue
}
for j, tok := range expected {
if lines[i].Tokens[j] != tok {
t.Errorf("line %d token %d: expected %q, got %q", i, j, tok, lines[i].Tokens[j])
}
}
}
}
func TestPreProcess_ASMTokens(t *testing.T) {
files := map[string][]string{
"test.c65": {
"ASM",
" lda #$42",
"ENDASM",
},
}
reader := NewMockFileReader(files)
lines, err := PreProcess("test.c65", reader)
if err != nil {
t.Fatalf("PreProcess failed: %v", err)
}
// ASM and ENDASM have empty token arrays
if len(lines[0].Tokens) != 0 {
t.Errorf("ASM should have empty tokens, got %d", len(lines[0].Tokens))
}
if len(lines[1].Tokens) != 0 {
t.Errorf("ASM content should have empty tokens, got %d", len(lines[1].Tokens))
}
if len(lines[2].Tokens) != 0 {
t.Errorf("ENDASM should have empty tokens, got %d", len(lines[2].Tokens))
}
}
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 non-directive lines from first include
count := 0
for _, line := range lines {
if strings.Contains(line.Text, "lda $d011") {
count++
}
}
if count != 1 {
t.Errorf("expected 1 lib lines 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_Tokenization(t *testing.T) {
files := map[string][]string{
"test.c65": {
"LDA #$42", // simple tokens
" STA $D020 ", // leading/trailing whitespace, multiple spaces
"LET var = $1000", // multiple tokens with =
" JMP $0810", // tabs
" CALL func ( a b c )", // spaces around everything
"", // empty line
"NOP", // single token
},
}
reader := NewMockFileReader(files)
lines, err := PreProcess("test.c65", reader)
if err != nil {
t.Fatalf("PreProcess failed: %v", err)
}
expected := [][]string{
{"LDA", "#$42"},
{"STA", "$D020"},
{"LET", "var", "=", "$1000"},
{"JMP", "$0810"},
{"CALL", "func", "(", "a", "b", "c", ")"},
{}, // empty line has no tokens
{"NOP"},
}
if len(lines) != len(expected) {
t.Fatalf("expected %d lines, got %d", len(expected), len(lines))
}
for i, exp := range expected {
if len(lines[i].Tokens) != len(exp) {
t.Errorf("line %d: expected %d tokens, got %d (tokens: %v)",
i, len(exp), len(lines[i].Tokens), lines[i].Tokens)
continue
}
for j, tok := range exp {
if lines[i].Tokens[j] != tok {
t.Errorf("line %d token %d: expected %q, got %q", i, j, tok, lines[i].Tokens[j])
}
}
}
}
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", // should not be emitted
"LINE4", // should not be emitted
},
}
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)
}
// Lines before HALT should still be processed
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", // should not be reached
},
"lib.c65": {
"LIB_LINE1",
"#HALT",
"LIB_LINE2", // should not be emitted
},
}
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)
}
// Should have MAIN_LINE1 and LIB_LINE1
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)
}
}