|
|
|
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",
|
|
|
|
"float.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
|
|
|
|
}
|