Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions IRBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include "IRBindings.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Config/llvm-config.h"
#include "llvm/IR/Attributes.h"
#include "llvm/IR/DebugLoc.h"
#include "llvm/IR/DebugInfoMetadata.h"
Expand All @@ -23,6 +24,17 @@

using namespace llvm;

LLVMAttributeRef LLVMGoCreateConstantRangeAttribute(
LLVMContextRef C, unsigned KindID, unsigned NumBits,
const uint64_t *LowerWords, const uint64_t *UpperWords) {
#if LLVM_VERSION_MAJOR >= 19
return LLVMCreateConstantRangeAttribute(C, KindID, NumBits, LowerWords,
UpperWords);
#else
return nullptr;
#endif
}

LLVMMetadataRef LLVMConstantAsMetadata(LLVMValueRef C) {
return wrap(ConstantAsMetadata::get(unwrap<Constant>(C)));
}
Expand Down
4 changes: 4 additions & 0 deletions IRBindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ struct LLVMDebugLocMetadata{

LLVMMetadataRef LLVMConstantAsMetadata(LLVMValueRef Val);

LLVMAttributeRef LLVMGoCreateConstantRangeAttribute(
LLVMContextRef C, unsigned KindID, unsigned NumBits,
const uint64_t *LowerWords, const uint64_t *UpperWords);

LLVMMetadataRef LLVMMDString2(LLVMContextRef C, const char *Str, unsigned SLen);
LLVMMetadataRef LLVMMDNode2(LLVMContextRef C, LLVMMetadataRef *MDs,
unsigned Count);
Expand Down
83 changes: 83 additions & 0 deletions constant_range_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package llvm

import (
"fmt"
"strconv"
"strings"
"testing"
)

func TestConstantRangeAttribute(t *testing.T) {
ctx := NewContext()
defer ctx.Dispose()
kind := AttributeKindID("range")
major, _ := strconv.Atoi(strings.SplitN(Version, ".", 2)[0])
if major < 19 {
if !ctx.CreateConstantRangeAttribute(kind, 32, []uint64{0}, []uint64{1}).IsNil() {
t.Fatal("constant-range attributes must be unavailable before LLVM 19")
}
return
}
if kind == 0 {
t.Fatal("range attribute kind not found")
}
for _, test := range []struct {
bits int
lower, upper []uint64
want string
}{
{1, []uint64{0}, []uint64{1}, "range(i1 0, -1)"},
{32, []uint64{0}, []uint64{1 << 31}, "range(i32 0, -2147483648)"},
{64, []uint64{0}, []uint64{1 << 63}, "range(i64 0, -9223372036854775808)"},
{65, []uint64{3, 0}, []uint64{9, 1}, "range(i65 3, -18446744073709551607)"},
{128, []uint64{3, 2}, []uint64{9, 4}, "range(i128 36893488147419103235, 73786976294838206473)"},
} {
t.Run(fmt.Sprint(test.bits), func(t *testing.T) {
mod := ctx.NewModule("range")
defer mod.Dispose()
fn := AddFunction(mod, "length", FunctionType(ctx.IntType(test.bits), nil, false))
attr := ctx.CreateConstantRangeAttribute(kind, test.bits, test.lower, test.upper)
if attr.IsNil() {
t.Fatal("constant-range attribute is nil")
}
fn.AddAttributeAtIndex(0, attr)
if attrs := fn.GetAttributesAtIndex(0); len(attrs) != 1 || attrs[0] != attr {
t.Fatal("return attribute did not round-trip")
}
// The context owns the APInts; it must not retain Go slice storage.
for i := range test.lower {
test.lower[i] = 0
test.upper[i] = 0
}
if ir := mod.String(); !strings.Contains(ir, test.want) {
t.Fatalf("missing %q:\n%s", test.want, ir)
}
if err := VerifyModule(mod, ReturnStatusAction); err != nil {
t.Fatal(err)
}
})
}
}

func TestConstantRangeAttributeInvalidBounds(t *testing.T) {
ctx := NewContext()
defer ctx.Dispose()
for _, test := range []struct {
bits int
lower, upper []uint64
}{
{0, nil, nil}, {-1, nil, nil},
{32, nil, []uint64{1}}, {32, []uint64{0}, nil},
{65, []uint64{0}, []uint64{1}},
{64, []uint64{0, 0}, []uint64{1}},
} {
t.Run(fmt.Sprint(test.bits, "/", len(test.lower), "/", len(test.upper)), func(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("invalid bounds did not panic")
}
}()
ctx.CreateConstantRangeAttribute(AttributeKindID("range"), test.bits, test.lower, test.upper)
})
}
}
21 changes: 21 additions & 0 deletions ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,27 @@ func (c Context) CreateTypeAttribute(kind uint, t Type) (a Attribute) {
return
}

// CreateConstantRangeAttribute creates a constant-range attribute such as
// "range". Bounds are unsigned words in least-significant-word-first order;
// each slice must contain exactly ceil(numBits/64) words and numBits must be
// positive. Invalid widths or word counts panic. On LLVM before 19, which does

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Doc says pre-19 returns nil, but validation runs on all versions first

The comment states "On LLVM before 19 ... it returns a nil Attribute," but the width/word-count validation (which can panic) runs unconditionally before the version-gated C call. So on a pre-19 build an invalid-argument call panics rather than returning nil. The behavior is arguably better, but the doc reads as a pure "always nil" path. Consider clarifying, e.g. "a nil Attribute is returned for otherwise-valid arguments." Non-blocking.

// not support constant-range attributes, it returns a nil Attribute.
func (c Context) CreateConstantRangeAttribute(kind uint, numBits int, lowerWords, upperWords []uint64) (a Attribute) {
if numBits <= 0 || uint64(numBits) > uint64(^uint32(0)) {
panic("llvm: invalid constant range bit width")
}
nwords := numBits / 64
if numBits%64 != 0 {
nwords++
}
if len(lowerWords) != nwords || len(upperWords) != nwords {
panic("llvm: constant range bounds have incorrect word counts")
}
a.C = C.LLVMGoCreateConstantRangeAttribute(c.C, C.unsigned(kind), C.unsigned(numBits),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Note the nwords>=1 invariant that keeps &lowerWords[0] safe

The &lowerWords[0] / &upperWords[0] dereference is safe today only because numBits > 0 forces nwords >= 1 and the preceding length check guarantees both slices are non-empty. This is a fragile coupling: any future loosening of the validation could silently introduce an out-of-bounds [0] access across the cgo boundary. A one-line comment noting that nwords >= 1 is guaranteed above would protect the invariant. Non-blocking.

(*C.uint64_t)(unsafe.Pointer(&lowerWords[0])), (*C.uint64_t)(unsafe.Pointer(&upperWords[0])))
return
}

func (a Attribute) GetTypeValue() (t Type) {
t.C = C.LLVMGetTypeAttributeValue(a.C)
return
Expand Down
Loading