Added SCRIPT LIBRARY block for reusable StarLark code.

This commit is contained in:
Mattias Hansson 2026-02-11 13:25:07 +01:00
parent 617f67ecb5
commit c6b67f8044
9 changed files with 448 additions and 39 deletions

View file

@ -0,0 +1,20 @@
#!/bin/sh
# Define filename as variable
PROGNAME="script_library_demo"
# Only set C65LIBPATH if not already defined
if [ -z "$C65LIBPATH" ]; then
export C65LIBPATH=$(readlink -f "../../lib")
fi
# Compile
c65gm -in ${PROGNAME}.c65 -out ${PROGNAME}.s
if [ $? -ne 0 ]; then
echo "Compilation terminated"
exit 1
fi
echo assemble.
acme ${PROGNAME}.s
if [ -f ${PROGNAME}.prg ]; then
rm ${PROGNAME}.prg
fi
# main.bin ${PROGNAME}.prg
mv main.bin main.prg

View file

@ -0,0 +1,85 @@
//-----------------------------------------------------------
// SCRIPT LIBRARY Demo
// Demonstrates reusable Starlark functions defined in
// SCRIPT LIBRARY blocks and called from SCRIPT blocks
//-----------------------------------------------------------
#INCLUDE <c64start.c65>
GOTO start
//-----------------------------------------------------------
// SCRIPT LIBRARY: Define reusable code generation functions
//-----------------------------------------------------------
SCRIPT LIBRARY
def emit_nops(count):
for i in range(count):
print(" nop")
def emit_delay(cycles):
# 4 cycles per iteration (2x nop)
for i in range(cycles // 4):
print(" nop")
print(" nop")
remainder = cycles % 4
if remainder >= 3:
print(" bit $ea")
remainder -= 3
# 2 cycles per nop
for i in range(remainder // 2):
print(" nop")
ENDSCRIPT
//-----------------------------------------------------------
// Second SCRIPT LIBRARY: Functions accumulate
//-----------------------------------------------------------
SCRIPT LIBRARY
def emit_border_flash(color1, color2):
print(" lda #%d" % color1)
print(" sta $d020")
print(" lda #%d" % color2)
print(" sta $d020")
def emit_load_store(value, addr):
print(" lda #%d" % value)
print(" sta %d" % addr)
ENDSCRIPT
//-----------------------------------------------------------
// Main code using library functions
//-----------------------------------------------------------
LABEL start
// Use emit_nops from first library
SCRIPT
print("; 5 nops from library")
emit_nops(5)
ENDSCRIPT
// Use emit_delay from first library
SCRIPT
print("; 10 cycle delay")
emit_delay(10)
ENDSCRIPT
// Use emit_border_flash from second library
SCRIPT
print("; border flash red/blue")
emit_border_flash(2, 6)
ENDSCRIPT
// Combine multiple library functions
SCRIPT
print("; combined: delay + flash + nops")
emit_delay(8)
emit_border_flash(0, 1)
emit_nops(3)
ENDSCRIPT
// Use emit_load_store
SCRIPT
print("; load/store to border")
emit_load_store(5, 0xd020)
ENDSCRIPT
SUBEND

View file

@ -0,0 +1 @@
x64 -autostartprgmode 1 main.prg

View file

@ -37,19 +37,24 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
var codeOutput []string
var lastKind = preproc.Source
var scriptBuffer []string
var scriptIsLibrary bool
for i, line := range lines {
// Detect kind transitions and emit markers
if line.Kind != lastKind {
// Execute and close previous Script block
if lastKind == preproc.Script {
scriptOutput, err := executeScript(scriptBuffer, c.ctx)
// Execute and close previous Script or ScriptLibrary block
if lastKind == preproc.Script || lastKind == preproc.ScriptLibrary {
scriptOutput, err := executeScript(scriptBuffer, c.ctx, scriptIsLibrary)
if err != nil {
return nil, fmt.Errorf("script execution failed: %w", err)
}
codeOutput = append(codeOutput, scriptOutput...)
scriptBuffer = nil
codeOutput = append(codeOutput, "; ENDSCRIPT")
if scriptIsLibrary {
codeOutput = append(codeOutput, "; ENDSCRIPT LIBRARY")
} else {
codeOutput = append(codeOutput, "; ENDSCRIPT")
}
}
// Close previous Assembler block
@ -62,6 +67,10 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
codeOutput = append(codeOutput, "; ASM")
} else if line.Kind == preproc.Script {
codeOutput = append(codeOutput, "; SCRIPT")
scriptIsLibrary = false
} else if line.Kind == preproc.ScriptLibrary {
codeOutput = append(codeOutput, "; SCRIPT LIBRARY")
scriptIsLibrary = true
}
lastKind = line.Kind
@ -89,7 +98,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
text = text[:start] + expandedName + text[end+1:]
}
codeOutput = append(codeOutput, text)
} else if line.Kind == preproc.Script {
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
// Collect script lines for execution
scriptBuffer = append(scriptBuffer, line.Text)
}
@ -127,11 +136,11 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
// Close any open block
if lastKind == preproc.Assembler {
//codeOutput = append(codeOutput, "; ENDASM")
return nil, fmt.Errorf("Unclosed ASM block.")
} else if lastKind == preproc.Script {
//codeOutput = append(codeOutput, "; ENDSCRIPT")
return nil, fmt.Errorf("Unclosed SCRIPT block.")
} else if lastKind == preproc.ScriptLibrary {
return nil, fmt.Errorf("Unclosed SCRIPT LIBRARY block.")
}
// Analyze for overlapping absolute addresses in function call chains

View file

@ -139,6 +139,9 @@ func TestCompilerContext(t *testing.T) {
if ctx.Pragma == nil {
t.Error("Pragma not initialized")
}
if ctx.ScriptLibraryGlobals == nil {
t.Error("ScriptLibraryGlobals not initialized")
}
// Test CurrentScope
scope := ctx.CurrentScope()
@ -146,3 +149,184 @@ func TestCompilerContext(t *testing.T) {
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")
}
}

View file

@ -2,6 +2,8 @@ package compiler
import (
"c65gm/internal/preproc"
"go.starlark.net/starlark"
)
// CompilerContext holds all shared resources needed by commands during compilation
@ -26,6 +28,9 @@ type CompilerContext struct {
// Pragma access for per-line pragma lookup
Pragma *preproc.Pragma
// ScriptLibraryGlobals holds persisted Starlark globals from SCRIPT LIBRARY blocks
ScriptLibraryGlobals starlark.StringDict
}
// NewCompilerContext creates a new compiler context with initialized resources
@ -35,16 +40,17 @@ func NewCompilerContext(pragma *preproc.Pragma) *CompilerContext {
generalStack := NewLabelStack("_L")
ctx := &CompilerContext{
SymbolTable: symTable,
ConstStrHandler: constStrHandler,
LoopStartStack: NewLabelStack("_LOOPSTART"),
LoopEndStack: NewLabelStack("_LOOPEND"),
IfStack: NewLabelStack("_I"),
GeneralStack: generalStack,
ForStack: NewForStack(),
SwitchStack: NewSwitchStack(),
CaseSkipStack: NewLabelStack("_SKIPCASE"),
Pragma: pragma,
SymbolTable: symTable,
ConstStrHandler: constStrHandler,
LoopStartStack: NewLabelStack("_LOOPSTART"),
LoopEndStack: NewLabelStack("_LOOPEND"),
IfStack: NewLabelStack("_I"),
GeneralStack: generalStack,
ForStack: NewForStack(),
SwitchStack: NewSwitchStack(),
CaseSkipStack: NewLabelStack("_SKIPCASE"),
Pragma: pragma,
ScriptLibraryGlobals: make(starlark.StringDict),
}
// FunctionHandler needs references to other components

View file

@ -8,20 +8,28 @@ import (
"go.starlark.net/starlark"
)
// executeScript runs a Starlark script and returns the output lines
func executeScript(scriptLines []string, ctx *CompilerContext) ([]string, error) {
// executeScript runs a Starlark script and returns the output lines.
// If isLibrary is true, the script is executed at top level (no _main wrapper)
// and resulting globals are persisted to ctx.ScriptLibraryGlobals.
func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) ([]string, error) {
// Join script lines
scriptText := strings.Join(scriptLines, "\n")
// Expand |varname| -> actual variable names
scriptText = expandVariables(scriptText, ctx)
// Wrap in function (Starlark requires control flow inside functions)
wrappedScript := "def _main():\n"
for _, line := range strings.Split(scriptText, "\n") {
wrappedScript += " " + line + "\n"
var finalScript string
if isLibrary {
// LIBRARY: execute at top level so defs become globals
finalScript = scriptText
} else {
// Regular SCRIPT: wrap in function (Starlark requires control flow inside functions)
finalScript = "def _main():\n"
for _, line := range strings.Split(scriptText, "\n") {
finalScript += " " + line + "\n"
}
finalScript += "_main()\n"
}
wrappedScript += "_main()\n"
// Capture print output
var output bytes.Buffer
@ -35,17 +43,27 @@ func executeScript(scriptLines []string, ctx *CompilerContext) ([]string, error)
// Set execution limit (prevent infinite loops)
thread.SetMaxExecutionSteps(1000000) // 1M steps
// Predeclared functions (math module)
// Build predeclared: math module + library globals
predeclared := starlark.StringDict{
"math": math.Module,
}
for k, v := range ctx.ScriptLibraryGlobals {
predeclared[k] = v
}
// Execute
_, err := starlark.ExecFile(thread, "script.star", wrappedScript, predeclared)
globals, err := starlark.ExecFile(thread, "script.star", finalScript, predeclared)
if err != nil {
return nil, err
}
// For LIBRARY: persist new globals (functions, variables defined at top level)
if isLibrary {
for k, v := range globals {
ctx.ScriptLibraryGlobals[k] = v
}
}
// Split output into lines for assembly
outputStr := output.String()
if outputStr == "" {

View file

@ -12,6 +12,7 @@ const (
Source LineKind = iota
Assembler
Script
ScriptLibrary
)
// Line represents one post-processed source line and its provenance.
@ -46,12 +47,13 @@ func PreProcess(rootFilename string, reader ...FileReader) ([]Line, *Pragma, err
// -------------------- internal --------------------
type preproc struct {
defs *DefineList // from definelist.go
pragma *Pragma // pragma handler
cond []bool // conditional stack; a line is active if all are true
inAsm bool // true when inside ASM/ENDASM block
inScript bool // true when inside SCRIPT/ENDSCRIPT block
reader FileReader // file reader abstraction
defs *DefineList // from definelist.go
pragma *Pragma // pragma handler
cond []bool // conditional stack; a line is active if all are true
inAsm bool // true when inside ASM/ENDASM block
inScript bool // true when inside SCRIPT/ENDSCRIPT block
inScriptLibrary bool // true when inside SCRIPT LIBRARY/ENDSCRIPT block
reader FileReader // file reader abstraction
}
func newPreproc(reader FileReader) *preproc {
@ -116,15 +118,19 @@ func (p *preproc) run(root string) ([]Line, error) {
tokens := strings.Fields(raw)
// ASM mode handling
if !p.inAsm && !p.inScript {
if !p.inAsm && !p.inScript && !p.inScriptLibrary {
// Check for ASM entry
if includeSource && len(tokens) > 0 && tokens[0] == "ASM" {
p.inAsm = true
continue // don't emit ASM marker
}
// Check for SCRIPT entry
// Check for SCRIPT entry (SCRIPT LIBRARY or plain SCRIPT)
if includeSource && len(tokens) > 0 && tokens[0] == "SCRIPT" {
p.inScript = true
if len(tokens) > 1 && tokens[1] == "LIBRARY" {
p.inScriptLibrary = true
} else {
p.inScript = true
}
continue // don't emit SCRIPT marker
}
} else if p.inAsm {
@ -144,20 +150,26 @@ func (p *preproc) run(root string) ([]Line, error) {
PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
})
continue
} else if p.inScript {
// We're in SCRIPT mode
} else if p.inScript || p.inScriptLibrary {
// We're in SCRIPT or SCRIPT LIBRARY mode
// Check for ENDSCRIPT
if len(tokens) > 0 && tokens[0] == "ENDSCRIPT" {
p.inScript = false
p.inScriptLibrary = false
continue // don't emit ENDSCRIPT marker
}
// Otherwise emit line verbatim as Script
// Determine the kind based on which mode we're in
kind := Script
if p.inScriptLibrary {
kind = ScriptLibrary
}
// Emit line verbatim with appropriate kind
out = append(out, Line{
RawText: raw,
Text: raw,
Filename: currFrame.path,
LineNo: currFrame.line,
Kind: Script,
Kind: kind,
PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
})
continue

View file

@ -947,6 +947,80 @@ func TestPreProcess_EmptyScriptBlock(t *testing.T) {
}
}
func TestPreProcess_ScriptLibraryBlock(t *testing.T) {
files := map[string][]string{
"test.c65": {
"SCRIPT LIBRARY",
"def my_func():",
" print('nop')",
"ENDSCRIPT",
"NOP",
},
}
reader := NewMockFileReader(files)
lines, _, err := PreProcess("test.c65", reader)
if err != nil {
t.Fatalf("PreProcess failed: %v", err)
}
// Should have 2 script lines + 1 source line
if len(lines) != 3 {
t.Fatalf("expected 3 lines, got %d", len(lines))
}
// Script library lines should have ScriptLibrary kind
if lines[0].Kind != ScriptLibrary {
t.Errorf("expected Kind=ScriptLibrary, got %v", lines[0].Kind)
}
if lines[0].Text != "def my_func():" {
t.Errorf("expected 'def my_func():', got %q", lines[0].Text)
}
if lines[1].Kind != ScriptLibrary {
t.Errorf("expected Kind=ScriptLibrary, got %v", lines[1].Kind)
}
// Source line after ENDSCRIPT
if lines[2].Kind != Source {
t.Errorf("expected Kind=Source, got %v", lines[2].Kind)
}
if lines[2].Text != "NOP" {
t.Errorf("expected 'NOP', got %q", lines[2].Text)
}
}
func TestPreProcess_ScriptVsScriptLibrary(t *testing.T) {
files := map[string][]string{
"test.c65": {
"SCRIPT LIBRARY",
"def foo(): pass",
"ENDSCRIPT",
"SCRIPT",
"foo()",
"ENDSCRIPT",
},
}
reader := NewMockFileReader(files)
lines, _, err := PreProcess("test.c65", reader)
if err != nil {
t.Fatalf("PreProcess failed: %v", err)
}
if len(lines) != 2 {
t.Fatalf("expected 2 lines, got %d", len(lines))
}
// First line is from SCRIPT LIBRARY
if lines[0].Kind != ScriptLibrary {
t.Errorf("line 0: expected ScriptLibrary, got %v", lines[0].Kind)
}
// Second line is from regular SCRIPT
if lines[1].Kind != Script {
t.Errorf("line 1: expected Script, got %v", lines[1].Kind)
}
}
func TestPreProcess_DollarEscapeExpansion(t *testing.T) {
files := map[string][]string{
"test.c65": {