c65gm/internal/compiler/funchandler_test.go

1076 lines
27 KiB
Go

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") {
foundLow = true
}
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") || 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",
},
//
/* This is no error anymore, just a warning.
{
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 TestHandleFuncCall_EmptyParens(t *testing.T) {
st := NewSymbolTable()
ls := NewLabelStack("L")
csh := NewConstantStringHandler()
pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma)
// Declare void function
fh.HandleFuncDecl(makeLine("FUNC init"))
tests := []struct {
name string
line string
}{
{"naked call", "init"},
{"empty parens", "init()"},
{"CALL keyword", "CALL init"},
{"CALL with parens", "CALL init()"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
asm, err := fh.HandleFuncCall(makeLine(tt.line))
if err != nil {
t.Fatalf("HandleFuncCall failed: %v", err)
}
// Should have JSR instruction
foundJSR := false
for _, line := range asm {
if strings.Contains(line, "jsr init") {
foundJSR = true
break
}
}
if !foundJSR {
t.Error("missing jsr instruction")
}
})
}
}
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())
}
}
func TestHandleFuncDecl_AbsoluteVariables(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_abs ( {BYTE param1 @ $fa} {WORD param2 @ $fb} )"))
if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err)
}
// Check that variables were declared as absolute
symByte := st.Lookup("param1", []string{"test_abs"})
if symByte == nil {
t.Fatal("parameter 'param1' not declared")
}
if !symByte.IsByte() {
t.Error("parameter 'param1' should be byte")
}
if !symByte.IsAbsolute() {
t.Error("parameter 'param1' should be absolute")
}
if symByte.AbsAddr != 0xfa {
t.Errorf("parameter 'param1' address = $%02x, want $fa", symByte.AbsAddr)
}
if !symByte.IsZeroPage() {
t.Error("parameter 'param1' should be in zero page")
}
symWord := st.Lookup("param2", []string{"test_abs"})
if symWord == nil {
t.Fatal("parameter 'param2' not declared")
}
if !symWord.IsWord() {
t.Error("parameter 'param2' should be word")
}
if !symWord.IsAbsolute() {
t.Error("parameter 'param2' should be absolute")
}
if symWord.AbsAddr != 0xfb {
t.Errorf("parameter 'param2' address = $%02x, want $fb", symWord.AbsAddr)
}
if !symWord.IsZeroPage() {
t.Error("parameter 'param2' should be in zero page")
}
// Check function declaration
funcDecl := fh.findFunc("test_abs")
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_AbsoluteWithDirections(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_abs_dir ( in:{BYTE input @ $fa} out:{BYTE output @ $fb} io:{WORD data @ $fc} )"))
if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err)
}
funcDecl := fh.findFunc("test_abs_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")
}
// Verify absolute addresses
if funcDecl.Params[0].Symbol.AbsAddr != 0xfa {
t.Errorf("param 0 address = $%02x, want $fa", funcDecl.Params[0].Symbol.AbsAddr)
}
if funcDecl.Params[1].Symbol.AbsAddr != 0xfb {
t.Errorf("param 1 address = $%02x, want $fb", funcDecl.Params[1].Symbol.AbsAddr)
}
if funcDecl.Params[2].Symbol.AbsAddr != 0xfc {
t.Errorf("param 2 address = $%02x, want $fc", funcDecl.Params[2].Symbol.AbsAddr)
}
}
func TestHandleFuncDecl_AbsoluteMixedParams(t *testing.T) {
st := NewSymbolTable()
ls := NewLabelStack("L")
csh := NewConstantStringHandler()
pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma)
// Mix absolute and regular params
_, err := fh.HandleFuncDecl(makeLine("FUNC test_mixed ( {BYTE abs_param @ $fa} {WORD reg_param} )"))
if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err)
}
symAbs := st.Lookup("abs_param", []string{"test_mixed"})
if symAbs == nil {
t.Fatal("parameter 'abs_param' not declared")
}
if !symAbs.IsAbsolute() {
t.Error("parameter 'abs_param' should be absolute")
}
symReg := st.Lookup("reg_param", []string{"test_mixed"})
if symReg == nil {
t.Fatal("parameter 'reg_param' not declared")
}
if symReg.IsAbsolute() {
t.Error("parameter 'reg_param' should not be absolute")
}
}
func TestHandleFuncDecl_AbsoluteErrors(t *testing.T) {
tests := []struct {
name string
line string
wantErr string
}{
{
name: "invalid operator",
line: "FUNC test ( {BYTE x = $fa} )",
wantErr: "expected '@' operator",
},
{
name: "invalid address",
line: "FUNC test ( {BYTE x @ invalid} )",
wantErr: "invalid address",
},
{
name: "address out of range",
line: "FUNC test ( {BYTE x @ $10000} )",
wantErr: "out of range",
},
{
name: "negative address",
line: "FUNC test ( {BYTE x @ -1} )",
wantErr: "invalid address",
},
}
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)
_, 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_AbsoluteParams(t *testing.T) {
st := NewSymbolTable()
ls := NewLabelStack("L")
csh := NewConstantStringHandler()
pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma)
// Declare function with absolute params
fh.HandleFuncDecl(makeLine("FUNC test_abs ( {BYTE param_a @ $fa} {WORD param_b @ $fb} )"))
// Declare caller variables
st.AddVar("var_a", "", KindByte, 0)
st.AddVar("var_b", "", KindWord, 0)
asm, err := fh.HandleFuncCall(makeLine("CALL test_abs ( var_a var_b )"))
if err != nil {
t.Fatalf("HandleFuncCall failed: %v", err)
}
// Check generated assembly uses correct names
expectedLines := []string{
" lda var_a",
" sta test_abs_param_a",
" lda var_b",
" sta test_abs_param_b",
" lda var_b+1",
" sta test_abs_param_b+1",
" jsr test_abs",
}
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_AbsoluteOutParams(t *testing.T) {
st := NewSymbolTable()
ls := NewLabelStack("L")
csh := NewConstantStringHandler()
pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma)
// Declare function with absolute out param
fh.HandleFuncDecl(makeLine("FUNC get_result ( out:{BYTE result @ $fa} )"))
// 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 TestParseImplicitDecl_Absolute(t *testing.T) {
st := NewSymbolTable()
ls := NewLabelStack("L")
csh := NewConstantStringHandler()
pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma)
tests := []struct {
name string
decl string
funcName string
wantErr bool
checkFn func(*testing.T, *SymbolTable)
}{
{
name: "byte at hex address",
decl: "BYTE x @ $fa",
funcName: "test",
wantErr: false,
checkFn: func(t *testing.T, st *SymbolTable) {
sym := st.Lookup("x", []string{"test"})
if sym == nil {
t.Fatal("symbol not found")
}
if !sym.IsAbsolute() || sym.AbsAddr != 0xfa {
t.Errorf("expected absolute at $fa, got absolute=%v addr=$%02x", sym.IsAbsolute(), sym.AbsAddr)
}
},
},
{
name: "word at decimal address",
decl: "WORD ptr @ 251",
funcName: "test",
wantErr: false,
checkFn: func(t *testing.T, st *SymbolTable) {
sym := st.Lookup("ptr", []string{"test"})
if sym == nil {
t.Fatal("symbol not found")
}
if !sym.IsAbsolute() || sym.AbsAddr != 251 {
t.Errorf("expected absolute at 251, got absolute=%v addr=%d", sym.IsAbsolute(), sym.AbsAddr)
}
},
},
{
name: "invalid operator",
decl: "BYTE x = $fa",
funcName: "test",
wantErr: true,
},
{
name: "too few parts",
decl: "BYTE x @",
funcName: "test",
wantErr: true,
},
{
name: "too many parts",
decl: "BYTE x @ $fa extra",
funcName: "test",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear symbol table for each test
st = NewSymbolTable()
fh.symTable = st
err := fh.parseImplicitDecl(tt.decl, tt.funcName)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tt.checkFn != nil {
tt.checkFn(t, st)
}
})
}
}