compiler.go raw
1 package jsbackend
2
3 import (
4 "encoding/json"
5 "fmt"
6 "go/token"
7 "go/types"
8 "os"
9 "path/filepath"
10 "sort"
11 "strings"
12 "time"
13
14 "golang.org/x/tools/go/ssa"
15 )
16
17 // Config holds JS backend configuration.
18 type Config struct {
19 OutputDir string // directory to write .mjs files
20 RuntimeDir string // path to jsruntime/ directory (embedded in output)
21 DumpSSA bool // print SSA for debugging
22 }
23
24 // PackageInfo holds the minimal info we need about a package from the loader.
25 // This avoids a dependency on the loader package (which pulls in LLVM via cgo).
26 type PackageInfo struct {
27 Path string // import path
28 TypePkg *types.Package // typechecker package
29 }
30
31 // ProgramCompiler compiles an entire Go program to JavaScript ES modules.
32 type ProgramCompiler struct {
33 config *Config
34 program *ssa.Program
35 typeMapper *TypeMapper
36 compiled map[string]bool
37 asyncFuncs map[*ssa.Function]bool // functions that need to be async
38 errors []error
39 manifest []string // relative paths of all output files
40 }
41
42 // CompileProgram is the main entry point.
43 // Takes an SSA program, a list of packages in dependency order, and the main package path.
44 func CompileProgram(config *Config, ssaProg *ssa.Program, packages []PackageInfo, mainPkgPath string) error {
45 pc := &ProgramCompiler{
46 config: config,
47 program: ssaProg,
48 typeMapper: NewTypeMapper(),
49 compiled: make(map[string]bool),
50 asyncFuncs: make(map[*ssa.Function]bool),
51 }
52
53 if err := os.MkdirAll(config.OutputDir, 0o755); err != nil {
54 return fmt.Errorf("create output dir: %w", err)
55 }
56
57 // Build all SSA packages.
58 ssaProg.Build()
59
60 // Compute which functions need to be async (transitive closure).
61 pc.computeAsyncFuncs(packages)
62
63 // Compile packages in dependency order.
64 for _, pkgInfo := range packages {
65 ssaPkg := ssaProg.Package(pkgInfo.TypePkg)
66 if ssaPkg == nil {
67 continue
68 }
69 if err := pc.compilePackage(ssaPkg); err != nil {
70 pc.errors = append(pc.errors, err)
71 }
72 }
73
74 // Write the main entry point.
75 if mainPkgPath != "" {
76 if err := pc.writeEntryPoint(mainPkgPath); err != nil {
77 pc.errors = append(pc.errors, err)
78 }
79 }
80
81 // Copy runtime files to output.
82 if err := pc.copyRuntime(); err != nil {
83 pc.errors = append(pc.errors, err)
84 }
85
86 // Write manifest of all output files.
87 if err := pc.writeManifest(); err != nil {
88 pc.errors = append(pc.errors, err)
89 }
90
91 if len(pc.errors) > 0 {
92 return pc.errors[0]
93 }
94 return nil
95 }
96
97 // compilePackage compiles a single Go package to a JS module file.
98 func (pc *ProgramCompiler) compilePackage(ssaPkg *ssa.Package) error {
99 pkgPath := ssaPkg.Pkg.Path()
100 if pc.compiled[pkgPath] {
101 return nil
102 }
103 pc.compiled[pkgPath] = true
104
105 if shouldSkipPackage(pkgPath) {
106 return nil
107 }
108
109 e := NewEmitter()
110
111 e.Comment("Package %s", pkgPath)
112 e.Comment("Generated by MoxieJS — Moxie JavaScript backend")
113 e.Newline()
114
115 e.ImportAll("$rt", pc.runtimeImportPath(pkgPath))
116 e.Newline()
117
118 pc.emitImports(e, ssaPkg)
119
120 // Collect and sort members.
121 var memberNames []string
122 for name := range ssaPkg.Members {
123 memberNames = append(memberNames, name)
124 }
125 sort.Slice(memberNames, func(i, j int) bool {
126 iPos := ssaPkg.Members[memberNames[i]].Pos()
127 jPos := ssaPkg.Members[memberNames[j]].Pos()
128 if iPos == jPos {
129 return memberNames[i] < memberNames[j]
130 }
131 return iPos < jPos
132 })
133
134 // Package-level variables.
135 e.Comment("Package-level variables")
136 for _, name := range memberNames {
137 if g, ok := ssaPkg.Members[name].(*ssa.Global); ok {
138 pc.emitGlobal(e, g)
139 }
140 }
141 e.Newline()
142
143 // Type registrations.
144 for _, name := range memberNames {
145 if t, ok := ssaPkg.Members[name].(*ssa.Type); ok {
146 pc.typeMapper.EmitTypeRegistration(e, t.Type())
147 }
148 }
149
150 // Functions.
151 for _, name := range memberNames {
152 if fn, ok := ssaPkg.Members[name].(*ssa.Function); ok {
153 if fn.TypeParams() != nil {
154 continue
155 }
156 pc.compileFunction(e, fn)
157 }
158 }
159
160 // Methods on named types.
161 for _, name := range memberNames {
162 if t, ok := ssaPkg.Members[name].(*ssa.Type); ok {
163 if types.IsInterface(t.Type()) {
164 continue
165 }
166 pc.compileTypeMethods(e, ssaPkg, t)
167 }
168 }
169
170 filename := PackageModuleName(pkgPath)
171 outputPath := filepath.Join(pc.config.OutputDir, filename)
172 pc.manifest = append(pc.manifest, filename)
173 return os.WriteFile(outputPath, []byte(e.String()), 0o644)
174 }
175
176 func (pc *ProgramCompiler) compileFunction(e *Emitter, fn *ssa.Function) {
177 if fn.Blocks == nil {
178 pc.emitExternalStub(e, fn)
179 return
180 }
181
182 // Bridge functions: if the package path contains "/jsbridge/",
183 // emit a pass-through to the JS runtime instead of compiling Go.
184 if pc.emitBridgeFunction(e, fn) {
185 return
186 }
187
188 if pc.config.DumpSSA {
189 fmt.Fprintf(os.Stderr, "=== %s ===\n", fn.String())
190 fn.WriteTo(os.Stderr)
191 fmt.Fprintln(os.Stderr)
192 }
193
194 fc := &FunctionCompiler{
195 pc: pc,
196 fn: fn,
197 e: e,
198 locals: make(map[ssa.Value]string),
199 varGen: 0,
200 }
201 fc.compile()
202 }
203
204 // emitBridgeFunction checks if fn belongs to a jsbridge package and emits
205 // a pass-through to the JS runtime. Returns true if handled.
206 func (pc *ProgramCompiler) emitBridgeFunction(e *Emitter, fn *ssa.Function) bool {
207 pkg := fn.Package()
208 if pkg == nil {
209 return false
210 }
211 pkgPath := pkg.Pkg.Path()
212
213 // Find the jsbridge subpackage name.
214 idx := strings.Index(pkgPath, "/jsbridge/")
215 if idx < 0 {
216 return false
217 }
218 bridgePkg := pkgPath[idx+len("/jsbridge/"):]
219 // bridgePkg is e.g. "dom", "ws", "localstorage"
220
221 goName := fn.Name()
222 // Don't bridge the init function — let it compile normally.
223 if goName == "init" {
224 return false
225 }
226 jsName := functionJsName(fn)
227 params := functionParams(fn)
228
229 // Check if function returns a value.
230 results := fn.Signature.Results()
231 hasReturn := results != nil && results.Len() > 0
232
233 e.Comment("jsbridge: %s.%s", bridgePkg, goName)
234 e.Block("export function %s(%s)", jsName, params)
235 if hasReturn {
236 e.Line("return $rt.%s.%s(%s);", bridgePkg, goName, params)
237 } else {
238 e.Line("$rt.%s.%s(%s);", bridgePkg, goName, params)
239 }
240 e.EndBlock()
241 e.Newline()
242 return true
243 }
244
245 func (pc *ProgramCompiler) compileTypeMethods(e *Emitter, ssaPkg *ssa.Package, t *ssa.Type) {
246 methods := getAllMethods(ssaPkg.Prog, t.Type())
247 methods = append(methods, getAllMethods(ssaPkg.Prog, types.NewPointer(t.Type()))...)
248
249 for _, method := range methods {
250 fn := ssaPkg.Prog.MethodValue(method)
251 if fn == nil || fn.Blocks == nil {
252 continue
253 }
254 if fn.Synthetic != "" && fn.Synthetic != "package initializer" {
255 continue
256 }
257 pc.compileFunction(e, fn)
258
259 typeID := pc.typeMapper.TypeID(t.Type())
260 jsName := methodJsName(fn)
261 e.Line("$rt.types.getType(%s)?.methods?.set(%s, %s);",
262 JsString(typeID), JsString(method.Obj().Name()), jsName)
263 }
264 }
265
266 func (pc *ProgramCompiler) emitGlobal(e *Emitter, g *ssa.Global) {
267 name := JsIdentifier(g.Name())
268 elemType := g.Type().(*types.Pointer).Elem()
269 zero := pc.typeMapper.ZeroExpr(elemType)
270 e.Line("export let %s = { $value: %s, $get() { return this.$value; }, $set(v) { this.$value = v; } };", name, zero)
271 }
272
273 func (pc *ProgramCompiler) emitImports(e *Emitter, pkg *ssa.Package) {
274 imports := make(map[string]bool)
275
276 for _, member := range pkg.Members {
277 if fn, ok := member.(*ssa.Function); ok {
278 collectImports(fn, imports)
279 }
280 }
281
282 // Also scan methods on named types.
283 for _, member := range pkg.Members {
284 if t, ok := member.(*ssa.Type); ok {
285 for _, mset := range []*types.MethodSet{
286 pkg.Prog.MethodSets.MethodSet(t.Type()),
287 pkg.Prog.MethodSets.MethodSet(types.NewPointer(t.Type())),
288 } {
289 for i := 0; i < mset.Len(); i++ {
290 fn := pkg.Prog.MethodValue(mset.At(i))
291 if fn != nil {
292 collectImports(fn, imports)
293 }
294 }
295 }
296 }
297 }
298
299 delete(imports, pkg.Pkg.Path())
300 for path := range imports {
301 if shouldSkipPackage(path) {
302 delete(imports, path)
303 }
304 }
305
306 paths := make([]string, 0, len(imports))
307 for p := range imports {
308 paths = append(paths, p)
309 }
310 sort.Strings(paths)
311
312 for _, p := range paths {
313 alias := JsIdentifier(p)
314 modFile := PackageModuleName(p)
315 e.ImportAll(alias, "./"+modFile)
316 }
317 if len(paths) > 0 {
318 e.Newline()
319 }
320 }
321
322 func (pc *ProgramCompiler) emitExternalStub(e *Emitter, fn *ssa.Function) {
323 name := functionJsName(fn)
324 params := functionParams(fn)
325
326 fullName := fn.String()
327 switch {
328 case fullName == "fmt.Println" || fullName == "(*fmt.pp).doPrintln":
329 e.Block("export function %s(%s)", name, params)
330 e.Line("$rt.runtime.println(...arguments);")
331 e.EndBlock()
332 case fullName == "fmt.Printf" || fullName == "(*fmt.pp).doPrintf":
333 e.Block("export function %s(%s)", name, params)
334 e.Line("$rt.runtime.print(...arguments);")
335 e.EndBlock()
336 case strings.HasPrefix(fullName, "runtime."):
337 e.Comment("runtime stub: %s", fullName)
338 e.Block("export function %s(%s)", name, params)
339 e.Comment("intrinsic: %s", fullName)
340 e.EndBlock()
341 default:
342 e.Comment("external: %s", fullName)
343 e.Block("export function %s(%s)", name, params)
344 e.Line("throw new Error('external function not implemented: %s');", fullName)
345 e.EndBlock()
346 }
347 e.Newline()
348 }
349
350 func (pc *ProgramCompiler) writeEntryPoint(mainPkgPath string) error {
351 e := NewEmitter()
352 e.Comment("MoxieJS Entry Point")
353 e.Comment("Generated by Moxie JavaScript backend")
354 // Build stamp ensures the entry script is byte-different on every build,
355 // so the browser detects the SW as updated even when only imports changed.
356 e.Comment(fmt.Sprintf("Build: %d", time.Now().UnixNano()))
357 e.Newline()
358
359 e.ImportAll("$rt", "./$runtime/index.mjs")
360 mainMod := PackageModuleName(mainPkgPath)
361 e.ImportAll("$main", "./"+mainMod)
362 e.Newline()
363
364 e.Block("async function $start()")
365 e.Line("if ($rt.crypto && $rt.crypto.init) await $rt.crypto.init();")
366 e.Line("if ($main.init) await $main.init();")
367 e.Line("if ($main.main) await $main.main();")
368 e.EndBlock()
369 e.Newline()
370
371 e.Line("$rt.goroutine.runMain($start);")
372
373 outputPath := filepath.Join(pc.config.OutputDir, "$entry.mjs")
374 pc.manifest = append(pc.manifest, "$entry.mjs")
375 return os.WriteFile(outputPath, []byte(e.String()), 0o644)
376 }
377
378 func (pc *ProgramCompiler) copyRuntime() error {
379 rtDir := filepath.Join(pc.config.OutputDir, "$runtime")
380 if err := os.MkdirAll(rtDir, 0o755); err != nil {
381 return err
382 }
383
384 if pc.config.RuntimeDir != "" {
385 entries, err := os.ReadDir(pc.config.RuntimeDir)
386 if err != nil {
387 return fmt.Errorf("read runtime dir: %w", err)
388 }
389 for _, entry := range entries {
390 if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".mjs") {
391 continue
392 }
393 data, err := os.ReadFile(filepath.Join(pc.config.RuntimeDir, entry.Name()))
394 if err != nil {
395 return err
396 }
397 if err := os.WriteFile(filepath.Join(rtDir, entry.Name()), data, 0o644); err != nil {
398 return err
399 }
400 pc.manifest = append(pc.manifest, "$runtime/"+entry.Name())
401 }
402 }
403 return nil
404 }
405
406 func (pc *ProgramCompiler) writeManifest() error {
407 sort.Strings(pc.manifest)
408 // JS module version.
409 var b strings.Builder
410 b.WriteString("// MoxieJS Manifest — all output files.\n")
411 b.WriteString("// Generated by moxiejs. Do not edit.\n\n")
412 b.WriteString("export const files = [\n")
413 for _, f := range pc.manifest {
414 b.WriteString(" ")
415 b.WriteString(JsString(f))
416 b.WriteString(",\n")
417 }
418 b.WriteString("];\n")
419 if err := os.WriteFile(filepath.Join(pc.config.OutputDir, "$manifest.mjs"), []byte(b.String()), 0o644); err != nil {
420 return err
421 }
422 // JSON version for runtime consumption.
423 data, err := json.Marshal(pc.manifest)
424 if err != nil {
425 return err
426 }
427 return os.WriteFile(filepath.Join(pc.config.OutputDir, "$manifest.json"), data, 0o644)
428 }
429
430 func (pc *ProgramCompiler) runtimeImportPath(pkgPath string) string {
431 return "./$runtime/index.mjs"
432 }
433
434 // computeAsyncFuncs determines which functions need to be async.
435 // A function is async if it directly contains channel/select/go operations,
436 // or if it calls any function that is async (transitive closure).
437 func (pc *ProgramCompiler) computeAsyncFuncs(packages []PackageInfo) {
438 // Phase 1: find direct async roots and build call graph.
439 callers := make(map[*ssa.Function][]*ssa.Function) // callee -> list of callers
440 var allFuncs []*ssa.Function
441
442 for _, pkgInfo := range packages {
443 ssaPkg := pc.program.Package(pkgInfo.TypePkg)
444 if ssaPkg == nil {
445 continue
446 }
447 for _, mem := range ssaPkg.Members {
448 fn, ok := mem.(*ssa.Function)
449 if ok {
450 allFuncs = append(allFuncs, fn)
451 // Also collect anonymous functions.
452 allFuncs = append(allFuncs, fn.AnonFuncs...)
453 }
454 if t, ok := mem.(*ssa.Type); ok {
455 methods := getAllMethods(ssaPkg.Prog, t.Type())
456 methods = append(methods, getAllMethods(ssaPkg.Prog, types.NewPointer(t.Type()))...)
457 for _, m := range methods {
458 mfn := ssaPkg.Prog.MethodValue(m)
459 if mfn != nil && mfn.Blocks != nil {
460 allFuncs = append(allFuncs, mfn)
461 allFuncs = append(allFuncs, mfn.AnonFuncs...)
462 }
463 }
464 }
465 }
466 }
467
468 // Mark direct async roots.
469 for _, fn := range allFuncs {
470 if isDirectAsync(fn) {
471 pc.asyncFuncs[fn] = true
472 }
473 }
474
475 // Build caller graph.
476 for _, fn := range allFuncs {
477 if fn.Blocks == nil {
478 continue
479 }
480 for _, block := range fn.Blocks {
481 for _, instr := range block.Instrs {
482 if call, ok := instr.(*ssa.Call); ok {
483 if callee := call.Common().StaticCallee(); callee != nil {
484 callers[callee] = append(callers[callee], fn)
485 }
486 }
487 }
488 }
489 }
490
491 // Phase 2: propagate — if a callee is async, all callers become async.
492 changed := true
493 for changed {
494 changed = false
495 for fn := range pc.asyncFuncs {
496 for _, caller := range callers[fn] {
497 if !pc.asyncFuncs[caller] {
498 pc.asyncFuncs[caller] = true
499 changed = true
500 }
501 }
502 }
503 }
504 }
505
506 // isDirectAsync checks if a function directly contains channel/select/go ops.
507 func isDirectAsync(fn *ssa.Function) bool {
508 if fn.Blocks == nil {
509 return false
510 }
511 for _, block := range fn.Blocks {
512 for _, instr := range block.Instrs {
513 switch instr.(type) {
514 case *ssa.Send, *ssa.Select, *ssa.Go:
515 return true
516 }
517 if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.ARROW {
518 return true
519 }
520 }
521 }
522 return false
523 }
524
525 // --- Helpers ---
526
527 func shouldSkipPackage(path string) bool {
528 skip := []string{
529 "runtime",
530 "runtime/internal",
531 "internal/task",
532 "internal/abi",
533 "internal/reflectlite",
534 "unsafe",
535 "internal/goarch",
536 "internal/goos",
537 }
538 for _, s := range skip {
539 if path == s || strings.HasPrefix(path, s+"/") {
540 return true
541 }
542 }
543 return false
544 }
545
546 func functionJsName(fn *ssa.Function) string {
547 name := fn.Name()
548 if fn.Signature.Recv() != nil {
549 recv := fn.Signature.Recv().Type()
550 if ptr, ok := recv.(*types.Pointer); ok {
551 recv = ptr.Elem()
552 }
553 typeName := "unknown"
554 if named, ok := recv.(*types.Named); ok {
555 typeName = named.Obj().Name()
556 }
557 name = typeName + "$" + name
558 }
559 return JsIdentifier(name)
560 }
561
562 func methodJsName(fn *ssa.Function) string {
563 return functionJsName(fn)
564 }
565
566 func functionParams(fn *ssa.Function) string {
567 var params []string
568 // Free variables come first (bound via .bind() in MakeClosure).
569 for _, fv := range fn.FreeVars {
570 params = append(params, JsIdentifier(fv.Name()))
571 }
572 for _, p := range fn.Params {
573 params = append(params, JsIdentifier(p.Name()))
574 }
575 return strings.Join(params, ", ")
576 }
577
578 func collectImports(fn *ssa.Function, imports map[string]bool) {
579 if fn.Blocks == nil {
580 return
581 }
582 for _, block := range fn.Blocks {
583 for _, instr := range block.Instrs {
584 if call, ok := instr.(*ssa.Call); ok {
585 if callee := call.Common().StaticCallee(); callee != nil {
586 if pkg := callee.Package(); pkg != nil {
587 imports[pkg.Pkg.Path()] = true
588 }
589 }
590 }
591 }
592 }
593 // Recurse into anonymous functions (closures).
594 for _, anon := range fn.AnonFuncs {
595 collectImports(anon, imports)
596 }
597 }
598
599 func getAllMethods(prog *ssa.Program, t types.Type) []*types.Selection {
600 mset := prog.MethodSets.MethodSet(t)
601 var methods []*types.Selection
602 for i := 0; i < mset.Len(); i++ {
603 methods = append(methods, mset.At(i))
604 }
605 return methods
606 }
607