mirror of https://github.com/tinygo-org/tinygo.git
wasmstm32webassemblymicrocontrollerarmavrspiwasiadafruitarduinocircuitplayground-expressgpioi2cllvmmicrobitnrf51nrf52nrf52840samd21tinygo
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
7.0 KiB
261 lines
7.0 KiB
package compiler
|
|
|
|
import (
|
|
"flag"
|
|
"go/types"
|
|
"os"
|
|
"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
|
|
scheduler 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()
|
|
|
|
// Determine Go minor version (e.g. 16 in go1.16.3).
|
|
_, goMinor, err := goenv.GetGorootVersion()
|
|
if err != nil {
|
|
t.Fatal("could not read Go version:", err)
|
|
}
|
|
|
|
// Determine which tests to run, depending on the Go and LLVM versions.
|
|
tests := []testCase{
|
|
{"basic.go", "", ""},
|
|
{"pointer.go", "", ""},
|
|
{"slice.go", "", ""},
|
|
{"string.go", "", ""},
|
|
{"float.go", "", ""},
|
|
{"interface.go", "", ""},
|
|
{"func.go", "", ""},
|
|
{"defer.go", "cortex-m-qemu", ""},
|
|
{"pragma.go", "", ""},
|
|
{"goroutine.go", "wasm", "asyncify"},
|
|
{"goroutine.go", "cortex-m-qemu", "tasks"},
|
|
{"channel.go", "", ""},
|
|
{"gc.go", "", ""},
|
|
{"zeromap.go", "", ""},
|
|
}
|
|
if goMinor >= 20 {
|
|
tests = append(tests, testCase{"go1.20.go", "", ""})
|
|
}
|
|
if goMinor >= 21 {
|
|
tests = append(tests, testCase{"go1.21.go", "", ""})
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
name := tc.file
|
|
targetString := "wasm"
|
|
if tc.target != "" {
|
|
targetString = tc.target
|
|
name += "-" + tc.target
|
|
}
|
|
if tc.scheduler != "" {
|
|
name += "-" + tc.scheduler
|
|
}
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
options := &compileopts.Options{
|
|
Target: targetString,
|
|
}
|
|
if tc.scheduler != "" {
|
|
options.Scheduler = tc.scheduler
|
|
}
|
|
|
|
mod, errs := testCompilePackage(t, options, tc.file)
|
|
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.
|
|
passOptions := llvm.NewPassBuilderOptions()
|
|
defer passOptions.Dispose()
|
|
err = mod.RunPasses("instcombine", llvm.TargetMachine{}, passOptions)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
outFilePrefix := tc.file[:len(tc.file)-3]
|
|
if tc.target != "" {
|
|
outFilePrefix += "-" + tc.target
|
|
}
|
|
if tc.scheduler != "" {
|
|
outFilePrefix += "-" + tc.scheduler
|
|
}
|
|
outPath := "./testdata/" + outFilePrefix + ".ll"
|
|
|
|
// Update test if needed. Do not check the result.
|
|
if *flagUpdate {
|
|
err := os.WriteFile(outPath, []byte(mod.String()), 0666)
|
|
if err != nil {
|
|
t.Error("failed to write updated output file:", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
expected, err := os.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
|
|
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 < 15 && strings.HasPrefix(line, "target datalayout = ") {
|
|
// The datalayout string may vary betewen LLVM versions.
|
|
// Right now test outputs are for LLVM 15 and higher.
|
|
continue
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestCompilerErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Read expected errors from the test file.
|
|
var expectedErrors []string
|
|
errorsFile, err := os.ReadFile("testdata/errors.go")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
errorsFileString := strings.ReplaceAll(string(errorsFile), "\r\n", "\n")
|
|
for _, line := range strings.Split(errorsFileString, "\n") {
|
|
if strings.HasPrefix(line, "// ERROR: ") {
|
|
expectedErrors = append(expectedErrors, strings.TrimPrefix(line, "// ERROR: "))
|
|
}
|
|
}
|
|
|
|
// Compile the Go file with errors.
|
|
options := &compileopts.Options{
|
|
Target: "wasm",
|
|
}
|
|
_, errs := testCompilePackage(t, options, "errors.go")
|
|
|
|
// Check whether the actual errors match the expected errors.
|
|
expectedErrorsIdx := 0
|
|
for _, err := range errs {
|
|
err := err.(types.Error)
|
|
position := err.Fset.Position(err.Pos)
|
|
position.Filename = "errors.go" // don't use a full path
|
|
if expectedErrorsIdx >= len(expectedErrors) || expectedErrors[expectedErrorsIdx] != err.Msg {
|
|
t.Errorf("unexpected compiler error: %s: %s", position.String(), err.Msg)
|
|
continue
|
|
}
|
|
expectedErrorsIdx++
|
|
}
|
|
}
|
|
|
|
// Build a package given a number of compiler options and a file.
|
|
func testCompilePackage(t *testing.T, options *compileopts.Options, file string) (llvm.Module, []error) {
|
|
target, err := compileopts.LoadTarget(options)
|
|
if err != nil {
|
|
t.Fatal("failed to load target:", err)
|
|
}
|
|
config := &compileopts.Config{
|
|
Options: options,
|
|
Target: target,
|
|
}
|
|
compilerConfig := &Config{
|
|
Triple: config.Triple(),
|
|
Features: config.Features(),
|
|
ABI: config.ABI(),
|
|
GOOS: config.GOOS(),
|
|
GOARCH: config.GOARCH(),
|
|
CodeModel: config.CodeModel(),
|
|
RelocationModel: config.RelocationModel(),
|
|
Scheduler: config.Scheduler(),
|
|
AutomaticStackSize: config.AutomaticStackSize(),
|
|
DefaultStackSize: config.StackSize(),
|
|
NeedsStackObjects: config.NeedsStackObjects(),
|
|
}
|
|
machine, err := NewTargetMachine(compilerConfig)
|
|
if err != nil {
|
|
t.Fatal("failed to create target machine:", err)
|
|
}
|
|
defer machine.Dispose()
|
|
|
|
// Load entire program AST into memory.
|
|
lprogram, err := loader.Load(config, "./testdata/"+file, 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", file, err)
|
|
}
|
|
|
|
// Compile AST to IR.
|
|
program := lprogram.LoadSSA()
|
|
pkg := lprogram.MainPkg()
|
|
return CompilePackage(file, pkg, program.Package(pkg.Pkg), machine, compilerConfig, false)
|
|
}
|
|
|