Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable querying constants and value kinds #936

Merged
merged 11 commits into from
Jul 26, 2023
37 changes: 37 additions & 0 deletions ffi/value.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,43 @@ LLVMPY_DisposeOperandsIter(LLVMOperandsIteratorRef GI) {
delete llvm::unwrap(GI);
}

API_EXPORT(bool)
LLVMPY_IsConstant(LLVMValueRef Val) { return LLVMIsConstant(Val); }

API_EXPORT(const uint64_t *)
LLVMPY_GetConstantIntRawValue(LLVMValueRef Val, bool *littleEndian) {
if (littleEndian) {
*littleEndian = llvm::sys::IsLittleEndianHost;
}
if (llvm::ConstantInt *CI =
llvm::dyn_cast<llvm::ConstantInt>((llvm::Value *)Val)) {
return CI->getValue().getRawData();
}
return nullptr;
}

API_EXPORT(unsigned)
LLVMPY_GetConstantIntNumWords(LLVMValueRef Val) {
if (llvm::ConstantInt *CI =
llvm::dyn_cast<llvm::ConstantInt>((llvm::Value *)Val)) {
return CI->getValue().getNumWords();
}
return 0;
}

API_EXPORT(double)
LLVMPY_GetConstantFPValue(LLVMValueRef Val, bool *losesInfo) {
LLVMBool losesInfo_internal;
double result = LLVMConstRealGetDouble(Val, &losesInfo_internal);
if (losesInfo) {
*losesInfo = losesInfo_internal;
}
return result;
}

API_EXPORT(int)
LLVMPY_GetValueKind(LLVMValueRef Val) { return (int)LLVMGetValueKind(Val); }

API_EXPORT(void)
LLVMPY_PrintValueToString(LLVMValueRef Val, const char **outstr) {
*outstr = LLVMPrintValueToString(Val);
Expand Down
101 changes: 100 additions & 1 deletion llvmlite/binding/value.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from ctypes import POINTER, c_char_p, c_int, c_size_t, c_uint, c_bool, c_void_p
from ctypes import (POINTER, byref, cast, c_char_p, c_double, c_int, c_size_t,
c_uint, c_uint64, c_bool, c_void_p)
import enum
import warnings

from llvmlite.binding import ffi
from llvmlite.binding.common import _decode_string, _encode_string
Expand Down Expand Up @@ -43,6 +45,41 @@ class StorageClass(enum.IntEnum):
dllexport = 2


class ValueKind(enum.IntEnum):
# The LLVMValueKind enum from llvm-c/Core.h

argument = 0
basic_block = 1
memory_use = 2
memory_def = 3
memory_phi = 4

function = 5
global_alias = 6
global_ifunc = 7
global_variable = 8
block_address = 9
constant_expr = 10
constant_array = 11
constant_struct = 12
constant_vector = 13

undef_value = 14
constant_aggregate_zero = 15
constant_data_array = 16
constant_data_vector = 17
constant_int = 18
constant_fp = 19
constant_pointer_null = 20
constant_token_none = 21

metadata_as_value = 22
inline_asm = 23

instruction = 24
poison_value = 25


class TypeRef(ffi.ObjectRef):
"""A weak reference to a LLVM type
"""
Expand Down Expand Up @@ -140,6 +177,14 @@ def is_instruction(self):
def is_operand(self):
return self._kind == 'operand'

@property
def is_constant(self):
tbennun marked this conversation as resolved.
Show resolved Hide resolved
return bool(ffi.lib.LLVMPY_IsConstant(self))

@property
def value_kind(self):
tbennun marked this conversation as resolved.
Show resolved Hide resolved
return ValueKind(ffi.lib.LLVMPY_GetValueKind(self))

@property
def name(self):
return _decode_string(ffi.lib.LLVMPY_GetValueName(self))
Expand Down Expand Up @@ -299,6 +344,43 @@ def opcode(self):
% (self._kind,))
return ffi.ret_string(ffi.lib.LLVMPY_GetOpcodeName(self))

def get_constant_value(self):
tbennun marked this conversation as resolved.
Show resolved Hide resolved
"""
Return the constant value, either as a literal (when supported)
or as a string.
"""
if not self.is_constant:
raise ValueError('expected constant value, got %s'
tbennun marked this conversation as resolved.
Show resolved Hide resolved
% (self._kind,))

