461 lines
11 KiB
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)
|
|
}
|
|
}
|
|
}
|