diff --git a/.circleci/config.yml b/.circleci/config.yml index 498861027..9f020f1bc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,7 +85,7 @@ commands: key: wasi-libc-sysroot-systemclang-v1 paths: - lib/wasi-libc/sysroot - - run: go test -v -tags=llvm<> ./cgo ./compileopts ./interp ./transform . + - run: go test -v -tags=llvm<> ./cgo ./compileopts ./compiler ./interp ./transform . - run: make gen-device -j4 - run: make smoketest - save_cache: diff --git a/Makefile b/Makefile index bacd9b5d3..351270c8f 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ endif clean: @rm -rf build -FMT_PATHS = ./*.go builder cgo compiler interp ir loader src/device/arm src/examples src/machine src/os src/reflect src/runtime src/sync src/syscall src/internal/reflectlite transform +FMT_PATHS = ./*.go builder cgo compiler compiler/testdata interp ir loader src/device/arm src/examples src/machine src/os src/reflect src/runtime src/sync src/syscall src/internal/reflectlite transform fmt: @gofmt -l -w $(FMT_PATHS) fmt-check: @@ -174,7 +174,7 @@ tinygo: CGO_CPPFLAGS="$(CGO_CPPFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" $(GO) build -o build/tinygo$(EXE) -tags byollvm . test: wasi-libc - CGO_CPPFLAGS="$(CGO_CPPFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" $(GO) test -v -tags byollvm ./cgo ./compileopts ./interp ./transform . + CGO_CPPFLAGS="$(CGO_CPPFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" $(GO) test -v -tags byollvm ./cgo ./compileopts ./compiler ./interp ./transform . tinygo-test: cd tests/tinygotest && tinygo test diff --git a/compiler/compiler.go b/compiler/compiler.go index cd7c16c3a..18c5d5c40 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -98,15 +98,9 @@ func NewTargetMachine(config *compileopts.Config) (llvm.TargetMachine, error) { return machine, nil } -// Compile the given package path or .go file path. Return an error when this -// fails (in any stage). If successful it returns the LLVM module and a list of -// extra C files to be compiled. If not, one or more errors will be returned. -// -// The fact that it returns a list of filenames to compile is a layering -// violation. Eventually, this Compile function should only compile a single -// package and not the whole program, and loading of the program (including CGo -// processing) should be moved outside the compiler package. -func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Config) (llvm.Module, []string, []error) { +// newCompilerContext builds a new *compilerContext based on the provided +// configuration, ready to compile Go SSA to LLVM IR. +func newCompilerContext(pkgName string, machine llvm.TargetMachine, config *compileopts.Config) *compilerContext { c := &compilerContext{ Config: config, difiles: make(map[string]llvm.Metadata), @@ -140,6 +134,20 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con c.funcPtrAddrSpace = dummyFunc.Type().PointerAddressSpace() dummyFunc.EraseFromParentAsFunction() + return c +} + +// Compile the given package path or .go file path. Return an error when this +// fails (in any stage). If successful it returns the LLVM module and a list of +// extra C files to be compiled. If not, one or more errors will be returned. +// +// The fact that it returns a list of filenames to compile is a layering +// violation. Eventually, this Compile function should only compile a single +// package and not the whole program, and loading of the program (including CGo +// processing) should be moved outside the compiler package. +func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Config) (llvm.Module, []string, []error) { + c := newCompilerContext(pkgName, machine, config) + // Prefix the GOPATH with the system GOROOT, as GOROOT is already set to // the TinyGo root. overlayGopath := goenv.Get("GOPATH") diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go new file mode 100644 index 000000000..1607f624e --- /dev/null +++ b/compiler/compiler_test.go @@ -0,0 +1,136 @@ +package compiler + +import ( + "bytes" + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "go/types" + "io/ioutil" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/tinygo-org/tinygo/compileopts" + "github.com/tinygo-org/tinygo/compiler/ircheck" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" + "tinygo.org/x/go-llvm" +) + +var flagUpdate = flag.Bool("update", false, "update all tests") + +func TestCompiler(t *testing.T) { + t.Parallel() + for _, name := range []string{"basic"} { + t.Run(name, func(t *testing.T) { + runCompilerTest(t, name) + }) + } +} + +func runCompilerTest(t *testing.T, name string) { + // Read the AST in memory. + path := filepath.Join("testdata", name+".go") + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, path, nil, parser.ParseComments) + if err != nil { + t.Fatal("could not parse Go source file:", err) + } + files := []*ast.File{f} + + // Create Go SSA from the AST. + var typecheckErrors []error + var typecheckErrorsLock sync.Mutex + typesConfig := types.Config{ + Error: func(err error) { + typecheckErrorsLock.Lock() + defer typecheckErrorsLock.Unlock() + typecheckErrors = append(typecheckErrors, err) + }, + Importer: simpleImporter{}, + Sizes: types.SizesFor("gccgo", "arm"), + } + pkg, _, err := ssautil.BuildPackage(&typesConfig, fset, types.NewPackage("main", ""), files, ssa.SanityCheckFunctions|ssa.BareInits|ssa.GlobalDebug) + for _, err := range typecheckErrors { + t.Error(err) + } + if err != nil && len(typecheckErrors) == 0 { + // Only report errors when no type errors are found (an + // unexpected condition). + t.Error(err) + } + if t.Failed() { + return + } + + // Configure the compiler. + config := compileopts.Config{ + Options: &compileopts.Options{}, + Target: &compileopts.TargetSpec{ + Triple: "armv7m-none-eabi", + BuildTags: []string{"cortexm", "baremetal", "linux", "arm"}, + Scheduler: "tasks", + }, + } + machine, err := NewTargetMachine(&config) + if err != nil { + t.Fatal(err) + } + c := newCompilerContext("main", machine, &config) + irbuilder := c.ctx.NewBuilder() + defer irbuilder.Dispose() + + // Create LLVM IR from the Go SSA. + c.createPackage(pkg, irbuilder) + + // Check the IR with the LLVM verifier. + if err := llvm.VerifyModule(c.mod, llvm.PrintMessageAction); err != nil { + t.Error("verification error after IR construction") + } + + // Check the IR with our own verifier (which checks for different things). + errs := ircheck.Module(c.mod) + for _, err := range errs { + t.Error(err) + } + + // Check whether the IR matches the expected IR. + ir := c.mod.String() + ir = ir[strings.Index(ir, "\ntarget datalayout = ")+1:] + outfile := filepath.Join("testdata", name+".ll") + if *flagUpdate { + err := ioutil.WriteFile(outfile, []byte(ir), 0666) + if err != nil { + t.Error("could not read output file:", err) + } + } else { + ir2, err := ioutil.ReadFile(outfile) + if err != nil { + t.Fatal("could not read input file:", err) + } + ir2 = bytes.Replace(ir2, []byte("\r\n"), []byte("\n"), -1) + if ir != string(ir2) { + t.Error("output did not match") + } + } +} + +// simpleImporter implements the types.Importer interface, but only allows +// importing the unsafe package. +type simpleImporter struct { +} + +// Import implements the Importer interface. For testing usage only: it only +// supports importing the unsafe package. +func (i simpleImporter) Import(path string) (*types.Package, error) { + switch path { + case "unsafe": + return types.Unsafe, nil + default: + return nil, fmt.Errorf("importer not implemented for package %s", path) + } +} diff --git a/compiler/testdata/basic.go b/compiler/testdata/basic.go new file mode 100644 index 000000000..bcd5a28ed --- /dev/null +++ b/compiler/testdata/basic.go @@ -0,0 +1,5 @@ +package main + +func add(x, y int) int { + return x + y +} diff --git a/compiler/testdata/basic.ll b/compiler/testdata/basic.ll new file mode 100644 index 000000000..c9c9b9e27 --- /dev/null +++ b/compiler/testdata/basic.ll @@ -0,0 +1,13 @@ +target datalayout = "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64" +target triple = "armv7m-none-eabi" + +define internal i32 @main.add(i32 %x, i32 %y, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %0 = add i32 %x, %y + ret i32 %0 +} + +define internal void @main.init(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + ret void +}