A Golang runtime and compilation backend for Delta Interaction Nets.
1package compiler
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strings"
9
10 "github.com/vic/godnet/pkg/lambda"
11)
12
13// Compiler translates lambda terms to Go code and invokes go build.
14type Compiler struct {
15 SourceFile string
16 OutputName string
17 GoFlags []string // Passed directly to go build
18 KeepTemp bool // For debugging
19}
20
21// Compile translates the lambda source to Go code and builds it.
22// Returns the output binary path on success.
23func (c *Compiler) Compile() (string, error) {
24 // Read and parse source
25 source, err := os.ReadFile(c.SourceFile)
26 if err != nil {
27 return "", fmt.Errorf("failed to read source: %w", err)
28 }
29
30 term, err := lambda.Parse(string(source))
31 if err != nil {
32 return "", fmt.Errorf("parse error: %w", err)
33 }
34
35 // Generate Go code
36 gen := CodeGenerator{
37 SourceFile: c.SourceFile,
38 SourceText: string(source),
39 }
40 goCode := gen.Generate(term)
41
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")
56 if err != nil {
57 return "", fmt.Errorf("failed to create temp file: %w", err)
58 }
59 tmpPath := tmpFile.Name()
60 defer func() {
61 tmpFile.Close()
62 if !c.KeepTemp {
63 os.Remove(tmpPath)
64 }
65 }()
66
67 if _, err := tmpFile.WriteString(goCode); err != nil {
68 return "", fmt.Errorf("failed to write generated code: %w", err)
69 }
70 tmpFile.Close()
71
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 }
91 }
92
93 // Find go.mod directory to set module context
94 goModDir := findGoModDir(c.SourceFile)
95
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
103 args := []string{"build", "-o", outputName}
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 }
122
123 cmd := exec.Command("go", args...)
124 cmd.Dir = buildDir
125 cmd.Stdout = os.Stdout
126 cmd.Stderr = os.Stderr
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
134 if err := cmd.Run(); err != nil {
135 return "", fmt.Errorf("go build failed: %w", err)
136 }
137
138 // Return absolute path to output
139 if !filepath.IsAbs(outputName) {
140 outputName = filepath.Join(outputDir, filepath.Base(outputName))
141 }
142
143 if c.KeepTemp {
144 fmt.Fprintf(os.Stderr, "Generated code kept at: %s\n", tmpPath)
145 }
146
147 return outputName, nil
148}
149
150// findGoModDir searches for go.mod starting from the given path
151func 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}