def _codegen_Uni(self, node, const=False): for name, vartype, expr, position in node.vars: var_ref = self.module.globals.get(name, None) if var_ref is not None: raise CodegenError( f'Duplicate found in universal symbol table: "{name}"', position) if const and expr is None: raise CodegenError( f'Constants must have an assignment: "{name}"', position) val, final_type = self._codegen_VarDef(expr, vartype) if final_type is None: final_type = self.vartypes._DEFAULT_TYPE str1 = ir.GlobalVariable(self.module, final_type, name) if const: str1.global_constant = True if val is None: if final_type.is_obj_ptr(): empty_obj = ir.GlobalVariable(self.module, final_type.pointee, name + '.init') empty_obj.initializer = ir.Constant( final_type.pointee, None) str1.initializer = empty_obj else: str1.initializer = ir.Constant(final_type, None) else: str1.initializer = val
def _codegen_VarDef(self, expr, vartype): if expr is None: val = None if vartype is None: vartype = self.vartypes._DEFAULT_TYPE final_type = vartype else: val = self._codegen(expr) if vartype is None: vartype = val.type if vartype == ir.types.FunctionType: pass # instead of conventional codegen, we generate the fp here if val.type != vartype: raise CodegenError( f'Type declaration and variable assignment type do not match (expected "{vartype.describe()}", got "{val.type.describe()}"', expr.position) if val.type.signed != vartype.signed: raise CodegenError( f'Type declaration and variable assignment type have signed/unsigned mismatch (expected "{vartype.describe()}", got "{val.type.describe()}")', expr.position) final_type = val.type return val, final_type
def _codegen_Return(self, node): ''' Generates a return from within a function, and sets the `self.func_returncalled` flag to notify that a return has been triggered. ''' retval = self._codegen(node.val) if self.func_returntype is None: raise CodegenError(f'Unknown return declaration error', node.position) if retval.type != self.func_returntype: raise CodegenError( f'In function "{self.func_incontext.public_name}", expected return type "{self.func_returntype.describe()}" but got "{retval.type.describe()}" instead', node.val.position) self.builder.store(retval, self.func_returnarg) self.builder.branch(self.func_returnblock) self.func_returncalled = True # Check for the presence of a returned object # that requires memory tracing # if so, add it to the set of functions that returns a trackable object to_check = self._extract_operand(retval) if to_check.tracked: self.gives_alloc.add(self.func_returnblock.parent) self.func_returnblock.parent.returns.append(to_check)
def _codegen_Var(self, node, local_alloca=False): for v in node.vars: name = v.name v_type = v.vartype expr = v.initializer position = v.position val, v_type = self._codegen_VarDef(expr, v_type) var_ref = self.func_symtab.get(name) if var_ref is not None: raise CodegenError(f'"{name}" already defined in local scope', position) var_ref = self.module.globals.get(name, None) if var_ref is not None: raise CodegenError( f'"{name}" already defined in universal scope', position) var_ref = self._alloca(name, v_type, current_block=local_alloca) if val: var_ref.heap_alloc = val.heap_alloc var_ref.input_arg = val.input_arg if var_ref.heap_alloc: var_ref.tracked = True self.func_symtab[name] = var_ref if expr: # if _do_not_allocate is set, we've already preallocated space # for the object, so all we have to do is set the name # to its existing pointer if val.do_not_allocate: self.func_symtab[name] = val else: self.builder.store(val, var_ref) else: if v_type.is_obj_ptr(): if not isinstance(v_type.pointee, ir.FunctionType): # allocate the actual object, not just a pointer to it # beacuse it doesn't actually exist yet! obj = self._alloca('obj', v_type.pointee) self.builder.store(obj, var_ref)
def _codegen_Assignment(self, lhs, rhs): if not isinstance(lhs, Variable): raise CodegenError( f'Left-hand side of expression is not a variable and cannot be assigned a value at runtime', lhs.position) ptr = self._codegen_Variable(lhs, noload=True) if getattr(ptr, 'global_constant', None): raise CodegenError( f'Universal constant "{lhs.name}" cannot be reassigned', lhs.position) is_func = ptr.type.is_func() if is_func: rhs_name = mangle_call(rhs.name, ptr.type.pointee.pointee.args) value = self.module.globals.get(rhs_name) if not value: raise CodegenError( f'Call to unknown function "{rhs.name}" with signature {[n.describe() for n in ptr.type.pointee.pointee.args]}" (maybe this call signature is not implemented for this function?)', rhs.position) if 'varfunc' not in value.decorators: raise CodegenError( f'Function "{rhs.name}" must be decorated with "@varfunc" to allow variable assignments', rhs.position) #ptr.decorators = value.decorators # XXX: Not possible to trace function decorators across # function pointer boundaries # One POSSIBLE way to do it would be to have a specialized type # that uses the function decorators mangled in the name. # That way the pointer could only point to one of a class of # function pointers allowed to do so (with bitcasting). # But this seems like a lot of work for little payoff. else: value = self._codegen(rhs) if ptr.type.pointee != value.type: if getattr(lhs, 'accessor', None): error_string = f'Cannot assign value of type "{value.type.describe()}" to element of array "{ptr.pointer.name}" of type "{ptr.type.pointee.describe()}"' else: error_string = f'Cannot assign value of type "{value.type.describe()}" to variable "{ptr.name}" of type "{ptr.type.pointee.describe()}"', raise CodegenError(error_string, rhs.position) self.builder.store(value, ptr) return value
def _check_pointer(self, obj, node): ''' Determines if a given item is a pointer or object. ''' if not isinstance(obj.type, ir.PointerType): raise CodegenError('Parameter must be a pointer or object', node.args[0].position)
def _codegen_Unary(self, node): operand = self._codegen(node.rhs) # TODO: no overflow checking yet! if node.op in BUILTIN_UNARY_OP: if node.op == 'not': if isinstance(operand.type, (ir.IntType, ir.DoubleType)): cond_expr = Binary(node.position, '==', node.rhs, Number(node.position, 0, operand.type)) return self._codegen_If( If( node.position, cond_expr, Number(node.position, 1, operand.type), Number(node.position, 0, operand.type), )) elif node.op == '-': lhs = ir.Constant(operand.type, 0) if isinstance(operand.type, ir.IntType): return self.builder.sub(lhs, operand, 'negop') elif isinstance(operand.type, ir.DoubleType): return self.builder.fsub(lhs, operand, 'fnegop') else: func = self.module.globals.get( f'unary.{node.op}{mangle_args((operand.type,))}') if not func: raise CodegenError( f'Undefined unary operator "{node.op}" for "{operand.type.describe()}"', node.position) return self.builder.call(func, [operand], 'unop')
def _codegen_Continue(self, node): # First, determine if we are in a loop context: if not self.loop_exit: raise CodegenError('"continue" called outside of loop', node.position) return self.builder.branch(self.loop_exit[-1][1])
def _codegen_Match(self, node): cond_item = self._codegen(node.cond_item) default = ir.Block(self.builder.function, 'defaultmatch') exit = ir.Block(self.builder.function, 'endmatch') switch_instr = self.builder.switch(cond_item, default) cases = [] exprs = {} values = set() for value, expr in node.match_list: val_codegen = self._codegen(value) if not isinstance(val_codegen, ir.values.Constant): raise CodegenError( f'Match parameter must be a constant, not an expression', value.position) if val_codegen.type != cond_item.type: raise CodegenError( f'Type of match object ("{cond_item.type.describe()}") and match parameter ("{val_codegen.type.describe()}") must be consistent)', value.position) if val_codegen.constant in values: raise CodegenError(f'Match parameter {value} duplicated', value.position) values.add(val_codegen.constant) if expr in exprs: switch_instr.add_case(val_codegen, exprs[expr]) else: n = ir.Block(self.builder.function, 'match') switch_instr.add_case(val_codegen, n) exprs[expr] = n cases.append([n, expr]) for block, expr in cases: self.builder.function.basic_blocks.append(block) self.builder.position_at_start(block) result = self._codegen(expr, False) #if result and not self.builder.block.is_terminated: if not self.builder.block.is_terminated: self.builder.branch(exit) self.builder.function.basic_blocks.append(default) self.builder.position_at_start(default) if node.default: self._codegen(node.default, False) if not self.builder.block.is_terminated: self.builder.branch(exit) self.builder.function.basic_blocks.append(exit) self.builder.position_at_start(exit) return cond_item
def _get_var(self, node, lhs): ''' Retrieves variable, if any, from a load op. Used mainly for += and -= ops. ''' if isinstance(lhs, ir.Constant): raise CodegenError(r"Can't assign value to literal", node.lhs.position) return self._extract_operand(lhs)
def _codegen_ArrayElement(self, node, array): ''' Returns a pointer to the requested element of an array. ''' elements = [self._codegen(n) for n in node.elements] accessor = [ self._i32(0), self._i32(1), ] + elements # First, try to obtain a conventional array accessor element try: ptr = self.builder.gep(array, accessor, True, f'{array.name}') except Exception as e: pass else: return ptr # If that fails, assume we're trying to manually index an object if not self.allow_unsafe: raise CodegenError( f'Accessor "{array.name}" into unindexed object requires "unsafe" block', node.position) try: ptr = self.builder.gep(array, [self._i32(0)] + elements, False, f'{array.name}') except AttributeError as e: raise CodegenError( f'Unindexed accessor for "{array.name}" requires a constant', node.position) except Exception: pass else: return ptr # If that fails, abort raise CodegenError( f'Invalid array accessor for "{array.name}" (maybe wrong number of dimensions?)', node.position)
def _codegen_Builtins_c_ref(self, node): ''' Returns a typed pointer to a primitive, like an int. ''' expr = self._get_obj_noload(node) if expr.type.is_obj_ptr(): raise CodegenError( f'"{node.args[0].name}" is not a scalar (use c_obj_ref for references to objects instead of scalars)', node.args[0].position) return expr
def _codegen(self, node, check_for_type=True): ''' Node visitor. Dispatches upon node type. For AST node of class Foo, calls self._codegen_Foo. Each visitor is expected to return a llvmlite.ir.Value. ''' method = '_codegen_' + node.__class__.__name__ result = getattr(self, method)(node) if check_for_type and not hasattr(result, 'type'): raise CodegenError( f'Expression does not return a value along all code paths, or expression returns an untyped value', node.position) return result
def _varaddr(self, node, report=True): ''' Retrieve the address of a variable indicated by a Variable AST object. ''' if report: name = node.name else: name = node v = self.func_symtab.get(name) if v is None: v = self.module.globals.get(name) if v is None: if not report: return None raise CodegenError(f"Undefined variable: {node.name}", node.position) return v
def _codegen_Builtins_c_deref(self, node): ''' Dereferences a pointer to a primitive, like an int. ''' ptr = self._get_obj_noload(node) ptr2 = self.builder.load(ptr) if hasattr(ptr2.type, 'pointee'): ptr2 = self.builder.load(ptr2) if hasattr(ptr2.type, 'pointee'): raise CodegenError( f'"{node.args[0].name}" is not a reference to a scalar (use c_obj_deref for references to objects instead of scalars)', node.args[0].position) # XXX: self.builder.load clobbers with core._llvmlite_custom._IntType return ptr2
def _codegen_Builtins_c_obj_free(self, node): ''' Deallocates memory for an object created with c_obj_alloc. ''' expr = self._get_obj_noload(node) if not expr.tracked: raise CodegenError(f'{node.args[0].name} is not an allocated object',node.args[0].position) # Mark the variable in question as untracked expr.tracked = False addr = self.builder.load(expr) addr2 = self.builder.bitcast(addr, self.vartypes.u_mem.as_pointer()).get_reference() call = self._codegen_Call( Call(node.position, 'c_free', [Number(node.position, addr2, self.vartypes.u_mem.as_pointer())])) # TODO: zero after free, automatically return call
def _codegen_Builtins_convert(self, node): ''' Converts data between primitive value types, such as i8 to i32. Checks for signing and bitwidth. Conversions from or to pointers are not supported here. ''' convert_from = self._codegen(node.args[0]) convert_to = self._codegen(node.args[1], False) convert_exception = CodegenError( f'Converting from type "{convert_from.type.describe()}" to type "{convert_to.describe()}" is not supported', node.args[0].position) while True: # Conversions from/to an object are not allowed if convert_from.type.is_obj_ptr() or convert_to.is_obj_ptr(): explanation = f'\n(Converting from/to object types will be added later.)' convert_exception.msg += explanation raise convert_exception # Convert from/to a pointer is not allowed if isinstance(convert_from.type, ir.PointerType) or isinstance(convert_to, ir.PointerType): convert_exception.msg += '\n(Converting from or to pointers is not allowed; use "cast" instead)' raise convert_exception # Convert from float to int is OK, but warn if isinstance(convert_from.type, ir.DoubleType): if not isinstance(convert_to, ir.IntType): raise convert_exception print( CodegenWarning( f'Float to integer conversions ("{convert_from.type.describe()}" to "{convert_to.describe()}") are inherently imprecise', node.args[0].position)) if convert_from.type.signed: op = self.builder.fptosi else: op = self.builder.fptoui break # Convert from ints if isinstance(convert_from.type, ir.IntType): # int to float if isinstance(convert_to, ir.DoubleType): print( CodegenWarning( f'Integer to float conversions ("{convert_from.type.describe()}" to "{convert_to.describe()}") are inherently imprecise', node.args[0].position)) if convert_from.type.signed: op = self.builder.sitofp else: op = self.builder.uitofp break # int to int if isinstance(convert_to, ir.IntType): # Don't allow mixing signed/unsigned if convert_from.type.signed and not convert_to.signed: raise CodegenError( f'Signed type "{convert_from.type.describe()}" cannot be converted to unsigned type "{convert_to.describe()}"', node.args[0].position) # Don't allow converting to a smaller bitwidth if convert_from.type.width > convert_to.width: raise CodegenError( f'Type "{convert_from.type.describe()}" cannot be converted to type "{convert_to.describe()}" without possible truncation', node.args[0].position) # otherwise, extend bitwidth to convert if convert_from.type.signed: op = self.builder.sext else: op = self.builder.zext break raise convert_exception result = op(convert_from, convert_to) result.type = convert_to return result
def _codegen_Call(self, node, obj_method=False): if not obj_method: if node.name in Dunders: return self._codegen_dunder_methods(node) if node.name in Builtins: return getattr(self, '_codegen_Builtins_' + node.name)(node) call_args = [] possible_opt_args_funcs = set() # The reason for the peculiar construction below # is to first process a blank argument list, so # we can match calls to functions that have # all optional arguments for arg in node.args + [None]: _ = mangle_types(node.name, call_args) if _ in self.opt_args_funcs: possible_opt_args_funcs.add(self.opt_args_funcs[_]) if arg: call_args.append(self._codegen(arg)) if obj_method: c = call_args[0] try: c1 = c.type.pointee.name except: c1 = c.type node.name = f'{c1}.__{node.name}__' if not possible_opt_args_funcs: mangled_name = mangle_types(node.name, call_args) callee_func = self.module.globals.get(mangled_name, None) else: try: match = False for f1 in possible_opt_args_funcs: if len(call_args) > len(f1.args): continue match = True for function_arg, call_arg in zip(f1.args, call_args): if function_arg.type != call_arg.type: match = False break if not match: raise TypeError except TypeError: raise ParseError( f'argument types do not match possible argument signature for optional-argument function "{f1.public_name}"', node.position) else: callee_func = f1 for n in range(len(call_args), len(f1.args)): call_args.append(f1.args[n].default_value) # Determine if this is a function pointer try: # if we don't yet have a reference, # since this might be a function pointer, # attempt to obtain one from the variable list if not callee_func: callee_func = self._varaddr(node.name, False) if callee_func.type.is_func(): # retrieve actual function pointer from the variable ref func_to_check = callee_func.type.pointee.pointee final_call = self.builder.load(callee_func) ftype = func_to_check final_call.decorators = [] #final_call.decorators = callee_func.decorators # It's not possible to trace decorators across # function pointers else: # this is a regular old function, not a function pointer func_to_check = callee_func final_call = callee_func ftype = getattr(func_to_check, 'ftype', None) except Exception: raise CodegenError( f'Call to unknown function "{node.name}" with signature "{[n.type.describe() for n in call_args]}" (maybe this call signature is not implemented for this function?)', node.position) if not ftype.var_arg: if len(func_to_check.args) != len(call_args): raise CodegenError( f'Call argument length mismatch for "{node.name}" (expected {len(callee_func.args)}, got {len(node.args)})', node.position) else: if len(call_args) < len(func_to_check.args): raise CodegenError( f'Call argument length mismatch for "{node.name}" (expected at least {len(callee_func.args)}, got {len(node.args)})', node.position) nomod = 'nomod' in final_call.decorators for x, n in enumerate(zip(call_args, func_to_check.args)): type0 = n[0].type # in some cases, such as with a function pointer, # the argument is not an Argument but a core.vartypes instance # so this check is necessary if type(n[1]) == ir.values.Argument: type1 = n[1].type else: type1 = n[1] if type0 != type1: raise CodegenError( f'Call argument type mismatch for "{node.name}" (position {x}: expected {type1.describe()}, got {type0.describe()})', node.args[x].position) # if this is a traced object, and we give it away, # then we can't delete it in this scope anymore # because we no longer have ownership of it if not nomod: to_check = self._extract_operand(n[0]) if to_check.heap_alloc: to_check.tracked = False call_to_return = self.builder.call(final_call, call_args, 'calltmp') # Check for the presence of an object returned from the call # that requires memory tracing if callee_func in self.gives_alloc: call_to_return.heap_alloc = True call_to_return.tracked = True # FIXME: There ought to be a better way to assign this if callee_func.tracked == True: call_to_return.heap_alloc = True call_to_return.tracked = True if 'unsafe_req' in final_call.decorators and not self.allow_unsafe: raise CodegenError( f'Function "{node.name}" is decorated with "@unsafe_req" and requires an "unsafe" block"', node.position) # if callee_func.do_not_allocate == True: # call_to_return.do_not_allocate = True # if 'nomod' in callee_func.decorators: # call_to_return.tracked=False return call_to_return
def _codegen_Variable(self, node, noload=False): current_node = node previous_node = None # At the bottom of each iteration of the loop, # we should return a DIRECT pointer to an object while True: if previous_node is None and isinstance(current_node, Variable): if isinstance(getattr(current_node, 'child', None), (Call, )): previous_node = current_node current_node = current_node.child continue latest = self._varaddr(current_node) current_load = not latest.type.is_obj_ptr() elif isinstance(current_node, ArrayAccessor): # eventually this will be coded as a call # to __index__ method of the element in question if latest.type.is_obj_ptr(): # objects are passed by reference array_element = latest else: if not isinstance(latest, ir.instructions.LoadInstr): # if the array object is not yet loaded, # then we need to allocate and sore ptr array_element = self.builder.alloca(latest.type) self.builder.store(latest, array_element) else: # otherwise, just point to the existing allocation array_element = self._varaddr(previous_node) latest = self._codegen_ArrayElement(current_node, array_element) current_load = not latest.type.is_obj_ptr() elif isinstance(current_node, Call): # eventually, when we have function pointers, # we'll need to have a pattern here similar to how # we handle ArrayAccessors above # e.g., encoded as __call__ latest = self._codegen_Call(current_node) current_load = False # TODO: why is a call the exception? elif isinstance(current_node, Variable): try: oo = latest.type.pointee except AttributeError: raise CodegenError(f'Not a pointer or object', current_node.position) _latest_vid = oo.v_id _cls = self.class_symtab[_latest_vid] _pos = _cls.v_types[current_node.name]['pos'] # for some reason we can't use i64 gep here index = [self._i32(0), self._i32(_pos)] latest = self.builder.gep( latest, index, True, previous_node.name + '.' + current_node.name) current_load = not latest.type.is_obj_ptr() # pathological case else: raise CodegenError(f'Unknown variable instance', current_node.position) child = getattr(current_node, 'child', None) if child is None: break if current_load: latest = self.builder.load(latest, node.name + '.accessor') previous_node = current_node current_node = child if noload is True: return latest if current_load: return self.builder.load(latest, node.name) else: return latest
def _codegen_Function(self, node): # Reset the symbol table. Prototype generation will pre-populate it with # function arguments. self.func_symtab = {} # Create the function skeleton from the prototype. func = self._codegen(node.proto, False) # Create the entry BB in the function and set a new builder to it. bb_entry = func.append_basic_block('entry') self.builder = ir.IRBuilder(bb_entry) self.func_incontext = func self.func_returncalled = False self.func_returntype = func.return_value.type self.func_returnblock = func.append_basic_block('exit') self.func_returnarg = self._alloca('%_return', self.func_returntype) # Add all arguments to the symbol table and create their allocas for _, arg in enumerate(func.args): if arg.type.is_obj_ptr(): alloca = arg else: alloca = self._alloca(arg.name, arg.type) self.builder.store(arg, alloca) # We don't shadow existing variables names, ever assert not self.func_symtab.get( arg.name) and "arg name redefined: " + arg.name self.func_symtab[arg.name] = alloca alloca.input_arg = _ alloca.tracked = False # Generate code for the body retval = self._codegen(node.body, False) if retval is None and self.func_returncalled is True: # we don't need to check for a final returned value, # because it's implied that there's an early return pass else: if not hasattr(retval, 'type'): raise CodegenError( f'Function "{node.proto.name}" has a return value of type "{func.return_value.type.describe()}" but no concluding expression with an explicit return type was supplied', node.position) if retval is None and func.return_value.type is not None: raise CodegenError( f'Function "{node.proto.name}" has a return value of type "{func.return_value.type.describe()}" but no expression with an explicit return type was supplied', node.position) if func.return_value.type != retval.type: if node.proto.name.startswith(_ANONYMOUS): func.return_value.type = retval.type self.func_returnarg = self._alloca('%_return', retval.type) else: raise CodegenError( f'Prototype for function "{node.proto.name}" has return type "{func.return_value.type.describe()}", but returns "{retval.type.describe()}" instead (maybe an implicit return?)', node.proto.position) self.builder.store(retval, self.func_returnarg) self.builder.branch(self.func_returnblock) self.builder = ir.IRBuilder(self.func_returnblock) # Check for the presence of a returned object # that requires memory tracing # if so, add it to the set of functions that returns a trackable object to_check = retval if retval: to_check = self._extract_operand(retval) if to_check.tracked: self.gives_alloc.add(self.func_returnblock.parent) self.func_returnblock.parent.returns.append(to_check) # Determine which variables need to be automatically disposed if to_check: self._codegen_autodispose(reversed(list(self.func_symtab.items())), to_check) self.builder.ret(self.builder.load(self.func_returnarg)) self.func_incontext = None self.func_returntype = None self.func_returnarg = None self.func_returnblock = None self.func_returncalled = None
def _codegen_Prototype(self, node): funcname = node.name # Create a function type vartypes = [] vartypes_with_defaults = [] append_to = vartypes for x in node.argnames: s = x.vartype if x.initializer is not None: append_to = vartypes_with_defaults append_to.append(s) # TODO: it isn't yet possible to have an implicitly # typed function that just uses the return type of the body # we might be able to do this by way of a special call # to this function # note that Extern functions MUST be typed if node.vartype is None: node.vartype = self.vartypes._DEFAULT_TYPE functype = ir.FunctionType(node.vartype, vartypes + vartypes_with_defaults) public_name = funcname opt_args = None linkage = None # TODO: identify anonymous functions with a property # not by way of their nomenclature if node.extern is False and not funcname.startswith( '_ANONYMOUS.') and funcname != 'main': linkage = 'private' if len(vartypes) > 0: funcname = public_name + mangle_args(vartypes) else: funcname = public_name + '@' required_args = funcname if len(vartypes_with_defaults) > 0: opt_args = mangle_optional_args(vartypes_with_defaults) funcname += opt_args # If a function with this name already exists in the module... if funcname in self.module.globals: # We only allow the case in which a declaration exists and now the # function is defined (or redeclared) with the same number of args. func = existing_func = self.module.globals[funcname] if not isinstance(existing_func, ir.Function): raise CodegenError( f'Function/universal name collision {funcname}', node.position) # If we're redefining a forward declaration, # erase the existing function body if not existing_func.is_declaration: existing_func.blocks = [] if len(existing_func.function_type.args) != len(functype.args): raise CodegenError( f'Redefinition of function "{public_name}" with different number of arguments', node.position) else: # Otherwise create a new function func = ir.Function(self.module, functype, funcname) # Name the arguments for i, arg in enumerate(func.args): arg.name = node.argnames[i].name if opt_args is not None: self.opt_args_funcs[required_args] = func # Set defaults (if any) for x, n in enumerate(node.argnames): if n.initializer is not None: func.args[x].default_value = self._codegen( n.initializer, False) if node.varargs: func.ftype.var_arg = True func.public_name = public_name func.returns = [] ############################################################## # Set LLVM function attributes ############################################################## # First, extract a copy of the function decorators # and use that to set up other attributes decorators = [n.name for n in self.func_decorators] varfunc = 'varfunc' in decorators for a, b in decorator_collisions: if a in decorators and b in decorators: raise CodegenError( f'Function cannot be decorated with both "@{a}" and "@{b}"', node.position) # Calling convention. # This is the default with no varargs if node.varargs is None: if not node.extern: func.calling_convention = 'fastcc' # Linkage. # Default is 'private' if it's not extern, an anonymous function, or main if linkage: func.linkage = linkage # Address is not relevant by default func.unnamed_addr = True # Enable optnone for main() or anything # designated as a target for a function pointer. if funcname == 'main' or varfunc: func.attributes.add('optnone') func.attributes.add('noinline') # Inlining. Operator functions are inlined by default. if ( # function is manually inlined ('inline' in decorators) or # function is an operator, not @varfunc, # and not @noinline (node.isoperator and not varfunc and 'noinline' not in decorators )): func.attributes.add('alwaysinline') # function is @noinline # or function is @varfunc if 'noinline' in decorators: func.attributes.add('noinline') # End inlining. # External calls, by default, no recursion if node.extern: func.attributes.add('norecurse') func.linkage = 'dllimport' # By default, no lazy binding func.attributes.add('nonlazybind') # By default, no stack unwinding func.attributes.add('nounwind') func.decorators = decorators return func
def _codegen_Builtins_cast(self, node): ''' Cast one data type as another, such as a pointer to a u64, or an i8 to a u32. Ignores signing. Will truncate bitwidths. ''' cast_from = self._codegen(node.args[0]) cast_to = self._codegen(node.args[1], False) cast_exception = CodegenError( f'Casting from type "{cast_from.type.describe()}" to type "{cast_to.describe()}" is not supported', node.args[0].position) while True: # If we're casting FROM a pointer... if isinstance(cast_from.type, ir.PointerType): # it can't be an object pointer (for now) if cast_from.type.is_obj_ptr(): #if cast_from.type.v_id != 'ptr_str': raise cast_exception # but it can be cast to another pointer # as long as it's not an object if isinstance(cast_to, ir.PointerType): if cast_to.is_obj_ptr(): raise cast_exception op = self.builder.bitcast break # and it can't be anything other than an int if not isinstance(cast_to, ir.IntType): raise cast_exception # and it has to be the same bitwidth if self.vartypes._pointer_bitwidth != cast_to.width: raise cast_exception op = self.builder.ptrtoint break # If we're casting TO a pointer ... if isinstance(cast_to, ir.PointerType): # it can't be from anything other than an int if not isinstance(cast_from.type, ir.IntType): raise cast_exception # and it has to be the same bitwidth if cast_from.type.width != self.pointer_bitwidth: raise cast_exception op = self.builder.inttoptr break # If we're casting non-pointers of the same bitwidth, # just use bitcast if cast_from.type.width == cast_to.width: op = self.builder.bitcast break # If we're going from a smaller to a larger bitwidth, # we need to use the right instruction if cast_from.type.width < cast_to.width: if isinstance(cast_from.type, ir.IntType): if isinstance(cast_to, ir.IntType): op = self.builder.zext break if isinstance(cast_to, ir.DoubleType): if cast_from.type.signed: op = self.builder.sitofp break else: op = self.builder.uitofp break else: op = self.builder.trunc break #cast_exception.msg += ' (data would be truncated)' #raise cast_exception raise cast_exception result = op(cast_from, cast_to) result.type = cast_to return result
def _codegen_Binary(self, node): # Assignment is handled specially because it doesn't follow the general # recipe of binary ops. if node.op == '=': return self._codegen_Assignment(node.lhs, node.rhs) lhs = self._codegen(node.lhs) rhs = self._codegen(node.rhs) if lhs.type != rhs.type: raise CodegenError( f'"{lhs.type.describe()}" ({node.lhs.name}) and "{rhs.type.describe()}" ({node.rhs.name}) are incompatible types for operation', node.position) else: vartype = lhs.type v_type = getattr(lhs.type, 'v_type', None) try: # For non-primitive types we need to look up the property if v_type is not None: if v_type == Str: raise NotImplementedError # TODO: no overflow checking! # we have to add that when we have exceptions, etc. # with fcmp_ordered this is assuming we are strictly comparing # float to float in all cases. # Integer operations if isinstance(vartype, ir.IntType): if lhs.type.signed: signed_op = self.builder.icmp_signed else: signed_op = self.builder.icmp_unsigned if node.op == '+': return self.builder.add(lhs, rhs, 'addop') elif node.op == '+=': operand = self._get_var(node, lhs) value = self.builder.add(lhs, rhs, 'addop') self.builder.store(value, operand) return value elif node.op == '-=': operand = self._get_var(node, lhs) value = self.builder.sub(lhs, rhs, 'addop') self.builder.store(value, operand) return value elif node.op == '-': return self.builder.sub(lhs, rhs, 'subop') elif node.op == '*': return self.builder.mul(lhs, rhs, 'multop') elif node.op == '<': x = signed_op('<', lhs, rhs, 'ltop') x.type = VarTypes.bool return x elif node.op == '>': x = signed_op('>', lhs, rhs, 'gtop') x.type = VarTypes.bool return x elif node.op == '>=': x = signed_op('>=', lhs, rhs, 'gteqop') x.type = VarTypes.bool return x elif node.op == '<=': x = signed_op('<=', lhs, rhs, 'lteqop') x.type = VarTypes.bool return x elif node.op == '==': x = signed_op('==', lhs, rhs, 'eqop') x.type = VarTypes.bool return x elif node.op == '!=': x = signed_op('!=', lhs, rhs, 'neqop') x.type = VarTypes.bool return x elif node.op == '/': if int(getattr(rhs, 'constant', 1)) == 0: raise CodegenError('Integer division by zero', node.rhs.position) return self.builder.sdiv(lhs, rhs, 'divop') elif node.op == 'and': x = self.builder.and_(lhs, rhs, 'andop') # pylint: disable=E1111 x.type = VarTypes.bool return x elif node.op == 'or': x = self.builder.or_(lhs, rhs, 'orop') # pylint: disable=E1111 x.type = VarTypes.bool return x else: return self._codegen_methodcall(node, lhs, rhs) # floating-point operations elif isinstance(vartype, (ir.DoubleType, ir.FloatType)): if node.op == '+': return self.builder.fadd(lhs, rhs, 'faddop') elif node.op == '+=': operand = self._get_var(node, lhs) value = self.builder.fadd(lhs, rhs, 'faddop') self.builder.store(value, operand) return value elif node.op == '-=': operand = self._get_var(node, lhs) value = self.builder.fsub(lhs, rhs, 'fsubop') self.builder.store(value, operand) return value elif node.op == '-': return self.builder.fsub(lhs, rhs, 'fsubop') elif node.op == '*': return self.builder.fmul(lhs, rhs, 'fmultop') elif node.op == '/': return self.builder.fdiv(lhs, rhs, 'fdivop') elif node.op == '<': cmp = self.builder.fcmp_ordered('<', lhs, rhs, 'fltop') return self.builder.uitofp(cmp, vartype, 'fltoptodouble') elif node.op == '>': cmp = self.builder.fcmp_ordered('>', lhs, rhs, 'fgtop') return self.builder.uitofp(cmp, vartype, 'flgoptodouble') elif node.op == '>=': cmp = self.builder.fcmp_ordered('>=', lhs, rhs, 'fgeqop') return self.builder.uitofp(cmp, vartype, 'fgeqopdouble') elif node.op == '<=': cmp = self.builder.fcmp_ordered('<=', lhs, rhs, 'fleqop') return self.builder.uitofp(cmp, vartype, 'fleqopdouble') elif node.op == '==': x = self.builder.fcmp_ordered('==', lhs, rhs, 'feqop') x.type = VarTypes.bool return x elif node.op == '!=': x = self.builder.fcmp_ordered('!=', lhs, rhs, 'fneqop') x.type = VarTypes.bool return x elif node.op in ('and', 'or'): raise CodegenError( 'Operator not supported for "float" or "double" types', node.lhs.position) else: return self._codegen_methodcall(node, lhs, rhs) # Pointer equality elif isinstance(vartype, ir.PointerType): # TODO: use vartype.is_obj_ptr() to determine # if this is a complex object that needs to invoke # its __eq__ method, but this is fine for now signed_op = self.builder.icmp_unsigned if isinstance(rhs.type, ir.PointerType): if node.op == '==': x = signed_op('==', lhs, rhs, 'eqptrop') x.type = VarTypes.bool return x else: return self._codegen_methodcall(node, lhs, rhs) except NotImplementedError: raise CodegenError( f'Unknown binary operator {node.op} for {vartype}', node.position)
def _codegen_If(self, node, codegen_when=False): # Emit comparison value cond_val = self._codegen(node.cond_expr) if_type = cond_val.type cond = ('!=', cond_val, ir.Constant(if_type, 0), 'notnull') if isinstance(if_type, (ir.FloatType, ir.DoubleType)): cmp_instr = self.builder.fcmp_unordered(*cond) elif isinstance(if_type, SignedInt): cmp_instr = self.builder.icmp_signed(*cond) else: cmp_instr = self.builder.icmp_unsigned(*cond) # Create basic blocks to express the control flow then_bb = ir.Block(self.builder.function, 'then') else_bb = ir.Block(self.builder.function, 'else') merge_bb = ir.Block(self.builder.function, 'endif') # branch to either then_bb or else_bb depending on cmp # if no else, then go straight to merge if node.else_expr is None: self.builder.cbranch(cmp_instr, then_bb, merge_bb) else: self.builder.cbranch(cmp_instr, then_bb, else_bb) # Emit the 'then' part self.builder.function.basic_blocks.append(then_bb) self.builder.position_at_start(then_bb) self.breaks = False then_val = self._codegen(node.then_expr, False) #if then_val or not self.builder.block.is_terminated: if not self.builder.block.is_terminated: self.builder.branch(merge_bb) # Emission of then_val could have generated a new basic block # (and thus modified the current basic block). # To properly set up the PHI, remember which block the 'then' part ends in. then_bb = self.builder.block # Emit the 'else' part, if needed if node.else_expr is None: codegen_when = True else_val = None self.builder.function.basic_blocks.append(else_bb) self.builder.position_at_start(else_bb) self.builder.branch(merge_bb) else_bb = self.builder.block else: self.builder.function.basic_blocks.append(else_bb) self.builder.position_at_start(else_bb) else_val = self._codegen(node.else_expr) if not self.builder.block.is_terminated: self.builder.branch(merge_bb) else_bb = self.builder.block # check for an early return, # prune unneeded phi operations self.builder.function.basic_blocks.append(merge_bb) self.builder.position_at_start(merge_bb) # the problem: # an if/elif still expects a value returned # we can't mix when/if in the same compound statement if codegen_when: return cond_val if then_val is None and else_val is None: # returns are present in each branch return elif not else_val: # return present in 1st branch only return then_val.type elif not then_val: # return present in 2nd branch only return else_val.type # otherwise no returns in any branch # make sure then/else are in agreement # so we're returning consistent types if then_val.type != else_val.type: raise CodegenError( f'"then/else" expression return types must be the same ("{then_val.type.describe()}" does not match "{else_val.type.describe()}"', node.position) phi = self.builder.phi(then_val.type, 'ifval') phi.add_incoming(then_val, then_bb) phi.add_incoming(else_val, else_bb) return phi
def _if_unsafe(self, node): if not self.allow_unsafe: raise CodegenError('Operation must be enclosed in an "unsafe" block', node.position)