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