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