c65gm/internal/compiler/compiler_test.go

461 lines
11 KiB
Go

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