diff --git a/.gitignore b/.gitignore index 900c38e..e243c20 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ c65gm .env .local/ opencode-config/package-lock.json +internal/preproc/lib/ +.bun/ diff --git a/AGENTS.md b/AGENTS.md index b1fad8d..ba396d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,11 +90,13 @@ LABEL lib_mylib_skip ## Development Guidelines ### Environment Constraints -**IMPORTANT**: The agent runs in a Docker container without access to compilers, testing tools, or external build systems. All compilation and testing must be performed by the user. The agent can only: +**CRITICAL - NO GO TOOLS AVAILABLE**: The agent runs in a Docker container without access to compilers, testing tools, or external build systems. The container does NOT have Go installed (`go` command not found). All compilation and testing must be performed by the user. The agent can only: 1. Read and analyze source code 2. Make code changes 3. Provide instructions for the user to compile and test +**NEVER attempt to run `go build`, `go test`, or any Go commands** - they will fail with "go: not found". + ### File Access Restrictions **CRITICAL**: The agent must only access normal project files within the current working directory. The agent must NEVER: 1. Look at files outside the project directory (e.g., `/tmp/`, `/etc/`, `/home/`, etc.) @@ -117,7 +119,7 @@ All file operations must be restricted to the project's source code and document 4. Consider adding examples in `examples/` directory ### Testing and Rebuilding -**IMPORTANT**: The agent cannot run tests or compile code. Provide these instructions to the user: +**CRITICAL**: The agent cannot run tests or compile code. The container has no Go installation. Provide these instructions to the user: #### Testing: - Run all tests: `go test ./...` diff --git a/build_c65gm.sh b/build_c65gm.sh new file mode 100755 index 0000000..7f9e453 --- /dev/null +++ b/build_c65gm.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +# Copy lib directory to internal/preproc for embedding +echo "Copying lib directory for embedding..." +rm -rf internal/preproc/lib +cp -r lib internal/preproc/ + +# Build the binary +echo "Building c65gm..." +go build -o c65gm + +echo "Build complete: c65gm" diff --git a/examples/shift_demo/cm.sh b/examples/shift_demo/cm.sh index 00e7025..0a06181 100755 --- a/examples/shift_demo/cm.sh +++ b/examples/shift_demo/cm.sh @@ -2,9 +2,9 @@ # Define filename as variable PROGNAME="shift_demo" # Only set C65LIBPATH if not already defined -if [ -z "$C65LIBPATH" ]; then - export C65LIBPATH=$(readlink -f "../../lib") -fi +#if [ -z "$C65LIBPATH" ]; then +# export C65LIBPATH=$(readlink -f "../../lib") +#fi # Compile - use absolute path to c65gm c65gm -in ${PROGNAME}.c65 -out ${PROGNAME}.s if [ $? -ne 0 ]; then diff --git a/internal/preproc/embeddedlib.go b/internal/preproc/embeddedlib.go new file mode 100644 index 0000000..ab9e1b8 --- /dev/null +++ b/internal/preproc/embeddedlib.go @@ -0,0 +1,38 @@ +package preproc + +import ( + "embed" + "fmt" + "io/fs" + "strings" +) + +//go:embed lib +var embeddedLib embed.FS + +// embeddedLibPath returns the path within the embedded filesystem for a library include. +// For example, "" becomes "lib/c64start.c65" +func embeddedLibPath(spec string) (string, error) { + if !strings.HasPrefix(spec, "<") || !strings.HasSuffix(spec, ">") { + return "", fmt.Errorf("not an angle-bracket include: %s", spec) + } + + base := strings.TrimSpace(spec[1 : len(spec)-1]) + return "lib/" + base, nil +} + +// readEmbeddedFile reads a file from the embedded filesystem. +func readEmbeddedFile(path string) ([]string, error) { + data, err := fs.ReadFile(embeddedLib, path) + if err != nil { + return nil, err + } + + // Split into lines, preserving empty lines + content := string(data) + lines := strings.Split(content, "\n") + + // If the file ends with newline, the last element will be empty + // This matches the behavior of strings.Split for disk files + return lines, nil +} \ No newline at end of file diff --git a/internal/preproc/filereader.go b/internal/preproc/filereader.go index 7c5d716..f638ad9 100644 --- a/internal/preproc/filereader.go +++ b/internal/preproc/filereader.go @@ -93,3 +93,16 @@ func (m *MockFileReader) ReadLines(includeSpec string, _ string) ([]string, stri } return lines, includeSpec, nil } + +// NewDefaultFileReader creates the appropriate FileReader based on C65LIBPATH environment variable. +// If C65LIBPATH is set, returns a DiskFileReader for external library access. +// If C65LIBPATH is not set, returns a HybridFileReader that uses embedded standard library. +func NewDefaultFileReader() FileReader { + libPath := os.Getenv("C65LIBPATH") + if libPath != "" { + fmt.Printf("Using external library from: %s\n", libPath) + return NewDiskFileReader() + } + fmt.Println("Using embedded standard library") + return NewHybridFileReader() +} diff --git a/internal/preproc/filereader_test.go b/internal/preproc/filereader_test.go index 76c4e60..c239f8d 100644 --- a/internal/preproc/filereader_test.go +++ b/internal/preproc/filereader_test.go @@ -3,6 +3,7 @@ package preproc import ( "os" "path/filepath" + "strings" "testing" ) @@ -67,6 +68,51 @@ func TestDiskFileReader_ReadLines_LibraryWithoutEnv(t *testing.T) { } } +func TestHybridFileReader_ReadLines_LibraryIncludes(t *testing.T) { + origPath := os.Getenv("C65LIBPATH") + defer func() { _ = os.Setenv("C65LIBPATH", origPath) }() + + // Test 1: With C65LIBPATH set, should use disk + libPath, _ := filepath.Abs("filereader_mocks/lib") + _ = os.Setenv("C65LIBPATH", libPath) + + reader1 := NewHybridFileReader() + lines1, path1, err1 := reader1.ReadLines("", "") + if err1 != nil { + t.Errorf("HybridFileReader with C65LIBPATH should work: %v", err1) + } + if len(lines1) == 0 { + t.Error("expected lines from disk library") + } + if !strings.Contains(path1, "filereader_mocks") { + t.Errorf("expected disk path, got: %s", path1) + } + + // Test 2: Without C65LIBPATH, should use embedded (will fail in test since no embedded mock) + _ = os.Setenv("C65LIBPATH", "") + reader2 := NewHybridFileReader() + _, _, err2 := reader2.ReadLines("", "") + if err2 == nil { + // This is expected to fail in unit tests since we can't mock embedded FS easily + // In real usage, embedded library would be used + t.Log("Note: HybridFileReader without C65LIBPATH would use embedded library") + } +} + +func TestHybridFileReader_ReadLines_RelativeIncludes(t *testing.T) { + reader := NewHybridFileReader() + + appDir, _ := filepath.Abs("filereader_mocks/app") + + lines, _, err := reader.ReadLines("test_app.c65", appDir) + if err != nil { + t.Errorf("HybridFileReader should handle relative includes: %v", err) + } + if len(lines) == 0 { + t.Error("expected lines from relative include") + } +} + func TestDiskFileReader_ReadLines_RelativeIncludes(t *testing.T) { reader := NewDiskFileReader() diff --git a/internal/preproc/hybridfilereader.go b/internal/preproc/hybridfilereader.go new file mode 100644 index 0000000..ab657a9 --- /dev/null +++ b/internal/preproc/hybridfilereader.go @@ -0,0 +1,74 @@ +package preproc + +import ( + "fmt" + "os" + "strings" +) + +// HybridFileReader implements FileReader with support for both embedded +// standard library (when C65LIBPATH is not set) and disk-based includes. +type HybridFileReader struct { + diskReader *DiskFileReader // For relative includes & C65LIBPATH overrides + libPath string // From C65LIBPATH env var + usedEmbedded bool // Track if we've used embedded library + embeddedCache map[string][]string // Cache for embedded files +} + +// NewHybridFileReader creates a new HybridFileReader. +func NewHybridFileReader() *HybridFileReader { + return &HybridFileReader{ + diskReader: NewDiskFileReader(), + libPath: os.Getenv("C65LIBPATH"), + embeddedCache: make(map[string][]string), + } +} + +// ReadLines implements the FileReader interface. +func (h *HybridFileReader) ReadLines(includeSpec string, currentDir string) ([]string, string, error) { + // Check if this is an angle-bracket include + if strings.HasPrefix(includeSpec, "<") && strings.HasSuffix(includeSpec, ">") { + // If C65LIBPATH is set, delegate to DiskFileReader + if h.libPath != "" { + return h.diskReader.ReadLines(includeSpec, currentDir) + } + + // Otherwise, use embedded library + if !h.usedEmbedded { + h.usedEmbedded = true + // Note: Library source message is printed by NewDefaultFileReader factory + } + + return h.readEmbeddedLibrary(includeSpec) + } + + // Relative includes always go to disk + return h.diskReader.ReadLines(includeSpec, currentDir) +} + +// readEmbeddedLibrary reads a file from the embedded standard library. +func (h *HybridFileReader) readEmbeddedLibrary(spec string) ([]string, string, error) { + // Convert angle-bracket spec to embedded path + embeddedPath, err := embeddedLibPath(spec) + if err != nil { + return nil, "", err + } + + // Check cache + if lines, ok := h.embeddedCache[embeddedPath]; ok { + // Return cached lines with virtual path for error messages + return lines, "/" + strings.TrimPrefix(embeddedPath, "lib/"), nil + } + + // Read from embedded filesystem + lines, err := readEmbeddedFile(embeddedPath) + if err != nil { + return nil, "", fmt.Errorf("embedded library include not found: %s", spec) + } + + // Cache the result + h.embeddedCache[embeddedPath] = lines + + // Return with virtual path for error messages + return lines, "/" + strings.TrimPrefix(embeddedPath, "lib/"), nil +} \ No newline at end of file diff --git a/internal/preproc/preproc.go b/internal/preproc/preproc.go index 0108cbd..7e683fa 100644 --- a/internal/preproc/preproc.go +++ b/internal/preproc/preproc.go @@ -38,7 +38,7 @@ func PreProcess(rootFilename string, reader ...FileReader) ([]Line, *Pragma, err if len(reader) > 0 && reader[0] != nil { r = reader[0] } else { - r = NewDiskFileReader() + r = NewDefaultFileReader() } pp := newPreproc(r) lines, err := pp.run(rootFilename)