def visit_assignment(self, target, value, is_aug_assign=False): value_region = self._region_of(value) if not is_aug_assign else self.youngest_region # If this is a variable, we might need to contract the live range. if isinstance(value_region, Region): for name in self._names_of(target): region = self._region_of(name) if isinstance(region, Region): region.contract(value_region) # If we assign to an attribute of a quoted value, there will be no names # in the assignment lhs. target_names = self._names_of(target) or [] # The assigned value should outlive the assignee target_regions = [self._region_of(name) for name in target_names] for target_region in target_regions: if not Region.outlives(value_region, target_region): if is_aug_assign: target_desc = "the assignment target, allocated here," else: target_desc = "the assignment target" note = diagnostic.Diagnostic("note", "this expression has type {type}", {"type": types.TypePrinter().name(value.type)}, value.loc) diag = diagnostic.Diagnostic("error", "the assigned value does not outlive the assignment target", {}, value.loc, [target.loc], notes=self._diagnostics_for(target_region, target.loc, target_desc) + self._diagnostics_for(value_region, value.loc, "the assigned value")) self.engine.process(diag)
def _diagnostics_for(self, region, loc, descr="the value of the expression"): if isinstance(region, Region): return [ diagnostic.Diagnostic("note", "{descr} is alive from this point...", {"descr": descr}, region.range.begin()), diagnostic.Diagnostic("note", "... to this point", {}, region.range.end()) ] elif isinstance(region, Global): return [ diagnostic.Diagnostic("note", "{descr} is alive forever", {"descr": descr}, loc) ] elif isinstance(region, Argument): return [ diagnostic.Diagnostic( "note", "{descr} is still alive after this function returns", {"descr": descr}, loc), diagnostic.Diagnostic( "note", "{descr} is introduced here as a formal argument", {"descr": descr}, region.loc) ] else: assert False
def visit_Name(self, node): typ = super()._try_find_name(node.id) if typ is not None: # Value from device environment. return asttyped.NameT(type=typ, id=node.id, ctx=node.ctx, loc=node.loc) else: # Try to find this value in the host environment and quote it. if node.id == "print": return self.quote(print, node.loc) elif node.id in self.host_environment: return self.quote(self.host_environment[node.id], node.loc) else: names = set() names.update(self.host_environment.keys()) for typing_env in reversed(self.env_stack): names.update(typing_env.keys()) suggestion = suggest_identifier(node.id, names) if suggestion is not None: diag = diagnostic.Diagnostic("fatal", "name '{name}' is not bound to anything; did you mean '{suggestion}'?", {"name": node.id, "suggestion": suggestion}, node.loc) self.engine.process(diag) else: diag = diagnostic.Diagnostic("fatal", "name '{name}' is not bound to anything", {"name": node.id}, node.loc) self.engine.process(diag)
def visit_ClassDef(self, node): if any(node.bases) or any(node.keywords) or \ node.starargs is not None or node.kwargs is not None: diag = diagnostic.Diagnostic("error", "inheritance is not supported", {}, node.lparen_loc.join(node.rparen_loc)) self.engine.process(diag) for child in node.body: if isinstance(child, (ast.Assign, ast.FunctionDef, ast.Pass)): continue diag = diagnostic.Diagnostic("fatal", "class body must contain only assignments and function definitions", {}, child.loc) self.engine.process(diag) if node.name in self.env_stack[-1]: diag = diagnostic.Diagnostic("fatal", "variable '{name}' is already defined", {"name":node.name}, node.name_loc) self.engine.process(diag) extractor = LocalExtractor(env_stack=self.env_stack, engine=self.engine) extractor.visit(node) # Now we create two types. # The first type is the type of instances created by the constructor. # Its attributes are those of the class environment, but wrapped # appropriately so that they are linked to the class from which they # originate. instance_type = types.TInstance(node.name, OrderedDict()) # The second type is the type of the constructor itself (in other words, # the class object): it is simply a singleton type that has the class # environment as attributes. constructor_type = types.TConstructor(instance_type) constructor_type.attributes = extractor.typing_env instance_type.constructor = constructor_type self.env_stack[-1][node.name] = constructor_type node = asttyped.ClassDefT( constructor_type=constructor_type, name=node.name, bases=self.visit(node.bases), keywords=self.visit(node.keywords), starargs=self.visit(node.starargs), kwargs=self.visit(node.kwargs), body=node.body, decorator_list=self.visit(node.decorator_list), keyword_loc=node.keyword_loc, name_loc=node.name_loc, lparen_loc=node.lparen_loc, star_loc=node.star_loc, dstar_loc=node.dstar_loc, rparen_loc=node.rparen_loc, colon_loc=node.colon_loc, at_locs=node.at_locs, loc=node.loc) try: old_in_class, self.in_class = self.in_class, node return self.generic_visit(node) finally: self.in_class = old_in_class
def visit_Return(self, node): region = self._region_of(node.value) if isinstance(region, Region): note = diagnostic.Diagnostic("note", "this expression has type {type}", {"type": types.TypePrinter().name(node.value.type)}, node.value.loc) diag = diagnostic.Diagnostic("error", "cannot return an allocated value that does not live forever", {}, node.value.loc, notes=self._diagnostics_for(region, node.value.loc) + [note]) self.engine.process(diag)
def _quote_function(self, function, loc): if isinstance(function, SpecializedFunction): host_function = function.host_function else: host_function = function if function in self.functions: pass elif not hasattr(host_function, "artiq_embedded") or \ (host_function.artiq_embedded.core_name is None and host_function.artiq_embedded.portable is False and host_function.artiq_embedded.syscall is None and host_function.artiq_embedded.forbidden is False): self._quote_rpc(function, loc) elif host_function.artiq_embedded.function is not None: if host_function.__name__ == "<lambda>": note = diagnostic.Diagnostic("note", "lambda created here", {}, self._function_loc(host_function.artiq_embedded.function)) diag = diagnostic.Diagnostic("fatal", "lambdas cannot be used as kernel functions", {}, loc, notes=[note]) self.engine.process(diag) core_name = host_function.artiq_embedded.core_name if core_name is not None and self.dmgr.get(core_name) != self.core: note = diagnostic.Diagnostic("note", "called from this function", {}, loc) diag = diagnostic.Diagnostic("fatal", "this function runs on a different core device '{name}'", {"name": host_function.artiq_embedded.core_name}, self._function_loc(host_function.artiq_embedded.function), notes=[note]) self.engine.process(diag) self._quote_embedded_function(function, flags=host_function.artiq_embedded.flags) elif host_function.artiq_embedded.syscall is not None: # Insert a storage-less global whose type instructs the compiler # to perform a system call instead of a regular call. self._quote_syscall(function, loc) elif host_function.artiq_embedded.forbidden is not None: diag = diagnostic.Diagnostic("fatal", "this function cannot be called as an RPC", {}, self._function_loc(host_function), notes=self._call_site_note(loc, fn_kind='rpc')) self.engine.process(diag) else: assert False return self.functions[function]
def _call_site_note(self, call_loc, is_syscall): if call_loc: if is_syscall: return [diagnostic.Diagnostic("note", "in system call here", {}, call_loc)] else: return [diagnostic.Diagnostic("note", "in function called remotely here", {}, call_loc)] else: return []
def generic_visit(self, node): super().generic_visit(node) if isinstance(node, asttyped.commontyped): if types.is_polymorphic(node.type): note = diagnostic.Diagnostic("note", "the expression has type {type}", {"type": types.TypePrinter().name(node.type)}, node.loc) diag = diagnostic.Diagnostic("error", "the type of this expression cannot be fully inferred", {}, node.loc, notes=[note]) self.engine.process(diag)
def visit_FunctionDefT(self, node): super().generic_visit(node) return_type = node.signature_type.find().ret if types.is_polymorphic(return_type): note = diagnostic.Diagnostic("note", "the function has return type {type}", {"type": types.TypePrinter().name(return_type)}, node.name_loc) diag = diagnostic.Diagnostic("error", "the return type of this function cannot be fully inferred", {}, node.name_loc, notes=[note]) self.engine.process(diag)
def _quote_syscall(self, function, loc): signature = inspect.signature(function) arg_types = OrderedDict() optarg_types = OrderedDict() for param in signature.parameters.values(): if param.kind != inspect.Parameter.POSITIONAL_OR_KEYWORD: diag = diagnostic.Diagnostic( "error", "system calls must only use positional arguments; '{argument}' isn't", {"argument": param.name}, self._function_loc(function), notes=self._call_site_note(loc, is_syscall=True)) self.engine.process(diag) if param.default is inspect.Parameter.empty: arg_types[param.name] = self._type_of_param(function, loc, param, is_syscall=True) else: diag = diagnostic.Diagnostic( "error", "system call argument '{argument}' must not have a default value", {"argument": param.name}, self._function_loc(function), notes=self._call_site_note(loc, is_syscall=True)) self.engine.process(diag) if signature.return_annotation is not inspect.Signature.empty: ret_type = self._extract_annot(function, signature.return_annotation, "return type", loc, is_syscall=True) else: diag = diagnostic.Diagnostic( "error", "system call must have a return type annotation", {}, self._function_loc(function), notes=self._call_site_note(loc, is_syscall=True)) self.engine.process(diag) ret_type = types.TVar() function_type = types.TCFunction(arg_types, ret_type, name=function.artiq_embedded.syscall, flags=function.artiq_embedded.flags) self.functions[function] = function_type return function_type
def _uninitialized_access(self, insn, var_name, pred_at_fault): if pred_at_fault is not None: visited = set() possible_preds = [pred_at_fault] uninitialized_loc = None while uninitialized_loc is None: possible_pred = possible_preds.pop(0) visited.add(possible_pred) for pred_insn in reversed(possible_pred.instructions): if pred_insn.loc is not None: uninitialized_loc = pred_insn.loc.begin() break for block in possible_pred.predecessors(): if block not in visited: possible_preds.append(block) assert uninitialized_loc is not None note = diagnostic.Diagnostic( "note", "variable is not initialized when control flows from this point", {}, uninitialized_loc) else: note = None if note is not None: notes = [note] else: notes = [] if isinstance(insn, ir.Closure): diag = diagnostic.Diagnostic( "error", "variable '{name}' can be captured in a closure uninitialized here", {"name": var_name}, insn.loc, notes=notes) else: diag = diagnostic.Diagnostic( "error", "variable '{name}' is not always initialized here", {"name": var_name}, insn.loc, notes=notes) self.engine.process(diag)
def visit_AugAssign(self, node): if builtins.is_list(node.target.type): note = diagnostic.Diagnostic("note", "try using `{lhs} = {lhs} {op} {rhs}` instead", {"lhs": node.target.loc.source(), "rhs": node.value.loc.source(), "op": node.op.loc.source()[:-1]}, node.loc) diag = diagnostic.Diagnostic("error", "lists cannot be mutated in-place", {}, node.op.loc, [node.target.loc], notes=[note]) self.engine.process(diag) self.visit_assignment(node.target, node.value)
def visit_assignment(self, target, value): value_region = self._region_of(value) # If we assign to an attribute of a quoted value, there will be no names # in the assignment lhs. target_names = self._names_of(target) or [] # Adopt the value region for any variables declared on the lhs. for name in target_names: region = self._region_of(name) if isinstance(region, Region) and not region.present(): # Find the name's environment to overwrite the region. for env in self.env_stack[::-1]: if name.id in env: env[name.id] = value_region break # The assigned value should outlive the assignee target_regions = [self._region_of(name) for name in target_names] for target_region in target_regions: if not Region.outlives(value_region, target_region): diag = diagnostic.Diagnostic( "error", "the assigned value does not outlive the assignment target", {}, value.loc, [target.loc], notes=self._diagnostics_for(target_region, target.loc, "the assignment target") + self._diagnostics_for(value_region, value.loc, "the assigned value")) self.engine.process(diag)
def visit_assignment(self, target, value): value_region = self._region_of(value) # If this is a variable, we might need to contract the live range. if isinstance(value_region, Region): for name in self._names_of(target): region = self._region_of(name) if isinstance(region, Region): region.contract(value_region) # If we assign to an attribute of a quoted value, there will be no names # in the assignment lhs. target_names = self._names_of(target) or [] # The assigned value should outlive the assignee target_regions = [self._region_of(name) for name in target_names] for target_region in target_regions: if not Region.outlives(value_region, target_region): diag = diagnostic.Diagnostic("error", "the assigned value does not outlive the assignment target", {}, value.loc, [target.loc], notes=self._diagnostics_for(target_region, target.loc, "the assignment target") + self._diagnostics_for(value_region, value.loc, "the assigned value")) self.engine.process(diag)
def _check_not_in(self, name, names, curkind, newkind, loc): if name in names: diag = diagnostic.Diagnostic("error", "name '{name}' cannot be {curkind} and {newkind} simultaneously", {"name": name, "curkind": curkind, "newkind": newkind}, loc) self.engine.process(diag) return True return False
def visit_Raise(self, node): node = self.generic_visit(node) if node.cause: diag = diagnostic.Diagnostic( "error", "'raise from' syntax is not supported", {}, node.from_loc) self.engine.process(diag) return node
def proxy_diagnostic(diag): note = diagnostic.Diagnostic( "note", "while inferring a type for an attribute '{attr}' of a host object", {"attr": attr_name}, loc) diag.notes.append(note) self.engine.process(diag)
def _find_name(self, name, loc): typ = self._try_find_name(name) if typ is not None: return typ diag = diagnostic.Diagnostic("fatal", "undefined variable '{name}'", {"name": name}, loc) self.engine.process(diag)
def visit_arg(self, node): if node.arg in self.params: diag = diagnostic.Diagnostic("error", "duplicate parameter '{name}'", {"name": node.arg}, node.loc) self.engine.process(diag) return self._assignable(node.arg) self.params.add(node.arg)
def _call_site_note(self, call_loc, fn_kind): if call_loc: if fn_kind == 'syscall': return [diagnostic.Diagnostic("note", "in system call here", {}, call_loc)] elif fn_kind == 'rpc': return [diagnostic.Diagnostic("note", "in function called remotely here", {}, call_loc)] elif fn_kind == 'kernel': return [diagnostic.Diagnostic("note", "in kernel function here", {}, call_loc)] else: assert False else: return []
def visit_arg(self, node): if node.annotation is not None: diag = diagnostic.Diagnostic("fatal", "type annotations are not supported here", {}, node.annotation.loc) self.engine.process(diag) return asttyped.argT(type=self._find_name(node.arg, node.loc), arg=node.arg, annotation=None, arg_loc=node.arg_loc, colon_loc=node.colon_loc, loc=node.loc)
def visit_SubscriptT(self, node): old_in_assign, self.in_assign = self.in_assign, False self.visit(node.value) self.visit(node.slice) self.in_assign = old_in_assign if self.in_assign and builtins.is_bytes(node.value.type): diag = diagnostic.Diagnostic("error", "type {typ} is not mutable", {"typ": "bytes"}, node.loc) self.engine.process(diag)
def _quote_rpc(self, function, loc): if isinstance(function, SpecializedFunction): host_function = function.host_function else: host_function = function ret_type = builtins.TNone() if isinstance(host_function, pytypes.BuiltinFunctionType): pass elif (isinstance(host_function, pytypes.FunctionType) or \ isinstance(host_function, pytypes.MethodType)): if isinstance(host_function, pytypes.FunctionType): signature = inspect.signature(host_function) else: # inspect bug? signature = inspect.signature(host_function.__func__) if signature.return_annotation is not inspect.Signature.empty: ret_type = self._extract_annot(host_function, signature.return_annotation, "return type", loc, fn_kind='rpc') else: assert False is_async = False if hasattr(host_function, "artiq_embedded") and \ "async" in host_function.artiq_embedded.flags: is_async = True if not builtins.is_none(ret_type) and is_async: note = diagnostic.Diagnostic("note", "function called here", {}, loc) diag = diagnostic.Diagnostic("fatal", "functions that return a value cannot be defined as async RPCs", {}, self._function_loc(host_function.artiq_embedded.function), notes=[note]) self.engine.process(diag) function_type = types.TRPC(ret_type, service=self.embedding_map.store_object(host_function), async=is_async) self.functions[function] = function_type return function_type
def visit_Num(self, node): if isinstance(node.n, int): typ = builtins.TInt() elif isinstance(node.n, float): typ = builtins.TFloat() else: diag = diagnostic.Diagnostic( "fatal", "numeric type {type} is not supported", {"type": node.n.__class__.__name__}, node.loc) self.engine.process(diag) return asttyped.NumT(type=typ, n=node.n, loc=node.loc)
def _extract_annot(self, function, annot, kind, call_loc, fn_kind): if not isinstance(annot, types.Type): diag = diagnostic.Diagnostic("error", "type annotation for {kind}, '{annot}', is not an ARTIQ type", {"kind": kind, "annot": repr(annot)}, self._function_loc(function), notes=self._call_site_note(call_loc, fn_kind)) self.engine.process(diag) return types.TVar() else: return annot
def finalize(self): inferencer = StitchingInferencer(engine=self.engine, value_map=self.value_map, quote=self._quote) typedtree_hasher = TypedtreeHasher() # Iterate inference to fixed point. old_typedtree_hash = None old_attr_count = None while True: inferencer.visit(self.typedtree) typedtree_hash = typedtree_hasher.visit(self.typedtree) attr_count = self.embedding_map.attribute_count() if old_typedtree_hash == typedtree_hash and old_attr_count == attr_count: break old_typedtree_hash = typedtree_hash old_attr_count = attr_count # After we've discovered every referenced attribute, check if any kernel_invariant # specifications refers to ones we didn't encounter. for host_type in self.embedding_map.type_map: instance_type, constructor_type = self.embedding_map.type_map[host_type] if not hasattr(instance_type, "constant_attributes"): # Exceptions lack user-definable attributes. continue for attribute in instance_type.constant_attributes: if attribute in instance_type.attributes: # Fast path; if the ARTIQ Python type has the attribute, then every observed # value is guaranteed to have it too. continue for value, loc in self.value_map[instance_type]: if hasattr(value, attribute): continue diag = diagnostic.Diagnostic("warning", "object {value} of type {typ} declares attribute '{attr}' as " "kernel invariant, but the instance referenced here does not " "have this attribute", {"value": repr(value), "typ": types.TypePrinter().name(instance_type, max_depth=0), "attr": attribute}, loc) self.engine.process(diag) # After we have found all functions, synthesize a module to hold them. source_buffer = source.Buffer("", "<synthesized>") self.typedtree = asttyped.ModuleT( typing_env=self.globals, globals_in_scope=set(), body=self.typedtree, loc=source.Range(source_buffer, 0, 0))
def proxy_diagnostic(diag): note = diagnostic.Diagnostic( "note", "expanded from here while trying to infer a type for an" " unannotated optional argument '{argument}' from its default value", {"argument": param.name}, self._function_loc(function)) diag.notes.append(note) note = self._call_site_note(loc, is_syscall) if note: diag.notes += note self.engine.process(diag)
def visit_AttributeT(self, node): self.generic_visit(node) if self.in_assign: typ = node.value.type.find() if types.is_instance(typ) and node.attr in typ.constant_attributes: diag = diagnostic.Diagnostic( "error", "cannot assign to constant attribute '{attr}' of class '{class}'", { "attr": node.attr, "class": typ.name }, node.loc) self.engine.process(diag) return
def visit_AttributeT(self, node): old_in_assign, self.in_assign = self.in_assign, False self.visit(node.value) self.in_assign = old_in_assign if self.in_assign: typ = node.value.type.find() if types.is_instance(typ) and node.attr in typ.constant_attributes: diag = diagnostic.Diagnostic( "error", "cannot assign to constant attribute '{attr}' of class '{class}'", { "attr": node.attr, "class": typ.name }, node.loc) self.engine.process(diag) return if builtins.is_array(typ): diag = diagnostic.Diagnostic( "error", "array attributes cannot be assigned to", {}, node.loc) self.engine.process(diag) return
def _type_of_param(self, function, loc, param, fn_kind): if param.annotation is not inspect.Parameter.empty: # Type specified explicitly. return self._extract_annot(function, param.annotation, "argument '{}'".format(param.name), loc, fn_kind) elif fn_kind == 'syscall': # Syscalls must be entirely annotated. diag = diagnostic.Diagnostic("error", "system call argument '{argument}' must have a type annotation", {"argument": param.name}, self._function_loc(function), notes=self._call_site_note(loc, fn_kind)) self.engine.process(diag) elif fn_kind == 'rpc' and param.default is not inspect.Parameter.empty: notes = [] notes.append(diagnostic.Diagnostic("note", "expanded from here while trying to infer a type for an" " unannotated optional argument '{argument}' from its default value", {"argument": param.name}, self._function_loc(function))) if loc is not None: notes.append(self._call_site_note(loc, fn_kind)) with self.engine.context(*notes): # Try and infer the type from the default value. # This is tricky, because the default value might not have # a well-defined type in APython. # In this case, we bail out, but mention why we do it. ast = self._quote(param.default, None) Inferencer(engine=self.engine).visit(ast) IntMonomorphizer(engine=self.engine).visit(ast) return ast.type else: # Let the rest of the program decide. return types.TVar()