mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
9c864f0d79
Written entirely by Claude (Anthropic's Claude Code), at the request of and under the direction of dgryski, on the dgryski/llvm23 branch (LLVM 22 support, in preparation for the eventual LLVM 23 release; the corresponding go-llvm branch of the same name adds the matching binding support this depends on). Found while running real-world Go test suites through a tinygo built against LLVM 22 (as a smoke test of the earlier captures(none)/ nocapture and lifetime-intrinsic fixes on this branch): `tinygo test` on github.com/dgryski/go-rc5 panicked at compile time with "unknown value" inside interp.(*runner).getValue, while compiling (*sync.Once).Do. Bisecting against LLVM 21 (unaffected) narrowed it to a change introduced in LLVM 22 specifically, unrelated to the earlier captures/lifetime work. Root cause: LLVM 22 stopped exposing switch-instruction case values as regular instruction operands -- only the condition and destination-block operands remain. interp/compiler.go's `case llvm.Switch:` handling walked raw operands assuming the old alternating (value, label) layout, so under LLVM 22 it read a destination *block* where it expected an integer case value, eventually panicking deep inside a getValue call it couldn't resolve. This only manifested on functions actually compiled by the interp package (TinyGo's compile-time evaluator for global initializers) that happen to reach a switch-based dispatch, such as sync.Once's defer-based Do method -- hence it needed a real, moderately complex package (crypto/cipher via go-rc5) to surface, and never showed up in smaller smoke tests. Fix: use the new version-independent Value.SuccessorsCount()/ Value.Successor(i) and Value.GetSwitchCaseValue(i) helpers just added to go-llvm, instead of raw Operand() indexing. Also applies the same captures(none)/nocapture golden-IR normalization already used in transform/transform_test.go and compiler/compiler_test.go to interp/interp_test.go's separate copy of the same comparator helper, which was missed in the earlier pass and started failing two of its subtests once actually run against LLVM 22. Verified: `tinygo test` on go-rc5 now passes reliably (repeated runs) against both LLVM 20 (default) and LLVM 22; full transform/compiler/ interp/cgo/goenv test suites pass on both versions. The `builder` package has pre-existing, environment-related test failures (stale GOROOT cache, local clang/target-feature drift) confirmed identical against an unmodified LLVM 20 build, so unrelated to this change.
153 lines
4.1 KiB
Go
153 lines
4.1 KiB
Go
package interp
|
|
|
|
import (
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"tinygo.org/x/go-llvm"
|
|
)
|
|
|
|
func TestInterp(t *testing.T) {
|
|
for _, name := range []string{
|
|
"basic",
|
|
"phi",
|
|
"consteval",
|
|
"intrinsics",
|
|
"copy",
|
|
"interface",
|
|
"revert",
|
|
"store",
|
|
"alloc",
|
|
"slicedata",
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
runTest(t, "testdata/"+name)
|
|
})
|
|
}
|
|
}
|
|
|
|
func runTest(t *testing.T, pathPrefix string) {
|
|
// Read the input IR.
|
|
ctx := llvm.NewContext()
|
|
defer ctx.Dispose()
|
|
buf, err := llvm.NewMemoryBufferFromFile(pathPrefix + ".ll")
|
|
os.Stat(pathPrefix + ".ll") // make sure this file is tracked by `go test` caching
|
|
if err != nil {
|
|
t.Fatalf("could not read file %s: %v", pathPrefix+".ll", err)
|
|
}
|
|
mod, err := ctx.ParseIR(buf)
|
|
if err != nil {
|
|
t.Fatalf("could not load module:\n%v", err)
|
|
}
|
|
defer mod.Dispose()
|
|
|
|
// Perform the transform.
|
|
err = Run(mod, 10*time.Minute, DefaultMaxInterpBlockEntries, false)
|
|
if err != nil {
|
|
if err, match := err.(*Error); match {
|
|
println(err.Error())
|
|
if len(err.Inst) != 0 {
|
|
println(err.Inst)
|
|
}
|
|
if len(err.Traceback) > 0 {
|
|
println("\ntraceback:")
|
|
for _, line := range err.Traceback {
|
|
println(line.Pos.String() + ":")
|
|
println(line.Inst)
|
|
}
|
|
}
|
|
}
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// To be sure, verify that the module is still valid.
|
|
if llvm.VerifyModule(mod, llvm.PrintMessageAction) != nil {
|
|
t.FailNow()
|
|
}
|
|
|
|
// Run some cleanup passes to get easy-to-read outputs.
|
|
to := llvm.NewPassBuilderOptions()
|
|
defer to.Dispose()
|
|
mod.RunPasses("globalopt,dse,adce", llvm.TargetMachine{}, to)
|
|
|
|
// Read the expected output IR.
|
|
out, err := os.ReadFile(pathPrefix + ".out.ll")
|
|
if err != nil {
|
|
t.Fatalf("could not read output file %s: %v", pathPrefix+".out.ll", err)
|
|
}
|
|
|
|
// See whether the transform output matches with the expected output IR.
|
|
expected := string(out)
|
|
actual := mod.String()
|
|
if !fuzzyEqualIR(expected, actual) {
|
|
t.Logf("output does not match expected output:\n%s", actual)
|
|
t.Fail()
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
// Golden files are written using the pre-LLVM21 'nocapture' spelling,
|
|
// which LLVM printed before any co-occurring attribute such as
|
|
// 'readonly' (e.g. "ptr nocapture readonly"). LLVM 21+ prints the
|
|
// equivalent 'captures(none)' instead, and after such attributes (e.g.
|
|
// "ptr readonly captures(none)"). Normalize both name and position back
|
|
// to the old spelling to keep a single golden file working across LLVM
|
|
// versions.
|
|
s1 = normalizeCapturesAttr(s1)
|
|
s2 = normalizeCapturesAttr(s2)
|
|
|
|
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
|
|
}
|
|
|
|
// capturesNoneAttrRe matches a co-occurring attribute directly followed by
|
|
// 'captures(none)', which is how LLVM 21+ orders these two attributes when
|
|
// printing IR (the pre-LLVM21 'nocapture' attribute printed the other way
|
|
// around).
|
|
var capturesNoneAttrRe = regexp.MustCompile(`\b(readonly|readnone|writeonly|nonnull)\s+captures\(none\)`)
|
|
|
|
// normalizeCapturesAttr rewrites LLVM 21+'s 'captures(none)' attribute back
|
|
// to the pre-LLVM21 'nocapture' spelling and position, so golden IR files
|
|
// written against LLVM <21 keep matching.
|
|
func normalizeCapturesAttr(s string) string {
|
|
s = capturesNoneAttrRe.ReplaceAllString(s, "nocapture $1")
|
|
s = strings.ReplaceAll(s, "captures(none)", "nocapture")
|
|
return s
|
|
}
|
|
|
|
// 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.TrimSpace(line) // drop '\r' on Windows
|
|
if line == "" || line[0] == ';' {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "source_filename = ") {
|
|
continue
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
return out
|
|
}
|