957 lines
24 KiB
Go
957 lines
24 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 TestExecuteMacro_LocalVariableExpansion(t *testing.T) {
|
|
pragma := preproc.NewPragma()
|
|
ctx := NewCompilerContext(pragma)
|
|
|
|
// Add a local variable in function scope "testfunc"
|
|
ctx.SymbolTable.AddVar("myvar", "testfunc", KindByte, 0)
|
|
|
|
// Enter the function scope by declaring the function
|
|
_, err := ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC testfunc"))
|
|
if err != nil {
|
|
t.Fatalf("HandleFuncDecl failed: %v", err)
|
|
}
|
|
|
|
// Register a macro that uses |%s| pattern - the ACTUAL use case
|
|
// The macro parameter 'varname' receives "myvar", then |%s| % varname
|
|
// produces |myvar| in the output, which should then be expanded
|
|
ctx.ScriptMacros["load_var"] = &ScriptMacro{
|
|
Name: "load_var",
|
|
Params: []string{"varname"},
|
|
Body: []string{
|
|
"print(' lda |%s|' % varname)",
|
|
},
|
|
}
|
|
|
|
// Execute macro with "myvar" as argument - should expand |myvar| to testfunc_myvar
|
|
output, err := ExecuteMacro("load_var", []string{"myvar"}, ctx)
|
|
if err != nil {
|
|
t.Fatalf("ExecuteMacro failed: %v", err)
|
|
}
|
|
|
|
if len(output) != 1 {
|
|
t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
|
|
}
|
|
|
|
expected := " lda testfunc_myvar"
|
|
if output[0] != expected {
|
|
t.Errorf("expected %q, got %q", expected, output[0])
|
|
}
|
|
}
|
|
|
|
func TestExecuteMacro_LocalVariableExpansion_MultipleVars(t *testing.T) {
|
|
pragma := preproc.NewPragma()
|
|
ctx := NewCompilerContext(pragma)
|
|
|
|
// Add local variables in function scope
|
|
ctx.SymbolTable.AddVar("color_index", "myfunc", KindByte, 0)
|
|
ctx.SymbolTable.AddVar("row_color", "myfunc", KindWord, 0)
|
|
|
|
// Add a global table (no function scope)
|
|
ctx.SymbolTable.AddVar("scroll_color_table", "", KindWord, 0)
|
|
|
|
// Enter the function scope
|
|
_, err := ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC myfunc"))
|
|
if err != nil {
|
|
t.Fatalf("HandleFuncDecl failed: %v", err)
|
|
}
|
|
|
|
// Register a macro using |%s| pattern - matches actual usage:
|
|
// SCRIPT MACRO table_lookup(table, index, dest)
|
|
// print(" ldx |%s|" % index)
|
|
// print(" lda %s,x" % table)
|
|
// print(" sta |%s|" % dest)
|
|
// ENDSCRIPT
|
|
ctx.ScriptMacros["table_lookup"] = &ScriptMacro{
|
|
Name: "table_lookup",
|
|
Params: []string{"table", "index", "dest"},
|
|
Body: []string{
|
|
"print(' ldx |%s|' % index)",
|
|
"print(' lda %s,x' % table)",
|
|
"print(' sta |%s|' % dest)",
|
|
},
|
|
}
|
|
|
|
// Execute macro with actual variable names as arguments
|
|
output, err := ExecuteMacro("table_lookup", []string{"scroll_color_table", "color_index", "row_color"}, ctx)
|
|
if err != nil {
|
|
t.Fatalf("ExecuteMacro failed: %v", err)
|
|
}
|
|
|
|
expectedLines := []string{
|
|
" ldx myfunc_color_index", // local var expanded with scope
|
|
" lda scroll_color_table,x", // global var (no pipes) stays as-is
|
|
" sta myfunc_row_color", // local var expanded with scope
|
|
}
|
|
|
|
if len(output) != len(expectedLines) {
|
|
t.Fatalf("expected %d output lines, got %d: %v", len(expectedLines), len(output), output)
|
|
}
|
|
|
|
for i, expected := range expectedLines {
|
|
if output[i] != expected {
|
|
t.Errorf("line %d: expected %q, got %q", i, expected, output[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExecuteScript_LocalVariableExpansion(t *testing.T) {
|
|
pragma := preproc.NewPragma()
|
|
ctx := NewCompilerContext(pragma)
|
|
|
|
// Add a local variable in function scope
|
|
ctx.SymbolTable.AddVar("counter", "loopfunc", KindByte, 0)
|
|
|
|
// Enter the function scope
|
|
_, err := ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC loopfunc"))
|
|
if err != nil {
|
|
t.Fatalf("HandleFuncDecl failed: %v", err)
|
|
}
|
|
|
|
// Script that uses |varname| syntax
|
|
scriptLines := []string{
|
|
"print(' inc |counter|')",
|
|
}
|
|
|
|
output, err := executeScript(scriptLines, ctx, false)
|
|
if err != nil {
|
|
t.Fatalf("executeScript failed: %v", err)
|
|
}
|
|
|
|
if len(output) != 1 {
|
|
t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
|
|
}
|
|
|
|
expected := " inc loopfunc_counter"
|
|
if output[0] != expected {
|
|
t.Errorf("expected %q, got %q", expected, output[0])
|
|
}
|
|
}
|
|
|
|
func TestExecuteScript_Library_GlobalVariableExpansion(t *testing.T) {
|
|
pragma := preproc.NewPragma()
|
|
ctx := NewCompilerContext(pragma)
|
|
|
|
// Add a global variable
|
|
ctx.SymbolTable.AddVar("global_counter", "", KindByte, 0)
|
|
|
|
// Library script that uses |varname| syntax at global scope
|
|
// Should expand to the global name (unchanged since it's already global)
|
|
libraryLines := []string{
|
|
"def inc_global():",
|
|
" print(' inc |global_counter|')",
|
|
}
|
|
|
|
_, err := executeScript(libraryLines, ctx, true)
|
|
if err != nil {
|
|
t.Fatalf("library script failed: %v", err)
|
|
}
|
|
|
|
// Now call the library function from a regular script
|
|
scriptLines := []string{
|
|
"inc_global()",
|
|
}
|
|
|
|
output, err := executeScript(scriptLines, ctx, false)
|
|
if err != nil {
|
|
t.Fatalf("executeScript failed: %v", err)
|
|
}
|
|
|
|
if len(output) != 1 {
|
|
t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
|
|
}
|
|
|
|
// Global variable should stay as-is (no function prefix)
|
|
expected := " inc global_counter"
|
|
if output[0] != expected {
|
|
t.Errorf("expected %q, got %q", expected, output[0])
|
|
}
|
|
}
|
|
|
|
func TestExecuteScript_Library_VariableExpansionAtDefinitionTime(t *testing.T) {
|
|
pragma := preproc.NewPragma()
|
|
ctx := NewCompilerContext(pragma)
|
|
|
|
// Add a local variable in a function scope
|
|
ctx.SymbolTable.AddVar("local_var", "caller", KindByte, 0)
|
|
|
|
// Library is defined at GLOBAL scope (not inside any function)
|
|
// So |varname| expansion happens at global scope during library definition
|
|
libraryLines := []string{
|
|
"def use_local():",
|
|
" print(' lda |local_var|')", // This expands at library definition time
|
|
}
|
|
|
|
// Library defined at global scope - |local_var| won't find caller's local
|
|
_, err := executeScript(libraryLines, ctx, true)
|
|
if err != nil {
|
|
t.Fatalf("library script failed: %v", err)
|
|
}
|
|
|
|
// Now enter the function scope and call the library function
|
|
_, err = ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC caller"))
|
|
if err != nil {
|
|
t.Fatalf("HandleFuncDecl failed: %v", err)
|
|
}
|
|
|
|
scriptLines := []string{
|
|
"use_local()",
|
|
}
|
|
|
|
output, err := executeScript(scriptLines, ctx, false)
|
|
if err != nil {
|
|
t.Fatalf("executeScript failed: %v", err)
|
|
}
|
|
|
|
if len(output) != 1 {
|
|
t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
|
|
}
|
|
|
|
// Variable expansion happened at library definition time (global scope),
|
|
// so local_var was NOT found and stays as literal "local_var"
|
|
expected := " lda local_var"
|
|
if output[0] != expected {
|
|
t.Errorf("expected %q, got %q (library expands |vars| at definition time, not call time)", expected, output[0])
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindAsmCommentStart(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want int
|
|
}{
|
|
// Basic cases
|
|
{"lda #$00", -1}, // no comment
|
|
{"; comment", 0}, // comment at start
|
|
{"lda #$00 ; comment", 9}, // comment after code
|
|
{" lda #$00 ; comment", 11}, // with leading whitespace
|
|
|
|
// Semicolon in double-quoted string
|
|
{`!text "hello; world"`, -1}, // no comment, ; inside string
|
|
{`!text "hello; world" ; comment`, 21}, // comment after string with ;
|
|
{`!text "a;b;c"`, -1}, // multiple ; in string
|
|
|
|
// Semicolon in single-quoted string
|
|
{`!byte ';'`, -1}, // ; as character literal
|
|
{`!byte ';' ; comment`, 10}, // comment after ; char literal
|
|
|
|
// Escape sequences in strings
|
|
{`!text "hello\"world"`, -1}, // escaped quote, no comment
|
|
{`!text "hello\"world" ; comment`, 21}, // comment after string with escaped quote
|
|
{`!text "path\\file"`, -1}, // escaped backslash
|
|
{`!text "a\\;b"`, -1}, // escaped backslash before ;
|
|
{`!text "a\;b"`, -1}, // escaped ; in string (stays in string)
|
|
|
|
// Mixed quotes
|
|
{`!text "it's"`, -1}, // single quote inside double
|
|
{`!byte '"'`, -1}, // double quote as char literal
|
|
{`!text "say \"hi\""`, -1}, // escaped quotes in string
|
|
|
|
// Edge cases
|
|
{"", -1}, // empty line
|
|
{`""`, -1}, // empty string
|
|
{`"" ; comment`, 3}, // empty string then comment
|
|
{`!text "unterminated`, -1}, // unterminated string (no ; found)
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := findAsmCommentStart(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("findAsmCommentStart(%q) = %d, want %d", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAsmBlock_VariableExpansion_IgnoresComments(t *testing.T) {
|
|
pragma := preproc.NewPragma()
|
|
comp := NewCompiler(pragma)
|
|
|
|
// Add a variable to the symbol table so expansion can work
|
|
comp.Context().SymbolTable.AddVar("myvar", "", KindByte, 0)
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "variable in code expands",
|
|
input: " lda |myvar|",
|
|
expected: " lda myvar",
|
|
},
|
|
{
|
|
name: "variable in comment stays unexpanded",
|
|
input: "; use |myvar| here",
|
|
expected: "; use |myvar| here",
|
|
},
|
|
{
|
|
name: "variable in code, different in comment",
|
|
input: " lda |myvar| ; load |myvar|",
|
|
expected: " lda myvar ; load |myvar|",
|
|
},
|
|
{
|
|
name: "only comment with variable",
|
|
input: " nop ; |myvar|",
|
|
expected: " nop ; |myvar|",
|
|
},
|
|
{
|
|
name: "variable in string not affected",
|
|
input: ` !text "|myvar|"`,
|
|
expected: ` !text "|myvar|"`,
|
|
},
|
|
{
|
|
name: "variable after string with semicolon",
|
|
input: ` !text "a;b" ; |myvar|`,
|
|
expected: ` !text "a;b" ; |myvar|`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create fresh compiler for each test
|
|
comp := NewCompiler(pragma)
|
|
comp.Context().SymbolTable.AddVar("myvar", "", KindByte, 0)
|
|
|
|
lines := []preproc.Line{
|
|
{
|
|
Text: tt.input,
|
|
Filename: "test.asm",
|
|
LineNo: 1,
|
|
Kind: preproc.Assembler,
|
|
},
|
|
{
|
|
// Empty source line to close the ASM block
|
|
Text: "",
|
|
Filename: "test.asm",
|
|
LineNo: 2,
|
|
Kind: preproc.Source,
|
|
},
|
|
}
|
|
|
|
output, err := comp.Compile(lines)
|
|
if err != nil {
|
|
t.Fatalf("Compile failed: %v", err)
|
|
}
|
|
|
|
// Find the ASM output line (between ; ASM and ; ENDASM markers)
|
|
var resultLine string
|
|
inAsmBlock := false
|
|
for _, line := range output {
|
|
if line == "; ASM" {
|
|
inAsmBlock = true
|
|
continue
|
|
}
|
|
if line == "; ENDASM" {
|
|
break
|
|
}
|
|
if inAsmBlock {
|
|
resultLine = line
|
|
break
|
|
}
|
|
}
|
|
|
|
if resultLine != tt.expected {
|
|
t.Errorf("got %q, want %q\nfull output: %v", resultLine, tt.expected, output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindPipeOutsideStrings(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
startFrom int
|
|
want int
|
|
}{
|
|
// Basic cases
|
|
{"lda |var|", 0, 4},
|
|
{"lda |var|", 5, 8},
|
|
{"no pipes", 0, -1},
|
|
|
|
// Pipe in string should be skipped
|
|
{`"a|b"`, 0, -1},
|
|
{`"a|b" |var|`, 0, 6},
|
|
{`'|' |x|`, 0, 4},
|
|
|
|
// Escape sequences
|
|
{`"a\"|b"`, 0, -1}, // escaped quote, pipe still in string
|
|
{`"a\\"|b|`, 0, 5}, // escaped backslash, pipe outside
|
|
{`"a\|b"`, 0, -1}, // escaped pipe stays in string
|
|
|
|
// Start from different positions
|
|
{"|a| |b|", 0, 0},
|
|
{"|a| |b|", 1, 2},
|
|
{"|a| |b|", 3, 4},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := findPipeOutsideStrings(tt.input, tt.startFrom)
|
|
if got != tt.want {
|
|
t.Errorf("findPipeOutsideStrings(%q, %d) = %d, want %d", tt.input, tt.startFrom, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAsmBlock_MacroExpansion_IgnoresComments(t *testing.T) {
|
|
pragma := preproc.NewPragma()
|
|
comp := NewCompiler(pragma)
|
|
|
|
// Register a test macro
|
|
comp.Context().ScriptMacros["delay"] = &ScriptMacro{
|
|
Name: "delay",
|
|
Params: []string{"cycles"},
|
|
Body: []string{
|
|
"for i in range(cycles):",
|
|
" print(' nop')",
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expectExpanded bool
|
|
expectedLines int // number of output lines (excluding comment wrappers)
|
|
}{
|
|
{
|
|
name: "macro in code expands",
|
|
input: " |@delay(3)|",
|
|
expectExpanded: true,
|
|
expectedLines: 3, // 3 nops
|
|
},
|
|
{
|
|
name: "macro in comment does not expand",
|
|
input: "; |@delay(3)|",
|
|
expectExpanded: false,
|
|
expectedLines: 1, // just the original line
|
|
},
|
|
{
|
|
name: "macro after semicolon comment does not expand",
|
|
input: " nop ; |@delay(3)|",
|
|
expectExpanded: false,
|
|
expectedLines: 1, // just the original line with nop
|
|
},
|
|
{
|
|
name: "macro in string does not expand",
|
|
input: ` !text "|@delay(3)|"`,
|
|
expectExpanded: false,
|
|
expectedLines: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create fresh compiler for each test to avoid state issues
|
|
comp := NewCompiler(pragma)
|
|
comp.Context().ScriptMacros["delay"] = &ScriptMacro{
|
|
Name: "delay",
|
|
Params: []string{"cycles"},
|
|
Body: []string{
|
|
"for i in range(cycles):",
|
|
" print(' nop')",
|
|
},
|
|
}
|
|
|
|
lines := []preproc.Line{
|
|
{
|
|
Text: tt.input,
|
|
Filename: "test.asm",
|
|
LineNo: 1,
|
|
Kind: preproc.Assembler,
|
|
},
|
|
{
|
|
// Empty source line to close the ASM block
|
|
Text: "",
|
|
Filename: "test.asm",
|
|
LineNo: 2,
|
|
Kind: preproc.Source,
|
|
},
|
|
}
|
|
|
|
output, err := comp.Compile(lines)
|
|
if err != nil {
|
|
t.Fatalf("Compile failed: %v", err)
|
|
}
|
|
|
|
// Count nop lines to determine if macro expanded
|
|
nopCount := 0
|
|
for _, line := range output {
|
|
if strings.TrimSpace(line) == "nop" {
|
|
nopCount++
|
|
}
|
|
}
|
|
|
|
if tt.expectExpanded {
|
|
if nopCount != tt.expectedLines {
|
|
t.Errorf("expected %d nop lines (macro expanded), got %d\nfull output: %v",
|
|
tt.expectedLines, nopCount, output)
|
|
}
|
|
} else {
|
|
if nopCount > 0 {
|
|
t.Errorf("expected no nop lines (macro should not expand in comment), got %d\nfull output: %v",
|
|
nopCount, output)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|