aboutsummaryrefslogtreecommitdiffhomepage
path: root/compiler/compiler_test.go
diff options
context:
space:
mode:
authorAyke van Laethem <[email protected]>2021-01-13 17:22:13 +0100
committerRon Evans <[email protected]>2021-01-15 14:43:43 +0100
commita848d720db6e533ea8b014e9bcc76e5c9c73f9b5 (patch)
tree80d73791eda6a8d3cf6640de305e3bea9310884a /compiler/compiler_test.go
parentdbc438b2ae23414241b693b03697410248b44768 (diff)
downloadtinygo-a848d720db6e533ea8b014e9bcc76e5c9c73f9b5.tar.gz
tinygo-a848d720db6e533ea8b014e9bcc76e5c9c73f9b5.zip
compiler: refactor and add tests
This commit finally introduces unit tests for the compiler, to check whether input Go code is converted to the expected output IR. To make this necessary, a few refactors were needed. Hopefully these refactors (to compile a program package by package instead of all at once) will eventually become standard, so that packages can all be compiled separate from each other and be cached between compiles.
Diffstat (limited to 'compiler/compiler_test.go')
-rw-r--r--compiler/compiler_test.go161
1 files changed, 161 insertions, 0 deletions
diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go
new file mode 100644
index 000000000..2ae112299
--- /dev/null
+++ b/compiler/compiler_test.go
@@ -0,0 +1,161 @@
+package compiler
+
+import (
+ "flag"
+ "go/types"
+ "io/ioutil"
+ "regexp"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/tinygo-org/tinygo/compileopts"
+ "github.com/tinygo-org/tinygo/loader"
+ "tinygo.org/x/go-llvm"
+)
+
+// Pass -update to go test to update the output of the test files.
+var flagUpdate = flag.Bool("update", false, "update tests based on test output")
+
+// Basic tests for the compiler. Build some Go files and compare the output with
+// the expected LLVM IR for regression testing.
+func TestCompiler(t *testing.T) {
+ target, err := compileopts.LoadTarget("i686--linux")
+ if err != nil {
+ t.Fatal("failed to load target:", err)
+ }
+ config := &compileopts.Config{
+ Options: &compileopts.Options{},
+ Target: target,
+ }
+ machine, err := NewTargetMachine(config)
+ if err != nil {
+ t.Fatal("failed to create target machine:", err)
+ }
+
+ tests := []string{
+ "basic.go",
+ "pointer.go",
+ "slice.go",
+ }
+
+ for _, testCase := range tests {
+ t.Run(testCase, func(t *testing.T) {
+ // Load entire program AST into memory.
+ lprogram, err := loader.Load(config, []string{"./testdata/" + testCase}, config.ClangHeaders, types.Config{
+ Sizes: Sizes(machine),
+ })
+ if err != nil {
+ t.Fatal("failed to create target machine:", err)
+ }
+ err = lprogram.Parse()
+ if err != nil {
+ t.Fatalf("could not parse test case %s: %s", testCase, err)
+ }
+
+ // Compile AST to IR.
+ pkg := lprogram.MainPkg()
+ mod, errs := CompilePackage(testCase, pkg, machine, config)
+ if errs != nil {
+ for _, err := range errs {
+ t.Log("error:", err)
+ }
+ return
+ }
+
+ // Optimize IR a little.
+ funcPasses := llvm.NewFunctionPassManagerForModule(mod)
+ defer funcPasses.Dispose()
+ funcPasses.AddInstructionCombiningPass()
+ funcPasses.InitializeFunc()
+ for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) {
+ funcPasses.RunFunc(fn)
+ }
+ funcPasses.FinalizeFunc()
+
+ outfile := "./testdata/" + testCase[:len(testCase)-3] + ".ll"
+
+ // Update test if needed. Do not check the result.
+ if *flagUpdate {
+ err := ioutil.WriteFile(outfile, []byte(mod.String()), 0666)
+ if err != nil {
+ t.Error("failed to write updated output file:", err)
+ }
+ return
+ }
+
+ expected, err := ioutil.ReadFile(outfile)
+ if err != nil {
+ t.Fatal("failed to read golden file:", err)
+ }
+
+ if !fuzzyEqualIR(mod.String(), string(expected)) {
+ t.Errorf("output does not match expected output:\n%s", mod.String())
+ }
+ })
+ }
+}
+
+var alignRegexp = regexp.MustCompile(", align [0-9]+$")
+
+// fuzzyEqualIR returns true if the two LLVM IR strings passed in are roughly
+// equal. That means, only relevant lines are compared (excluding comments
+// etc.).
+func fuzzyEqualIR(s1, s2 string) bool {
+ lines1 := filterIrrelevantIRLines(strings.Split(s1, "\n"))
+ lines2 := filterIrrelevantIRLines(strings.Split(s2, "\n"))
+ if len(lines1) != len(lines2) {
+ return false
+ }
+ for i, line1 := range lines1 {
+ line2 := lines2[i]
+ match1 := alignRegexp.MatchString(line1)
+ match2 := alignRegexp.MatchString(line2)
+ if match1 != match2 {
+ // Only one of the lines has the align keyword. Remove it.
+ // This is a change to make the test work in both LLVM 10 and LLVM
+ // 11 (LLVM 11 appears to automatically add alignment everywhere).
+ line1 = alignRegexp.ReplaceAllString(line1, "")
+ line2 = alignRegexp.ReplaceAllString(line2, "")
+ }
+ if line1 != line2 {
+ return false
+ }
+ }
+
+ return true
+}
+
+// filterIrrelevantIRLines removes lines from the input slice of strings that
+// are not relevant in comparing IR. For example, empty lines and comments are
+// stripped out.
+func filterIrrelevantIRLines(lines []string) []string {
+ var out []string
+ llvmVersion, err := strconv.Atoi(strings.Split(llvm.Version, ".")[0])
+ if err != nil {
+ // Note: this should never happen and if it does, it will always happen
+ // for a particular build because llvm.Version is a constant.
+ panic(err)
+ }
+ for _, line := range lines {
+ line = strings.Split(line, ";")[0] // strip out comments/info
+ line = strings.TrimRight(line, "\r ") // drop '\r' on Windows and remove trailing spaces from comments
+ if line == "" {
+ continue
+ }
+ if strings.HasPrefix(line, "source_filename = ") {
+ continue
+ }
+ if llvmVersion < 10 && strings.HasPrefix(line, "attributes ") {
+ // Ignore attribute groups. These may change between LLVM versions.
+ // Right now test outputs are for LLVM 10.
+ continue
+ }
+ if llvmVersion < 10 && strings.HasPrefix(line, "target datalayout ") {
+ // Ignore the target layout. This may change between LLVM versions.
+ continue
+ }
+ out = append(out, line)
+ }
+ return out
+}