main.go raw
1 // mxcheck validates the native Moxie type checker (B1) against real packages.
2 // It loads a package through the moxie loader (same pipeline as the compiler),
3 // runs the native checker on the syntax files, and diffs against go/types.
4 //
5 // Usage: mxcheck [-compare] <import-path>
6 package main
7
8 import (
9 "flag"
10 "fmt"
11 "go/types"
12 "os"
13 "sort"
14 "strings"
15
16 "moxie/compileopts"
17 "moxie/goenv"
18 "moxie/loader"
19 "moxie/syntax"
20 "moxie/typecheck"
21 "moxie/typecheck/bridge"
22 "runtime"
23 )
24
25 var flagCompare = flag.Bool("compare", false, "compare native errors against go/types")
26
27 func main() {
28 flag.Parse()
29 if flag.NArg() == 0 {
30 fmt.Fprintln(os.Stderr, "usage: mxcheck [-compare] <import-path|.>")
31 os.Exit(1)
32 }
33 target := flag.Arg(0)
34
35 // Load via the moxie loader so we get the full package graph,
36 // including rewrites (chan literals, slice literals, string literals).
37 opts := &compileopts.Options{
38 GOOS: "linux",
39 GOARCH: "amd64",
40 }
41 cfg, err := newConfig(opts)
42 if err != nil {
43 fatalf("config: %v", err)
44 }
45
46 var goTypeErrors []string
47 typeCfg := types.Config{
48 Error: func(e error) {
49 goTypeErrors = append(goTypeErrors, normaliseError(e.Error()))
50 },
51 }
52
53 prog, err := loader.Load(cfg, target, typeCfg)
54 if err != nil {
55 fatalf("load: %v", err)
56 }
57 prog.EnableSyntaxFiles()
58 if err := prog.Parse(); err != nil {
59 // The loader enforces that the target package is named "main" for
60 // compilation. For library packages (fmt, bytes, moxie/typecheck, etc.)
61 // this check fails, but all dependencies are still parsed and
62 // type-checked. We ignore this specific error so mxcheck can operate
63 // on library packages.
64 if !isMainNameError(err.Error()) {
65 fatalf("parse: %v", err)
66 }
67 }
68
69 mainPkg := prog.MainPkg()
70
71 // Build bridge importer from the full package graph.
72 pkgMap := make(map[string]*types.Package, len(prog.Packages))
73 for path, p := range prog.Packages {
74 if p.Pkg != nil {
75 pkgMap[path] = p.Pkg
76 }
77 }
78 imp := bridge.New(pkgMap)
79
80 // Run the native checker on the main package's syntax files.
81 syntaxFiles := mainPkg.SyntaxFiles
82 if len(syntaxFiles) == 0 {
83 fmt.Println("no syntax files for", mainPkg.ImportPath)
84 os.Exit(1)
85 }
86
87 _, _, nativeErr := typecheck.Check(mainPkg.ImportPath, syntaxFiles, imp)
88
89 if *flagCompare {
90 sort.Strings(goTypeErrors)
91 var nativeErrs []string
92 if nativeErr != nil {
93 nativeErrs = append(nativeErrs, normaliseError(nativeErr.Error()))
94 }
95 sort.Strings(nativeErrs)
96 compare(goTypeErrors, nativeErrs, mainPkg.ImportPath)
97 return
98 }
99
100 if nativeErr != nil {
101 fmt.Fprintln(os.Stderr, nativeErr)
102 os.Exit(1)
103 }
104 fmt.Printf("ok\t%s\t(%d files)\n", mainPkg.ImportPath, len(syntaxFiles))
105 }
106
107 func compare(goErrs, nativeErrs []string, pkgPath string) {
108 fmt.Printf("%-40s go/types: %d native: %d\n", pkgPath, len(goErrs), len(nativeErrs))
109
110 onlyGo := setDiff(goErrs, nativeErrs)
111 onlyNative := setDiff(nativeErrs, goErrs)
112
113 if len(onlyGo) == 0 && len(onlyNative) == 0 {
114 fmt.Println("PASS")
115 return
116 }
117 for _, e := range onlyGo {
118 fmt.Println(" - go/types:", e)
119 }
120 for _, e := range onlyNative {
121 fmt.Println(" + native: ", e)
122 }
123 os.Exit(1)
124 }
125
126 func setDiff(a, b []string) []string {
127 set := make(map[string]bool, len(b))
128 for _, s := range b {
129 set[s] = true
130 }
131 var out []string
132 for _, s := range a {
133 if !set[s] {
134 out = append(out, s)
135 }
136 }
137 return out
138 }
139
140 func normaliseError(e string) string {
141 if i := strings.Index(e, ": "); i >= 0 {
142 return strings.TrimSpace(e[i+2:])
143 }
144 return strings.TrimSpace(e)
145 }
146
147 func fatalf(format string, args ...interface{}) {
148 fmt.Fprintf(os.Stderr, "mxcheck: "+format+"\n", args...)
149 os.Exit(1)
150 }
151
152 // newConfig builds a compileopts.Config without importing moxie/builder
153 // (which would drag in LLVM). Mirrors builder.NewConfig exactly.
154 func newConfig(options *compileopts.Options) (*compileopts.Config, error) {
155 spec, err := compileopts.LoadTarget(options)
156 if err != nil {
157 return nil, err
158 }
159 _, gorootMinor, err := goenv.GetGorootVersion()
160 if err != nil {
161 return nil, err
162 }
163 _, buildMinor, _, err := goenv.Parse(runtime.Version())
164 if err != nil {
165 return nil, err
166 }
167 if buildMinor < gorootMinor {
168 return nil, fmt.Errorf("go toolchain mismatch: built with %s, running %d.%d",
169 runtime.Version(), 1, gorootMinor)
170 }
171 return &compileopts.Config{
172 Options: options,
173 Target: spec,
174 GoMinorVersion: gorootMinor,
175 TestConfig: options.TestConfig,
176 }, nil
177 }
178
179 func isMainNameError(msg string) bool {
180 return strings.Contains(msg, "expected main package to have name")
181 }
182
183 // Keep syntax imported for the SyntaxFiles type.
184 var _ []*syntax.File
185