Local variable expansion when calling macro

This commit is contained in:
Mattias Hansson 2026-03-04 22:32:53 +01:00
parent b614bcb043
commit 4f4df41c18
2 changed files with 221 additions and 0 deletions

View file

@ -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) { func TestParseMacroInvocation(t *testing.T) {
tests := []struct { tests := []struct {
input string input string

View file

@ -164,6 +164,11 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri
if outputStr == "" { if outputStr == "" {
return []string{}, nil 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 return strings.Split(strings.TrimRight(outputStr, "\n"), "\n"), nil
} }