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