diff --git a/artiq/compiler/asttyped.py b/artiq/compiler/asttyped.py index f15726579..c9e543ae2 100644 --- a/artiq/compiler/asttyped.py +++ b/artiq/compiler/asttyped.py @@ -25,15 +25,15 @@ class scoped(object): class argT(ast.arg, commontyped): pass -class ClassDefT(ast.ClassDef, scoped): - pass +class ClassDefT(ast.ClassDef): + _types = ("constructor_type",) class FunctionDefT(ast.FunctionDef, scoped): _types = ("signature_type",) class ModuleT(ast.Module, scoped): pass class ExceptHandlerT(ast.ExceptHandler): - _fields = ("filter", "name", "body") # rename ast.ExceptHandler.type + _fields = ("filter", "name", "body") # rename ast.ExceptHandler.type to filter _types = ("name_type",) class SliceT(ast.Slice, commontyped): diff --git a/artiq/compiler/builtins.py b/artiq/compiler/builtins.py index c34ccf126..60c7af718 100644 --- a/artiq/compiler/builtins.py +++ b/artiq/compiler/builtins.py @@ -96,44 +96,32 @@ class TException(types.TMono): def __init__(self, name="Exception"): super().__init__(name) -class TIndexError(TException): - def __init__(self): - super().__init__("IndexError") - -class TValueError(TException): - def __init__(self): - super().__init__("ValueError") - -class TZeroDivisionError(TException): - def __init__(self): - super().__init__("ZeroDivisionError") - def fn_bool(): - return types.TConstructor("bool") + return types.TConstructor(TBool()) def fn_int(): - return types.TConstructor("int") + return types.TConstructor(TInt()) def fn_float(): - return types.TConstructor("float") + return types.TConstructor(TFloat()) def fn_str(): - return types.TConstructor("str") + return types.TConstructor(TStr()) def fn_list(): - return types.TConstructor("list") + return types.TConstructor(TList()) def fn_Exception(): - return types.TExceptionConstructor("Exception") + return types.TExceptionConstructor(TException("Exception")) def fn_IndexError(): - return types.TExceptionConstructor("IndexError") + return types.TExceptionConstructor(TException("IndexError")) def fn_ValueError(): - return types.TExceptionConstructor("ValueError") + return types.TExceptionConstructor(TException("ValueError")) def fn_ZeroDivisionError(): - return types.TExceptionConstructor("ZeroDivisionError") + return types.TExceptionConstructor(TException("ZeroDivisionError")) def fn_range(): return types.TBuiltinFunction("range") @@ -215,4 +203,4 @@ def is_collection(typ): def is_allocated(typ): return typ.fold(False, lambda accum, typ: is_list(typ) or is_str(typ) or types.is_function(typ) or - is_exception(typ)) + is_exception(typ) or types.is_constructor(typ)) diff --git a/artiq/compiler/ir.py b/artiq/compiler/ir.py index 2561d4ea4..0f3ca3131 100644 --- a/artiq/compiler/ir.py +++ b/artiq/compiler/ir.py @@ -636,9 +636,9 @@ class SetAttr(Instruction): assert isinstance(attr, (str, int)) assert isinstance(value, Value) if isinstance(attr, int): - assert value.type == obj.type.elts[attr] + assert value.type == obj.type.elts[attr].find() else: - assert value.type == obj.type.attributes[attr] + assert value.type == obj.type.attributes[attr].find() super().__init__([obj, value], builtins.TNone(), name) self.attr = attr diff --git a/artiq/compiler/transforms/artiq_ir_generator.py b/artiq/compiler/transforms/artiq_ir_generator.py index a822f8815..01fd558b2 100644 --- a/artiq/compiler/transforms/artiq_ir_generator.py +++ b/artiq/compiler/transforms/artiq_ir_generator.py @@ -73,6 +73,7 @@ class ARTIQIRGenerator(algorithm.Visitor): self.name = [module_name] if module_name != "" else [] self.current_loc = None self.current_function = None + self.current_class = None self.current_globals = set() self.current_block = None self.current_env = None @@ -156,6 +157,17 @@ class ARTIQIRGenerator(algorithm.Visitor): # Statement visitors + def visit_ClassDefT(self, node): + klass = self.append(ir.Alloc([], node.constructor_type, + name="class.{}".format(node.name))) + self._set_local(node.name, klass) + + try: + old_class, self.current_class = self.current_class, klass + self.visit(node.body) + finally: + self.current_class = old_class + def visit_function(self, node, is_lambda, is_internal): if is_lambda: name = "lambda@{}:{}".format(node.loc.line(), node.loc.column()) @@ -239,9 +251,12 @@ class ARTIQIRGenerator(algorithm.Visitor): return self.append(ir.Closure(func, self.current_env)) - def visit_FunctionDefT(self, node): + def visit_FunctionDefT(self, node, in_class=None): func = self.visit_function(node, is_lambda=False, is_internal=len(self.name) > 2) - self._set_local(node.name, func) + if in_class is None: + self._set_local(node.name, func) + else: + self.append(ir.SetAttr(in_class, node.name, func)) def visit_Return(self, node): if node.value is None: @@ -668,9 +683,19 @@ class ARTIQIRGenerator(algorithm.Visitor): return self.current_env def _get_local(self, name): - return self.append(ir.GetLocal(self._env_for(name), name, name="local." + name)) + if self.current_class is not None and \ + name in self.current_class.type.attributes: + return self.append(ir.GetAttr(self.current_class, name, + name="local." + name)) + + return self.append(ir.GetLocal(self._env_for(name), name, + name="local." + name)) def _set_local(self, name, value): + if self.current_class is not None and \ + name in self.current_class.type.attributes: + return self.append(ir.SetAttr(self.current_class, name, value)) + self.append(ir.SetLocal(self._env_for(name), name, value)) def visit_NameT(self, node): @@ -706,7 +731,7 @@ class ARTIQIRGenerator(algorithm.Visitor): head = self.current_block self.current_block = out_of_bounds_block = self.add_block() - exn = self.alloc_exn(builtins.TIndexError(), + exn = self.alloc_exn(builtins.TException("IndexError"), ir.Constant("index {0} out of bounds 0:{1}", builtins.TStr()), index, length) self.raise_exn(exn, loc=loc) @@ -790,7 +815,7 @@ class ARTIQIRGenerator(algorithm.Visitor): step = self.visit(node.slice.step) self._make_check( self.append(ir.Compare(ast.NotEq(loc=None), step, ir.Constant(0, step.type))), - lambda: self.alloc_exn(builtins.TValueError(), + lambda: self.alloc_exn(builtins.TException("ValueError"), ir.Constant("step cannot be zero", builtins.TStr())), loc=node.slice.step.loc) else: @@ -811,7 +836,7 @@ class ARTIQIRGenerator(algorithm.Visitor): name="slice.size")) self._make_check( self.append(ir.Compare(ast.LtE(loc=None), slice_size, length)), - lambda: self.alloc_exn(builtins.TValueError(), + lambda: self.alloc_exn(builtins.TException("ValueError"), ir.Constant("slice size {0} is larger than iterable length {1}", builtins.TStr()), slice_size, length), @@ -894,7 +919,7 @@ class ARTIQIRGenerator(algorithm.Visitor): self._make_check( self.append(ir.Compare(ast.Eq(loc=None), length, ir.Constant(len(node.elts), self._size_type))), - lambda: self.alloc_exn(builtins.TValueError(), + lambda: self.alloc_exn(builtins.TException("ValueError"), ir.Constant("list must be {0} elements long to decompose", builtins.TStr()), length)) @@ -1002,13 +1027,13 @@ class ARTIQIRGenerator(algorithm.Visitor): # Check for negative shift amount. self._make_check( self.append(ir.Compare(ast.GtE(loc=None), rhs, ir.Constant(0, rhs.type))), - lambda: self.alloc_exn(builtins.TValueError(), + lambda: self.alloc_exn(builtins.TException("ValueError"), ir.Constant("shift amount must be nonnegative", builtins.TStr())), loc=node.right.loc) elif isinstance(node.op, (ast.Div, ast.FloorDiv)): self._make_check( self.append(ir.Compare(ast.NotEq(loc=None), rhs, ir.Constant(0, rhs.type))), - lambda: self.alloc_exn(builtins.TZeroDivisionError(), + lambda: self.alloc_exn(builtins.TException("ZeroDivisionError"), ir.Constant("cannot divide by zero", builtins.TStr())), loc=node.right.loc) diff --git a/artiq/compiler/transforms/asttyped_rewriter.py b/artiq/compiler/transforms/asttyped_rewriter.py index 51b84efb0..5f02431c4 100644 --- a/artiq/compiler/transforms/asttyped_rewriter.py +++ b/artiq/compiler/transforms/asttyped_rewriter.py @@ -3,7 +3,8 @@ to a typedtree (:mod:`..asttyped`). """ -from pythonparser import algorithm, diagnostic +from collections import OrderedDict +from pythonparser import ast, algorithm, diagnostic from .. import asttyped, types, builtins # This visitor will be called for every node with a scope, @@ -16,7 +17,7 @@ class LocalExtractor(algorithm.Visitor): self.in_root = False self.in_assign = False - self.typing_env = {} + self.typing_env = OrderedDict() # which names are global have to be recorded in the current scope self.global_ = set() @@ -189,6 +190,7 @@ class ASTTypedRewriter(algorithm.Transformer): self.engine = engine self.globals = None self.env_stack = [prelude] + self.in_class = None def _try_find_name(self, name): for typing_env in reversed(self.env_stack): @@ -196,11 +198,17 @@ class ASTTypedRewriter(algorithm.Transformer): return typing_env[name] def _find_name(self, name, loc): + if self.in_class is not None: + typ = self.in_class.constructor_type.attributes.get(name) + if typ is not None: + return typ + typ = self._try_find_name(name) if typ is not None: return typ + diag = diagnostic.Diagnostic("fatal", - "name '{name}' is not bound to anything", {"name":name}, loc) + "undefined variable '{name}'", {"name":name}, loc) self.engine.process(diag) # Visitors that replace node with a typed node @@ -239,6 +247,66 @@ class ASTTypedRewriter(algorithm.Transformer): finally: self.env_stack.pop() + 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":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.TMono(node.name) + instance_type.attributes = OrderedDict({}) # TODO + + # 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 + + 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_arg(self, node): return asttyped.argT(type=self._find_name(node.arg, node.loc), arg=node.arg, annotation=self.visit(node.annotation), @@ -426,7 +494,6 @@ class ASTTypedRewriter(algorithm.Transformer): visit_YieldFrom = visit_unsupported # stmt - visit_ClassDef = visit_unsupported visit_Delete = visit_unsupported visit_Import = visit_unsupported visit_ImportFrom = visit_unsupported diff --git a/artiq/compiler/transforms/inferencer.py b/artiq/compiler/transforms/inferencer.py index 0fe000189..5c906939d 100644 --- a/artiq/compiler/transforms/inferencer.py +++ b/artiq/compiler/transforms/inferencer.py @@ -467,7 +467,7 @@ class Inferencer(algorithm.Visitor): else: diagnose(valid_forms()) - self._unify(node.type, getattr(builtins, "T" + typ.name)(), + self._unify(node.type, typ.instance, node.loc, None) elif types.is_builtin(typ, "bool"): valid_forms = lambda: [ @@ -895,38 +895,47 @@ class Inferencer(algorithm.Visitor): "decorators are not supported", {}, node.at_locs[index], [decorator.loc]) self.engine.process(diag) - return - old_function, self.function = self.function, node - old_in_loop, self.in_loop = self.in_loop, False - old_has_return, self.has_return = self.has_return, False + try: + old_function, self.function = self.function, node + old_in_loop, self.in_loop = self.in_loop, False + old_has_return, self.has_return = self.has_return, False - self.generic_visit(node) + self.generic_visit(node) - # Lack of return statements is not the only case where the return - # type cannot be inferred. The other one is infinite (possibly mutual) - # recursion. Since Python functions don't have to return a value, - # we ignore that one. - if not self.has_return: - def makenotes(printer, typea, typeb, loca, locb): - return [ - diagnostic.Diagnostic("note", - "function with return type {typea}", - {"typea": printer.name(typea)}, - node.name_loc), - ] - self._unify(node.return_type, builtins.TNone(), - node.name_loc, None, makenotes) - - self.function = old_function - self.in_loop = old_in_loop - self.has_return = old_has_return + # Lack of return statements is not the only case where the return + # type cannot be inferred. The other one is infinite (possibly mutual) + # recursion. Since Python functions don't have to return a value, + # we ignore that one. + if not self.has_return: + def makenotes(printer, typea, typeb, loca, locb): + return [ + diagnostic.Diagnostic("note", + "function with return type {typea}", + {"typea": printer.name(typea)}, + node.name_loc), + ] + self._unify(node.return_type, builtins.TNone(), + node.name_loc, None, makenotes) + finally: + self.function = old_function + self.in_loop = old_in_loop + self.has_return = old_has_return signature_type = self._type_from_arguments(node.args, node.return_type) if signature_type: self._unify(node.signature_type, signature_type, node.name_loc, None) + def visit_ClassDefT(self, node): + if any(node.decorator_list): + diag = diagnostic.Diagnostic("error", + "decorators are not supported", {}, + node.at_locs[0], [node.decorator_list[0].loc]) + self.engine.process(diag) + + self.generic_visit(node) + def visit_Return(self, node): if not self.function: diag = diagnostic.Diagnostic("error", diff --git a/artiq/compiler/transforms/llvm_ir_generator.py b/artiq/compiler/transforms/llvm_ir_generator.py index 4fd341763..5b2dfb929 100644 --- a/artiq/compiler/transforms/llvm_ir_generator.py +++ b/artiq/compiler/transforms/llvm_ir_generator.py @@ -437,7 +437,7 @@ class LLVMIRGenerator: size=llsize) llvalue = self.llbuilder.insert_value(llvalue, llalloc, 1, name=insn.name) return llvalue - elif builtins.is_exception(insn.type): + elif builtins.is_exception(insn.type) or types.is_constructor(insn.type): llalloc = self.llbuilder.alloca(self.llty_of_type(insn.type, bare=True)) for index, operand in enumerate(insn.operands): lloperand = self.map(operand) diff --git a/artiq/compiler/types.py b/artiq/compiler/types.py index 631a9107e..93b3b3d20 100644 --- a/artiq/compiler/types.py +++ b/artiq/compiler/types.py @@ -319,9 +319,17 @@ class TConstructor(TBuiltin): A type of a constructor of a builtin class, e.g. ``list``. Note that this is not the same as the type of an instance of the class, which is ``TMono("list", ...)``. + + :ivar instance: (:class:`Type`) + the type of the instance created by this constructor """ -class TExceptionConstructor(TBuiltin): + def __init__(self, instance): + assert isinstance(instance, TMono) + super().__init__(instance.name) + self.instance = instance + +class TExceptionConstructor(TConstructor): """ A type of a constructor of a builtin exception, e.g. ``Exception``. Note that this is not the same as the type of an instance of @@ -402,6 +410,14 @@ def is_builtin(typ, name=None): return isinstance(typ, TBuiltin) and \ typ.name == name +def is_constructor(typ, name=None): + typ = typ.find() + if name is not None: + return isinstance(typ, TConstructor) and \ + typ.name == name + else: + return isinstance(typ, TConstructor) + def is_exn_constructor(typ, name=None): typ = typ.find() if name is not None: @@ -459,9 +475,11 @@ class TypePrinter(object): elif isinstance(typ, TFunction): return signature elif isinstance(typ, TBuiltinFunction): - return "" % typ.name + return "".format(typ.name) elif isinstance(typ, (TConstructor, TExceptionConstructor)): - return "" % typ.name + attrs = ", ".join(["{}: {}".format(attr, self.name(typ.attributes[attr])) + for attr in typ.attributes]) + return "".format(typ.name, attrs) elif isinstance(typ, TValue): return repr(typ.value) else: diff --git a/artiq/compiler/validators/escape.py b/artiq/compiler/validators/escape.py index df87c6afc..9fc053fe6 100644 --- a/artiq/compiler/validators/escape.py +++ b/artiq/compiler/validators/escape.py @@ -208,7 +208,7 @@ class EscapeValidator(algorithm.Visitor): loc) ] - def visit_in_region(self, node, region): + def visit_in_region(self, node, region, typing_env): try: old_youngest_region = self.youngest_region self.youngest_region = region @@ -216,8 +216,8 @@ class EscapeValidator(algorithm.Visitor): old_youngest_env = self.youngest_env self.youngest_env = {} - for name in node.typing_env: - if builtins.is_allocated(node.typing_env[name]): + for name in typing_env: + if builtins.is_allocated(typing_env[name]): self.youngest_env[name] = Region(None) # not yet known else: self.youngest_env[name] = None # lives forever @@ -230,11 +230,15 @@ class EscapeValidator(algorithm.Visitor): self.youngest_region = old_youngest_region def visit_ModuleT(self, node): - self.visit_in_region(node, None) + self.visit_in_region(node, None, node.typing_env) def visit_FunctionDefT(self, node): self.youngest_env[node.name] = self.youngest_region - self.visit_in_region(node, Region(node.loc)) + self.visit_in_region(node, Region(node.loc), node.typing_env) + + def visit_ClassDefT(self, node): + self.youngest_env[node.name] = self.youngest_region + self.visit_in_region(node, Region(node.loc), node.constructor_type.attributes) # Only three ways for a pointer to escape: # * Assigning or op-assigning it (we ensure an outlives relationship) diff --git a/lit-test/test/inferencer/builtin_calls.py b/lit-test/test/inferencer/builtin_calls.py index 50597151f..1ba629fba 100644 --- a/lit-test/test/inferencer/builtin_calls.py +++ b/lit-test/test/inferencer/builtin_calls.py @@ -1,28 +1,28 @@ # RUN: %python -m artiq.compiler.testbench.inferencer %s >%t # RUN: OutputCheck %s --file-to-check=%t -# CHECK-L: bool:():bool +# CHECK-L: bool:():bool bool() -# CHECK-L: bool:([]:list(elt='a)):bool +# CHECK-L: bool:([]:list(elt='a)):bool bool([]) -# CHECK-L: int:():int(width='b) +# CHECK-L: int:():int(width='b) int() -# CHECK-L: int:(1.0:float):int(width='c) +# CHECK-L: int:(1.0:float):int(width='c) int(1.0) -# CHECK-L: int:(1.0:float, width=64:int(width='d)):int(width=64) +# CHECK-L: int:(1.0:float, width=64:int(width='d)):int(width=64) int(1.0, width=64) -# CHECK-L: float:():float +# CHECK-L: float:():float float() -# CHECK-L: float:(1:int(width='e)):float +# CHECK-L: float:(1:int(width='e)):float float(1) -# CHECK-L: list:():list(elt='f) +# CHECK-L: list:():list(elt='f) list() # CHECK-L: len:([]:list(elt='g)):int(width=32) diff --git a/lit-test/test/inferencer/class.py b/lit-test/test/inferencer/class.py new file mode 100644 index 000000000..4b14a2737 --- /dev/null +++ b/lit-test/test/inferencer/class.py @@ -0,0 +1,14 @@ +# RUN: %python -m artiq.compiler.testbench.inferencer %s >%t +# RUN: OutputCheck %s --file-to-check=%t + +class c: + a = 1 + def f(): + pass + +# CHECK-L: c:NoneType +c.f diff --git a/lit-test/test/inferencer/error_class.py b/lit-test/test/inferencer/error_class.py new file mode 100644 index 000000000..0cc075b05 --- /dev/null +++ b/lit-test/test/inferencer/error_class.py @@ -0,0 +1,10 @@ +# RUN: %python -m artiq.compiler.testbench.inferencer +diag %s >%t +# RUN: OutputCheck %s --file-to-check=%t + +# CHECK-L: ${LINE:+1}: error: inheritance is not supported +class a(1): + pass + +class b: + # CHECK-L: ${LINE:+1}: fatal: class body must contain only assignments and function definitions + x += 1 diff --git a/lit-test/test/inferencer/error_local_unbound.py b/lit-test/test/inferencer/error_local_unbound.py index 6b4350157..7327a8548 100644 --- a/lit-test/test/inferencer/error_local_unbound.py +++ b/lit-test/test/inferencer/error_local_unbound.py @@ -1,5 +1,5 @@ # RUN: %python -m artiq.compiler.testbench.inferencer +diag %s >%t # RUN: OutputCheck %s --file-to-check=%t -# CHECK-L: ${LINE:+1}: fatal: name 'x' is not bound to anything +# CHECK-L: ${LINE:+1}: fatal: undefined variable 'x' x diff --git a/lit-test/test/inferencer/exception.py b/lit-test/test/inferencer/exception.py index bfdbbfb7c..e0e0f9645 100644 --- a/lit-test/test/inferencer/exception.py +++ b/lit-test/test/inferencer/exception.py @@ -1,7 +1,7 @@ # RUN: %python -m artiq.compiler.testbench.inferencer %s >%t # RUN: OutputCheck %s --file-to-check=%t -# CHECK-L: Exception: +# CHECK-L: Exception: Exception try: diff --git a/lit-test/test/inferencer/unify.py b/lit-test/test/inferencer/unify.py index c6f41b495..31679beb6 100644 --- a/lit-test/test/inferencer/unify.py +++ b/lit-test/test/inferencer/unify.py @@ -58,16 +58,16 @@ k = "x" # CHECK-L: k:str IndexError() -# CHECK-L: IndexError:():IndexError +# CHECK-L: IndexError:():IndexError IndexError("x") -# CHECK-L: IndexError:("x":str):IndexError +# CHECK-L: IndexError:("x":str):IndexError IndexError("x", 1) -# CHECK-L: IndexError:("x":str, 1:int(width=64)):IndexError +# CHECK-L: IndexError:("x":str, 1:int(width=64)):IndexError IndexError("x", 1, 1) -# CHECK-L: IndexError:("x":str, 1:int(width=64), 1:int(width=64)):IndexError +# CHECK-L: IndexError:("x":str, 1:int(width=64), 1:int(width=64)):IndexError IndexError("x", 1, 1, 1) -# CHECK-L: IndexError:("x":str, 1:int(width=64), 1:int(width=64), 1:int(width=64)):IndexError +# CHECK-L: IndexError:("x":str, 1:int(width=64), 1:int(width=64), 1:int(width=64)):IndexError diff --git a/lit-test/test/integration/class.py b/lit-test/test/integration/class.py new file mode 100644 index 000000000..1937ec85f --- /dev/null +++ b/lit-test/test/integration/class.py @@ -0,0 +1,12 @@ +# RUN: %python -m artiq.compiler.testbench.jit %s +# RUN: %python %s + +class c: + a = 1 + def f(): + return 2 + +# CHECK-L: a 1 +print("a", c.a) +# CHECK-L: f() 2 +print("f()", c.f())