aboutsummaryrefslogtreecommitdiffhomepage
path: root/compiler/compiler_test.go
blob: 0fb9fa913d6d56739c9d0b182b8efce9889796c6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package compiler

import (
	"flag"
	"go/types"
	"io/ioutil"
	"strconv"
	"strings"
	"testing"

	"github.com/tinygo-org/tinygo/compileopts"
	"github.com/tinygo-org/tinygo/goenv"
	"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")

type testCase struct {
	file   string
	target string
}

// 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) {
	t.Parallel()

	// Check LLVM version.
	llvmMajor, err := strconv.Atoi(strings.SplitN(llvm.Version, ".", 2)[0])
	if err != nil {
		t.Fatal("could not parse LLVM version:", llvm.Version)
	}
	if llvmMajor < 11 {
		// It is likely this version needs to be bumped in the future.
		// The goal is to at least test the LLVM version that's used by default
		// in TinyGo and (if possible without too many workarounds) also some
		// previous versions.
		t.Skip("compiler tests require LLVM 11 or above, got LLVM ", llvm.Version)
	}

	tests := []testCase{
		{"basic.go", ""},
		{"pointer.go", ""},
		{"slice.go", ""},
		{"string.go", ""},
		{"float.go", ""},
		{"interface.go", ""},
		{"func.go", ""},
		{"pragma.go", ""},
		{"goroutine.go", "wasm"},
		{"goroutine.go", "cortex-m-qemu"},
		{"intrinsics.go", "cortex-m-qemu"},
		{"intrinsics.go", "wasm"},
	}

	_, minor, err := goenv.GetGorootVersion(goenv.Get("GOROOT"))
	if err != nil {
		t.Fatal("could not read Go version:", err)
	}
	if minor >= 17 {
		tests = append(tests, testCase{"go1.17.go", ""})
	}

	for _, tc := range tests {
		name := tc.file
		targetString := "wasm"
		if tc.target != "" {
			targetString = tc.target
			name = tc.file + "-" + tc.target
		}

		t.Run(name, func(t *testing.T) {
			target, err := compileopts.LoadTarget(targetString)
			if err != nil {
				t.Fatal("failed to load target:", err)
			}
			config := &compileopts.Config{
				Options: &compileopts.Options{},
				Target:  target,
			}
			compilerConfig := &Config{
				Triple:             config.Triple(),
				GOOS:               config.GOOS(),
				GOARCH:             config.GOARCH(),
				CodeModel:          config.CodeModel(),
				RelocationModel:    config.RelocationModel(),
				Scheduler:          config.Scheduler(),
				FuncImplementation: config.FuncImplementation(),
				AutomaticStackSize: config.AutomaticStackSize(),
			}
			machine, err := NewTargetMachine(compilerConfig)
			if err != nil {
				t.Fatal("failed to create target machine:", err)
			}

			// Load entire program AST into memory.
			lprogram, err := loader.Load(config, []string{"./testdata/" + tc.file}, 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", tc.file, err)
			}

			// Compile AST to IR.
			program := lprogram.LoadSSA()
			pkg := lprogram.MainPkg()
			mod, errs := CompilePackage(tc.file, pkg, program.Package(pkg.Pkg), machine, compilerConfig, false)
			if errs != nil {
				for _, err := range errs {
					t.Error(err)
				}
				return
			}

			err = llvm.VerifyModule(mod, llvm.PrintMessageAction)
			if err != nil {
				t.Error(err)
			}

			// 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()

			outFilePrefix := tc.file[:len(tc.file)-3]
			if tc.target != "" {
				outFilePrefix += "-" + tc.target
			}
			outPath := "./testdata/" + outFilePrefix + ".ll"

			// Update test if needed. Do not check the result.
			if *flagUpdate {
				err := ioutil.WriteFile(outPath, []byte(mod.String()), 0666)
				if err != nil {
					t.Error("failed to write updated output file:", err)
				}
				return
			}

			expected, err := ioutil.ReadFile(outPath)
			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())
			}
		})
	}
}

// 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]
		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
	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
		}
		out = append(out, line)
	}
	return out
}