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
34 changes: 34 additions & 0 deletions IRBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,50 @@
#include "llvm/ADT/SmallVector.h"
#include "llvm/Config/llvm-config.h"
#include "llvm/IR/Attributes.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/DebugLoc.h"
#include "llvm/IR/DebugInfoMetadata.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include <algorithm>

using namespace llvm;

LLVMValueRef LLVMGoConstFPFromBits(LLVMTypeRef Ty, const uint64_t *Words,
unsigned NumWords) {
if (!Ty || !unwrap(Ty)->isFloatingPointTy())
return nullptr;
auto *T = unwrap(Ty);
unsigned BitWidth = T->getScalarSizeInBits();
if (NumWords != (BitWidth + 63) / 64 || !Words)
return nullptr;
if (BitWidth % 64 && (Words[NumWords - 1] >> (BitWidth % 64)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The high-bit check correctly guards non-canonical inputs and correctly uses BitWidth % 64 to avoid an undefined full-width shift. It relies on getScalarSizeInBits() matching the width of bitcastToAPInt() for every FP semantics (they agree for all current types, including x86_fp80 at 80 bits, which the fp80_unused_high_bits test guards). A one-line comment noting the word count is derived from the scalar/primitive size (not the in-memory size) would help future readers, given the x86_fp80 80-vs-128-bit distinction.

return nullptr;
#if LLVM_VERSION_MAJOR >= 22
return LLVMConstFPFromBits(Ty, Words);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The LLVM_VERSION_MAJOR >= 22 branch delegates to LLVMConstFPFromBits(Ty, Words), which is not defined anywhere in this tree — it's an assumption about a future upstream C API whose exact name and signature can't be verified today. If upstream lands with a different signature (e.g. an added NumWords parameter), this branch will fail to compile once LLVM 22 is adopted. Consider a comment referencing the expected upstream patch so a future maintainer can confirm the signature. (Note: the pre-validation at lines 31-38 still runs before this branch, so input validation is preserved — good.)

#else
return wrap(ConstantFP::get(
T->getContext(), APFloat(T->getFltSemantics(),
APInt(BitWidth, ArrayRef<uint64_t>(Words, NumWords)))));
#endif
}

unsigned LLVMGoConstFPGetBits(LLVMValueRef Val, uint64_t *Words) {
if (!Val)
return 0;
auto *FP = dyn_cast<ConstantFP>(unwrap(Val));
if (!FP)
return 0;
APInt Bits = FP->getValueAPF().bitcastToAPInt();
unsigned NumWords = Bits.getNumWords();
if (Words)
std::copy_n(Bits.getRawData(), NumWords, Words);
return NumWords;
}

LLVMAttributeRef LLVMGoCreateConstantRangeAttribute(
LLVMContextRef C, unsigned KindID, unsigned NumBits,
const uint64_t *LowerWords, const uint64_t *UpperWords) {
Expand Down
4 changes: 4 additions & 0 deletions IRBindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ struct LLVMDebugLocMetadata{
LLVMMetadataRef InlinedAt;
};

LLVMValueRef LLVMGoConstFPFromBits(LLVMTypeRef Ty, const uint64_t *Words,
unsigned NumWords);
unsigned LLVMGoConstFPGetBits(LLVMValueRef Val, uint64_t *Words);

LLVMMetadataRef LLVMConstantAsMetadata(LLVMValueRef Val);

LLVMAttributeRef LLVMGoCreateConstantRangeAttribute(
Expand Down
115 changes: 115 additions & 0 deletions float_bits_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

package llvm

import (
"os"
"path/filepath"
"reflect"
"testing"
)

func TestFloatBits(t *testing.T) {
// Parse independently specified IR, then rebuild in another context. Values
// include precision below float64's significand and NaN payload/signaling bits.
cases := []struct {
name, ir string
words []uint64
}{
{"half", "half 0xH8000", []uint64{0x8000}},
{"bfloat", "bfloat 0xR7FC1", []uint64{0x7fc1}},
{"float_nan", "float 0x7FF82468A0000000", []uint64{0x7fc12345}},
{"double_negative_zero", "double 0x8000000000000000", []uint64{0x8000000000000000}},
{"double_snan", "double 0x7FF0000000000001", []uint64{0x7ff0000000000001}},
{"double_infinity", "double 0x7FF0000000000000", []uint64{0x7ff0000000000000}},
{"double_subnormal", "double 0x0000000000000001", []uint64{1}},
{"fp80_precision", "x86_fp80 0xK3FFF8000000000000001", []uint64{0x8000000000000001, 0x3fff}},
{"fp80_nan", "x86_fp80 0xK7FFFC000000000000123", []uint64{0xc000000000000123, 0x7fff}},
{"fp128_precision", "fp128 0xL00000000000000013FFF000000000000", []uint64{1, 0x3fff000000000000}},
{"fp128_nan", "fp128 0xL00000000000001237FFF800000000000", []uint64{0x123, 0x7fff800000000000}},
{"ppc_fp128_low_only", "ppc_fp128 0xM00000000000000003FF0000000000000", []uint64{0, 0x3ff0000000000000}},
{"ppc_fp128_negative_zero", "ppc_fp128 0xM80000000000000000000000000000000", []uint64{0x8000000000000000, 0}},
{"ppc_fp128_noncanonical", "ppc_fp128 0xM3FF00000000000003FF0000000000000", []uint64{0x3ff0000000000000, 0x3ff0000000000000}},
{"ppc_fp128_nan", "ppc_fp128 0xM7FF80000000001230000000000000000", []uint64{0x7ff8000000000123, 0}},
{"ppc_fp128_precision", "ppc_fp128 0xM3FF00000000000003C90000000000000", []uint64{0x3ff0000000000000, 0x3c90000000000000}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
srcCtx, dstCtx := NewContext(), NewContext()
defer dstCtx.Dispose()
path := filepath.Join(t.TempDir(), "float.ll")
if err := os.WriteFile(path, []byte("@value = constant "+tc.ir+"\n"), 0600); err != nil {
t.Fatal(err)
}
buf, err := NewMemoryBufferFromFile(path)
if err != nil {
t.Fatal(err)
}
src, err := srcCtx.ParseIR(buf)
if err != nil {
t.Fatal(err)
}
value := src.NamedGlobal("value").Initializer()
words := value.FloatBits()
if !reflect.DeepEqual(words, tc.words) {
t.Fatalf("bits = %x, want %x", words, tc.words)
}
// Reparse just to obtain the identical destination-context type.
buf, err = NewMemoryBufferFromFile(path)
if err != nil {
t.Fatal(err)
}
dst, err := dstCtx.ParseIR(buf)
if err != nil {
t.Fatal(err)
}
defer dst.Dispose()
expected := dst.NamedGlobal("value").Initializer()
cloned := ConstFloatFromBits(expected.Type(), words)
if cloned != expected {
t.Fatalf("rebuilt %s, want %s", cloned, expected)
}
src.Dispose()
srcCtx.Dispose()
if got := cloned.FloatBits(); !reflect.DeepEqual(got, tc.words) {
t.Fatalf("after source disposal: %x", got)
}
words[0] ^= 1
if got := cloned.FloatBits(); !reflect.DeepEqual(got, tc.words) {
t.Fatalf("returned slice aliases constant: %x", got)
}
if err := VerifyModule(dst, ReturnStatusAction); err != nil {
t.Fatal(err)
}
})
}
}

func TestFloatBitsInvalidInput(t *testing.T) {
ctx := NewContext()
defer ctx.Dispose()
for name, call := range map[string]func(){
"nil_type": func() { ConstFloatFromBits(Type{}, []uint64{0}) },
"integer_type": func() { ConstFloatFromBits(ctx.Int64Type(), []uint64{0}) },
"vector_type": func() { ConstFloatFromBits(VectorType(ctx.DoubleType(), 2), []uint64{0, 0}) },
"empty": func() { ConstFloatFromBits(ctx.DoubleType(), nil) },
"too_few": func() { ConstFloatFromBits(ctx.FP128Type(), []uint64{0}) },
"too_many": func() { ConstFloatFromBits(ctx.DoubleType(), []uint64{0, 0}) },
"unused_high_bits": func() { ConstFloatFromBits(ctx.FloatType(), []uint64{1 << 32}) },
"fp80_unused_high_bits": func() { ConstFloatFromBits(ctx.X86FP80Type(), []uint64{0, 1 << 16}) },
"nil_value": func() { Value{}.FloatBits() },
"integer_value": func() { ConstInt(ctx.Int64Type(), 0, false).FloatBits() },
"undef": func() { Undef(ctx.DoubleType()).FloatBits() },
} {
t.Run(name, func(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected panic")
}
}()
call()
})
}
}
31 changes: 31 additions & 0 deletions ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,37 @@ func ConstFloatFromString(t Type, str string) (v Value) {
return
}

// ConstFloatFromBits constructs a scalar floating-point constant without rounding
// through float64. words holds the raw representation, least significant word
// first, independently of host byte order. Its length must be ceil(bitWidth/64),
// and unused high bits in the final word must be zero. It panics for an invalid
// type or representation. For ppc_fp128, the first word is the leading double
// and the second word is the trailing double, as in LLVM's APFloat representation.
func ConstFloatFromBits(t Type, words []uint64) (v Value) {
var data *C.uint64_t
if len(words) != 0 {
data = (*C.uint64_t)(unsafe.Pointer(&words[0]))
}
v.C = C.LLVMGoConstFPFromBits(t.C, data, C.uint(len(words)))
if v.IsNil() {
panic("llvm: invalid floating-point type or bit representation")
}
return
}

// FloatBits returns a copy of a scalar floating-point constant's raw APFloat
// representation in least-significant-word-first order. Unused high bits in the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor doc clarity: unlike ConstFloatFromBits, this doc doesn't repeat the ppc_fp128 leading/trailing-double word semantics. Since the two functions are inverses sharing that non-obvious ordering, a brief cross-reference ("see ConstFloatFromBits for ppc_fp128 word semantics") would help a caller reading only FloatBits.

// final word are zero. It panics if v is not a scalar floating-point constant.
func (v Value) FloatBits() []uint64 {
n := C.LLVMGoConstFPGetBits(v.C, nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor performance note (not blocking): FloatBits() calls LLVMGoConstFPGetBits twice, and each call re-runs FP->getValueAPF().bitcastToAPInt(), which allocates a fresh APInt for wide types (fp80/fp128/ppc_fp128). The word count is derivable from the type alone ((getScalarSizeInBits()+63)/64), so the sizing pass doesn't strictly need the full bitcast. Negligible for typical usage; would matter only if called in a tight loop over many constants.

if n == 0 {
panic("llvm: FloatBits requires a scalar floating-point constant")
}
words := make([]uint64, int(n))
C.LLVMGoConstFPGetBits(v.C, (*C.uint64_t)(unsafe.Pointer(&words[0])))
return words
}

func (v Value) ZExtValue() uint64 { return uint64(C.LLVMConstIntGetZExtValue(v.C)) }
func (v Value) SExtValue() int64 { return int64(C.LLVMConstIntGetSExtValue(v.C)) }
func (v Value) DoubleValue() (result float64, inexact bool) {
Expand Down
Loading