artiq/artiq/compiler/transforms/asttyped_rewriter.py

528 lines
20 KiB
Python

"""
:class:`ASTTypedRewriter` rewrites a parsetree (:mod:`pythonparser.ast`)
to a typedtree (:mod:`..asttyped`).
"""
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,
# i.e.: class, function, comprehension, lambda
class LocalExtractor(algorithm.Visitor):
def __init__(self, env_stack, engine):
super().__init__()
self.env_stack = env_stack
self.engine = engine
self.in_root = False
self.in_assign = False
self.typing_env = OrderedDict()
# which names are global have to be recorded in the current scope
self.global_ = set()
# which names are nonlocal only affects whether the current scope
# gets a new binding or not, so we throw this away
self.nonlocal_ = set()
# parameters can't be declared as global or nonlocal
self.params = set()
def visit_in_assign(self, node, in_assign):
try:
old_in_assign, self.in_assign = self.in_assign, in_assign
return self.visit(node)
finally:
self.in_assign = old_in_assign
def visit_Assign(self, node):
self.visit(node.value)
self.visit_in_assign(node.targets, in_assign=True)
def visit_For(self, node):
self.visit(node.iter)
self.visit_in_assign(node.target, in_assign=True)
self.visit(node.body)
self.visit(node.orelse)
def visit_withitem(self, node):
self.visit(node.context_expr)
self.visit_in_assign(node.optional_vars, in_assign=True)
def visit_comprehension(self, node):
self.visit(node.iter)
self.visit_in_assign(node.target, in_assign=True)
self.visit(node.ifs)
def visit_generator(self, node):
if self.in_root:
return
self.in_root = True
self.visit(list(reversed(node.generators)))
self.visit(node.elt)
visit_ListComp = visit_generator
visit_SetComp = visit_generator
visit_GeneratorExp = visit_generator
def visit_DictComp(self, node):
if self.in_root:
return
self.in_root = True
self.visit(list(reversed(node.generators)))
self.visit(node.key)
self.visit(node.value)
def visit_root(self, node):
if self.in_root:
return
self.in_root = True
self.generic_visit(node)
visit_Module = visit_root # don't look at inner scopes
visit_ClassDef = visit_root
visit_Lambda = visit_root
def visit_FunctionDef(self, node):
if self.in_root:
self._assignable(node.name)
self.visit_root(node)
def _assignable(self, name):
assert name is not None
if name not in self.typing_env and name not in self.nonlocal_:
self.typing_env[name] = types.TVar()
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 visit_Name(self, node):
if self.in_assign:
# code like:
# x = 1
# def f():
# x = 1
# creates a new binding for x in f's scope
self._assignable(node.id)
def visit_Attribute(self, node):
self.visit_in_assign(node.value, in_assign=False)
def visit_Subscript(self, node):
self.visit_in_assign(node.value, in_assign=False)
self.visit_in_assign(node.slice, in_assign=False)
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_Global(self, node):
for name, loc in zip(node.names, node.name_locs):
if self._check_not_in(name, self.nonlocal_, "nonlocal", "global", loc) or \
self._check_not_in(name, self.params, "a parameter", "global", loc):
continue
self.global_.add(name)
if len(self.env_stack) == 1:
self._assignable(name) # already in global scope
else:
if name not in self.env_stack[1]:
self.env_stack[1][name] = types.TVar()
self.typing_env[name] = self.env_stack[1][name]
def visit_Nonlocal(self, node):
for name, loc in zip(node.names, node.name_locs):
if self._check_not_in(name, self.global_, "global", "nonlocal", loc) or \
self._check_not_in(name, self.params, "a parameter", "nonlocal", loc):
continue
# nonlocal does not search prelude and global scopes
found = False
for outer_env in reversed(self.env_stack[2:]):
if name in outer_env:
found = True
break
if not found:
diag = diagnostic.Diagnostic("error",
"cannot declare name '{name}' as nonlocal: it is not bound in any outer scope",
{"name": name},
loc, [node.keyword_loc])
self.engine.process(diag)
continue
self.nonlocal_.add(name)
def visit_ExceptHandler(self, node):
self.visit(node.type)
if node.name is not None:
self._assignable(node.name)
for stmt in node.body:
self.visit(stmt)
class ASTTypedRewriter(algorithm.Transformer):
"""
:class:`ASTTypedRewriter` converts an untyped AST to a typed AST
where all type fields of non-literals are filled with fresh type variables,
and type fields of literals are filled with corresponding types.
:class:`ASTTypedRewriter` also discovers the scope of variable bindings
via :class:`LocalExtractor`.
"""
def __init__(self, engine, prelude):
self.engine = engine
self.globals = None
self.env_stack = [prelude]
self.in_class = None
def _try_find_name(self, name):
if self.in_class is not None:
typ = self.in_class.constructor_type.attributes.get(name)
if typ is not None:
return typ
for typing_env in reversed(self.env_stack):
if name in typing_env:
return typing_env[name]
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)
# Visitors that replace node with a typed node
#
def visit_Module(self, node):
extractor = LocalExtractor(env_stack=self.env_stack, engine=self.engine)
extractor.visit(node)
node = asttyped.ModuleT(
typing_env=extractor.typing_env, globals_in_scope=extractor.global_,
body=node.body, loc=node.loc)
self.globals = node.typing_env
try:
self.env_stack.append(node.typing_env)
return self.generic_visit(node)
finally:
self.env_stack.pop()
def visit_FunctionDef(self, node):
extractor = LocalExtractor(env_stack=self.env_stack, engine=self.engine)
extractor.visit(node)
signature_type = self._find_name(node.name, node.name_loc)
node = asttyped.FunctionDefT(
typing_env=extractor.typing_env, globals_in_scope=extractor.global_,
signature_type=signature_type, return_type=types.TVar(),
name=node.name, args=node.args, returns=node.returns,
body=node.body, decorator_list=node.decorator_list,
keyword_loc=node.keyword_loc, name_loc=node.name_loc,
arrow_loc=node.arrow_loc, colon_loc=node.colon_loc, at_locs=node.at_locs,
loc=node.loc, remote_fn=False)
try:
self.env_stack.append(node.typing_env)
return self.generic_visit(node)
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":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_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_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 visit_Str(self, node):
if isinstance(node.s, str):
typ = builtins.TStr()
elif isinstance(node.s, bytes):
typ = builtins.TBytes()
else:
assert False
return asttyped.StrT(type=typ, s=node.s,
begin_loc=node.begin_loc, end_loc=node.end_loc, loc=node.loc)
def visit_Name(self, node):
return asttyped.NameT(type=self._find_name(node.id, node.loc),
id=node.id, ctx=node.ctx, loc=node.loc)
def visit_NameConstant(self, node):
if node.value is True or node.value is False:
typ = builtins.TBool()
elif node.value is None:
typ = builtins.TNone()
return asttyped.NameConstantT(type=typ, value=node.value, loc=node.loc)
def visit_Tuple(self, node):
node = self.generic_visit(node)
return asttyped.TupleT(type=types.TTuple([x.type for x in node.elts]),
elts=node.elts, ctx=node.ctx, loc=node.loc)
def visit_List(self, node):
node = self.generic_visit(node)
node = asttyped.ListT(type=builtins.TList(),
elts=node.elts, ctx=node.ctx,
begin_loc=node.begin_loc, end_loc=node.end_loc, loc=node.loc)
return self.visit(node)
def visit_Attribute(self, node):
node = self.generic_visit(node)
node = asttyped.AttributeT(type=types.TVar(),
value=node.value, attr=node.attr, ctx=node.ctx,
dot_loc=node.dot_loc, attr_loc=node.attr_loc, loc=node.loc)
return self.visit(node)
def visit_Slice(self, node):
node = self.generic_visit(node)
node = asttyped.SliceT(type=types.TVar(),
lower=node.lower, upper=node.upper, step=node.step,
bound_colon_loc=node.bound_colon_loc,
step_colon_loc=node.step_colon_loc,
loc=node.loc)
return self.visit(node)
def visit_Subscript(self, node):
node = self.generic_visit(node)
node = asttyped.SubscriptT(type=types.TVar(),
value=node.value, slice=node.slice, ctx=node.ctx,
begin_loc=node.begin_loc, end_loc=node.end_loc, loc=node.loc)
return self.visit(node)
def visit_BoolOp(self, node):
node = self.generic_visit(node)
node = asttyped.BoolOpT(type=types.TVar(),
op=node.op, values=node.values,
op_locs=node.op_locs, loc=node.loc)
return self.visit(node)
def visit_UnaryOp(self, node):
node = self.generic_visit(node)
node = asttyped.UnaryOpT(type=types.TVar(),
op=node.op, operand=node.operand,
loc=node.loc)
return self.visit(node)
def visit_BinOp(self, node):
node = self.generic_visit(node)
node = asttyped.BinOpT(type=types.TVar(),
left=node.left, op=node.op, right=node.right,
loc=node.loc)
return self.visit(node)
def visit_Compare(self, node):
node = self.generic_visit(node)
node = asttyped.CompareT(type=types.TVar(),
left=node.left, ops=node.ops, comparators=node.comparators,
loc=node.loc)
return self.visit(node)
def visit_IfExp(self, node):
node = self.generic_visit(node)
node = asttyped.IfExpT(type=types.TVar(),
test=node.test, body=node.body, orelse=node.orelse,
if_loc=node.if_loc, else_loc=node.else_loc, loc=node.loc)
return self.visit(node)
def visit_ListComp(self, node):
extractor = LocalExtractor(env_stack=self.env_stack, engine=self.engine)
extractor.visit(node)
node = asttyped.ListCompT(
typing_env=extractor.typing_env, globals_in_scope=extractor.global_,
type=types.TVar(),
elt=node.elt, generators=node.generators,
begin_loc=node.begin_loc, end_loc=node.end_loc, loc=node.loc)
try:
self.env_stack.append(node.typing_env)
return self.generic_visit(node)
finally:
self.env_stack.pop()
def visit_Call(self, node):
node = self.generic_visit(node)
node = asttyped.CallT(type=types.TVar(), iodelay=None, arg_exprs={},
remote_fn=False, func=node.func,
args=node.args, keywords=node.keywords,
starargs=node.starargs, kwargs=node.kwargs,
star_loc=node.star_loc, dstar_loc=node.dstar_loc,
begin_loc=node.begin_loc, end_loc=node.end_loc, loc=node.loc)
return node
def visit_Lambda(self, node):
extractor = LocalExtractor(env_stack=self.env_stack, engine=self.engine)
extractor.visit(node)
node = asttyped.LambdaT(
typing_env=extractor.typing_env, globals_in_scope=extractor.global_,
type=types.TVar(),
args=node.args, body=node.body,
lambda_loc=node.lambda_loc, colon_loc=node.colon_loc, loc=node.loc)
try:
self.env_stack.append(node.typing_env)
return self.generic_visit(node)
finally:
self.env_stack.pop()
def visit_ExceptHandler(self, node):
node = self.generic_visit(node)
if node.name is not None:
name_type = self._find_name(node.name, node.name_loc)
else:
name_type = types.TVar()
node = asttyped.ExceptHandlerT(
name_type=name_type,
filter=node.type, name=node.name, body=node.body,
except_loc=node.except_loc, as_loc=node.as_loc, name_loc=node.name_loc,
colon_loc=node.colon_loc, loc=node.loc)
return node
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 visit_For(self, node):
node = self.generic_visit(node)
node = asttyped.ForT(
target=node.target, iter=node.iter, body=node.body, orelse=node.orelse,
trip_count=None, trip_interval=None,
keyword_loc=node.keyword_loc, in_loc=node.in_loc, for_colon_loc=node.for_colon_loc,
else_loc=node.else_loc, else_colon_loc=node.else_colon_loc, loc=node.loc)
return node
def visit_withitem(self, node):
node = self.generic_visit(node)
node = asttyped.withitemT(
context_expr=node.context_expr, optional_vars=node.optional_vars,
enter_type=types.TVar(), exit_type=types.TVar(),
as_loc=node.as_loc, loc=node.loc)
return node
# Unsupported visitors
#
def visit_unsupported(self, node):
diag = diagnostic.Diagnostic("fatal",
"this syntax is not supported", {},
node.loc)
self.engine.process(diag)
# expr
visit_Dict = visit_unsupported
visit_DictComp = visit_unsupported
visit_Ellipsis = visit_unsupported
visit_GeneratorExp = visit_unsupported
# visit_Set = visit_unsupported
visit_SetComp = visit_unsupported
visit_Starred = visit_unsupported
visit_Yield = visit_unsupported
visit_YieldFrom = visit_unsupported
# stmt
visit_Delete = visit_unsupported
visit_Import = visit_unsupported
visit_ImportFrom = visit_unsupported