if self.value_kind == ValueKind.constant_int:
# Python integers are also arbitrary-precision
little_endian = c_bool(False)
words = ffi.lib.LLVMPY_GetConstantIntNumWords(self)
ptr = ffi.lib.LLVMPY_GetConstantIntRawValue(
self, byref(little_endian))
asbytes = bytes(cast(ptr, POINTER(c_uint64 * words)).contents)
return int.from_bytes(
asbytes,
('little' if little_endian.value else 'big'),
signed=False,
)
elif self.value_kind == ValueKind.constant_fp:
# Convert floating-point values to double (Python float)
accuracy_loss = c_bool(False)
value = ffi.lib.LLVMPY_GetConstantFPValue(self,
byref(accuracy_loss))
if accuracy_loss.value:
warnings.warn(
Copy link
Member

Choose a reason for hiding this comment

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

Needs to add test for the warning

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sklam For some reason, despite how much I try, I can't seem to trigger the losesInfo boolean with a single constant (maybe this appears during passes). Not sure how to proceed here, any suggestions?

Copy link
Member

Choose a reason for hiding this comment

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

Sorry for the delay.

After some digging into the LLVM source, the floating-point constants are encoded as double-precision so there will not be any information loss here. To force info loss, we will need to use bigger types like the long-double. For example: %const = fadd fp128 0xLF3CB1CCF26FBC178452FB4EC7F91DEAD, 0xLF3CB1CCF26FBC178452FB4EC7F91973F. This will require converting the 128-bit float to 64-bit float thus triggering the warning.

However, this brings to my attention that a warning is not appropriate here. This API is not giving user a choice to read float bigger than double-precision. In the LLVM OCAML binding, the similar function will return None (https://github.com/llvm/llvm-project/blob/1defa781243f9d0bc66719465e4de33e9fb7a243/llvm/bindings/ocaml/llvm/llvm_ocaml.c#L1030-L1032). I think the Python API should raise an exception.

Also, maybe it is better to have separate functions for int constant and float constant, so that the float version can provide more control on what to do if information is lost. For instance, if user is okay with the info loss, they can force the downcast.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's great, I agree and already modified the constant float test. Only thing I'm not sure about is the separate functions for int and float constants. One of the main benefits of Python is that you can return multiple types. Maybe this should be another keyword argument, and keyword args should act like preferences?

I am already extending the behavior to support other types of constants (e.g. structs), and it was beneficial to be able to call the same method on the nested data: tbennun@3992ea1#diff-27ab06b9fa73e854c37e5ccf261269588cd8e02fadbd71c788102bc46c4a2f1bR390

Copy link
Member

Choose a reason for hiding this comment

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

Maybe this should be another keyword argument, and keyword args should act like preferences?

Yes, that works too

I am already extending the behavior to support other types of constants (e.g. structs), and it was beneficial to be able to call the same method on the nested data: tbennun@3992ea1#diff-27ab06b9fa73e854c37e5ccf261269588cd8e02fadbd71c788102bc46c4a2f1bR390

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sklam all changes are complete and the PR is ready for re(re)review.

'Accuracy loss encountered in conversion of constant '
f'value {str(self)}'
)

return value

# Otherwise, return the IR string
return str(self)
tbennun marked this conversation as resolved.
Show resolved Hide resolved


class _ValueIterator(ffi.ObjectRef):

Expand Down Expand Up @@ -516,3 +598,20 @@ def _next(self):

ffi.lib.LLVMPY_GetOpcodeName.argtypes = [ffi.LLVMValueRef]
ffi.lib.LLVMPY_GetOpcodeName.restype = c_void_p

ffi.lib.LLVMPY_IsConstant.argtypes = [ffi.LLVMValueRef]
ffi.lib.LLVMPY_IsConstant.restype = c_bool

ffi.lib.LLVMPY_GetValueKind.argtypes = [ffi.LLVMValueRef]
ffi.lib.LLVMPY_GetValueKind.restype = c_int

ffi.lib.LLVMPY_GetConstantIntRawValue.argtypes = [ffi.LLVMValueRef,
POINTER(c_bool)]
ffi.lib.LLVMPY_GetConstantIntRawValue.restype = POINTER(c_uint64)

ffi.lib.LLVMPY_GetConstantIntNumWords.argtypes = [ffi.LLVMValueRef]
ffi.lib.LLVMPY_GetConstantIntNumWords.restype = c_uint

ffi.lib.LLVMPY_GetConstantFPValue.argtypes = [ffi.LLVMValueRef,
POINTER(c_bool)]
ffi.lib.LLVMPY_GetConstantFPValue.restype = c_double
83 changes: 83 additions & 0 deletions llvmlite/tests/test_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ def no_de_locale():
}}
"""

asm_sum3 = r"""
; ModuleID = '<string>'
target triple = "{triple}"

