From b094e1e9de03987e2e30136c1093be414f08219a Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Sun, 6 Sep 2026 01:12:58 +0800 Subject: [PATCH 1/2] ssa: describe audited runtime function contracts --- runtime/internal/test/runtime_attrs_test.go | 128 ++++++++++ ssa/decl.go | 1 + ssa/runtime_attrs.go | 102 ++++++++ ssa/runtime_attrs_test.go | 247 ++++++++++++++++++++ ssa/runtime_range.go | 40 ++++ 5 files changed, 518 insertions(+) create mode 100644 runtime/internal/test/runtime_attrs_test.go create mode 100644 ssa/runtime_attrs.go create mode 100644 ssa/runtime_attrs_test.go create mode 100644 ssa/runtime_range.go diff --git a/runtime/internal/test/runtime_attrs_test.go b/runtime/internal/test/runtime_attrs_test.go new file mode 100644 index 0000000000..5a2dc0e447 --- /dev/null +++ b/runtime/internal/test/runtime_attrs_test.go @@ -0,0 +1,128 @@ +//go:build llgo + +package test + +import ( + "testing" + "unsafe" + + rt "github.com/xgo-dev/llgo/runtime/internal/runtime" +) + +//go:linkname runtimeMemequal github.com/xgo-dev/llgo/runtime/internal/runtime.memequal +func runtimeMemequal(a, b unsafe.Pointer, size uintptr) bool + +//go:linkname reflectTypedmemmove reflect.typedmemmove +func reflectTypedmemmove(typ *rt.Type, dst, src unsafe.Pointer) + +func TestRuntimeCheckedPointer(t *testing.T) { + x := 42 + p := unsafe.Pointer(&x) + if rt.AssertNilDerefPtr(p) != p { + t.Fatal("checked pointer identity changed") + } + defer func() { + if recover() == nil { + t.Fatal("nil pointer check did not raise a recoverable panic") + } + }() + rt.AssertNilDerefPtr(nil) + t.Fatal("nil pointer check returned") +} + +func TestRuntimeMemoryContracts(t *testing.T) { + if !runtimeMemequal(nil, nil, 0) { + t.Fatal("zero-byte equality must accept nil") + } + a, b := [4]byte{1, 2, 3, 4}, [4]byte{1, 2, 3, 4} + if !runtimeMemequal(unsafe.Pointer(&a), unsafe.Pointer(&b), 4) { + t.Fatal("equal bytes differ") + } + b[2] = 9 + if runtimeMemequal(unsafe.Pointer(&a), unsafe.Pointer(&b), 4) { + t.Fatal("equality did not observe an intervening write") + } + // memmove permits overlap in both directions. The reflect linkname entry + // must retain the same contract as the compiler's runtime entry. + for _, move := range []func(*rt.Type, unsafe.Pointer, unsafe.Pointer){rt.Typedmemmove, reflectTypedmemmove} { + x := [5]byte{1, 2, 3, 4, 5} + typ := rt.Type{Size_: 4} + move(&typ, unsafe.Pointer(&x[1]), unsafe.Pointer(&x[0])) + if x != [5]byte{1, 1, 2, 3, 4} { + t.Fatalf("forward overlap: %v", x) + } + move(&typ, unsafe.Pointer(&x[0]), unsafe.Pointer(&x[1])) + if x != [5]byte{1, 2, 3, 4, 4} { + t.Fatalf("backward overlap: %v", x) + } + move(nil, nil, nil) // equality fast path does not inspect the type + } + rt.Typedmemclr(&rt.Type{Size_: 4}, unsafe.Pointer(&a)) + if a != [4]byte{} { + t.Fatalf("clear: %v", a) + } + buf := [8]byte{7, 7, 7, 7, 7, 7, 7, 7} + for _, s := range []string{"", "abc"} { + result := rt.CStrCopy(unsafe.Pointer(&buf[0]), *(*rt.String)(unsafe.Pointer(&s))) + if unsafe.Pointer(result) != unsafe.Pointer(&buf[0]) || buf[len(s)] != 0 || string(buf[:len(s)]) != s { + t.Fatalf("C string copy %q: %v", s, buf) + } + } +} + +func TestRuntimeReadContracts(t *testing.T) { + if rt.MapLen(nil) != 0 || rt.ChanCap(nil) != 0 { + t.Fatal("nil length/capacity must be zero") + } + m := map[int]int{1: 10} + mp := (*rt.Map)(*(*unsafe.Pointer)(unsafe.Pointer(&m))) + if rt.MapLen(mp) != 1 { + t.Fatal("map length") + } + m[2] = 20 + if rt.MapLen(mp) != 2 { + t.Fatal("map length did not observe insertion") + } + ch := make(chan int, 3) + cp := (*rt.Chan)(*(*unsafe.Pointer)(unsafe.Pointer(&ch))) + if rt.ChanCap(cp) != 3 { + t.Fatal("channel capacity") + } + for _, pair := range []struct { + a, b string + equal, less bool + }{ + {"", "", true, false}, {"", "x", false, true}, + {"ab", "abc", false, true}, {"abc", "abd", false, true}, + {"abc", "abc", true, false}, {"abd", "abc", false, false}, + } { + ra, rb := *(*rt.String)(unsafe.Pointer(&pair.a)), *(*rt.String)(unsafe.Pointer(&pair.b)) + if rt.StringEqual(ra, rb) != pair.equal || rt.StringLess(ra, rb) != pair.less { + t.Fatalf("string comparison: %q %q", pair.a, pair.b) + } + } +} + +func TestRuntimePanicContracts(t *testing.T) { + for _, raise := range []func(){ + func() { rt.Panic("test") }, + func() { rt.Panic(nil) }, + func() { rt.PanicErrorString("test") }, + func() { rt.PanicIndex(3, 2) }, + func() { rt.PanicIndexU(3, 2) }, + func() { rt.PanicSliceConvert(3, 2) }, + func() { rt.PanicTypeAssertionError("test") }, + } { + func() { + defer func() { + if recover() == nil { + t.Fatal("panic was not recoverable") + } + }() + raise() + t.Fatal("panic entry returned normally") + }() + } + // Rethrow is also used with no active panic and must return normally. + rt.Rethrow(nil) +} diff --git a/ssa/decl.go b/ssa/decl.go index 95df7dd43f..255b858aac 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -380,6 +380,7 @@ func (p Package) newFunc( llvmName = p.Prog.stdcallSymbolName(name) } fn := llvm.AddFunction(p.mod, llvmName, t.ll) + p.Prog.addRuntimeAttributes(fn, name) if bg == InStdcall { fn.SetFunctionCallConv(p.Prog.stdcallCallConv()) } diff --git a/ssa/runtime_attrs.go b/ssa/runtime_attrs.go new file mode 100644 index 0000000000..f3cb1d53fd --- /dev/null +++ b/ssa/runtime_attrs.go @@ -0,0 +1,102 @@ +package ssa + +import "github.com/xgo-dev/llvm" + +// runtimeContracts names the fixed runtime entries whose implementations obey +// these contracts, including declarations imported into a different module. +// Match resolved symbols so go:linkname callers get the same attributes. +var runtimeContracts = map[string]string{ + PkgRuntime + ".AssertNilDerefPtr": "checked", + PkgRuntime + ".CStrCopy": "cstrcopy", + PkgRuntime + ".memequal": "compare", + PkgRuntime + ".StringEqual": "read", + PkgRuntime + ".StringLess": "read", + PkgRuntime + ".Typedmemmove": "move", + "reflect.typedmemmove": "move", + PkgRuntime + ".Typedmemclr": "clear", + PkgRuntime + ".MapLen": "length", + PkgRuntime + ".ChanCap": "length", + PkgRuntime + ".Memhash": "hash", + PkgRuntime + ".Memhash32": "hash", + PkgRuntime + ".Memhash64": "hash", + + PkgRuntime + ".Panic": "panic", + PkgRuntime + ".PanicErrorString": "panic", + PkgRuntime + ".PanicIndex": "panic", + PkgRuntime + ".PanicIndexU": "panic", + PkgRuntime + ".PanicSliceConvert": "panic", + PkgRuntime + ".PanicTypeAssert": "panic", + PkgRuntime + ".PanicTypeAssertionError": "panic", + PkgRuntime + ".PanicExtendIndex": "panic", + PkgRuntime + ".PanicExtendIndexU": "panic", +} + +// LLVM 22 MemoryEffects uses two ModRef bits per location, with argument +// memory first. Ref=1, ModRef=3; there are six locations (including target +// memory). CaptureInfo stores return components in the low four bits. +const ( + runtimeArgRead = 1 + runtimeArgReadWrite = 3 + runtimeMemoryRead = 0x555 + runtimeCaptureReturn = 0xf +) + +func (p Program) addRuntimeAttributes(fn llvm.Value, name string) { + contract, ok := runtimeContracts[name] + if !ok { + return + } + add := func(index int, name string, value uint64) { + fn.AddAttributeAtIndex(index, p.ctx.CreateEnumAttribute(llvm.AttributeKindID(name), value)) + } + pointer := func(index int, access string) { + add(index, access, 0) + add(index, "captures", 0) + } + switch contract { + case "checked": + // nil is a valid input: the runtime must still raise a recoverable + // panic. Only a normal return guarantees non-nullness. + add(0, "nonnull", 0) + add(1, "returned", 0) + return + case "panic": + add(-1, "cold", 0) + add(-1, "noreturn", 0) + return + } + for _, attr := range []string{"nofree", "nosync", "nounwind", "willreturn"} { + add(-1, attr, 0) + } + switch contract { + case "cstrcopy": + add(1, "returned", 0) + add(1, "writeonly", 0) + add(1, "captures", runtimeCaptureReturn) + // The source string is an aggregate, not a pointer argument. + add(-1, "memory", runtimeMemoryRead|runtimeArgReadWrite) + case "compare": + pointer(1, "readonly") + pointer(2, "readonly") + add(-1, "memory", runtimeArgRead) + case "read": + // String data is carried in aggregate parameters. + add(-1, "memory", runtimeMemoryRead) + case "move", "clear": + pointer(1, "readonly") + pointer(2, "writeonly") + if contract == "move" { + pointer(3, "readonly") + } + // No noalias or nonnull: overlap and zero-byte copies are valid. + add(-1, "memory", runtimeArgReadWrite) + case "length": + pointer(1, "readonly") + add(-1, "memory", runtimeArgRead) + addNonNegativeReturnRange(p.ctx, fn, p.Int().ll.IntTypeWidth()) + case "hash": + pointer(1, "readonly") + // Hashing also reads the process-global hashkey seed. + add(-1, "memory", runtimeMemoryRead) + } +} diff --git a/ssa/runtime_attrs_test.go b/ssa/runtime_attrs_test.go new file mode 100644 index 0000000000..9be6bfbddb --- /dev/null +++ b/ssa/runtime_attrs_test.go @@ -0,0 +1,247 @@ +package ssa + +import ( + "fmt" + "go/token" + "go/types" + "regexp" + "strings" + "testing" + + "github.com/xgo-dev/llvm" +) + +func runtimeContractSignature(params []types.Type, result types.Type) *types.Signature { + vars := make([]*types.Var, len(params)) + for i, typ := range params { + vars[i] = types.NewParam(token.NoPos, nil, "", typ) + } + var results *types.Tuple + if result != nil { + results = types.NewTuple(types.NewParam(token.NoPos, nil, "", result)) + } + return types.NewSignatureType(nil, nil, nil, types.NewTuple(vars...), results, false) +} + +func TestRuntimeContracts(t *testing.T) { + tests := []struct { + name string + want []string + absent []string + }{ + {"AssertNilDerefPtr", []string{"declare nonnull ptr", "ptr returned"}, []string{"ptr nonnull", "memory(", "noreturn", "willreturn", "nounwind"}}, + {"CStrCopy", []string{"ptr returned writeonly captures(ret: address, provenance)", "memory(read, argmem: readwrite)"}, []string{"noalias", "nonnull"}}, + {"memequal", []string{"ptr readonly captures(none)", "memory(argmem: read)"}, []string{"nonnull", "noalias"}}, + {"StringEqual", []string{"memory(read)"}, []string{"memory(argmem:"}}, + {"StringLess", []string{"memory(read)"}, nil}, + {"Typedmemmove", []string{"ptr readonly captures(none), ptr writeonly captures(none), ptr readonly captures(none)", "memory(argmem: readwrite)"}, []string{"nonnull", "noalias"}}, + {"Typedmemclr", []string{"ptr readonly captures(none), ptr writeonly captures(none)", "memory(argmem: readwrite)"}, []string{"nonnull"}}, + {"MapLen", []string{"range(i%d 0, %s)", "ptr readonly captures(none)", "memory(argmem: read)"}, []string{"nonnull"}}, + {"ChanCap", []string{"range(i%d 0, %s)", "memory(argmem: read)"}, []string{"nonnull"}}, + {"Memhash", []string{"ptr readonly captures(none)", "memory(read)"}, []string{"memory(argmem:"}}, + {"Memhash32", []string{"memory(read)"}, nil}, + {"Memhash64", []string{"memory(read)"}, nil}, + } + for _, target := range []Target{{GOOS: "linux", GOARCH: "amd64"}, {GOOS: "darwin", GOARCH: "arm64"}, {GOOS: "windows", GOARCH: "386"}, {GOOS: "wasip1", GOARCH: "wasm64"}} { + t.Run(target.GOARCH, func(t *testing.T) { + prog := NewProgram(&target) + defer prog.Dispose() + setTestRuntime(t, prog) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Exercise definitions and declarations using the runtime source signature. + for _, definition := range []bool{false, true} { + pkg := prog.NewPackage(fmt.Sprint(test.name, definition), "test/contracts") + var sig *types.Signature + if test.name == "memequal" { + // This private linkname entry is absent from export data. + sig = runtimeContractSignature([]types.Type{types.Typ[types.UnsafePointer], types.Typ[types.UnsafePointer], types.Typ[types.Uintptr]}, types.Typ[types.Bool]) + } else { + sig = prog.runtime().Scope().Lookup(test.name).Type().(*types.Signature) + } + fn := pkg.NewFunc(PkgRuntime+"."+test.name, sig, InGo) + if definition { + b := prog.ctx.NewBuilder() + b.SetInsertPointAtEnd(prog.ctx.AddBasicBlock(fn.impl, "entry")) + b.CreateUnreachable() + b.Dispose() + } + ir := pkg.String() + ir = regexp.MustCompile(` %[0-9]+`).ReplaceAllString(ir, "") + for _, want := range test.want { + if strings.Contains(want, "%d") { + bits := prog.Int().ll.IntTypeWidth() + upper := "-9223372036854775808" + if bits == 32 { + upper = "-2147483648" + } + want = fmt.Sprintf(want, bits, upper) + } + if definition { + want = strings.ReplaceAll(want, "declare ", "define ") + } + if !strings.Contains(ir, want) { + t.Errorf("missing %q:\n%s", want, ir) + } + } + for _, absent := range test.absent { + if strings.Contains(ir, absent) { + t.Errorf("unexpected %q:\n%s", absent, ir) + } + } + if test.name != "AssertNilDerefPtr" { + for _, want := range []string{"nofree", "nosync", "nounwind", "willreturn"} { + if !strings.Contains(ir, want) { + t.Errorf("missing %s:\n%s", want, ir) + } + } + } + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + } + }) + } + }) + } +} + +func TestRuntimePanicContractsAndExclusions(t *testing.T) { + prog := NewProgram(nil) + defer prog.Dispose() + for _, name := range []string{"Panic", "PanicErrorString", "PanicIndex", "PanicIndexU", "PanicSliceConvert", "PanicTypeAssert", "PanicTypeAssertionError", "PanicExtendIndex", "PanicExtendIndexU", "Rethrow", "throw", "AssertIndex", "PanicWrapNilPointer", "ChanLen", "Recover", "Implements", "AllocU", "AllocZ", "AllocRoot", "NewItab"} { + pkg := prog.NewPackage(name, "test/panic") + fn := pkg.NewFunc(PkgRuntime+"."+name, NoArgsNoRet, InGo) + panicEntry := strings.HasPrefix(name, "Panic") && name != "PanicWrapNilPointer" + for _, attr := range []string{"cold", "noreturn"} { + if got := !fn.impl.GetEnumAttributeAtIndex(-1, llvm.AttributeKindID(attr)).IsNil(); got != panicEntry { + t.Errorf("%s %s = %v, want %v", name, attr, got, panicEntry) + } + } + for _, attr := range []string{"memory", "nounwind", "nofree", "nosync", "willreturn"} { + if !fn.impl.GetEnumAttributeAtIndex(-1, llvm.AttributeKindID(attr)).IsNil() { + t.Errorf("%s unexpectedly has %s", name, attr) + } + } + } + // A matching short name in another package must not inherit runtime contracts. + pkg := prog.NewPackage("ordinary", "example.com/ordinary") + fn := pkg.NewFunc("example.com/ordinary.Panic", NoArgsNoRet, InGo) + if !fn.impl.GetEnumAttributeAtIndex(-1, llvm.AttributeKindID("noreturn")).IsNil() { + t.Fatal("ordinary Panic got noreturn") + } +} + +func TestRuntimeResolvedLinknameContract(t *testing.T) { + prog := NewProgram(nil) + defer prog.Dispose() + setTestRuntime(t, prog) + pkg := prog.NewPackage("linkname", "test/linkname") + pkg.SetResolveLinkname(func(name string) string { + if name == PkgRuntime+".Typedmemmove" { + return "reflect.typedmemmove" + } + return name + }) + fn := pkg.RuntimeFunc("Typedmemmove").impl + if fn.Name() != "reflect.typedmemmove" { + t.Fatal(fn.Name()) + } + for i, access := range []string{"readonly", "writeonly", "readonly"} { + if fn.GetEnumAttributeAtIndex(i+1, llvm.AttributeKindID(access)).IsNil() { + t.Fatalf("linkname lost parameter %d %s", i, access) + } + } + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } +} + +func TestRuntimeContractOptimizations(t *testing.T) { + for _, target := range []Target{{GOOS: "linux", GOARCH: "amd64"}, {GOOS: "windows", GOARCH: "386"}, {GOOS: "wasip1", GOARCH: "wasm64"}} { + for _, pipeline := range []string{"default", "lto"} { + t.Run(target.GOARCH+"/"+pipeline, func(t *testing.T) { + prog := NewProgram(&target) + defer prog.Dispose() + setTestRuntime(t, prog) + pkg := prog.NewPackage("opt", "test/opt") + ptr, integer := types.Typ[types.UnsafePointer], types.Typ[types.Int] + checked := pkg.NewFunc(PkgRuntime+".AssertNilDerefPtr", runtimeContractSignature([]types.Type{ptr}, ptr), InGo).impl + length := pkg.NewFunc(PkgRuntime+".MapLen", runtimeContractSignature([]types.Type{ptr}, integer), InGo).impl + equal := pkg.NewFunc(PkgRuntime+".memequal", runtimeContractSignature([]types.Type{ptr, ptr, integer}, types.Typ[types.Bool]), InGo).impl + hash := pkg.NewFunc(PkgRuntime+".Memhash64", runtimeContractSignature([]types.Type{ptr, integer}, integer), InGo).impl + copystr := pkg.RuntimeFunc("CStrCopy").impl + global := llvm.AddGlobal(pkg.Module(), prog.Int().ll, "hashkey") + b := prog.ctx.NewBuilder() + defer b.Dispose() + for _, name := range []string{"checked", "negative", "copy", "repeat", "write", "globalwrite"} { + fn := llvm.AddFunction(pkg.Module(), name, llvm.FunctionType(prog.ctx.Int1Type(), []llvm.Type{prog.VoidPtr().ll, prog.VoidPtr().ll}, false)) + b.SetInsertPointAtEnd(prog.ctx.AddBasicBlock(fn, "entry")) + p, q := fn.Param(0), fn.Param(1) + call := func(f llvm.Value, args ...llvm.Value) llvm.Value { + return b.CreateCall(f.GlobalValueType(), f, args, "") + } + switch name { + case "checked": + r := call(checked, p) + b.CreateRet(b.CreateICmp(llvm.IntEQ, r, llvm.ConstNull(r.Type()), "")) + case "negative": + r := call(length, p) + b.CreateRet(b.CreateICmp(llvm.IntSLT, r, llvm.ConstNull(r.Type()), "")) + case "copy": + r := call(copystr, p, llvm.ConstNull(copystr.GlobalValueType().ParamTypes()[1])) + b.CreateRet(b.CreateICmp(llvm.IntEQ, r, p, "")) + case "repeat", "write": + args := []llvm.Value{p, q, llvm.ConstInt(prog.Int().ll, 1, false)} + x := call(equal, args...) + if name == "write" { + b.CreateStore(llvm.ConstInt(prog.ctx.Int8Type(), 42, false), p) + } + y := call(equal, args...) + b.CreateRet(b.CreateXor(x, y, "")) + case "globalwrite": + seed := llvm.ConstInt(prog.Int().ll, 1, false) + x := call(hash, p, seed) + b.CreateStore(seed, global) + y := call(hash, p, seed) + b.CreateRet(b.CreateICmp(llvm.IntEQ, x, y, "")) + } + } + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + pbo := llvm.NewPassBuilderOptions() + defer pbo.Dispose() + if err := pkg.Module().RunPasses(pipeline, prog.TargetMachine(), pbo); err != nil { + t.Fatal(err) + } + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + for _, name := range []string{"checked", "negative", "repeat"} { + ir := pkg.Module().NamedFunction(name).String() + if !strings.Contains(ir, "ret i1 false") { + t.Errorf("%s failed to fold:\n%s", name, ir) + } + if name == "checked" && !strings.Contains(ir, "call ptr @") { + t.Errorf("nil check call was removed:\n%s", ir) + } + } + for _, name := range []string{"write", "globalwrite"} { + ir := pkg.Module().NamedFunction(name).String() + if strings.Count(ir, " call ") != 2 { + t.Errorf("%s lost memory invalidation:\n%s", name, ir) + } + } + if ir := pkg.Module().NamedFunction("copy").String(); !strings.Contains(ir, "ret i1 true") || !strings.Contains(ir, "call ptr @") { + t.Errorf("copy must retain the call and fold its returned identity:\n%s", ir) + } + asm, err := prog.TargetMachine().EmitToMemoryBuffer(pkg.Module(), llvm.AssemblyFile) + if err != nil { + t.Fatal(err) + } + asm.Dispose() + }) + } + } +} diff --git a/ssa/runtime_range.go b/ssa/runtime_range.go new file mode 100644 index 0000000000..514914175c --- /dev/null +++ b/ssa/runtime_range.go @@ -0,0 +1,40 @@ +package ssa + +/* +#include + +// These two public LLVM-C APIs bridge the ConstantRange constructor missing +// from github.com/xgo-dev/llvm v0.9.9. Opaque declarations avoid duplicating +// the binding package's platform-specific LLVM header search paths. Linking +// still uses that package's selected LLVM library. +typedef struct LLVMOpaqueContext *LLVMContextRef; +typedef struct LLVMOpaqueValue *LLVMValueRef; +typedef struct LLVMOpaqueAttributeRef *LLVMAttributeRef; +extern LLVMAttributeRef LLVMCreateConstantRangeAttribute( + LLVMContextRef, unsigned, unsigned, const uint64_t *, const uint64_t *); +extern void LLVMAddAttributeAtIndex(LLVMValueRef, unsigned, LLVMAttributeRef); + +static void llgoAddNonNegativeReturnRange(void *ctx, void *fn, + unsigned kind, unsigned bits) { + uint64_t lower = 0; + uint64_t upper = UINT64_C(1) << (bits - 1); + LLVMAttributeRef attr = LLVMCreateConstantRangeAttribute( + (LLVMContextRef)ctx, kind, bits, &lower, &upper); + LLVMAddAttributeAtIndex((LLVMValueRef)fn, 0, attr); +} +*/ +import "C" + +import ( + "unsafe" + + "github.com/xgo-dev/llvm" +) + +func addNonNegativeReturnRange(ctx llvm.Context, fn llvm.Value, bits int) { + if bits != 32 && bits != 64 { + panic("ssa: unsupported runtime integer width") + } + C.llgoAddNonNegativeReturnRange(unsafe.Pointer(ctx.C), unsafe.Pointer(fn.C), + C.unsigned(llvm.AttributeKindID("range")), C.unsigned(bits)) +} From 4980c24d45ce40fb0d31e244bb55b0bcd50a0c99 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Sun, 6 Sep 2026 07:06:16 +0800 Subject: [PATCH 2/2] ssa: use constant-range API from LLVM bindings --- go.mod | 3 +++ go.sum | 2 ++ ssa/runtime_attrs.go | 9 +++++++++ ssa/runtime_range.go | 40 ---------------------------------------- 4 files changed, 14 insertions(+), 40 deletions(-) delete mode 100644 ssa/runtime_range.go diff --git a/go.mod b/go.mod index d3b414c3d4..46ddb8f947 100644 --- a/go.mod +++ b/go.mod @@ -26,3 +26,6 @@ require ( ) replace github.com/xgo-dev/llgo/runtime => ./runtime + +// Temporary dependency on https://github.com/xgo-dev/llvm/pull/52. +replace github.com/xgo-dev/llvm => github.com/zhouguangyuan0718/go-llvm v0.0.0-20260905230337-4de18f5844ba diff --git a/go.sum b/go.sum index 7c70052fd5..7e3b98d8fc 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/xgo-dev/llvm v0.9.9 h1:u4RZvy0vxu8nNt4oTrhttyMSyscYtmiy9geP+Kl/RYg= github.com/xgo-dev/llvm v0.9.9/go.mod h1:42vav2/cI5BAIcL543DZSMO9do8/aCK2z7JERH+AE+M= github.com/xgo-dev/plan9asm v0.5.2 h1:P5eGHZiuKNcL2tlkkICa2XcxghUSnp6QzfN52x0HW1g= github.com/xgo-dev/plan9asm v0.5.2/go.mod h1:ZzybDk59y/AXoQeYUEqxVkKWqalmCfmcfCtM9y85kQU= +github.com/zhouguangyuan0718/go-llvm v0.0.0-20260905230337-4de18f5844ba h1:P+HZNghDqxC307G1+EhqlSj6m6PL7lRzjtVCUaDQdLs= +github.com/zhouguangyuan0718/go-llvm v0.0.0-20260905230337-4de18f5844ba/go.mod h1:42vav2/cI5BAIcL543DZSMO9do8/aCK2z7JERH+AE+M= go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= diff --git a/ssa/runtime_attrs.go b/ssa/runtime_attrs.go index f3cb1d53fd..2bb497a68a 100644 --- a/ssa/runtime_attrs.go +++ b/ssa/runtime_attrs.go @@ -100,3 +100,12 @@ func (p Program) addRuntimeAttributes(fn llvm.Value, name string) { add(-1, "memory", runtimeMemoryRead) } } + +func addNonNegativeReturnRange(ctx llvm.Context, fn llvm.Value, bits int) { + if bits != 32 && bits != 64 { + panic("ssa: unsupported runtime integer width") + } + attr := ctx.CreateConstantRangeAttribute(llvm.AttributeKindID("range"), bits, + []uint64{0}, []uint64{uint64(1) << (bits - 1)}) + fn.AddAttributeAtIndex(0, attr) +} diff --git a/ssa/runtime_range.go b/ssa/runtime_range.go deleted file mode 100644 index 514914175c..0000000000 --- a/ssa/runtime_range.go +++ /dev/null @@ -1,40 +0,0 @@ -package ssa - -/* -#include - -// These two public LLVM-C APIs bridge the ConstantRange constructor missing -// from github.com/xgo-dev/llvm v0.9.9. Opaque declarations avoid duplicating -// the binding package's platform-specific LLVM header search paths. Linking -// still uses that package's selected LLVM library. -typedef struct LLVMOpaqueContext *LLVMContextRef; -typedef struct LLVMOpaqueValue *LLVMValueRef; -typedef struct LLVMOpaqueAttributeRef *LLVMAttributeRef; -extern LLVMAttributeRef LLVMCreateConstantRangeAttribute( - LLVMContextRef, unsigned, unsigned, const uint64_t *, const uint64_t *); -extern void LLVMAddAttributeAtIndex(LLVMValueRef, unsigned, LLVMAttributeRef); - -static void llgoAddNonNegativeReturnRange(void *ctx, void *fn, - unsigned kind, unsigned bits) { - uint64_t lower = 0; - uint64_t upper = UINT64_C(1) << (bits - 1); - LLVMAttributeRef attr = LLVMCreateConstantRangeAttribute( - (LLVMContextRef)ctx, kind, bits, &lower, &upper); - LLVMAddAttributeAtIndex((LLVMValueRef)fn, 0, attr); -} -*/ -import "C" - -import ( - "unsafe" - - "github.com/xgo-dev/llvm" -) - -func addNonNegativeReturnRange(ctx llvm.Context, fn llvm.Value, bits int) { - if bits != 32 && bits != 64 { - panic("ssa: unsupported runtime integer width") - } - C.llgoAddNonNegativeReturnRange(unsafe.Pointer(ctx.C), unsafe.Pointer(fn.C), - C.unsigned(llvm.AttributeKindID("range")), C.unsigned(bits)) -}