package compiler import ( "strings" "testing" "c65gm/internal/preproc" ) // Helper to create a test Line func makeLine(text string) preproc.Line { return preproc.Line{ RawText: text, Text: text, Filename: "test.c65", LineNo: 1, Kind: preproc.Source, PragmaSetIndex: 0, } } func TestFixIntuitiveFuncs(t *testing.T) { tests := []struct { input string expected string }{ {"func(a,b)", "func ( a b )"}, {"func( a, b )", "func ( a b )"}, {"func(a,b,c)", "func ( a b c )"}, {"CALL func()", "CALL func ( )"}, {"func()", "func ( )"}, {`func("hello",x)`, `func ( "hello" x )`}, {`func("a,b",c)`, `func ( "a,b" c )`}, {"func ( a , b )", "func ( a b )"}, } for _, tt := range tests { result := fixIntuitiveFuncs(tt.input) if result != tt.expected { t.Errorf("fixIntuitiveFuncs(%q) = %q, want %q", tt.input, result, tt.expected) } } } func TestBuildComplexParams(t *testing.T) { tests := []struct { input []string expected []string wantErr bool }{ { input: []string{"a", "b", "c"}, expected: []string{"a", "b", "c"}, wantErr: false, }, { input: []string{"{BYTE", "x}"}, expected: []string{"{BYTE x}"}, wantErr: false, }, { input: []string{"{WORD", "ptr}"}, expected: []string{"{WORD ptr}"}, wantErr: false, }, { input: []string{"{BYTE", "a}", "{WORD", "b}"}, expected: []string{"{BYTE a}", "{WORD b}"}, wantErr: false, }, { input: []string{"x", "{BYTE", "a}", "y"}, expected: []string{"x", "{BYTE a}", "y"}, wantErr: false, }, { input: []string{"{BYTE", "x"}, expected: nil, wantErr: true, // unclosed }, { input: []string{"x}"}, expected: nil, wantErr: true, // unmatched close }, { input: []string{"{BYTE", "{WORD", "x}"}, expected: nil, wantErr: true, // nested open }, } for _, tt := range tests { result, err := buildComplexParams(tt.input) if tt.wantErr { if err == nil { t.Errorf("buildComplexParams(%v) expected error, got nil", tt.input) } continue } if err != nil { t.Errorf("buildComplexParams(%v) unexpected error: %v", tt.input, err) continue } if len(result) != len(tt.expected) { t.Errorf("buildComplexParams(%v) = %v, want %v", tt.input, result, tt.expected) continue } for i := range result { if result[i] != tt.expected[i] { t.Errorf("buildComplexParams(%v)[%d] = %q, want %q", tt.input, i, result[i], tt.expected[i]) } } } } func TestParseParams(t *testing.T) { tests := []struct { input string expected []string wantErr bool }{ {"FUNC test", []string{"FUNC", "test"}, false}, {"FUNC test ( a b )", []string{"FUNC", "test", "(", "a", "b", ")"}, false}, {`CALL print ( "hello world" )`, []string{"CALL", "print", "(", `"hello world"`, ")"}, false}, {" FUNC test ", []string{"FUNC", "test"}, false}, {`func("unterminated`, nil, true}, } for _, tt := range tests { result, err := parseParams(tt.input) if tt.wantErr { if err == nil { t.Errorf("parseParams(%q) expected error, got nil", tt.input) } continue } if err != nil { t.Errorf("parseParams(%q) unexpected error: %v", tt.input, err) continue } if len(result) != len(tt.expected) { t.Errorf("parseParams(%q) = %v, want %v", tt.input, result, tt.expected) continue } for i := range result { if result[i] != tt.expected[i] { t.Errorf("parseParams(%q)[%d] = %q, want %q", tt.input, i, result[i], tt.expected[i]) } } } } func TestParseParamSpec(t *testing.T) { tests := []struct { input string wantDir ParamDirection wantName string wantImplicit bool wantImplDecl string wantErr bool }{ {"varname", DirIn, "varname", false, "", false}, {"in:varname", DirIn, "varname", false, "", false}, {"out:varname", DirOut, "varname", false, "", false}, {"io:varname", DirIn | DirOut, "varname", false, "", false}, {"{BYTE temp}", DirIn, "temp", true, "BYTE temp", false}, {"{WORD result}", DirIn, "result", true, "WORD result", false}, {"out:{BYTE x}", DirOut, "x", true, "BYTE x", false}, {"io:{WORD ptr}", DirIn | DirOut, "ptr", true, "WORD ptr", false}, {"invalid:dir:x", 0, "", false, "", true}, } for _, tt := range tests { dir, name, isImpl, implDecl, err := parseParamSpec(tt.input) if tt.wantErr { if err == nil { t.Errorf("parseParamSpec(%q) expected error, got nil", tt.input) } continue } if err != nil { t.Errorf("parseParamSpec(%q) unexpected error: %v", tt.input, err) continue } if dir != tt.wantDir { t.Errorf("parseParamSpec(%q) direction = %v, want %v", tt.input, dir, tt.wantDir) } if name != tt.wantName { t.Errorf("parseParamSpec(%q) name = %q, want %q", tt.input, name, tt.wantName) } if isImpl != tt.wantImplicit { t.Errorf("parseParamSpec(%q) implicit = %v, want %v", tt.input, isImpl, tt.wantImplicit) } if implDecl != tt.wantImplDecl { t.Errorf("parseParamSpec(%q) implDecl = %q, want %q", tt.input, implDecl, tt.wantImplDecl) } } } func TestHandleFuncDecl_VoidFunction(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) asm, err := fh.HandleFuncDecl(makeLine("FUNC test_void")) if err != nil { t.Fatalf("HandleFuncDecl failed: %v", err) } if len(asm) != 1 { t.Fatalf("expected 1 asm line, got %d", len(asm)) } if asm[0] != "test_void" { t.Errorf("expected label 'test_void', got %q", asm[0]) } if !fh.FuncExists("test_void") { t.Error("function should exist") } } func TestHandleFuncDecl_WithExistingParams(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) // Pre-declare parameters st.AddVar("x", "test_func", KindByte, 0) st.AddVar("y", "test_func", KindWord, 0) asm, err := fh.HandleFuncDecl(makeLine("FUNC test_func ( x y )")) if err != nil { t.Fatalf("HandleFuncDecl failed: %v", err) } if len(asm) != 1 { t.Fatalf("expected 1 asm line, got %d", len(asm)) } funcDecl := fh.findFunc("test_func") if funcDecl == nil { t.Fatal("function not found") } if len(funcDecl.Params) != 2 { t.Fatalf("expected 2 params, got %d", len(funcDecl.Params)) } } func TestHandleFuncDecl_ImplicitDeclarations(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) asm, err := fh.HandleFuncDecl(makeLine("FUNC test_impl ( {BYTE a} {WORD b} )")) if err != nil { t.Fatalf("HandleFuncDecl failed: %v", err) } if len(asm) != 1 { t.Fatalf("expected 1 asm line, got %d", len(asm)) } // Check that variables were declared symA := st.Lookup("a", []string{"test_impl"}) if symA == nil { t.Fatal("parameter 'a' not declared") } if !symA.IsByte() { t.Error("parameter 'a' should be byte") } symB := st.Lookup("b", []string{"test_impl"}) if symB == nil { t.Fatal("parameter 'b' not declared") } if !symB.IsWord() { t.Error("parameter 'b' should be word") } } func TestHandleFuncDecl_WithDirections(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) _, err := fh.HandleFuncDecl(makeLine("FUNC test_dir ( in:{BYTE a} out:{BYTE b} io:{WORD c} )")) if err != nil { t.Fatalf("HandleFuncDecl failed: %v", err) } funcDecl := fh.findFunc("test_dir") if funcDecl == nil { t.Fatal("function not found") } if len(funcDecl.Params) != 3 { t.Fatalf("expected 3 params, got %d", len(funcDecl.Params)) } if funcDecl.Params[0].Direction != DirIn { t.Error("param 0 should be DirIn") } if funcDecl.Params[1].Direction != DirOut { t.Error("param 1 should be DirOut") } if funcDecl.Params[2].Direction != (DirIn | DirOut) { t.Error("param 2 should be DirIn|DirOut") } } func TestHandleFuncDecl_Errors(t *testing.T) { tests := []struct { name string line string preDecl func(*SymbolTable) wantErr string }{ { name: "redeclaration", line: "FUNC duplicate ( {BYTE x} )", preDecl: func(st *SymbolTable) {}, wantErr: "already declared", }, { name: "missing param", line: "FUNC test ( missing )", wantErr: "not declared", }, { name: "const param", line: "FUNC test ( constval )", preDecl: func(st *SymbolTable) { st.AddConst("constval", "test", KindByte, 42) }, wantErr: "cannot be a constant", }, { name: "invalid implicit", line: "FUNC test ( {INVALID x} )", wantErr: "must be BYTE or WORD", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) if tt.preDecl != nil { tt.preDecl(st) } // Special case for redeclaration test if tt.name == "redeclaration" { fh.HandleFuncDecl(makeLine("FUNC duplicate ( {BYTE x} )")) } _, err := fh.HandleFuncDecl(makeLine(tt.line)) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), tt.wantErr) { t.Errorf("error %q does not contain %q", err.Error(), tt.wantErr) } }) } } func TestHandleFuncCall_VarArgs(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) // Declare function with params st.AddVar("param_a", "test_func", KindByte, 0) st.AddVar("param_b", "test_func", KindWord, 0) fh.HandleFuncDecl(makeLine("FUNC test_func ( param_a param_b )")) // Declare caller variables st.AddVar("var_a", "", KindByte, 0) st.AddVar("var_b", "", KindWord, 0) asm, err := fh.HandleFuncCall(makeLine("CALL test_func ( var_a var_b )")) if err != nil { t.Fatalf("HandleFuncCall failed: %v", err) } // Check generated assembly expectedLines := []string{ " lda var_a", " sta test_func_param_a", " lda var_b", " sta test_func_param_b", " lda var_b+1", " sta test_func_param_b+1", " jsr test_func", } if len(asm) != len(expectedLines) { t.Fatalf("expected %d asm lines, got %d", len(expectedLines), len(asm)) } for i, expected := range expectedLines { if asm[i] != expected { t.Errorf("asm[%d] = %q, want %q", i, asm[i], expected) } } } func TestHandleFuncCall_OutParams(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) // Declare function with out param st.AddVar("result", "get_result", KindByte, 0) fh.HandleFuncDecl(makeLine("FUNC get_result ( out:result )")) // Declare caller variable st.AddVar("output", "", KindByte, 0) asm, err := fh.HandleFuncCall(makeLine("CALL get_result ( output )")) if err != nil { t.Fatalf("HandleFuncCall failed: %v", err) } // Should have JSR and OUT assignment found_jsr := false found_out := false for _, line := range asm { if strings.Contains(line, "jsr get_result") { found_jsr = true } if strings.Contains(line, "lda get_result_result") { found_out = true } } if !found_jsr { t.Error("missing jsr instruction") } if !found_out { t.Error("missing out assignment") } } func TestHandleFuncCall_ConstArgs(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) // Declare function st.AddVar("x", "test_const", KindByte, 0) st.AddVar("y", "test_const", KindWord, 0) fh.HandleFuncDecl(makeLine("FUNC test_const ( x y )")) asm, err := fh.HandleFuncCall(makeLine("CALL test_const ( 42 $1234 )")) if err != nil { t.Fatalf("HandleFuncCall failed: %v", err) } // Check for immediate loads foundByte := false foundWord := false for _, line := range asm { if strings.Contains(line, "lda #42") { foundByte = true } if strings.Contains(line, "lda #18") { // 0x12 foundWord = true } } if !foundByte { t.Error("missing byte constant load") } if !foundWord { t.Error("missing word constant load") } } func TestHandleFuncCall_LabelArg(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) // Declare function st.AddVar("ptr", "test_label", KindWord, 0) fh.HandleFuncDecl(makeLine("FUNC test_label ( ptr )")) asm, err := fh.HandleFuncCall(makeLine("CALL test_label ( @my_label )")) if err != nil { t.Fatalf("HandleFuncCall failed: %v", err) } // Check for label reference foundLow := false foundHigh := false for _, line := range asm { if strings.Contains(line, "#my_label") { foundHigh = true } } if !foundLow || !foundHigh { t.Error("missing label reference code") } } func TestHandleFuncCall_StringArg(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) // Declare function st.AddVar("str_ptr", "print", KindWord, 0) fh.HandleFuncDecl(makeLine("FUNC print ( str_ptr )")) asm, err := fh.HandleFuncCall(makeLine(`CALL print ( "hello" )`)) if err != nil { t.Fatalf("HandleFuncCall failed: %v", err) } // Check that label was generated if ls.Size() != 1 { t.Errorf("expected 1 label generated, got %d", ls.Size()) } // Check for label reference in asm foundLabel := false for _, line := range asm { if strings.Contains(line, "#L1") { foundLabel = true break } } if !foundLabel { t.Error("missing string label reference") } } func TestHandleFuncCall_Errors(t *testing.T) { tests := []struct { name string setup func(*FunctionHandler, *SymbolTable) line string wantErr string }{ { name: "function not declared", setup: func(fh *FunctionHandler, st *SymbolTable) {}, line: "CALL undefined ( )", wantErr: "not declared", }, { name: "wrong arg count", setup: func(fh *FunctionHandler, st *SymbolTable) { st.AddVar("x", "test", KindByte, 0) fh.HandleFuncDecl(makeLine("FUNC test ( x )")) }, line: "CALL test ( 1 2 )", wantErr: "expected 1 arguments, got 2", }, { name: "type mismatch", setup: func(fh *FunctionHandler, st *SymbolTable) { st.AddVar("param", "test", KindByte, 0) fh.HandleFuncDecl(makeLine("FUNC test ( param )")) st.AddVar("wvar", "", KindWord, 0) }, line: "CALL test ( wvar )", wantErr: "type mismatch", }, { name: "const to out param", setup: func(fh *FunctionHandler, st *SymbolTable) { st.AddVar("result", "test", KindByte, 0) fh.HandleFuncDecl(makeLine("FUNC test ( out:result )")) }, line: "CALL test ( 42 )", wantErr: "out/io parameter", }, { name: "label to byte param", setup: func(fh *FunctionHandler, st *SymbolTable) { st.AddVar("x", "test", KindByte, 0) fh.HandleFuncDecl(makeLine("FUNC test ( x )")) }, line: "CALL test ( @label )", wantErr: "byte parameter", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) tt.setup(fh, st) _, err := fh.HandleFuncCall(makeLine(tt.line)) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), tt.wantErr) { t.Errorf("error %q does not contain %q", err.Error(), tt.wantErr) } }) } } func TestEndFunction(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) // Declare function (pushes to stack) fh.HandleFuncDecl(makeLine("FUNC test ( {BYTE x} )")) if fh.CurrentFunction() != "test" { t.Errorf("current function = %q, want 'test'", fh.CurrentFunction()) } // End function fh.EndFunction() if fh.CurrentFunction() != "" { t.Errorf("current function = %q, want ''", fh.CurrentFunction()) } } func TestCurrentFunction(t *testing.T) { st := NewSymbolTable() ls := NewLabelStack("L") csh := NewConstantStringHandler() pragma := preproc.NewPragma() fh := NewFunctionHandler(st, ls, csh, pragma) if fh.CurrentFunction() != "" { t.Error("expected empty current function initially") } fh.HandleFuncDecl(makeLine("FUNC func1 ( {BYTE x} )")) if fh.CurrentFunction() != "func1" { t.Errorf("expected 'func1', got %q", fh.CurrentFunction()) } fh.HandleFuncDecl(makeLine("FUNC func2 ( {BYTE y} )")) if fh.CurrentFunction() != "func2" { t.Errorf("expected 'func2', got %q", fh.CurrentFunction()) } fh.EndFunction() if fh.CurrentFunction() != "" { t.Errorf("expected '', got %q", fh.CurrentFunction()) } }