A Golang runtime and compilation backend for Delta Interaction Nets.

backend

+439 -150
+5 -5
cmd/godnet/main.go
··· 18 18 runCompile() 19 19 return 20 20 } 21 - 21 + 22 22 // Default: eval mode 23 23 runEval() 24 24 } ··· 28 28 fmt.Fprintf(os.Stderr, "Usage: godnet compile <source.lam> [go build flags...]\n") 29 29 os.Exit(1) 30 30 } 31 - 31 + 32 32 sourceFile := os.Args[2] 33 33 goFlags := os.Args[3:] 34 - 34 + 35 35 c := compiler.Compiler{ 36 36 SourceFile: sourceFile, 37 37 GoFlags: goFlags, 38 38 } 39 - 39 + 40 40 outputName, err := c.Compile() 41 41 if err != nil { 42 42 fmt.Fprintf(os.Stderr, "Compilation failed: %v\n", err) 43 43 os.Exit(1) 44 44 } 45 - 45 + 46 46 fmt.Fprintf(os.Stderr, "Successfully compiled to: %s\n", outputName) 47 47 } 48 48
+97 -9
pkg/compiler/compiler.go
··· 39 39 } 40 40 goCode := gen.Generate(term) 41 41 42 - // Write to temporary file 43 - tmpFile, err := os.CreateTemp("", "godnet-*.go") 42 + // Determine output name first (needed for temp file location) 43 + outputName := c.OutputName 44 + if outputName == "" { 45 + // Default: strip .lam extension 46 + outputName = strings.TrimSuffix(filepath.Base(c.SourceFile), filepath.Ext(c.SourceFile)) 47 + } 48 + 49 + // Write to temporary file in same directory as output (required by go build) 50 + outputDir := filepath.Dir(outputName) 51 + if outputDir == "." || outputDir == "" { 52 + outputDir, _ = os.Getwd() 53 + } 54 + 55 + tmpFile, err := os.CreateTemp(outputDir, "godnet-*.go") 44 56 if err != nil { 45 57 return "", fmt.Errorf("failed to create temp file: %w", err) 46 58 } ··· 57 69 } 58 70 tmpFile.Close() 59 71 60 - // Determine output name 61 - outputName := c.OutputName 62 - if outputName == "" { 63 - // Default: strip .lam extension 64 - outputName = strings.TrimSuffix(filepath.Base(c.SourceFile), filepath.Ext(c.SourceFile)) 72 + // Copy user-provided .go files to output directory (required by go build) 73 + var copiedFiles []string 74 + for _, flag := range c.GoFlags { 75 + if strings.HasSuffix(flag, ".go") { 76 + srcData, err := os.ReadFile(flag) 77 + if err != nil { 78 + return "", fmt.Errorf("failed to read %s: %w", flag, err) 79 + } 80 + dstPath := filepath.Join(outputDir, filepath.Base(flag)) 81 + if err := os.WriteFile(dstPath, srcData, 0644); err != nil { 82 + return "", fmt.Errorf("failed to copy %s: %w", flag, err) 83 + } 84 + copiedFiles = append(copiedFiles, dstPath) 85 + defer func(path string) { 86 + if !c.KeepTemp { 87 + os.Remove(path) 88 + } 89 + }(dstPath) 90 + } 65 91 } 66 92 93 + // Find go.mod directory to set module context 94 + goModDir := findGoModDir(c.SourceFile) 95 + 67 96 // Build with go build 97 + buildDir := outputDir 98 + if goModDir != "" { 99 + // If we found go.mod, build from module root for proper dependency resolution 100 + buildDir = goModDir 101 + } 102 + 68 103 args := []string{"build", "-o", outputName} 69 - args = append(args, c.GoFlags...) 70 - args = append(args, tmpPath) 104 + 105 + // Add non-.go flags 106 + for _, flag := range c.GoFlags { 107 + if !strings.HasSuffix(flag, ".go") { 108 + args = append(args, flag) 109 + } 110 + } 111 + 112 + // Add all Go files (use full paths if building from different directory) 113 + if buildDir != outputDir { 114 + args = append(args, tmpPath) 115 + args = append(args, copiedFiles...) 116 + } else { 117 + args = append(args, filepath.Base(tmpPath)) 118 + for _, copied := range copiedFiles { 119 + args = append(args, filepath.Base(copied)) 120 + } 121 + } 71 122 72 123 cmd := exec.Command("go", args...) 124 + cmd.Dir = buildDir 73 125 cmd.Stdout = os.Stdout 74 126 cmd.Stderr = os.Stderr 75 127 128 + // Debug: show what we're running 129 + if c.KeepTemp { 130 + fmt.Fprintf(os.Stderr, "Build dir: %s\n", buildDir) 131 + fmt.Fprintf(os.Stderr, "Build cmd: go %s\n", strings.Join(args, " ")) 132 + } 133 + 76 134 if err := cmd.Run(); err != nil { 77 135 return "", fmt.Errorf("go build failed: %w", err) 78 136 } 79 137 138 + // Return absolute path to output 139 + if !filepath.IsAbs(outputName) { 140 + outputName = filepath.Join(outputDir, filepath.Base(outputName)) 141 + } 142 + 80 143 if c.KeepTemp { 81 144 fmt.Fprintf(os.Stderr, "Generated code kept at: %s\n", tmpPath) 82 145 } 83 146 84 147 return outputName, nil 85 148 } 149 + 150 + // findGoModDir searches for go.mod starting from the given path 151 + func findGoModDir(startPath string) string { 152 + dir := filepath.Dir(startPath) 153 + if !filepath.IsAbs(dir) { 154 + if abs, err := filepath.Abs(dir); err == nil { 155 + dir = abs 156 + } 157 + } 158 + 159 + for { 160 + goModPath := filepath.Join(dir, "go.mod") 161 + if _, err := os.Stat(goModPath); err == nil { 162 + return dir 163 + } 164 + 165 + parent := filepath.Dir(dir) 166 + if parent == dir { 167 + break // Reached root 168 + } 169 + dir = parent 170 + } 171 + 172 + return "" // Not found 173 + }
+40 -30
pkg/compiler/compiler_test.go
··· 21 21 } 22 22 23 23 func TestCompileChurchSucc(t *testing.T) { 24 - testCompile(t, "church_succ", 24 + testCompile(t, "church_succ", 25 25 "let succ = n: f: x: f (n f x); zero = f: x: x in succ zero", 26 26 "(x0: (x1: (x0") 27 27 } ··· 35 35 } 36 36 37 37 func TestCompileSCombinator(t *testing.T) { 38 - testCompile(t, "s_combinator", 38 + testCompile(t, "s_combinator", 39 39 "(x: y: z: (x z) (y z)) (a: a) (b: b) d", 40 40 "(d d)") 41 41 } 42 42 43 43 func testCompile(t *testing.T, name string, source string, expected string) { 44 44 t.Helper() 45 - 46 - // Create temp directory for test files 47 - tmpDir, err := os.MkdirTemp("", "godnet_compile_test_*") 45 + 46 + // Create temp directory in project root for module support 47 + cwd, _ := os.Getwd() 48 + projectRoot := filepath.Join(cwd, "../..") 49 + tmpDir, err := os.MkdirTemp(projectRoot, "test_build_*") 48 50 if err != nil { 49 51 t.Fatalf("Failed to create temp dir: %v", err) 50 52 } 51 53 defer os.RemoveAll(tmpDir) 52 - 54 + 53 55 // Write source file 54 56 sourceFile := filepath.Join(tmpDir, name+".lam") 55 57 if err := os.WriteFile(sourceFile, []byte(source), 0644); err != nil { 56 58 t.Fatalf("Failed to write source file: %v", err) 57 59 } 58 - 60 + 59 61 // Compile with absolute output path 60 62 outputFile := filepath.Join(tmpDir, name) 61 63 c := Compiler{ ··· 63 65 OutputName: outputFile, 64 66 KeepTemp: false, 65 67 } 66 - 68 + 67 69 builtFile, err := c.Compile() 68 70 if err != nil { 69 71 t.Fatalf("Compilation failed: %v", err) 70 72 } 71 - 73 + 72 74 if builtFile != outputFile { 73 75 t.Fatalf("Output file mismatch: expected %s, got %s", outputFile, builtFile) 74 76 } 75 - 77 + 76 78 // Make sure binary exists 77 79 if _, err := os.Stat(outputFile); os.IsNotExist(err) { 78 80 t.Fatalf("Output binary not found: %s", outputFile) 79 81 } 80 82 defer os.Remove(outputFile) 81 - 83 + 82 84 // Run the binary 83 85 cmd := exec.Command(outputFile) 84 86 output, err := cmd.Output() ··· 88 90 } 89 91 t.Fatalf("Binary execution failed: %v", err) 90 92 } 91 - 93 + 92 94 // Check result 93 95 result := strings.TrimSpace(string(output)) 94 96 if !strings.HasPrefix(result, expected) { ··· 97 99 } 98 100 99 101 func TestCompileWithFlags(t *testing.T) { 100 - tmpDir, err := os.MkdirTemp("", "godnet_compile_flags_*") 102 + cwd, _ := os.Getwd() 103 + projectRoot := filepath.Join(cwd, "../..") 104 + tmpDir, err := os.MkdirTemp(projectRoot, "test_build_*") 101 105 if err != nil { 102 106 t.Fatalf("Failed to create temp dir: %v", err) 103 107 } 104 108 defer os.RemoveAll(tmpDir) 105 - 109 + 106 110 sourceFile := filepath.Join(tmpDir, "test.lam") 107 111 if err := os.WriteFile(sourceFile, []byte("x: x"), 0644); err != nil { 108 112 t.Fatalf("Failed to write source file: %v", err) 109 113 } 110 - 114 + 111 115 // Set custom output name 112 116 customOut := filepath.Join(tmpDir, "custom_name") 113 - 117 + 114 118 c := Compiler{ 115 119 SourceFile: sourceFile, 116 120 OutputName: customOut, 117 121 GoFlags: []string{"-v"}, // verbose go build 118 122 } 119 - 123 + 120 124 outputFile, err := c.Compile() 121 125 if err != nil { 122 126 t.Fatalf("Compilation with flags failed: %v", err) 123 127 } 124 - 128 + 125 129 if outputFile != customOut { 126 130 t.Errorf("Expected output name %s, got %s", customOut, outputFile) 127 131 } 128 - 132 + 129 133 if _, err := os.Stat(outputFile); os.IsNotExist(err) { 130 134 t.Errorf("Custom output file not created: %s", outputFile) 131 135 } 132 136 } 133 137 134 138 func TestCompileKeepTemp(t *testing.T) { 135 - tmpDir, err := os.MkdirTemp("", "godnet_keep_temp_*") 139 + cwd, _ := os.Getwd() 140 + projectRoot := filepath.Join(cwd, "../..") 141 + tmpDir, err := os.MkdirTemp(projectRoot, "test_build_*") 136 142 if err != nil { 137 143 t.Fatalf("Failed to create temp dir: %v", err) 138 144 } 139 145 defer os.RemoveAll(tmpDir) 140 - 146 + 141 147 sourceFile := filepath.Join(tmpDir, "test.lam") 142 148 if err := os.WriteFile(sourceFile, []byte("x: x"), 0644); err != nil { 143 149 t.Fatalf("Failed to write source file: %v", err) 144 150 } 145 - 151 + 152 + outputFile := filepath.Join(tmpDir, "test_keeptemp") 146 153 c := Compiler{ 147 154 SourceFile: sourceFile, 155 + OutputName: outputFile, 148 156 KeepTemp: true, 149 157 } 150 - 151 - outputFile, err := c.Compile() 158 + 159 + builtFile, err := c.Compile() 152 160 if err != nil { 153 161 t.Fatalf("Compilation failed: %v", err) 154 162 } 155 - defer os.Remove(outputFile) 156 - 163 + defer os.Remove(builtFile) 164 + 157 165 // Note: The temp file is created in /tmp, not next to source 158 166 // We just verify KeepTemp was respected (stderr message printed) 159 167 // Actual temp file cleanup is handled by OS 160 168 } 161 169 162 170 func TestCompileInvalidSource(t *testing.T) { 163 - tmpDir, err := os.MkdirTemp("", "godnet_invalid_*") 171 + cwd, _ := os.Getwd() 172 + projectRoot := filepath.Join(cwd, "../..") 173 + tmpDir, err := os.MkdirTemp(projectRoot, "test_build_*") 164 174 if err != nil { 165 175 t.Fatalf("Failed to create temp dir: %v", err) 166 176 } 167 177 defer os.RemoveAll(tmpDir) 168 - 178 + 169 179 sourceFile := filepath.Join(tmpDir, "invalid.lam") 170 180 if err := os.WriteFile(sourceFile, []byte("((("), 0644); err != nil { 171 181 t.Fatalf("Failed to write source file: %v", err) 172 182 } 173 - 183 + 174 184 c := Compiler{ 175 185 SourceFile: sourceFile, 176 186 } 177 - 187 + 178 188 _, err = c.Compile() 179 189 if err == nil { 180 190 t.Error("Expected compilation to fail for invalid syntax")
+24 -24
pkg/compiler/generator.go
··· 26 26 // Generate produces Go source code from a lambda term. 27 27 func (g *CodeGenerator) Generate(term lambda.Term) string { 28 28 g.vars = make(map[string]*varInfo) 29 - 29 + 30 30 g.writeHeader() 31 31 g.writeBuildNetFunction(term) 32 32 g.writeMainFunction() 33 - 33 + 34 34 return g.buf.String() 35 35 } 36 36 ··· 54 54 g.writeLine("func buildNet(net *deltanet.Network) (deltanet.Node, int, map[uint64]string) {") 55 55 g.writeLine("\tvarNames := make(map[uint64]string)") 56 56 g.writeLine("") 57 - 57 + 58 58 // Translate the term 59 59 rootNode, rootPort := g.translateTerm(term, 0, 0) 60 - 60 + 61 61 g.writeLine("") 62 62 g.writeLine("\treturn %s, %d, varNames", rootNode, rootPort) 63 63 g.writeLine("}") ··· 116 116 if info, ok := g.vars[v.Name]; ok { 117 117 // Bound variable - expand replicator 118 118 g.writeComment("Variable: %s (bound)", v.Name) 119 - 119 + 120 120 if strings.HasPrefix(info.nodeName, "rep_") { 121 121 // Already a replicator, expand it 122 122 oldRepName := info.nodeName 123 123 newRepName := g.nextNode("rep") 124 124 delta := level - (info.level + 1) 125 125 nextPort := info.uses + 1 // Next aux port index (1-based, since port 0 is principal) 126 - 126 + 127 127 g.writeLine("\t// Expand replicator for variable '%s' (use #%d)", v.Name, info.uses+1) 128 128 g.writeLine("\t%s := net.NewReplicator(%d, append(%s.Deltas(), %d))", 129 129 newRepName, info.level, oldRepName, delta) 130 - 130 + 131 131 // Move principal connection 132 132 g.writeLine("\tsourceNode, sourcePort := net.GetLink(%s, 0)", oldRepName) 133 133 g.writeLine("\tnet.LinkAt(%s, 0, sourceNode, sourcePort, %d)", newRepName, depth) 134 - 134 + 135 135 // Move existing aux ports 136 136 g.writeLine("\tfor i := 0; i < len(%s.Deltas()); i++ {", oldRepName) 137 137 g.writeLine("\t\tdestNode, destPort := net.GetLink(%s, i+1)", oldRepName) ··· 139 139 g.writeLine("\t\t\tnet.LinkAt(%s, i+1, destNode, destPort, %d)", newRepName, depth) 140 140 g.writeLine("\t\t}") 141 141 g.writeLine("\t}") 142 - 142 + 143 143 info.nodeName = newRepName 144 144 info.uses++ 145 145 return newRepName, nextPort ··· 149 149 repName := g.nextNode("rep") 150 150 delta := level - (info.level + 1) 151 151 repLevel := info.level + 1 152 - 152 + 153 153 g.writeLine("\t%s := net.NewReplicator(%d, []int{%d})", repName, repLevel, delta) 154 154 g.writeLine("\tnet.LinkAt(%s, 0, %s, %d, %d)", repName, info.nodeName, info.port, depth) 155 - 155 + 156 156 info.nodeName = repName 157 157 info.uses = 1 158 158 return repName, 1 ··· 162 162 g.writeComment("Variable: %s (free)", v.Name) 163 163 varName := g.nextNode("var") 164 164 repName := g.nextNode("rep") 165 - 165 + 166 166 g.writeLine("\t%s := net.NewVar()", varName) 167 167 g.writeLine("\tvarNames[%s.ID()] = \"%s\"", varName, v.Name) 168 168 g.writeLine("\t%s := net.NewReplicator(0, []int{%d})", repName, level-1) 169 169 g.writeLine("\tnet.LinkAt(%s, 0, %s, 0, %d)", repName, varName, depth) 170 - 170 + 171 171 g.vars[v.Name] = &varInfo{ 172 172 nodeName: repName, 173 173 port: 0, 174 174 level: 0, 175 175 } 176 - 176 + 177 177 return repName, 1 178 178 } 179 179 } 180 180 181 181 func (g *CodeGenerator) genAbs(abs lambda.Abs, level int, depth uint64) (string, int) { 182 182 g.writeComment("Abstraction: λ%s. ...", abs.Arg) 183 - 183 + 184 184 fanName := g.nextNode("fan") 185 185 eraName := g.nextNode("era") 186 - 186 + 187 187 g.writeLine("\t%s := net.NewFan()", fanName) 188 188 g.writeLine("\t%s := net.NewEraser()", eraName) 189 189 g.writeLine("\tnet.LinkAt(%s, 0, %s, 2, %d)", eraName, fanName, depth) 190 - 190 + 191 191 // Save old binding if shadowing 192 192 oldVar := g.vars[abs.Arg] 193 193 g.vars[abs.Arg] = &varInfo{ ··· 195 195 port: 2, 196 196 level: level, 197 197 } 198 - 198 + 199 199 // Generate body 200 200 bodyNode, bodyPort := g.translateTerm(abs.Body, level, depth) 201 201 g.writeLine("\tnet.LinkAt(%s, 1, %s, %d, %d)", fanName, bodyNode, bodyPort, depth) 202 - 202 + 203 203 // Restore old binding 204 204 if oldVar != nil { 205 205 g.vars[abs.Arg] = oldVar 206 206 } else { 207 207 delete(g.vars, abs.Arg) 208 208 } 209 - 209 + 210 210 return fanName, 0 211 211 } 212 212 213 213 func (g *CodeGenerator) genApp(app lambda.App, level int, depth uint64) (string, int) { 214 214 g.writeComment("Application") 215 - 215 + 216 216 fanName := g.nextNode("fan") 217 217 g.writeLine("\t%s := net.NewFan()", fanName) 218 - 218 + 219 219 // Generate function 220 220 funNode, funPort := g.translateTerm(app.Fun, level, depth) 221 221 g.writeLine("\tnet.LinkAt(%s, 0, %s, %d, %d)", fanName, funNode, funPort, depth) 222 - 222 + 223 223 // Generate argument (level + 1) 224 224 argNode, argPort := g.translateTerm(app.Arg, level+1, depth+1) 225 225 g.writeLine("\tnet.LinkAt(%s, 2, %s, %d, %d)", fanName, argNode, argPort, depth+1) 226 - 226 + 227 227 return fanName, 1 228 228 } 229 229
+273 -82
pkg/compiler/native_test.go
··· 8 8 "testing" 9 9 ) 10 10 11 - // Test compiling and running code with native pure functions 12 - func TestCompileWithPureFunctions(t *testing.T) { 13 - tmpDir, err := os.MkdirTemp("", "godnet_pure_test_*") 11 + // Test compiling with additional Go files that provide native functions 12 + func TestCompileWithNativeFunctions(t *testing.T) { 13 + // Create temp dir in a test subdirectory within the project 14 + cwd, _ := os.Getwd() 15 + projectRoot := filepath.Join(cwd, "../..") 16 + tmpDir, err := os.MkdirTemp(projectRoot, "test_build_*") 14 17 if err != nil { 15 18 t.Fatalf("Failed to create temp dir: %v", err) 16 19 } 17 20 defer os.RemoveAll(tmpDir) 18 21 19 - // Create a test that will use pure functions 20 - // For now, just test that the compiler can handle the syntax 21 - // Actual pure function integration requires runtime support 22 - sourceFile := filepath.Join(tmpDir, "pure_test.lam") 23 - source := `x: x` 22 + // Create a Go file with native function implementations 23 + nativeGoFile := filepath.Join(tmpDir, "natives.go") 24 + nativeGoCode := `package main 25 + 26 + import "github.com/vic/godnet/pkg/deltanet" 27 + 28 + func registerNatives(net *deltanet.Network) { 29 + // String concatenation 30 + net.RegisterNative("str_concat", func(a interface{}) (interface{}, error) { 31 + aStr, ok := a.(string) 32 + if !ok { 33 + return nil, nil // Return nil for non-string 34 + } 35 + return func(b interface{}) (interface{}, error) { 36 + bStr, ok := b.(string) 37 + if !ok { 38 + return nil, nil 39 + } 40 + return aStr + bStr, nil 41 + }, nil 42 + }) 24 43 44 + // String length 45 + net.RegisterNative("str_len", func(a interface{}) (interface{}, error) { 46 + aStr, ok := a.(string) 47 + if !ok { 48 + return 0, nil 49 + } 50 + return len(aStr), nil 51 + }) 52 + } 53 + ` 54 + if err := os.WriteFile(nativeGoFile, []byte(nativeGoCode), 0644); err != nil { 55 + t.Fatalf("Failed to write natives.go: %v", err) 56 + } 57 + 58 + // Create lambda source that references these natives 59 + sourceFile := filepath.Join(tmpDir, "test.lam") 60 + source := `x: x` // Simple test - actual native invocation syntax TBD 61 + 25 62 if err := os.WriteFile(sourceFile, []byte(source), 0644); err != nil { 26 63 t.Fatalf("Failed to write source file: %v", err) 27 64 } 28 65 29 - outputFile := filepath.Join(tmpDir, "pure_test") 66 + // Compile with the native Go file included (must be in same directory) 67 + outputFile := filepath.Join(tmpDir, "test_with_natives") 30 68 c := Compiler{ 31 69 SourceFile: sourceFile, 32 70 OutputName: outputFile, 71 + GoFlags: []string{nativeGoFile}, // Will be copied to output dir 33 72 KeepTemp: false, 34 73 } 35 74 36 75 builtFile, err := c.Compile() 37 76 if err != nil { 38 - t.Fatalf("Compilation failed: %v", err) 77 + t.Fatalf("Compilation with natives failed: %v", err) 39 78 } 40 79 41 80 if _, err := os.Stat(builtFile); os.IsNotExist(err) { 42 81 t.Fatalf("Output binary not found: %s", builtFile) 43 82 } 44 83 45 - // Run the binary 84 + // Verify binary runs 46 85 cmd := exec.Command(builtFile) 47 86 output, err := cmd.Output() 48 87 if err != nil { ··· 58 97 } 59 98 } 60 99 61 - // Test that would use effect handlers (placeholder for when runtime support is added) 100 + // Test compiling with additional Go files that provide effect handlers 62 101 func TestCompileWithEffectHandlers(t *testing.T) { 63 - t.Skip("Effect handlers in compiled code require runtime integration - placeholder test") 64 - 65 - tmpDir, err := os.MkdirTemp("", "godnet_effects_test_*") 102 + cwd, _ := os.Getwd() 103 + projectRoot := filepath.Join(cwd, "../..") 104 + tmpDir, err := os.MkdirTemp(projectRoot, "test_build_*") 66 105 if err != nil { 67 106 t.Fatalf("Failed to create temp dir: %v", err) 68 107 } 69 108 defer os.RemoveAll(tmpDir) 70 109 71 - // Future test: compile code that performs effects 72 - // Example pseudo-code (syntax TBD): 73 - // let result = effect Print "hello" in result 110 + // Create a Go file with effect handler implementations 111 + handlersGoFile := filepath.Join(tmpDir, "handlers.go") 112 + handlersGoCode := `package main 113 + 114 + import ( 115 + "fmt" 116 + "github.com/vic/godnet/pkg/deltanet" 117 + ) 118 + 119 + func installHandlers(net *deltanet.Network) *deltanet.HandlerScope { 120 + scope := deltanet.NewHandlerScope() 74 121 75 - sourceFile := filepath.Join(tmpDir, "effects_test.lam") 76 - source := `x: x` 122 + // Print effect handler 123 + scope.Register("Print", func(eff deltanet.Effect, cont *deltanet.Continuation) (interface{}, error) { 124 + fmt.Println(eff.Payload) 125 + return cont.Resume(nil) 126 + }) 77 127 128 + // FileRead effect handler (mock) 129 + scope.Register("FileRead", func(eff deltanet.Effect, cont *deltanet.Continuation) (interface{}, error) { 130 + path, ok := eff.Payload.(string) 131 + if !ok { 132 + return cont.Resume("invalid path") 133 + } 134 + // Mock file read 135 + content := fmt.Sprintf("contents of %s", path) 136 + return cont.Resume(content) 137 + }) 138 + 139 + return scope 140 + } 141 + ` 142 + if err := os.WriteFile(handlersGoFile, []byte(handlersGoCode), 0644); err != nil { 143 + t.Fatalf("Failed to write handlers.go: %v", err) 144 + } 145 + 146 + // Create lambda source 147 + sourceFile := filepath.Join(tmpDir, "test.lam") 148 + source := `x: x` // Simple test - actual effect syntax TBD 149 + 78 150 if err := os.WriteFile(sourceFile, []byte(source), 0644); err != nil { 79 151 t.Fatalf("Failed to write source file: %v", err) 80 152 } 81 153 82 - outputFile := filepath.Join(tmpDir, "effects_test") 154 + // Compile with the handlers Go file included 155 + outputFile := filepath.Join(tmpDir, "test_with_handlers") 83 156 c := Compiler{ 84 157 SourceFile: sourceFile, 85 158 OutputName: outputFile, 159 + GoFlags: []string{handlersGoFile}, // Link with handlers.go 86 160 KeepTemp: false, 87 161 } 88 162 89 - _, err = c.Compile() 163 + builtFile, err := c.Compile() 90 164 if err != nil { 91 - t.Fatalf("Compilation failed: %v", err) 165 + t.Fatalf("Compilation with handlers failed: %v", err) 166 + } 167 + 168 + if _, err := os.Stat(builtFile); os.IsNotExist(err) { 169 + t.Fatalf("Output binary not found: %s", builtFile) 170 + } 171 + 172 + // Verify binary runs 173 + cmd := exec.Command(builtFile) 174 + output, err := cmd.Output() 175 + if err != nil { 176 + if exitErr, ok := err.(*exec.ExitError); ok { 177 + t.Fatalf("Binary execution failed: %v\nStderr: %s", err, exitErr.Stderr) 178 + } 179 + t.Fatalf("Binary execution failed: %v", err) 180 + } 181 + 182 + result := strings.TrimSpace(string(output)) 183 + if !strings.Contains(result, "x0: x0") { 184 + t.Errorf("Expected identity function, got: %s", result) 92 185 } 93 186 } 94 187 95 - // Test compiling code that would use both pure functions and effects 96 - func TestCompileMixedPureAndEffects(t *testing.T) { 97 - t.Skip("Mixed pure/effect code requires full runtime integration - placeholder test") 98 - 99 - tmpDir, err := os.MkdirTemp("", "godnet_mixed_test_*") 188 + // Test compiling with both native functions and effect handlers 189 + func TestCompileMixedNativesAndHandlers(t *testing.T) { 190 + cwd, _ := os.Getwd() 191 + projectRoot := filepath.Join(cwd, "../..") 192 + tmpDir, err := os.MkdirTemp(projectRoot, "test_build_*") 100 193 if err != nil { 101 194 t.Fatalf("Failed to create temp dir: %v", err) 102 195 } 103 196 defer os.RemoveAll(tmpDir) 104 197 105 - // Future test: compile code that uses both pure functions and effects 106 - // Example: let concat = pure "string_concat"; x = concat "hello" " world" in effect Print x 198 + // Create Go file with both natives and handlers 199 + runtimeGoFile := filepath.Join(tmpDir, "runtime.go") 200 + runtimeGoCode := `package main 201 + 202 + import ( 203 + "fmt" 204 + "github.com/vic/godnet/pkg/deltanet" 205 + ) 206 + 207 + func setupRuntime(net *deltanet.Network) *deltanet.HandlerScope { 208 + // Register native functions 209 + net.RegisterNative("add", func(a interface{}) (interface{}, error) { 210 + aInt, ok := a.(int) 211 + if !ok { 212 + return nil, fmt.Errorf("add: expected int") 213 + } 214 + return func(b interface{}) (interface{}, error) { 215 + bInt, ok := b.(int) 216 + if !ok { 217 + return nil, fmt.Errorf("add: expected int") 218 + } 219 + return aInt + bInt, nil 220 + }, nil 221 + }) 107 222 108 - sourceFile := filepath.Join(tmpDir, "mixed_test.lam") 109 - source := `x: x` 223 + // Register effect handlers 224 + scope := deltanet.NewHandlerScope() 225 + scope.Register("Log", func(eff deltanet.Effect, cont *deltanet.Continuation) (interface{}, error) { 226 + fmt.Printf("[LOG] %v\n", eff.Payload) 227 + return cont.Resume(nil) 228 + }) 110 229 230 + return scope 231 + } 232 + ` 233 + if err := os.WriteFile(runtimeGoFile, []byte(runtimeGoCode), 0644); err != nil { 234 + t.Fatalf("Failed to write runtime.go: %v", err) 235 + } 236 + 237 + // Create lambda source 238 + sourceFile := filepath.Join(tmpDir, "test.lam") 239 + source := `x: x` // Simple test - actual native/effect syntax TBD 240 + 111 241 if err := os.WriteFile(sourceFile, []byte(source), 0644); err != nil { 112 242 t.Fatalf("Failed to write source file: %v", err) 113 243 } 114 244 115 - outputFile := filepath.Join(tmpDir, "mixed_test") 245 + // Compile with runtime file 246 + outputFile := filepath.Join(tmpDir, "test_mixed") 116 247 c := Compiler{ 117 248 SourceFile: sourceFile, 118 249 OutputName: outputFile, 250 + GoFlags: []string{runtimeGoFile}, // Link with runtime.go 119 251 KeepTemp: false, 120 252 } 121 253 122 - _, err = c.Compile() 254 + builtFile, err := c.Compile() 123 255 if err != nil { 124 256 t.Fatalf("Compilation failed: %v", err) 125 257 } 258 + 259 + if _, err := os.Stat(builtFile); os.IsNotExist(err) { 260 + t.Fatalf("Output binary not found: %s", builtFile) 261 + } 262 + 263 + // Verify binary runs 264 + cmd := exec.Command(builtFile) 265 + output, err := cmd.Output() 266 + if err != nil { 267 + if exitErr, ok := err.(*exec.ExitError); ok { 268 + t.Fatalf("Binary execution failed: %v\nStderr: %s", err, exitErr.Stderr) 269 + } 270 + t.Fatalf("Binary execution failed: %v", err) 271 + } 272 + 273 + result := strings.TrimSpace(string(output)) 274 + if !strings.Contains(result, "x0: x0") { 275 + t.Errorf("Expected identity function, got: %s", result) 276 + } 126 277 } 127 278 128 - // Test that compiled code can register and use native pure functions 129 - func TestCompiledNativeRegistration(t *testing.T) { 130 - t.Skip("Native function registration in compiled code needs design - placeholder test") 131 - 132 - // Future test design: 133 - // 1. Extend Nix syntax to reference native functions (maybe `@native("func_name")`) 134 - // 2. CodeGenerator emits net.RegisterNative calls in generated code 135 - // 3. Generated code includes native function implementations or imports 136 - // 4. Test that reduction properly invokes the native functions 137 - 138 - // Example generated code structure: 139 - // func buildNet(net *deltanet.Network) { 140 - // // Register natives 141 - // net.RegisterNative("string_concat", func(a interface{}) (interface{}, error) { 142 - // return func(b interface{}) (interface{}, error) { 143 - // return a.(string) + b.(string), nil 144 - // }, nil 145 - // }) 146 - // 147 - // // Build term that uses the native 148 - // pure := net.NewPure("string_concat") 149 - // data1 := net.NewData("hello") 150 - // // ... etc 151 - // } 279 + // Test multiple Go files can be linked together 280 + func TestCompileWithMultipleRuntimeFiles(t *testing.T) { 281 + cwd, _ := os.Getwd() 282 + projectRoot := filepath.Join(cwd, "../..") 283 + tmpDir, err := os.MkdirTemp(projectRoot, "test_build_*") 284 + if err != nil { 285 + t.Fatalf("Failed to create temp dir: %v", err) 286 + } 287 + defer os.RemoveAll(tmpDir) 288 + 289 + // Create separate files for natives and handlers 290 + nativesFile := filepath.Join(tmpDir, "natives.go") 291 + nativesCode := `package main 292 + 293 + import "github.com/vic/godnet/pkg/deltanet" 294 + 295 + func setupNatives(net *deltanet.Network) { 296 + net.RegisterNative("mul", func(a interface{}) (interface{}, error) { 297 + aInt := a.(int) 298 + return func(b interface{}) (interface{}, error) { 299 + bInt := b.(int) 300 + return aInt * bInt, nil 301 + }, nil 302 + }) 303 + } 304 + ` 305 + if err := os.WriteFile(nativesFile, []byte(nativesCode), 0644); err != nil { 306 + t.Fatalf("Failed to write natives: %v", err) 307 + } 308 + 309 + handlersFile := filepath.Join(tmpDir, "handlers.go") 310 + handlersCode := `package main 311 + 312 + import ( 313 + "fmt" 314 + "github.com/vic/godnet/pkg/deltanet" 315 + ) 316 + 317 + func setupHandlers() *deltanet.HandlerScope { 318 + scope := deltanet.NewHandlerScope() 319 + scope.Register("Debug", func(eff deltanet.Effect, cont *deltanet.Continuation) (interface{}, error) { 320 + fmt.Printf("[DEBUG] %v\n", eff.Payload) 321 + return cont.Resume(nil) 322 + }) 323 + return scope 152 324 } 325 + ` 326 + if err := os.WriteFile(handlersFile, []byte(handlersCode), 0644); err != nil { 327 + t.Fatalf("Failed to write handlers: %v", err) 328 + } 153 329 154 - // Test that compiled code can install and use effect handlers 155 - func TestCompiledEffectHandlers(t *testing.T) { 156 - t.Skip("Effect handlers in compiled code need design - placeholder test") 157 - 158 - // Future test design: 159 - // 1. Extend Nix syntax for effects (maybe `perform Effect payload` and `handle ... with ...`) 160 - // 2. CodeGenerator emits handler registration and effect nodes 161 - // 3. Generated code includes handler implementations 162 - // 4. Test that reduction properly invokes handlers with continuations 163 - 164 - // Example generated code structure: 165 - // func buildNet(net *deltanet.Network) { 166 - // // Create handler scope 167 - // scope := deltanet.NewHandlerScope() 168 - // scope.Register("Print", func(eff deltanet.Effect, cont *deltanet.Continuation) (interface{}, error) { 169 - // fmt.Println(eff.Payload) 170 - // return cont.Resume(nil) 171 - // }) 172 - // 173 - // // Build term with effects 174 - // handler := net.NewHandler(scope) 175 - // effect := net.NewEffect(deltanet.Effect{Name: "Print", Payload: "hello"}) 176 - // // ... etc 177 - // } 330 + // Create lambda source 331 + sourceFile := filepath.Join(tmpDir, "test.lam") 332 + source := `x: x` 333 + 334 + if err := os.WriteFile(sourceFile, []byte(source), 0644); err != nil { 335 + t.Fatalf("Failed to write source: %v", err) 336 + } 337 + 338 + // Compile with multiple runtime files 339 + outputFile := filepath.Join(tmpDir, "test_multi") 340 + c := Compiler{ 341 + SourceFile: sourceFile, 342 + OutputName: outputFile, 343 + GoFlags: []string{nativesFile, handlersFile}, 344 + KeepTemp: false, 345 + } 346 + 347 + builtFile, err := c.Compile() 348 + if err != nil { 349 + t.Fatalf("Compilation with multiple files failed: %v", err) 350 + } 351 + 352 + if _, err := os.Stat(builtFile); os.IsNotExist(err) { 353 + t.Fatalf("Output binary not found: %s", builtFile) 354 + } 355 + 356 + // Verify binary runs 357 + cmd := exec.Command(builtFile) 358 + if output, err := cmd.Output(); err != nil { 359 + if exitErr, ok := err.(*exec.ExitError); ok { 360 + t.Fatalf("Binary failed: %v\nStderr: %s", err, exitErr.Stderr) 361 + } 362 + t.Fatalf("Binary failed: %v", err) 363 + } else { 364 + result := strings.TrimSpace(string(output)) 365 + if !strings.Contains(result, "x0: x0") { 366 + t.Errorf("Expected identity, got: %s", result) 367 + } 368 + } 178 369 }