define i64 @sum(i64 %.1, i64 %.2) {{
%.3 = add i64 %.1, %.2
%.4 = add i64 5, %.3
%.5 = add i64 -5, %.4
ret i64 %.5
}}
"""

asm_mul = r"""
; ModuleID = '<string>'
target triple = "{triple}"
Expand Down Expand Up @@ -1280,6 +1292,77 @@ def test_function_attributes(self):
self.assertEqual(list(args[0].attributes), [b'returned'])
self.assertEqual(list(args[1].attributes), [])

def test_value_kind(self):
mod = self.module()
self.assertEqual(mod.get_global_variable('glob').value_kind,
llvm.ValueKind.global_variable)
func = mod.get_function('sum')
self.assertEqual(func.value_kind, llvm.ValueKind.function)
block = list(func.blocks)[0]
self.assertEqual(block.value_kind, llvm.ValueKind.basic_block)
inst = list(block.instructions)[1]
self.assertEqual(inst.value_kind, llvm.ValueKind.instruction)
self.assertEqual(list(inst.operands)[0].value_kind,
llvm.ValueKind.constant_int)
self.assertEqual(list(inst.operands)[1].value_kind,
llvm.ValueKind.instruction)

iasm_func = self.module(asm_inlineasm).get_function('foo')
iasm_inst = list(list(iasm_func.blocks)[0].instructions)[0]
self.assertEqual(list(iasm_inst.operands)[0].value_kind,
llvm.ValueKind.inline_asm)

def test_is_constant(self):
mod = self.module()
self.assertTrue(mod.get_global_variable('glob').is_constant)
constant_operands = 0
for func in mod.functions:
self.assertTrue(func.is_constant)
for block in func.blocks:
self.assertFalse(block.is_constant)
for inst in block.instructions:
self.assertFalse(inst.is_constant)
for op in inst.operands:
if op.is_constant:
constant_operands += 1

self.assertEqual(constant_operands, 1)

def test_constant_int(self):
mod = self.module()
func = mod.get_function('sum')
insts = list(list(func.blocks)[0].instructions)
self.assertEqual(insts[1].opcode, 'add')
operands = list(insts[1].operands)
self.assertTrue(operands[0].is_constant)
self.assertFalse(operands[1].is_constant)
self.assertEqual(operands[0].get_constant_value(), 0)

mod = self.module(asm_sum3)
func = mod.get_function('sum')
insts = list(list(func.blocks)[0].instructions)
posint64 = list(insts[1].operands)[0]
negint64 = list(insts[2].operands)[0]
self.assertEqual(posint64.get_constant_value(), 5)

# Convert from unsigned arbitrary-precision integer to signed i64
as_u64 = negint64.get_constant_value()
as_i64 = int.from_bytes(as_u64.to_bytes(8, 'little'), 'little',
signed=True)
self.assertEqual(as_i64, -5)

def test_constant_fp(self):
mod = self.module(asm_double_locale)
func = mod.get_function('foo')
insts = list(list(func.blocks)[0].instructions)
self.assertEqual(len(insts), 2)
self.assertEqual(insts[0].opcode, 'fadd')
operands = list(insts[0].operands)
self.assertTrue(operands[0].is_constant)
self.assertAlmostEqual(operands[0].get_constant_value(), 0.0)
self.assertTrue(operands[1].is_constant)
self.assertAlmostEqual(operands[1].get_constant_value(), 3.14)


class TestTarget(BaseTest):

Expand Down