diff --git a/internal/compiler/compiler_test.go b/internal/compiler/compiler_test.go index 8ba41d8..aeca3e0 100644 --- a/internal/compiler/compiler_test.go +++ b/internal/compiler/compiler_test.go @@ -423,6 +423,222 @@ func TestExecuteMacro_StringParameter(t *testing.T) { } } +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 diff --git a/internal/compiler/scriptexec.go b/internal/compiler/scriptexec.go index 923c0d1..ef52758 100644 --- a/internal/compiler/scriptexec.go +++ b/internal/compiler/scriptexec.go @@ -164,6 +164,11 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri if outputStr == "" { return []string{}, nil } + + // Expand |varname| -> actual variable names in the OUTPUT + // This happens at call site, so local variables are resolved using caller's scope + outputStr = expandVariables(outputStr, ctx) + return strings.Split(strings.TrimRight(outputStr, "\n"), "\n"), nil }