package main import ( "bytes" "flag" "fmt" "go/parser" "go/printer" "go/token" "os" "path/filepath" "strings" ) func main() { outDir := flag.String("o", "./mx-out", "output directory") flag.Parse() args := flag.Args() if len(args) == 0 { fmt.Fprintf(os.Stderr, "usage: mx-transpile [-o outdir] ./path/to/package/\n") os.Exit(1) } srcDir := args[0] entries, err := os.ReadDir(srcDir) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } var goFiles []string for _, e := range entries { name := e.Name() if !e.IsDir() && strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") { goFiles = append(goFiles, name) } } if len(goFiles) == 0 { fmt.Fprintf(os.Stderr, "no .go files in %s\n", srcDir) os.Exit(1) } os.MkdirAll(*outDir, 0755) var processed, todos, failed int for _, name := range goFiles { fset := token.NewFileSet() f, err := parser.ParseFile(fset, filepath.Join(srcDir, name), nil, parser.ParseComments) if err != nil { fmt.Fprintf(os.Stderr, "parse error: %s: %v\n", name, err) failed++ continue } n := transformAST(fset, f) var buf bytes.Buffer printer.Fprint(&buf, fset, f) out, m := postProcess(buf.String()) n += m mxName := strings.TrimSuffix(name, ".go") + ".mx" outPath := filepath.Join(*outDir, mxName) header := "// transpiled from Go by mx-transpile - review TODOs before use\n\n" if err := os.WriteFile(outPath, []byte(header+out), 0644); err != nil { fmt.Fprintf(os.Stderr, "write error: %s: %v\n", outPath, err) failed++ continue } processed++ todos += n fmt.Printf(" %s -> %s", name, mxName) if n > 0 { fmt.Printf(" (%d TODOs)", n) } fmt.Println() } fmt.Printf("\n%d files processed, %d TODOs generated, %d files failed\n", processed, todos, failed) } func postProcess(src string) (string, int) { todos := 0 lines := strings.Split(src, "\n") var out []string // remove empty import blocks for i := 0; i < len(lines); i++ { t := strings.TrimSpace(lines[i]) if t == "import ()" || t == "import (\n)" { continue } if t == "import (" && i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == ")" { i++ continue } out = append(out, lines[i]) } lines = out out = nil for _, line := range lines { trimmed := strings.TrimLeft(line, " \t") // strip build tags if strings.HasPrefix(trimmed, "//go:build ") || strings.HasPrefix(trimmed, "// +build ") { continue } // go -> spawn if strings.HasPrefix(trimmed, "go ") && !strings.HasPrefix(trimmed, "goto ") { idx := strings.Index(line, "go ") line = line[:idx] + "spawn " + line[idx+3:] } // comment out reflect references if strings.Contains(line, "reflect.") && !strings.HasPrefix(trimmed, "//") { indent := line[:len(line)-len(trimmed)] out = append(out, indent+"// TODO: reflect removed: "+trimmed) todos++ continue } // fix trailing commas from zero-position type nodes line = strings.ReplaceAll(line, ",)", ")") // flag fmt import t := strings.TrimSpace(line) if t == `"fmt"` || t == `import "fmt"` { line = strings.TrimRight(line, " \t") + " // TODO: verify fmt compatibility with Moxie" todos++ } // flag unsafe import if t == `"unsafe"` || t == `import "unsafe"` { line = strings.TrimRight(line, " \t") + " // TODO: unsafe needs review for Moxie" todos++ } out = append(out, line) } return strings.Join(out, "\n"), todos }