main.go raw
1 package main
2
3 import (
4 "bytes"
5 "flag"
6 "fmt"
7 "go/parser"
8 "go/printer"
9 "go/token"
10 "os"
11 "path/filepath"
12 "strings"
13 )
14
15 func main() {
16 outDir := flag.String("o", "./mx-out", "output directory")
17 flag.Parse()
18
19 args := flag.Args()
20 if len(args) == 0 {
21 fmt.Fprintf(os.Stderr, "usage: mx-transpile [-o outdir] ./path/to/package/\n")
22 os.Exit(1)
23 }
24
25 srcDir := args[0]
26 entries, err := os.ReadDir(srcDir)
27 if err != nil {
28 fmt.Fprintf(os.Stderr, "error: %v\n", err)
29 os.Exit(1)
30 }
31
32 var goFiles []string
33 for _, e := range entries {
34 name := e.Name()
35 if !e.IsDir() && strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") {
36 goFiles = append(goFiles, name)
37 }
38 }
39 if len(goFiles) == 0 {
40 fmt.Fprintf(os.Stderr, "no .go files in %s\n", srcDir)
41 os.Exit(1)
42 }
43
44 os.MkdirAll(*outDir, 0755)
45
46 var processed, todos, failed int
47
48 for _, name := range goFiles {
49 fset := token.NewFileSet()
50 f, err := parser.ParseFile(fset, filepath.Join(srcDir, name), nil, parser.ParseComments)
51 if err != nil {
52 fmt.Fprintf(os.Stderr, "parse error: %s: %v\n", name, err)
53 failed++
54 continue
55 }
56
57 n := transformAST(fset, f)
58
59 var buf bytes.Buffer
60 printer.Fprint(&buf, fset, f)
61
62 out, m := postProcess(buf.String())
63 n += m
64
65 mxName := strings.TrimSuffix(name, ".go") + ".mx"
66 outPath := filepath.Join(*outDir, mxName)
67
68 header := "// transpiled from Go by mx-transpile - review TODOs before use\n\n"
69 if err := os.WriteFile(outPath, []byte(header+out), 0644); err != nil {
70 fmt.Fprintf(os.Stderr, "write error: %s: %v\n", outPath, err)
71 failed++
72 continue
73 }
74
75 processed++
76 todos += n
77 fmt.Printf(" %s -> %s", name, mxName)
78 if n > 0 {
79 fmt.Printf(" (%d TODOs)", n)
80 }
81 fmt.Println()
82 }
83
84 fmt.Printf("\n%d files processed, %d TODOs generated, %d files failed\n",
85 processed, todos, failed)
86 }
87
88 func postProcess(src string) (string, int) {
89 todos := 0
90 lines := strings.Split(src, "\n")
91 var out []string
92
93 // remove empty import blocks
94 for i := 0; i < len(lines); i++ {
95 t := strings.TrimSpace(lines[i])
96 if t == "import ()" || t == "import (\n)" {
97 continue
98 }
99 if t == "import (" && i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == ")" {
100 i++
101 continue
102 }
103 out = append(out, lines[i])
104 }
105 lines = out
106 out = nil
107
108 for _, line := range lines {
109 trimmed := strings.TrimLeft(line, " \t")
110
111 // strip build tags
112 if strings.HasPrefix(trimmed, "//go:build ") || strings.HasPrefix(trimmed, "// +build ") {
113 continue
114 }
115
116 // go -> spawn
117 if strings.HasPrefix(trimmed, "go ") && !strings.HasPrefix(trimmed, "goto ") {
118 idx := strings.Index(line, "go ")
119 line = line[:idx] + "spawn " + line[idx+3:]
120 }
121
122 // comment out reflect references
123 if strings.Contains(line, "reflect.") && !strings.HasPrefix(trimmed, "//") {
124 indent := line[:len(line)-len(trimmed)]
125 out = append(out, indent+"// TODO: reflect removed: "+trimmed)
126 todos++
127 continue
128 }
129
130 // fix trailing commas from zero-position type nodes
131 line = strings.ReplaceAll(line, ",)", ")")
132
133 // flag fmt import
134 t := strings.TrimSpace(line)
135 if t == `"fmt"` || t == `import "fmt"` {
136 line = strings.TrimRight(line, " \t") +
137 " // TODO: verify fmt compatibility with Moxie"
138 todos++
139 }
140
141 // flag unsafe import
142 if t == `"unsafe"` || t == `import "unsafe"` {
143 line = strings.TrimRight(line, " \t") +
144 " // TODO: unsafe needs review for Moxie"
145 todos++
146 }
147
148 out = append(out, line)
149 }
150
151 return strings.Join(out, "\n"), todos
152 }
153