From 9c864f0d79c286c68677647b391f28e0034df6d6 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Mon, 6 Jul 2026 21:30:24 -0700 Subject: [PATCH] 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. --- interp/compiler.go | 22 +++++++++++++++++----- interp/interp_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/interp/compiler.go b/interp/compiler.go index e909df80b..f4c8fd23b 100644 --- a/interp/compiler.go +++ b/interp/compiler.go @@ -140,13 +140,25 @@ func (r *runner) compileFunction(llvmFn llvm.Value) *function { panic("unknown number of operands") } 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 // label. - numOperands := llvmInst.OperandsCount() - for i := 0; i < numOperands; i += 2 { - inst.operands = append(inst.operands, r.getValue(llvmInst.Operand(i))) - inst.operands = append(inst.operands, literalValue{uint32(blockIndices[llvmInst.Operand(i+1)])}) + // + // Successor 0 is always the default destination; successors + // 1..N-1 are the individual cases. This must be read via + // 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: inst.name = llvmInst.Name() diff --git a/interp/interp_test.go b/interp/interp_test.go index bfb1ae2be..b1223e081 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -2,6 +2,7 @@ package interp import ( "os" + "regexp" "strings" "testing" "time" @@ -92,6 +93,16 @@ func runTest(t *testing.T, pathPrefix string) { // 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) { @@ -107,6 +118,21 @@ func fuzzyEqualIR(s1, s2 string) bool { 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.