transform, compiler: support LLVM 21's captures(none) attribute; add LLVM 22 build support

Written entirely by Claude (Anthropic's Claude Code), at the request of
and under the direction of dgryski, as part of an effort to get TinyGo
building against upcoming LLVM releases (this branch currently targets
LLVM 22, verified against real LLVM 22.1.8; a corresponding go-llvm
branch of the same name adds the matching binding support).

LLVM 21 replaced the boolean 'nocapture' enum attribute with the more
expressive 'captures' int attribute, where captures(none) (value 0) is
the equivalent of the old nocapture. This matters for
transform.OptimizeAllocs, which relies on reading this attribute for
its interprocedural escape analysis, and for compiler/symbol.go, which
emits it on a number of runtime/generated functions. Confirmed
empirically (via `opt -passes=function-attrs`) that the cutoff is
LLVM 20 emits/expects nocapture, LLVM 21+ emits/expects
captures(none). Since TinyGo must keep working with LLVM 20, both
sites now go through new version-gated helpers in
compiler/llvmutil (NoCaptureAttrName/IsNoCapture) rather than switching
unconditionally.

Also fixes a second, unrelated but load-bearing break found while
testing against LLVM 22: llvm.lifetime.start/end dropped their i64
size argument (confirmed via `opt -passes=verify`, the cutoff here is
one version later, at LLVM 22). compiler/llvmutil now builds the
right call signature based on version.

Adds llvm21 and llvm22 build-tag config files to the cgo package,
which parses cgo fragments via libclang and had never been updated
past LLVM 20 even though the compiler package itself already gained
LLVM 21 support previously -- a latent gap that would have caused a
version mismatch between the cgo preprocessor and the rest of the
compiler when building with -tags llvm21 or llvm22.

Finally, updates the golden-IR test comparators in
transform/transform_test.go and compiler/compiler_test.go to
normalize a few cosmetic LLVM 21/22 output differences (the
captures(none) rename/reordering, a new 'nocreateundeforpoison'
intrinsic attribute, and the lifetime intrinsic arity change) so a
single golden file continues to match output from either LLVM
version.

Verified by building a full (non-byollvm) tinygo binary against real
LLVM 22.1.8 and running a compiled Go program end-to-end (exercising
OptimizeAllocs' stack-allocation path), and by running the
transform/compiler/cgo test suites against both LLVM 20 (default) and
LLVM 22.

Not yet addressed: the byollvm embedded-clang/lld build path hits
separate, unrelated Clang C++ API breakage against LLVM 22
(DiagnosticOptions reference-to-pointer change, missing headers) --
that is a larger follow-up effort.
This commit is contained in:
Damian Gryski
2026-07-06 15:24:19 -07:00
committed by Ron Evans
parent ea003da13f
commit 6203b624c5
8 changed files with 194 additions and 27 deletions
+3 -2
View File
@@ -12,6 +12,7 @@ import (
"os"
"regexp"
"github.com/tinygo-org/tinygo/compiler/llvmutil"
"tinygo.org/x/go-llvm"
)
@@ -260,7 +261,7 @@ func callValueEscapesAt(call, value llvm.Value, allowReturn bool, visiting map[l
if called.IsAFunction().IsNil() {
return escapeResult{escapeAt: call}
}
kindNoCapture := llvm.AttributeKindID("nocapture")
kindNoCapture := llvm.AttributeKindID(llvmutil.NoCaptureAttrName())
kindReturned := llvm.AttributeKindID("returned")
matched := false
var result escapeResult
@@ -270,7 +271,7 @@ func callValueEscapesAt(call, value llvm.Value, allowReturn bool, visiting map[l
}
matched = true
index := i + 1 // param attributes start at 1
nocapture := !called.GetEnumAttributeAtIndex(index, kindNoCapture).IsNil()
nocapture := llvmutil.IsNoCapture(called.GetEnumAttributeAtIndex(index, kindNoCapture))
returnedParam := !called.GetEnumAttributeAtIndex(index, kindReturned).IsNil()
if returnedParam {
result.returned = true
+43
View File
@@ -8,6 +8,7 @@ import (
"go/types"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
@@ -99,6 +100,29 @@ func testTransform(t *testing.T, pathPrefix string, transform func(mod llvm.Modu
// 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)
// LLVM 21+ also added an explicit 'nocreateundeforpoison' attribute to
// certain intrinsic declarations (e.g. llvm.umin) that were implicitly
// assumed not to create undef/poison before. It's unrelated to the
// escape-analysis behavior under test, so ignore it for comparison.
s1 = strings.ReplaceAll(s1, "nocreateundeforpoison ", "")
s2 = strings.ReplaceAll(s2, "nocreateundeforpoison ", "")
// LLVM 22 dropped the (redundant) i64 size argument from
// llvm.lifetime.start/end. Normalize away that argument so golden files
// written against the two-argument form still match.
s1 = lifetimeSizeArgRe.ReplaceAllString(s1, "$1")
s2 = lifetimeSizeArgRe.ReplaceAllString(s2, "$1")
lines1 := filterIrrelevantIRLines(strings.Split(s1, "\n"))
lines2 := filterIrrelevantIRLines(strings.Split(s2, "\n"))
if len(lines1) != len(lines2) {
@@ -114,6 +138,25 @@ 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\)`)
// lifetimeSizeArgRe matches the i64 size argument of an
// llvm.lifetime.start/end call or declaration, which LLVM 22 removed.
var lifetimeSizeArgRe = regexp.MustCompile(`(@llvm\.lifetime\.(?:start|end)\.p0\()i64(?: immarg| \d+), `)
// 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.