main.go raw

   1  // moxiejs compiles Moxie source to JavaScript ES modules.
   2  //
   3  // Usage:
   4  //
   5  //	moxiejs [-o outputdir] [-runtime runtimedir] [-dump-ssa] <package>
   6  package main
   7  
   8  import (
   9  	"flag"
  10  	"fmt"
  11  	"go/ast"
  12  	"go/importer"
  13  	"go/parser"
  14  	"go/token"
  15  	"go/types"
  16  	"os"
  17  	"path/filepath"
  18  	"runtime"
  19  
  20  	"moxie/jsbackend"
  21  	"golang.org/x/tools/go/ssa"
  22  )
  23  
  24  // readAndRewrite reads a source file and applies moxie literal rewrites.
  25  // For .mx files, it rewrites moxie-specific syntax (slice/chan literals)
  26  // to standard Go before parsing. For .go files, returns content as-is.
  27  func readAndRewrite(path string) ([]byte, error) {
  28  	// discover.go maps .go filenames to .mx on disk
  29  	actual := path
  30  	if filepath.Ext(path) == ".go" {
  31  		mxPath := path[:len(path)-3] + ".mx"
  32  		if _, err := os.Stat(mxPath); err == nil {
  33  			actual = mxPath
  34  		}
  35  	}
  36  	src, err := os.ReadFile(actual)
  37  	if err != nil {
  38  		return nil, err
  39  	}
  40  	if filepath.Ext(actual) == ".mx" {
  41  		src = rewriteMoxieLiterals(src)
  42  	}
  43  	return src, nil
  44  }
  45  
  46  func main() {
  47  	outputDir := flag.String("o", "jsout", "output directory for .mjs files")
  48  	runtimeDir := flag.String("runtime", "", "path to jsruntime/ directory (if empty, must be copied manually)")
  49  	dumpSSA := flag.Bool("dump-ssa", false, "dump SSA for debugging")
  50  	flag.Parse()
  51  
  52  	args := flag.Args()
  53  	if len(args) == 0 {
  54  		fmt.Fprintln(os.Stderr, "usage: moxiejs [-o dir] <file.mx or package path>")
  55  		os.Exit(1)
  56  	}
  57  
  58  	input := args[0]
  59  
  60  	if filepath.Ext(input) == ".mx" {
  61  		if err := compileFile(input, *outputDir, *runtimeDir, *dumpSSA); err != nil {
  62  			fmt.Fprintf(os.Stderr, "error: %v\n", err)
  63  			os.Exit(1)
  64  		}
  65  	} else {
  66  		if err := compilePackage(input, *outputDir, *runtimeDir, *dumpSSA); err != nil {
  67  			fmt.Fprintf(os.Stderr, "error: %v\n", err)
  68  			os.Exit(1)
  69  		}
  70  	}
  71  }
  72  
  73  // compileFile compiles a single .mx file to JavaScript.
  74  func compileFile(filename string, outputDir, runtimeDir string, dumpSSA bool) error {
  75  	fset := token.NewFileSet()
  76  
  77  	src, err := os.ReadFile(filename)
  78  	if err != nil {
  79  		return fmt.Errorf("read %s: %w", filename, err)
  80  	}
  81  	src = rewriteMoxieLiterals(src)
  82  
  83  	file, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
  84  	if err != nil {
  85  		return fmt.Errorf("parse: %w", err)
  86  	}
  87  
  88  	conf := types.Config{
  89  		Importer: importer.Default(),
  90  	}
  91  	info := &types.Info{
  92  		Types:      make(map[ast.Expr]types.TypeAndValue),
  93  		Defs:       make(map[*ast.Ident]types.Object),
  94  		Uses:       make(map[*ast.Ident]types.Object),
  95  		Instances:  make(map[*ast.Ident]types.Instance),
  96  		Implicits:  make(map[ast.Node]types.Object),
  97  		Scopes:     make(map[ast.Node]*types.Scope),
  98  		Selections: make(map[*ast.SelectorExpr]*types.Selection),
  99  	}
 100  
 101  	pkg, err := conf.Check("main", fset, []*ast.File{file}, info)
 102  	if err != nil {
 103  		return fmt.Errorf("typecheck: %w", err)
 104  	}
 105  
 106  	ssaProg := ssa.NewProgram(fset, ssa.BareInits|ssa.InstantiateGenerics)
 107  	ssaProg.CreatePackage(pkg, []*ast.File{file}, info, true)
 108  
 109  	packages := []jsbackend.PackageInfo{
 110  		{Path: pkg.Path(), TypePkg: pkg},
 111  	}
 112  
 113  	config := &jsbackend.Config{
 114  		OutputDir:  outputDir,
 115  		RuntimeDir: runtimeDir,
 116  		DumpSSA:    dumpSSA,
 117  	}
 118  
 119  	return jsbackend.CompileProgram(config, ssaProg, packages, "main")
 120  }
 121  
 122  // compilePackage compiles a Moxie package and its dependencies to JavaScript.
 123  // Uses moxie-native package discovery (no go list, supports moxie.mod and .mx files).
 124  func compilePackage(pattern string, outputDir, runtimeDir string, dumpSSA bool) error {
 125  	wd, err := os.Getwd()
 126  	if err != nil {
 127  		return err
 128  	}
 129  
 130  	goroot := runtime.GOROOT()
 131  
 132  	pkgJSONs, err := discoverPackages(goroot, wd, pattern)
 133  	if err != nil {
 134  		return fmt.Errorf("discover: %w", err)
 135  	}
 136  	if len(pkgJSONs) == 0 {
 137  		return fmt.Errorf("no packages found for %q", pattern)
 138  	}
 139  
 140  	fset := token.NewFileSet()
 141  	ssaProg := ssa.NewProgram(fset, ssa.BareInits|ssa.InstantiateGenerics)
 142  
 143  	typePkgs := make(map[string]*types.Package)
 144  	var allPkgs []jsbackend.PackageInfo
 145  
 146  	for _, pj := range pkgJSONs {
 147  		if pj.ImportPath == "unsafe" {
 148  			typePkgs["unsafe"] = types.Unsafe
 149  			ssaProg.CreatePackage(types.Unsafe, nil, nil, true)
 150  			allPkgs = append(allPkgs, jsbackend.PackageInfo{
 151  				Path:    "unsafe",
 152  				TypePkg: types.Unsafe,
 153  			})
 154  			continue
 155  		}
 156  
 157  		var files []*ast.File
 158  		for _, f := range pj.GoFiles {
 159  			path := filepath.Join(pj.Dir, f)
 160  			src, readErr := readAndRewrite(path)
 161  			if readErr != nil {
 162  				return fmt.Errorf("read %s: %w", path, readErr)
 163  			}
 164  			file, parseErr := parser.ParseFile(fset, path, src, parser.ParseComments)
 165  			if parseErr != nil {
 166  				return fmt.Errorf("parse %s: %w", path, parseErr)
 167  			}
 168  			files = append(files, file)
 169  		}
 170  
 171  		conf := types.Config{
 172  			Importer: &mapImporter{pkgs: typePkgs},
 173  		}
 174  		info := &types.Info{
 175  			Types:      make(map[ast.Expr]types.TypeAndValue),
 176  			Defs:       make(map[*ast.Ident]types.Object),
 177  			Uses:       make(map[*ast.Ident]types.Object),
 178  			Instances:  make(map[*ast.Ident]types.Instance),
 179  			Implicits:  make(map[ast.Node]types.Object),
 180  			Scopes:     make(map[ast.Node]*types.Scope),
 181  			Selections: make(map[*ast.SelectorExpr]*types.Selection),
 182  		}
 183  
 184  		typePkg, checkErr := conf.Check(pj.ImportPath, fset, files, info)
 185  		if checkErr != nil {
 186  			return fmt.Errorf("typecheck %s: %w", pj.ImportPath, checkErr)
 187  		}
 188  		typePkgs[pj.ImportPath] = typePkg
 189  
 190  		ssaProg.CreatePackage(typePkg, files, info, pj.ImportPath == pattern || pj.Name == "main")
 191  
 192  		allPkgs = append(allPkgs, jsbackend.PackageInfo{
 193  			Path:    pj.ImportPath,
 194  			TypePkg: typePkg,
 195  		})
 196  	}
 197  
 198  	mainPkgPath := ""
 199  	last := pkgJSONs[len(pkgJSONs)-1]
 200  	if last.Name == "main" {
 201  		mainPkgPath = last.ImportPath
 202  	}
 203  
 204  	config := &jsbackend.Config{
 205  		OutputDir:  outputDir,
 206  		RuntimeDir: runtimeDir,
 207  		DumpSSA:    dumpSSA,
 208  	}
 209  
 210  	return jsbackend.CompileProgram(config, ssaProg, allPkgs, mainPkgPath)
 211  }
 212  
 213  // mapImporter resolves imports from already type-checked packages.
 214  type mapImporter struct {
 215  	pkgs map[string]*types.Package
 216  }
 217  
 218  func (m *mapImporter) Import(path string) (*types.Package, error) {
 219  	if path == "unsafe" {
 220  		return types.Unsafe, nil
 221  	}
 222  	if pkg, ok := m.pkgs[path]; ok {
 223  		return pkg, nil
 224  	}
 225  	return nil, fmt.Errorf("package not imported: %s", path)
 226  }
 227