github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/compiler/compiler_test.go (about) 1 package compiler 2 3 import ( 4 "flag" 5 "go/types" 6 "os" 7 "strconv" 8 "strings" 9 "testing" 10 11 "github.com/tinygo-org/tinygo/compileopts" 12 "github.com/tinygo-org/tinygo/goenv" 13 "github.com/tinygo-org/tinygo/loader" 14 "tinygo.org/x/go-llvm" 15 ) 16 17 // Pass -update to go test to update the output of the test files. 18 var flagUpdate = flag.Bool("update", false, "update tests based on test output") 19 20 type testCase struct { 21 file string 22 target string 23 scheduler string 24 } 25 26 // Basic tests for the compiler. Build some Go files and compare the output with 27 // the expected LLVM IR for regression testing. 28 func TestCompiler(t *testing.T) { 29 t.Parallel() 30 31 // Determine Go minor version (e.g. 16 in go1.16.3). 32 _, goMinor, err := goenv.GetGorootVersion() 33 if err != nil { 34 t.Fatal("could not read Go version:", err) 35 } 36 37 // Determine which tests to run, depending on the Go and LLVM versions. 38 tests := []testCase{ 39 {"basic.go", "", ""}, 40 {"pointer.go", "", ""}, 41 {"slice.go", "", ""}, 42 {"string.go", "", ""}, 43 {"float.go", "", ""}, 44 {"interface.go", "", ""}, 45 {"func.go", "", ""}, 46 {"defer.go", "cortex-m-qemu", ""}, 47 {"pragma.go", "", ""}, 48 {"goroutine.go", "wasm", "asyncify"}, 49 {"goroutine.go", "cortex-m-qemu", "tasks"}, 50 {"channel.go", "", ""}, 51 {"gc.go", "", ""}, 52 {"zeromap.go", "", ""}, 53 } 54 if goMinor >= 20 { 55 tests = append(tests, testCase{"go1.20.go", "", ""}) 56 } 57 if goMinor >= 21 { 58 tests = append(tests, testCase{"go1.21.go", "", ""}) 59 } 60 61 for _, tc := range tests { 62 name := tc.file 63 targetString := "wasm" 64 if tc.target != "" { 65 targetString = tc.target 66 name += "-" + tc.target 67 } 68 if tc.scheduler != "" { 69 name += "-" + tc.scheduler 70 } 71 72 t.Run(name, func(t *testing.T) { 73 options := &compileopts.Options{ 74 Target: targetString, 75 } 76 if tc.scheduler != "" { 77 options.Scheduler = tc.scheduler 78 } 79 80 mod, errs := testCompilePackage(t, options, tc.file) 81 if errs != nil { 82 for _, err := range errs { 83 t.Error(err) 84 } 85 return 86 } 87 88 err := llvm.VerifyModule(mod, llvm.PrintMessageAction) 89 if err != nil { 90 t.Error(err) 91 } 92 93 // Optimize IR a little. 94 passOptions := llvm.NewPassBuilderOptions() 95 defer passOptions.Dispose() 96 err = mod.RunPasses("instcombine", llvm.TargetMachine{}, passOptions) 97 if err != nil { 98 t.Error(err) 99 } 100 101 outFilePrefix := tc.file[:len(tc.file)-3] 102 if tc.target != "" { 103 outFilePrefix += "-" + tc.target 104 } 105 if tc.scheduler != "" { 106 outFilePrefix += "-" + tc.scheduler 107 } 108 outPath := "./testdata/" + outFilePrefix + ".ll" 109 110 // Update test if needed. Do not check the result. 111 if *flagUpdate { 112 err := os.WriteFile(outPath, []byte(mod.String()), 0666) 113 if err != nil { 114 t.Error("failed to write updated output file:", err) 115 } 116 return 117 } 118 119 expected, err := os.ReadFile(outPath) 120 if err != nil { 121 t.Fatal("failed to read golden file:", err) 122 } 123 124 if !fuzzyEqualIR(mod.String(), string(expected)) { 125 t.Errorf("output does not match expected output:\n%s", mod.String()) 126 } 127 }) 128 } 129 } 130 131 // fuzzyEqualIR returns true if the two LLVM IR strings passed in are roughly 132 // equal. That means, only relevant lines are compared (excluding comments 133 // etc.). 134 func fuzzyEqualIR(s1, s2 string) bool { 135 lines1 := filterIrrelevantIRLines(strings.Split(s1, "\n")) 136 lines2 := filterIrrelevantIRLines(strings.Split(s2, "\n")) 137 if len(lines1) != len(lines2) { 138 return false 139 } 140 for i, line1 := range lines1 { 141 line2 := lines2[i] 142 if line1 != line2 { 143 return false 144 } 145 } 146 147 return true 148 } 149 150 // filterIrrelevantIRLines removes lines from the input slice of strings that 151 // are not relevant in comparing IR. For example, empty lines and comments are 152 // stripped out. 153 func filterIrrelevantIRLines(lines []string) []string { 154 var out []string 155 llvmVersion, err := strconv.Atoi(strings.Split(llvm.Version, ".")[0]) 156 if err != nil { 157 // Note: this should never happen and if it does, it will always happen 158 // for a particular build because llvm.Version is a constant. 159 panic(err) 160 } 161 for _, line := range lines { 162 line = strings.Split(line, ";")[0] // strip out comments/info 163 line = strings.TrimRight(line, "\r ") // drop '\r' on Windows and remove trailing spaces from comments 164 if line == "" { 165 continue 166 } 167 if strings.HasPrefix(line, "source_filename = ") { 168 continue 169 } 170 if llvmVersion < 15 && strings.HasPrefix(line, "target datalayout = ") { 171 // The datalayout string may vary betewen LLVM versions. 172 // Right now test outputs are for LLVM 15 and higher. 173 continue 174 } 175 out = append(out, line) 176 } 177 return out 178 } 179 180 func TestCompilerErrors(t *testing.T) { 181 t.Parallel() 182 183 // Read expected errors from the test file. 184 var expectedErrors []string 185 errorsFile, err := os.ReadFile("testdata/errors.go") 186 if err != nil { 187 t.Error(err) 188 } 189 errorsFileString := strings.ReplaceAll(string(errorsFile), "\r\n", "\n") 190 for _, line := range strings.Split(errorsFileString, "\n") { 191 if strings.HasPrefix(line, "// ERROR: ") { 192 expectedErrors = append(expectedErrors, strings.TrimPrefix(line, "// ERROR: ")) 193 } 194 } 195 196 // Compile the Go file with errors. 197 options := &compileopts.Options{ 198 Target: "wasm", 199 } 200 _, errs := testCompilePackage(t, options, "errors.go") 201 202 // Check whether the actual errors match the expected errors. 203 expectedErrorsIdx := 0 204 for _, err := range errs { 205 err := err.(types.Error) 206 position := err.Fset.Position(err.Pos) 207 position.Filename = "errors.go" // don't use a full path 208 if expectedErrorsIdx >= len(expectedErrors) || expectedErrors[expectedErrorsIdx] != err.Msg { 209 t.Errorf("unexpected compiler error: %s: %s", position.String(), err.Msg) 210 continue 211 } 212 expectedErrorsIdx++ 213 } 214 } 215 216 // Build a package given a number of compiler options and a file. 217 func testCompilePackage(t *testing.T, options *compileopts.Options, file string) (llvm.Module, []error) { 218 target, err := compileopts.LoadTarget(options) 219 if err != nil { 220 t.Fatal("failed to load target:", err) 221 } 222 config := &compileopts.Config{ 223 Options: options, 224 Target: target, 225 } 226 compilerConfig := &Config{ 227 Triple: config.Triple(), 228 Features: config.Features(), 229 ABI: config.ABI(), 230 GOOS: config.GOOS(), 231 GOARCH: config.GOARCH(), 232 CodeModel: config.CodeModel(), 233 RelocationModel: config.RelocationModel(), 234 Scheduler: config.Scheduler(), 235 AutomaticStackSize: config.AutomaticStackSize(), 236 DefaultStackSize: config.StackSize(), 237 NeedsStackObjects: config.NeedsStackObjects(), 238 } 239 machine, err := NewTargetMachine(compilerConfig) 240 if err != nil { 241 t.Fatal("failed to create target machine:", err) 242 } 243 defer machine.Dispose() 244 245 // Load entire program AST into memory. 246 lprogram, err := loader.Load(config, "./testdata/"+file, types.Config{ 247 Sizes: Sizes(machine), 248 }) 249 if err != nil { 250 t.Fatal("failed to create target machine:", err) 251 } 252 err = lprogram.Parse() 253 if err != nil { 254 t.Fatalf("could not parse test case %s: %s", file, err) 255 } 256 257 // Compile AST to IR. 258 program := lprogram.LoadSSA() 259 pkg := lprogram.MainPkg() 260 return CompilePackage(file, pkg, program.Package(pkg.Pkg), machine, compilerConfig, false) 261 }