interp: fix switch-instruction handling for LLVM 22's new case-value API

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.
This commit is contained in:
Damian Gryski
2026-07-06 21:30:24 -07:00
committed by Ron Evans
parent 6203b624c5
commit 9c864f0d79
2 changed files with 43 additions and 5 deletions
+17 -5
View File
@@ -140,13 +140,25 @@ func (r *runner) compileFunction(llvmFn llvm.Value) *function {
panic("unknown number of operands") panic("unknown number of operands")
} }
case llvm.Switch: case llvm.Switch:
// A switch is an array of (value, label) pairs, of which the // Compile to an array of (value, label) pairs, of which the
// first one indicates the to-switch value and the default // first one indicates the to-switch value and the default
// label. // label.
numOperands := llvmInst.OperandsCount() //
for i := 0; i < numOperands; i += 2 { // Successor 0 is always the default destination; successors
inst.operands = append(inst.operands, r.getValue(llvmInst.Operand(i))) // 1..N-1 are the individual cases. This must be read via
inst.operands = append(inst.operands, literalValue{uint32(blockIndices[llvmInst.Operand(i+1)])}) // GetSwitchCaseValue/Successor rather than raw operands,
// because LLVM 22 stopped exposing switch case values as
// regular instruction operands (only the condition and
// destination-block operands remain).
inst.operands = append(inst.operands,
r.getValue(llvmInst.Operand(0)),
literalValue{uint32(blockIndices[llvmInst.Successor(0).AsValue()])},
)
for i := 1; i < llvmInst.SuccessorsCount(); i++ {
inst.operands = append(inst.operands,
r.getValue(llvmInst.GetSwitchCaseValue(i)),
literalValue{uint32(blockIndices[llvmInst.Successor(i).AsValue()])},
)
} }
case llvm.PHI: case llvm.PHI:
inst.name = llvmInst.Name() inst.name = llvmInst.Name()
+26
View File
@@ -2,6 +2,7 @@ package interp
import ( import (
"os" "os"
"regexp"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -92,6 +93,16 @@ func runTest(t *testing.T, pathPrefix string) {
// equal. That means, only relevant lines are compared (excluding comments // equal. That means, only relevant lines are compared (excluding comments
// etc.). // etc.).
func fuzzyEqualIR(s1, s2 string) bool { 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")) lines1 := filterIrrelevantIRLines(strings.Split(s1, "\n"))
lines2 := filterIrrelevantIRLines(strings.Split(s2, "\n")) lines2 := filterIrrelevantIRLines(strings.Split(s2, "\n"))
if len(lines1) != len(lines2) { if len(lines1) != len(lines2) {
@@ -107,6 +118,21 @@ func fuzzyEqualIR(s1, s2 string) bool {
return true 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 // filterIrrelevantIRLines removes lines from the input slice of strings that
// are not relevant in comparing IR. For example, empty lines and comments are // are not relevant in comparing IR. For example, empty lines and comments are
// stripped out. // stripped out.