diff --git a/llvm_cbuilder/__init__.py b/llvm_cbuilder/__init__.py new file mode 100644 index 0000000..b32e754 --- /dev/null +++ b/llvm_cbuilder/__init__.py @@ -0,0 +1,3 @@ +from .builder import * +from .executor import CExecutor + diff --git a/llvm_cbuilder/builder.py b/llvm_cbuilder/builder.py new file mode 100644 index 0000000..67644db --- /dev/null +++ b/llvm_cbuilder/builder.py @@ -0,0 +1,1383 @@ +# +# TODO: Add support for vector. +# + +import contextlib +import llvm.core as lc +import llvm.ee as le +import llvm.passes as lp +from llvm import LLVMException +from . import shortnames as types + +### +# Utilities +### + +class FunctionAlreadyExists(NameError): + pass + +def _is_int(ty): + return isinstance(ty, lc.IntegerType) + +def _is_real(ty): + tys = [ lc.Type.float(), + lc.Type.double(), + lc.Type.x86_fp80(), + lc.Type.fp128(), + lc.Type.ppc_fp128() ] + return any(ty == x for x in tys) + +def _is_vector(ty, of=None): + if isinstance(ty, lc.VectorType): + if of is not None: + return of(ty.element) + return True + else: + return False + +def _is_pointer(ty): + return isinstance(ty, lc.PointerType) + +def _is_block_terminated(bb): + instrs = bb.instructions + return len(instrs) > 0 and instrs[-1].is_terminator + +def _is_struct(ty): + return isinstance(ty, lc.StructType) + +def _is_cstruct(ty): + try: + return issubclass(ty, CStruct) + except TypeError: + return False + +def _list_values(iterable): + return [i.value for i in iterable] + +def _auto_coerce_index(cbldr, idx): + if not isinstance(idx, CValue): + idx = cbldr.constant(types.int, idx) + return idx + +@contextlib.contextmanager +def _change_block_temporarily(builder, bb): + origbb = builder.basic_block + builder.position_at_end(bb) + yield + builder.position_at_end(origbb) + +@contextlib.contextmanager +def _change_block_temporarily_dummy(*args): + yield + +class CastError(TypeError): + def __init__(self, orig, to): + super(CastError, self).__init__("Cannot cast from %s to %s" % (orig, to)) + +class _IfElse(object): + '''if-else construct. + + Example + ------- + with cbuilder.ifelse(cond) as ifelse: + with ifelse.then(): + # code when cond is true + # this block is mandatory + with ifelse.otherwise(): + # code when cond is false + # this block is optional + ''' + + def __init__(self, parent, cond): + self.parent = parent + self.cond = cond + self._to_close = [] + + @contextlib.contextmanager + def then(self): + self._bbif = self.parent.function.append_basic_block('if.then') + self._bbelse = self.parent.function.append_basic_block('if.else') + + builder = self.parent.builder + builder.cbranch(self.cond.value, self._bbif, self._bbelse) + + builder.position_at_end(self._bbif) + + yield + + self._to_close.extend([self._bbif, self._bbelse, builder.basic_block]) + + @contextlib.contextmanager + def otherwise(self): + builder = self.parent.builder + builder.position_at_end(self._bbelse) + yield + self._to_close.append(builder.basic_block) + + def close(self): + self._to_close.append(self.parent.builder.basic_block) + bbend = self.parent.function.append_basic_block('if.end') + builder = self.parent.builder + closed_count = 0 + for bb in self._to_close: + if not _is_block_terminated(bb): + with _change_block_temporarily(builder, bb): + builder.branch(bbend) + closed_count += 1 + builder.position_at_end(bbend) + if not closed_count: + self.parent.unreachable() + +class _Loop(object): + '''while...do loop. + + Example + ------- + with cbuilder.loop() as loop: + with loop.condition() as setcond: + # Put the condition evaluation here + setcond( cond ) # set loop condition + # Do not put code after setcond(...) + with loop.body(): + # Put the code of the loop body here + + Use loop.break_loop() to break out of the loop. + Use loop.continue_loop() to jump the condition evaulation. + ''' + + def __init__(self, parent): + self.parent = parent + + @contextlib.contextmanager + def condition(self): + builder = self.parent.builder + self._bbcond = self.parent.function.append_basic_block('loop.cond') + self._bbbody = self.parent.function.append_basic_block('loop.body') + self._bbend = self.parent.function.append_basic_block('loop.end') + + builder.branch(self._bbcond) + + builder.position_at_end(self._bbcond) + + def setcond(cond): + builder.cbranch(cond.value, self._bbbody, self._bbend) + + yield setcond + + @contextlib.contextmanager + def body(self): + builder = self.parent.builder + builder.position_at_end(self._bbbody) + yield self + # close last block + if not _is_block_terminated(builder.basic_block): + builder.branch(self._bbcond) + + def break_loop(self): + self.parent.builder.branch(self._bbend) + + def continue_loop(self): + self.parent.builder.branch(self._bbcond) + + def close(self): + builder = self.parent.builder + builder.position_at_end(self._bbend) + +class CBuilder(object): + ''' + A wrapper class for features in llvm-py package + to allow user to use C-like high-level language contruct easily. + ''' + + def __init__(self, function): + '''constructor + + function : is an empty function to be populating. + ''' + self.function = function + self.declare_block = self.function.append_basic_block('decl') + self.first_body_block = self.function.append_basic_block('body') + self.builder = lc.Builder.new(self.first_body_block) + self.target_data = le.TargetData.new(self.function.module.data_layout) + self._auto_inline_list = [] + # Prepare arguments. Make all function arguments behave like variables. + self.args = [] + for arg in function.args: + var = self.var(arg.type, arg, name=arg.name) + self.args.append(var) + + @staticmethod + def new_function(mod, name, ret, args): + '''factory method + + Create a new function in the module and return a CBuilder instance. + ''' + functype = lc.Type.function(ret, args) + func = mod.add_function(functype, name=name) + return CBuilder(func) + + def depends(self, fndecl): + '''add function dependency + + Returns a CFunc instance and define the function if it is not defined. + + fndecl : is a callable that takes a `llvm.core.Module` and returns + a function pointer. + ''' + return CFunc(self, fndecl(self.function.module)) + + def printf(self, fmt, *args): + '''printf() from libc + + fmt : a character string holding printf format string. + *args : additional variable arguments. + ''' + from .libc import LibC + libc = LibC(self) + ret = libc.printf(fmt, *args) + return CTemp(self, ret) + + def debug(self, *args): + '''debug print + + Use printf to dump the values of all arguments. + ''' + type_mapper = { + 'i8' : '%c', + 'i16': '%hd', + 'i32': '%d', + 'i64': '%ld', + 'double': '%e', + } + itemsfmt = [] + items = [] + for i in args: + if isinstance(i, str): + itemsfmt.append(i.replace('%', '%%')) + elif isinstance(i.type, lc.PointerType): + itemsfmt.append("%p") + items.append(i) + else: + tyname = str(i.type) + if tyname == 'float': + # auto convert float to double + ty = '%e' + i = i.cast(types.double) + else: + ty = type_mapper[tyname] + itemsfmt.append(ty) + items.append(i) + fmt = ' '.join(itemsfmt) + '\n' + return self.printf(self.constant_string(fmt), *items) + + def sizeof(self, ty): + bldr = self.builder + ptrty = types.pointer(ty) + first = lc.Constant.null(ptrty) + second = bldr.gep(first, [lc.Constant.int(types.intp, 1)]) + + firstint = bldr.ptrtoint(first, types.intp) + secondint = bldr.ptrtoint(second, types.intp) + diff = bldr.sub(secondint, firstint) + return CTemp(self, diff) + + def min(self, x, y): + z = self.var(x.type) + with self.ifelse( x < y ) as ifelse: + with ifelse.then(): + z.assign(x) + with ifelse.otherwise(): + z.assign(y) + return z + + def var(self, ty, value=None, name=''): + '''allocate variable on the stack + + ty : variable type + value : [optional] initializer value + name : [optional] name used in LLVM IR + ''' + with _change_block_temporarily(self.builder, self.declare_block): + # goto the first block + is_cstruct = _is_cstruct(ty) + if is_cstruct: + cstruct = ty + ty = ty.llvm_type() + ptr = self.builder.alloca(ty, name=name) + # back to the body + if value is not None: + if isinstance(value, CValue): + value = value.value + elif not isinstance(value, lc.Value): + value = self.constant(ty, value).value + self.builder.store(value, ptr) + if is_cstruct: + return cstruct(self, ptr) + else: + return CVar(self, ptr) + + def var_copy(self, val, name=''): + '''allocate a new variable by copying another value + + The new variable has the same type and value of `val`. + ''' + return self.var(val.type, val, name=name) + + def array(self, ty, count, name=''): + '''allocate an array on the stack + + ty : array element type + count : array size; can be python int, llvm.core.Constant, or CValue + name : [optional] name used in LLVM IR + ''' + if isinstance(count, int) or isinstance(count, lc.Constant): + # Only go to the first block if array size is fixed. + contexthelper = _change_block_temporarily + else: + # Do not go to the first block if the array size is dynamic. + contexthelper = _change_block_temporarily_dummy + + with contexthelper(self.builder, self.declare_block): + if _is_cstruct(ty): # array of struct? + cstruct = ty + ty = ty.llvm_type() + + if isinstance(count, CValue): + count = count.value + elif not isinstance(count, lc.Value): + count = self.constant(types.int, count).value + + ptr = self.builder.alloca_array(ty, count, name=name) + return CArray(self, ptr) + + def ret(self, val=None): + '''insert return statement + + val : if is `None`, insert return-void + else, return `val` + ''' + retty = self.function.type.pointee.return_type + if val is not None: + if val.type != retty: + errmsg = "Return type mismatch" + raise TypeError(errmsg) + self.builder.ret(val.value) + else: + if retty != types.void: + # errmsg = "Cannot return void" + # raise TypeError(errmsg, retty) + bb = self.function.append_basic_block('unreachable_bb') + self.builder.position_at_end(bb) + self.builder.ret(self.builder.alloca(retty, name='undef')) + else: + self.builder.ret_void() + + @contextlib.contextmanager + def ifelse(self, cond): + '''start a if-else block + + cond : branch condition + ''' + cb = _IfElse(self, cond) + yield cb + cb.close() + + @contextlib.contextmanager + def loop(self): + '''start a loop block + ''' + cb = _Loop(self) + yield cb + cb.close() + + @contextlib.contextmanager + def forever(self): + '''start a forever loop block + ''' + with self.loop() as loop: + with loop.condition() as setcond: + NULL = self.constant_null(types.int) + setcond( NULL == NULL ) + with loop.body(): + yield loop + + @contextlib.contextmanager + def for_range(self, *args): + '''start a for-range block. + + *args : same as arguments of builtin `range()` + ''' + def check_arg(x): + if isinstance(x, int): + return self.constant(types.int, x) + if not isinstance(x, IntegerValue): + raise TypeError(x, "All args must be of integer type.") + return x + + if len(args) == 3: + start, stop, step = map(check_arg, args) + elif len(args) == 2: + start, stop = map(check_arg, args) + step = self.constant(start.type, 1) + elif len(args) == 1: + stop = check_arg(args[0]) + start = self.constant(stop.type, 0) + step = self.constant(stop.type, 1) + else: + raise TypeError("Invalid # of arguments: 1, 2 or 3") + + idx = self.var_copy(start) + with self.loop() as loop: + with loop.condition() as setcond: + setcond( idx < stop ) + with loop.body(): + yield loop, idx + idx += step + + def position_at_end(self, bb): + '''reposition inserter to the end of basic-block + + bb : a basic block + ''' + self.basic_block = bb + self.builder.position_at_end(bb) + + def close(self): + '''end code generation + ''' + # Close declaration block + with _change_block_temporarily(self.builder, self.declare_block): + self.builder.branch(self.first_body_block) + + # Do the auto inlining + for callinst in self._auto_inline_list: + lc.inline_function(callinst) + + def constant(self, ty, val): + '''create a constant + + ty : data type + val : initializer + ''' + if isinstance(ty, lc.IntegerType): + res = lc.Constant.int(ty, val) + elif ty == types.float or ty == types.double: + res = lc.Constant.real(ty, val) + else: + raise TypeError("Cannot auto build constant " + "from %s and value %s" % (ty, val)) + return CTemp(self, res) + + def constant_null(self, ty): + '''create a zero filled constant + + ty : data type + ''' + res = lc.Constant.null(ty) + return CTemp(self, res) + + def constant_string(self, string): + '''create a constant string + + This will de-duplication string of same content to minimize memory use. + ''' + mod = self.function.module + collision = 0 + name_fmt = '.conststr.%x.%x' + content = lc.Constant.stringz(string) + while True: + name = name_fmt % (hash(string), collision) + try: + # check if the name already exists + globalstr = mod.get_global_variable_named(name) + except LLVMException: + # new constant string + globalstr = mod.add_global_variable(content.type, name=name) + globalstr.initializer = content + globalstr.global_constant = True + else: + # compare existing content + existed = str(globalstr.initializer) + if existed != str(content): + collision += 1 + continue # loop until we resolve the name collision + + return CTemp(self, globalstr.bitcast( + types.pointer(content.type.element))) + + + def get_intrinsic(self, intrinsic_id, tys): + '''get intrinsic function + + intrinsic_id : numerical ID of target intrinsic + tys : type argument for the intrinsic + ''' + lfunc = lc.Function.intrinsic(self.function.module, intrinsic_id, tys) + return CFunc(self, lfunc) + + def get_function_named(self, name): + '''get function by name + ''' + m = self.function.module + func = m.get_function_named(name) + return CFunc(self, func) + + def is_terminated(self): + '''is the current basic-block terminated? + ''' + return _is_block_terminated(self.builder.basic_block) + + def atomic_cmpxchg(self, ptr, old, val, ordering, crossthread=True): + '''atomic compare-exchange + + ptr : pointer to data + old : old value to compare to + val : new value + ordering : memory ordering as a string + crossthread : set to `False` for single-thread code + + Returns the old value on success. + ''' + res = self.builder.atomic_cmpxchg(ptr.value, old.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_xchg(self, ptr, val, ordering, crossthread=True): + '''atomic exchange + + ptr : pointer to data + val : new value + ordering : memory ordering as a string + crossthread : set to `False` for single-thread code + + Returns the old value + ''' + + res = self.builder.atomic_xchg(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_add(self, ptr, val, ordering, crossthread=True): + '''atomic add + + ptr : pointer to data + val : new value + ordering : memory ordering as a string + crossthread : set to `False` for single-thread code + + Returns the computation result of the operation + ''' + + res = self.builder.atomic_add(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_sub(self, ptr, val, ordering, crossthread=True): + '''atomic sub + + See `atomic_add` for parameters documentation + ''' + res = self.builder.atomic_sub(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_and(self, ptr, val, ordering, crossthread=True): + '''atomic bitwise and + + See `atomic_add` for parameters documentation + ''' + res = self.builder.atomic_and(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_nand(self, ptr, val, ordering, crossthread=True): + '''atomic bitwise nand + + See `atomic_add` for parameters documentation + ''' + res = self.builder.atomic_nand(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_or(self, ptr, val, ordering, crossthread=True): + '''atomic bitwise or + + See `atomic_add` for parameters documentation + ''' + res = self.builder.atomic_or(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_xor(self, ptr, val, ordering, crossthread=True): + '''atomic bitwise xor + + See `atomic_add` for parameters documentation + ''' + + res = self.builder.atomic_xor(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_max(self, ptr, val, ordering, crossthread=True): + '''atomic signed maximum between value at `ptr` and `val` + + See `atomic_add` for parameters documentation + ''' + res = self.builder.atomic_max(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_min(self, ptr, val, ordering, crossthread=True): + '''atomic signed minimum between value at `ptr` and `val` + + See `atomic_add` for parameters documentation + ''' + res = self.builder.atomic_min(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_umax(self, ptr, val, ordering, crossthread=True): + '''atomic unsigned maximum between value at `ptr` and `val` + + See `atomic_add` for parameters documentation + ''' + res = self.builder.atomic_umax(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_umin(self, ptr, val, ordering, crossthread=True): + '''atomic unsigned minimum between value at `ptr` and `val` + + See `atomic_add` for parameters documentation + ''' + res = self.builder.atomic_umin(ptr.value, val.value, + ordering, crossthread) + return CTemp(self, res) + + def atomic_load(self, ptr, ordering, align=1, crossthread=True): + '''atomic load + + ptr : pointer to the value to load + align : memory alignment in bytes + See `atomic_add` for other documentation of other parameters + ''' + res = self.builder.atomic_load(ptr.value, ordering, align, crossthread) + return CTemp(self, res) + + def atomic_store(self, val, ptr, ordering, align=1, crossthread=True): + '''atomic store + + ptr : pointer to where to store + val : value to store + align : memory alignment in bytes + See `atomic_add` for other documentation of other parameters + ''' + + res = self.builder.atomic_store(val.value, ptr.value, ordering, + align, crossthread) + return CTemp(self, res) + + def fence(self, ordering, crossthread=True): + '''insert memory fence + ''' + res = self.builder.fence(ordering, crossthread) + return CTemp(self, res) + + def alignment(self, ty): + '''get minimum alignment of `ty` + ''' + return self.abi.abi_alignment(ty) + + @property + def abi(self): + return self.target_data + + def unreachable(self): + '''insert instruction that causes segfault some platform (Intel), + or no-op on others. + + It has no defined semantic. + ''' + self.builder.unreachable() + + def add_auto_inline(self, callinst): + self._auto_inline_list.append(callinst) + + + def set_memop_non_temporal(self, ldst): + const_one = self.constant(types.int, 1).value + md = lc.MetaData.get(self.function.module, [const_one]) + ldst.set_metadata('nontemporal', md) + + +class CFuncRef(object): + '''create a function reference to use with `CBuilder.depends` + + Either from name, type and pointer, + Or from a llvm.core.FunctionType instance + ''' + def __init__(self, *args, **kwargs): + def one_arg(fn): + self._fn = fn + self._name = fn.name + + def three_arg(name, ty, ptr): + self._name = name + self._type = ty + self._ptr = ptr + + try: + three_arg(*args, **kwargs) + self._meth = self._from_pointer + except TypeError: + one_arg(*args, **kwargs) + self._meth = self._from_func + + def __call__(self, module): + return self._meth() + + def _from_func(self): + return self._fn + + def _from_pointer(self): + fnptr = types.pointer(self._type) + ptr = lc.Constant.int(types.intp, self._ptr) + ptr = ptr.inttoptr(fnptr) + return ptr + + def __str__(self): + return self._name + + +class CDefinition(object): + '''represents function definition + + Inherit from this class to create a new function definition. + + Class Members + ------------- + _name_ : name of the function + _retty_ : return type + _argtys_ : argument names and types as list of tuples; + e.g. [ ( 'myarg', lc.Type.int() ), ... ] + ''' + _name_ = '' # name of the function; should overide in subclass + _retty_ = types.void # return type; can overide in subclass + _argtys_ = [] # a list of tuple(name, type, [attributes]); can overide in subclass + + def __init__(self, *args, **kwargs): + self.specialize(*args, **kwargs) + self.cbuilder = None + + def specialize(self, *args, **kwargs): + """ + Override in subclasses + """ + + def specialize_name(self): + """ + Specialize the class name to enable multiple function definitions + """ + cls = type(self) + + counter = getattr(cls, 'counter', 0) + cls._name_ = "%s_%d" % (cls._name_, counter) + cls.counter = counter + 1 + + def define(self, module): + '''define the function in the module. + + Raises NameError if a function of the same name has already been + defined. + ''' + functype = lc.Type.function(self._retty_, [arg[1] for arg in self._argtys_]) + name = self._name_ + if not name: + raise AttributeError("Function name cannot be empty.") + + func = module.get_or_insert_function(functype, name=name) + + if not func.is_declaration: # already defined? + raise FunctionAlreadyExists(func) + + # Name all arguments + for i, arginfo in enumerate(self._argtys_): + name = arginfo[0] + func.args[i].name = name + if len(arginfo) > 2: + for attr in arginfo[2]: + func.args[i].add_attribute(attr) + + # Create builder and populate body + self.cbuilder = CBuilder(func) + self.body(*self.cbuilder.args) + self.cbuilder.close() + self.cbuilder = None + + # optimize + fpm = lp.FunctionPassManager.new(module) + pmb = lp.PassManagerBuilder.new() + pmb.opt_level = 3 + pmb.vectorize = True + pmb.populate(fpm) + fpm.run(func) + return func + + def __call__(self, module): + # We don't really have to overload __call__ to do things like + # defining functions... + try: + func = self.define(module) + except FunctionAlreadyExists as e: + (func,) = e + return func + + def __getattr__(self, attr): + return getattr(self.cbuilder, attr) + + def body(self): + '''overide this function to define the body. + ''' + raise NotImplementedError + + def __str__(self): + return self._name_ + +class CValue(object): + def __init__(self, parent, handle): + self.__parent = parent + self.__handle = handle + + @property + def handle(self): + return self.__handle + + @property + def parent(self): + return self.__parent + + def _temp(self, val): + return CTemp(self.parent, val) + +def _get_operator_provider(ty): + if _is_pointer(ty): + return PointerValue + elif _is_int(ty): + return IntegerValue + elif _is_real(ty): + return RealValue + elif _is_vector(ty): + inner = _get_operator_provider(ty.element) + return type(str(ty), (inner, VectorIndexing), {}) + elif _is_struct(ty): + return StructValue + else: + assert False, (str(ty), type(ty)) + +class CTemp(CValue): + def __new__(cls, parent, handle): + meta = _get_operator_provider(handle.type) + base = type(str('%s_%s' % (cls.__name__, handle.type)), (cls, meta), {}) + return object.__new__(base) + + def __init__(self, *args, **kws): + super(CTemp, self).__init__(*args, **kws) + self._init_mixin() + + @property + def value(self): + return self.handle + + @property + def type(self): + return self.value.type + +class CVar(CValue): + def __new__(cls, parent, ptr): + meta = _get_operator_provider(ptr.type.pointee) + base = type(str('%s_%s' % (cls.__name__, ptr.type.pointee)), (cls, meta), {}) + return object.__new__(base) + + def __init__(self, parent, ptr): + super(CVar, self).__init__(parent, ptr) + self._init_mixin() + self.invariant = False + + def reference(self): + return self._temp(self.handle) + + @property + def ref(self): + return self.reference() + + @property + def value(self): + return self.parent.builder.load(self.ref.value) + + @property + def type(self): + return self.ref.type.pointee + + def assign(self, val, **kws): + if self.invariant: + raise TypeError("Storing to invariant variable.") + self.parent.builder.store(val.value, self.ref.value, **kws) + return self + + def __iadd__(self, rhs): + return self.assign(self.__add__(rhs)) + + def __isub__(self, rhs): + return self.assign(self.__sub__(rhs)) + + def __imul__(self, rhs): + return self.assign(self.__mul__(rhs)) + + def __idiv__(self, rhs): + return self.assign(self.__div__(rhs)) + + def __imod__(self, rhs): + return self.assign(self.__mod__(rhs)) + + def __ilshift__(self, rhs): + return self.assign(self.__lshift__(rhs)) + + def __irshift__(self, rhs): + return self.assign(self.__rshift__(rhs)) + + def __iand__(self, rhs): + return self.assign(self.__and__(rhs)) + + def __ior__(self, rhs): + return self.assign(self.__or__(rhs)) + + def __ixor__(self, rhs): + return self.assign(self.__xor__(rhs)) + + +class OperatorMixin(object): + def _init_mixin(self): + pass + +class IntegerValue(OperatorMixin): + + def _init_mixin(self): + self._unsigned = False + + def _get_unsigned(self): + return self._unsigned + + def _set_unsigned(self, unsigned): + self._unsigned = bool(unsigned) + + unsigned = property(_get_unsigned, _set_unsigned) + + def __add__(self, rhs): + return self._temp(self.parent.builder.add(self.value, rhs.value)) + + def __sub__(self, rhs): + return self._temp(self.parent.builder.sub(self.value, rhs.value)) + + def __mul__(self, rhs): + return self._temp(self.parent.builder.mul(self.value, rhs.value)) + + def __div__(self, rhs): + if self.unsigned: + return self._temp(self.parent.builder.udiv(self.value, rhs.value)) + else: + return self._temp(self.parent.builder.sdiv(self.value, rhs.value)) + + def __mod__(self, rhs): + if self.unsigned: + return self._temp(self.parent.builder.urem(self.value, rhs.value)) + else: + return self._temp(self.parent.builder.srem(self.value, rhs.value)) + + def __ilshift__(self, rhs): + return self._temp(self.parent.builder.shl(self.value, rhs.value)) + + def __irshift__(self, rhs): + if self.unsigned: + return self._temp(self.self.parent.builder.lshr(self.value, rhs.value)) + else: + return self._temp(self.parent.builder.ashr(self.value, rhs.value)) + + def __iand__(self, rhs): + return self._temp(self.parent.builder.and_(self.value, rhs.value)) + + def __ior__(self, rhs): + return self._temp(self.parent.builder.or_(self.value, rhs.value)) + + def __ixor__(self, rhs): + return self._temp(self.parent.builder.xor(self.value, rhs.value)) + + def __lt__(self, rhs): + if self.unsigned: + return self._temp(self.parent.builder.icmp(lc.ICMP_ULT, self.value, rhs.value)) + else: + return self._temp(self.parent.builder.icmp(lc.ICMP_SLT, self.value, rhs.value)) + + def __le__(self, rhs): + if self.unsigned: + return self._temp(self.parent.builder.icmp(lc.ICMP_ULE, self.value, rhs.value)) + else: + return self._temp(self.parent.builder.icmp(lc.ICMP_SLE, self.value, rhs.value)) + + def __eq__(self, rhs): + return self._temp(self.parent.builder.icmp(lc.ICMP_EQ, self.value, rhs.value)) + + def __ne__(self, rhs): + return self._temp(self.parent.builder.icmp(lc.ICMP_NE, self.value, rhs.value)) + + def __gt__(self, rhs): + if self.unsigned: + return self._temp(self.parent.builder.icmp(lc.ICMP_UGT, self.value, rhs.value)) + else: + return self._temp(self.parent.builder.icmp(lc.ICMP_SGT, self.value, rhs.value)) + + def __ge__(self, rhs): + if self.unsigned: + return self._temp(self.parent.builder.icmp(lc.ICMP_UGE, self.value, rhs.value)) + else: + return self._temp(self.parent.builder.icmp(lc.ICMP_SGE, self.value, rhs.value)) + + def __neg__(self): + return self._temp(self.parent.builder.neg(self.value)) + + def cast(self, ty, unsigned=False): + if ty == self.type: + return self._temp(self.value) + + if _is_real(ty): + if self.unsigned or unsigned: + return self._temp(self.parent.builder.uitofp(self.value, ty)) + else: + return self._temp(self.parent.builder.sitofp(self.value, ty)) + elif _is_int(ty): + if self.parent.abi.size(self.type) < self.parent.abi.size(ty): + if self.unsigned or unsigned: + return self._temp(self.parent.builder.zext(self.value, ty)) + else: + return self._temp(self.parent.builder.sext(self.value, ty)) + else: + return self._temp(self.parent.builder.trunc(self.value, ty)) + elif _is_pointer(ty) and _is_int(self.type): + return self._temp(self.parent.builder.inttoptr(self.value, ty)) + elif _is_int(ty) and _is_pointer(self.type): + return self._temp(self.parent.builder.ptrtoint(self.value, ty)) + + raise CastError(self.type, ty) + +class RealValue(OperatorMixin): + def __add__(self, rhs): + return self._temp(self.parent.builder.fadd(self.value, rhs.value)) + + def __sub__(self, rhs): + return self._temp(self.parent.builder.fsub(self.value, rhs.value)) + + def __mul__(self, rhs): + return self._temp(self.parent.builder.fmul(self.value, rhs.value)) + + def __div__(self, rhs): + return self._temp(self.parent.builder.fdiv(self.value, rhs.value)) + + def __mod__(self, rhs): + return self._temp(self.parent.builder.frem(self.value, rhs.value)) + + def __lt__(self, rhs): + return self._temp(self.parent.builder.fcmp(lc.FCMP_OLT, self.value, rhs.value)) + + def __le__(self, rhs): + return self._temp(self.parent.builder.fcmp(lc.FCMP_OLE, self.value, rhs.value)) + + def __eq__(self, rhs): + return self._temp(self.parent.builder.fcmp(lc.FCMP_OEQ, self.value, rhs.value)) + + def __ne__(self, rhs): + return self._temp(self.parent.builder.fcmp(lc.FCMP_ONE, self.value, rhs.value)) + + def __gt__(self, rhs): + return self._temp(self.parent.builder.fcmp(lc.FCMP_OGT, self.value, rhs.value)) + + def __ge__(self, rhs): + return self._temp(self.parent.builder.fcmp(lc.FCMP_OGE, self.value, rhs.value)) + + def cast(self, ty, unsigned=False): + if ty == self.type: + return self._temp(self.value) + + if _is_int(ty): + if unsigned: + return self._temp(self.parent.builder.fptoui(self.value, ty)) + else: + return self._temp(self.parent.builder.fptosi(self.value, ty)) + + if _is_real(ty): + if self.parent.abi.size(self.type) > self.parent.abi.size(ty): + return self._temp(self.parent.builder.fptrunc(self.value, ty)) + else: + return self._temp(self.parent.builder.fpext(self.value, ty)) + + raise CastError(self.type, ty) + + +class PointerIndexing(OperatorMixin): + def __getitem__(self, idx): + '''implement access indexing + + Uses GEP. + ''' + bldr = self.parent.builder + if type(idx) is slice: + # just handle case by case + # Case #1: A[idx:] get pointer offset by idx + if not idx.step and not idx.stop: + idx = _auto_coerce_index(self.parent, idx.start) + ptr = bldr.gep(self.value, [idx.value]) + return CArray(self.parent, ptr) + else: # return an variable at idx + idx = _auto_coerce_index(self.parent, idx) + ptr = bldr.gep(self.value, [idx.value]) + return CVar(self.parent, ptr) + + def __setitem__(self, idx, val): + idx = _auto_coerce_index(self.parent, idx) + bldr = self.parent.builder + self[idx].assign(val) + +class PointerCasting(OperatorMixin): + def cast(self, ty): + if ty == self.type: + return self._temp(self.value) + + if _is_pointer(ty): + return self._temp(self.parent.builder.bitcast(self.value, ty)) + raise CastError(self.type, ty) + + + +class VectorIndexing(OperatorMixin): + def __getitem__(self, idx): + '''implement access indexing + + Uses GEP. + ''' + bldr = self.parent.builder + idx = _auto_coerce_index(self.parent, idx) + val = bldr.extract_element(self.value, idx.value) + return CTemp(self.parent, val) + + def __setitem__(self, idx, val): + idx = _auto_coerce_index(self.parent, idx) + bldr = self.parent.builder + vec = bldr.insert_element(self.value, val.value, idx.value) + bldr.store(vec, self.ref.value) + +class PointerValue(PointerIndexing, PointerCasting): + + def load(self, **kws): + return self._temp(self.parent.builder.load(self.value, **kws)) + + def store(self, val, nontemporal=False, **kws): + inst = self.parent.builder.store(val.value, self.value, **kws) + if nontemporal: + self.parent.set_memop_non_temporal(inst) + + + def atomic_load(self, ordering, align=None, crossthread=True): + '''atomic load memory for pointer types + + align : overide to control memory alignment; otherwise the default + alignment of the type is used. + + Other parameters are the same as `CBuilder.atomic_load` + ''' + if align is None: + align = self.parent.alignment(self.type.pointee) + inst = self.parent.builder.atomic_load(self.value, ordering, align, + crossthread=crossthread) + return self._temp(inst) + + def atomic_store(self, value, ordering, align=None, crossthread=True): + '''atomic memory store for pointer types + + align : overide to control memory alignment; otherwise the default + alignment of the type is used. + + Other parameters are the same as `CBuilder.atomic_store` + ''' + if align is None: + align = self.parent.alignment(self.type.pointee) + self.parent.builder.atomic_store(value.ptr, self.value, ordering, + align=align, crossthread=crossthread) + + def atomic_cmpxchg(self, old, new, ordering, crossthread=True): + '''atomic compare-exchange for pointer types + + Other parameters are the same as `CBuilder.atomic_cmpxchg` + ''' + inst = self.parent.builder.atomic_cmpxchg(self.value, old.value, + new.value, ordering, + crossthread=crossthread) + return self._temp(inst) + + def as_struct(self, cstruct_class, volatile=False): + '''load a pointer to a structure and assume a structure interface + ''' + ptr = self.parent.builder.load(self.value, volatile=volatile) + return cstruct_class(self.parent, self.value) + +class StructValue(OperatorMixin): + + def as_struct(self, cstruct_class): + '''assume a structure interface + ''' + return cstruct_class(self.parent, self.ref.value) + + +class CFunc(CValue, PointerCasting): + '''Wraps function pointer + ''' + def __init__(self, parent, func): + super(CFunc, self).__init__(parent, func) + + @property + def function(self): + return self.handle + + def __call__(self, *args, **opts): + '''Call the function with the given arguments + + *args : variable arguments of CValue instances + ''' + arg_values = _list_values(args) + ftype = self.function.type.pointee + for i, (exp, got) in enumerate(zip(ftype.args, arg_values)): + if exp != got.type: + raise TypeError("At call to %s, " + "argument %d mismatch: %s != %s" + % (self.function.name, i, exp, got.type)) + res = self.parent.builder.call(self.function, arg_values) + + if hasattr(self.function, 'calling_convention'): + res.calling_convention = self.function.calling_convention + + if opts.get('inline'): + self.parent.add_auto_inline(res) + + if ftype.return_type != lc.Type.void(): + return CTemp(self.parent, res) + + @property + def value(self): + return self.function + + @property + def type(self): + return self.function.type + + +class CArray(CValue, PointerIndexing, PointerCasting): + '''wraps a array + + Similar to C arrays + ''' + def __init__(self, parent, base): + super(CArray, self).__init__(parent, base) + + @property + def value(self): + return self.handle + + def reference(self): + return self._temp(self.value) + + @property + def type(self): + return self.value.type + + def vector_load(self, count, align=0): + parent = self.parent + builder = parent.builder + values = [self[i] for i in range(count)] + + vecty = types.vector(self.type.pointee, count) + vec = builder.load(builder.bitcast(self.value, types.pointer(vecty)), + align=align) + return self._temp(vec) + + def vector_store(self, vec, align=0): + if vec.type.element != self.type.pointee: + raise TypeError("Type mismatch; expect %s but got %s" % \ + (vec.type.element, self.type.pointee)) + parent = self.parent + builder = parent.builder + vecptr = builder.bitcast(self.value, types.pointer(vec.type)) + builder.store(vec.value, vecptr, align=align) + return self + + +class CStruct(CValue): + '''Wraps a structure + + Structure in LLVM can be identified by name of layout. + + Subclass to define a new structure. All fields are defined in the + `_fields_` class attribute as a list of tuple (name, type). + + Can define new methods which gets inlined to the parent CBuilder. + ''' + + @classmethod + def llvm_type(cls): + return lc.Type.struct([v for k, v in cls._fields_]) + + def __init__(self, parent, ptr): + super(CStruct, self).__init__(parent, ptr) + makeind = lambda x: self.parent.constant(types.int, x).value + + for i, (fd, _) in enumerate(self._fields_): + gep = self.parent.builder.gep(ptr, [makeind(0), makeind(i)]) + gep.name = "%s.%s" % (type(self).__name__, fd) + if hasattr(self, fd): + raise AttributeError("Field name shadows another attribute") + setattr(self, fd, CVar(self.parent, gep)) + + self.type = self.llvm_type() + + def reference(self): + return self._temp(self.handle) + + +class CExternal(object): + '''subclass to define external interface + + All class attributes that are `llvm.core.FunctionType` are converted + to `CFunc` instance during instantiation. + ''' + + _calling_convention_ = None # default + + def __init__(self, cbuilder): + is_func = lambda x: isinstance(x, lc.FunctionType) + non_magic = lambda s: not ( s.startswith('__') and s.endswith('__') ) + + to_declare = [] + for fname in filter(non_magic, vars(type(self))): + ftype = getattr(self, fname) + if is_func(ftype): + to_declare.append((fname, ftype)) + + mod = cbuilder.function.module + for fname, ftype in to_declare: + func = mod.get_or_insert_function(ftype, name=fname) + if self._calling_convention_: + func.calling_convention = self._calling_convention_ + + if func.type.pointee != ftype: + raise NameError("Function has already been declared " + "with a different type: %s != %s" + % (func.type, ftype) ) + setattr(self, fname, CFunc(cbuilder, func)) + + diff --git a/llvm_cbuilder/executor.py b/llvm_cbuilder/executor.py new file mode 100644 index 0000000..fedaeae --- /dev/null +++ b/llvm_cbuilder/executor.py @@ -0,0 +1,67 @@ +''' +This is mostly a convenience module for testing with ctypes. +''' + +from llvm.core import Type, Module +import llvm.ee as le +import ctypes as ct + +MAP_CTYPES = { +'void' : None, +'bool' : ct.c_bool, +'char' : ct.c_char, +'uchar' : ct.c_ubyte, +'short' : ct.c_short, +'ushort' : ct.c_ushort, +'int' : ct.c_int, +'uint' : ct.c_uint, +'long' : ct.c_long, +'ulong' : ct.c_ulong, + +'int8' : ct.c_int8, +'uint8' : ct.c_uint8, +'int16' : ct.c_int16, +'uint16' : ct.c_uint16, +'int32' : ct.c_int32, +'uint32' : ct.c_uint32, +'int64' : ct.c_int64, +'uint64' : ct.c_uint64, + +'float' : ct.c_float, +'double' : ct.c_double, +'longdouble' : ct.c_longdouble, +} + +class CExecutor(object): + '''a convenient class for creating ctype functions from LLVM modules + ''' + def __init__(self, mod_or_engine): + if isinstance(mod_or_engine, Module): + self.engine = le.EngineBuilder.new(mod_or_engine).opt(3).create() + else: + self.engine = mod_or_engine + + def get_ctype_function(self, fn, *typeinfo): + '''create a ctype function from a LLVM function + + typeinfo : string of types (see `MAP_CTYPES`) or + list of ctypes datatype. + First value is the return type. + A function that takes no argument and return nothing + should use `"void"` or `None`. + ''' + if len(typeinfo)==1 and isinstance(typeinfo[0], str): + types = [ MAP_CTYPES[s.strip()] for s in typeinfo[0].split(',') ] + if not types: + retty = None + argtys = [] + else: + retty = types[0] + argtys = types[1:] + else: + retty = typeinfo[0] + argtys = typeinfo[1:] + prototype = ct.CFUNCTYPE(retty, *argtys) + fnptr = self.engine.get_pointer_to_function(fn) + return prototype(fnptr) + diff --git a/llvm_cbuilder/libc.py b/llvm_cbuilder/libc.py new file mode 100644 index 0000000..8d4a5c1 --- /dev/null +++ b/llvm_cbuilder/libc.py @@ -0,0 +1,8 @@ +from .builder import CExternal +import llvm.core as lc +from . import shortnames as types + +class LibC(CExternal): + printf = lc.Type.function(types.int, [types.char_p], True) + # TODO a lot more to add + diff --git a/llvm_cbuilder/shortnames.py b/llvm_cbuilder/shortnames.py new file mode 100644 index 0000000..2acd69d --- /dev/null +++ b/llvm_cbuilder/shortnames.py @@ -0,0 +1,41 @@ +from numba.minivect import minitypes +import numba.decorators +from llvm.core import Type + +void = Type.void() +char = Type.int(8) +short = Type.int(16) +int = Type.int(32) +int16 = short +int32 = int +int64 = Type.int(64) + +float = Type.float() +double = Type.double() + +npy_intp = minitypes.npy_intp.to_llvm(numba.decorators.context) +py_ssize_t = minitypes.Py_ssize_t.to_llvm(numba.decorators.context) + +# pointers + +pointer = Type.pointer + +void_p = pointer(char) +char_p = pointer(char) +npy_intp_p = pointer(npy_intp) + +# platform dependent + +def _determine_pointer_size(): + from ctypes import sizeof, c_void_p + return sizeof(c_void_p) * 8 + +pointer_size = _determine_pointer_size() + +intp = {32: int32, 64: int64}[pointer_size] + + +# vector +def vector(ty, ct): + return Type.vector(ty, 4) + diff --git a/llvm_cbuilder/translator.py b/llvm_cbuilder/translator.py new file mode 100644 index 0000000..8fef17b --- /dev/null +++ b/llvm_cbuilder/translator.py @@ -0,0 +1,194 @@ +# A handy translator that converts control flow into the appropriate +# llvm_cbuilder constructs +from numba.functions import _get_ast, fix_ast_lineno +import inspect, functools, ast +import logging + +logger = logging.getLogger(__name__) + +def translate(func): + # TODO use meta package + wrapper = functools.wraps(func) + caller_frame = inspect.currentframe().f_back + tree = _get_ast(func) + tree = ast.Module(body=tree.body) + tree = ExpandControlFlow().visit(tree) + fix_ast_lineno(tree) + + # prepare locals for execution + local_dict = locals() + local_dict.update(caller_frame.f_locals) + local_dict.update(caller_frame.f_globals) + + try: + compiled = compile(tree, '', 'exec') + return eval(compiled) + except Exception, e: + logger.debug(ast.dump(tree)) + from ArminRonacher import codegen # uses Armin Ronacher's codegen to debug + # http://dev.pocoo.org/hg/sandbox/file/852a1248c8eb/ast/codegen.py + logger.debug(codegen.to_source(tree)) + raise + + +_if_else_template = ''' +with self.ifelse(__CONDITION__) as _ifelse_: + with _ifelse_.then(): + __THEN__ + with _ifelse_.otherwise(): + __OTHERWISE__ +''' + +_while_template = ''' +with self.loop() as _loop_: + with _loop_.condition() as _setcond_: + _setcond_(__CONDITION__) + with _loop_.body(): + __BODY__ +''' + +_for_range_template = ''' +with self.for_range(*__ARGS__) as (_loop_, __ITER__): + __BODY__ +''' + +_return_template = 'self.ret(__RETURN__)' + +_const_int_template = 'self.constant(C.int, __VALUE__)' +_const_long_template = 'self.constant(C.long, __VALUE__)' +_const_float_template = 'self.constant(C.double, __VALUE__)' + +def load_template(string): + ''' + Since ast.parse() returns a ast.Module node, + it is more useful to trim the Module and get to the first item of body + ''' + tree = ast.parse(string) # return a Module + assert isinstance(tree, ast.Module) + return tree.body[0] # get the first item of body + +class ExpandControlFlow(ast.NodeTransformer): + ''' + Expand control flow contructs. + These are the most tedious thing to do in llvm_cbuilder. + ''' + + ## Use breadcumb to track parent nodes + # def __init__(self): + # self.breadcumb = [] + # + # def visit(self, node): + # self.breadcumb.append(node) + # try: + # return super(ExpandControlFlow, self).visit(node) + # finally: + # self.breadcumb.pop() + # + # @property + # def parent(self): + # return self.breadcumb[-2] + + def visit_If(self, node): + mapping = { + '__CONDITION__' : node.test, + '__THEN__' : node.body, + '__OTHERWISE__' : node.orelse, + } + + ifelse = load_template(_if_else_template) + ifelse = MacroExpander(mapping).visit(ifelse) + newnode = self.generic_visit(ifelse) + return newnode + + def visit_While(self, node): + mapping = { + '__CONDITION__' : node.test, + '__BODY__' : node.body, + } + whileloop = load_template(_while_template) + whileloop = MacroExpander(mapping).visit(whileloop) + newnode = self.generic_visit(whileloop) + return newnode + + def visit_For(self, node): + try: + if node.iter.func.id not in ['range', 'xrange']: + return node + except AttributeError: + return node + + mapping = { + '__ITER__' : node.target, + '__BODY__' : node.body, + '__ARGS__' : ast.Tuple(elts=node.iter.args, ctx=ast.Load()), + } + + forloop = load_template(_for_range_template) + forloop = MacroExpander(mapping).visit(forloop) + newnode = self.generic_visit(forloop) + return newnode + + def visit_Return(self, node): + mapping = {'__RETURN__' : node.value} + ret = load_template(_return_template) + repl = MacroExpander(mapping).visit(ret) + return repl + + def visit_Num(self, node): + '''convert immediate values + ''' + typemap = { + int : _const_int_template, + long : _const_long_template, # TODO: disable long for py3 + float : _const_float_template, + } + + template = load_template(typemap[type(node.n)]) + + mapping = { + '__VALUE__' : node, + } + constant = MacroExpander(mapping).visit(template).value + newnode = constant + return newnode + +class MacroExpander(ast.NodeTransformer): + def __init__(self, mapping): + self.mapping = mapping + + def visit_With(self, node): + ''' + Expand X in the following: + with blah: + X + Nothing should go before or after X. + X must be a list of nodes. + ''' + if (len(node.body)==1 # the body of + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.Name)): + try: + repl = self.mapping.pop(node.body[0].value.id) + except KeyError: + pass + else: + old = node.body[0] + node.body = repl + + return self.generic_visit(node) # recursively apply expand all macros + + def visit_Name(self, node): + ''' + Expand all Name node to simple value + ''' + + try: + repl = self.mapping.pop(node.id) + except KeyError: + pass + else: + if repl is not None and not isinstance(repl, list): + return repl + return node + +