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  }