usse/funda-scraper/venv/lib/python3.10/site-packages/mypyc/irbuild/statement.py

695 lines
26 KiB
Python
Raw Normal View History

2023-02-20 22:38:24 +00:00
"""Transform mypy statement ASTs to mypyc IR (Intermediate Representation).
The top-level AST transformation logic is implemented in mypyc.irbuild.visitor
and mypyc.irbuild.builder.
A few statements are transformed in mypyc.irbuild.function (yield, for example).
"""
from typing import Optional, List, Tuple, Sequence, Callable
import importlib.util
from mypy.nodes import (
Block, ExpressionStmt, ReturnStmt, AssignmentStmt, OperatorAssignmentStmt, IfStmt, WhileStmt,
ForStmt, BreakStmt, ContinueStmt, RaiseStmt, TryStmt, WithStmt, AssertStmt, DelStmt,
Expression, StrExpr, TempNode, Lvalue, Import, ImportFrom, ImportAll, TupleExpr, ListExpr,
StarExpr
)
from mypyc.ir.ops import (
Assign, Unreachable, RaiseStandardError, LoadErrorValue, BasicBlock, TupleGet, Value, Register,
Branch, NO_TRACEBACK_LINE_NO
)
from mypyc.ir.rtypes import RInstance, exc_rtuple, is_tagged
from mypyc.primitives.generic_ops import py_delattr_op
from mypyc.primitives.misc_ops import type_op, import_from_op
from mypyc.primitives.exc_ops import (
raise_exception_op, reraise_exception_op, error_catch_op, exc_matches_op, restore_exc_info_op,
get_exc_value_op, keep_propagating_op, get_exc_info_op
)
from mypyc.irbuild.targets import (
AssignmentTarget, AssignmentTargetRegister, AssignmentTargetIndex, AssignmentTargetAttr,
AssignmentTargetTuple
)
from mypyc.irbuild.nonlocalcontrol import (
ExceptNonlocalControl, FinallyNonlocalControl, TryFinallyNonlocalControl
)
from mypyc.irbuild.for_helpers import for_loop_helper
from mypyc.irbuild.builder import IRBuilder, int_borrow_friendly_op
from mypyc.irbuild.ast_helpers import process_conditional, is_borrow_friendly_expr
GenFunc = Callable[[], None]
def transform_block(builder: IRBuilder, block: Block) -> None:
if not block.is_unreachable:
for stmt in block.body:
builder.accept(stmt)
# Raise a RuntimeError if we hit a non-empty unreachable block.
# Don't complain about empty unreachable blocks, since mypy inserts
# those after `if MYPY`.
elif block.body:
builder.add(RaiseStandardError(RaiseStandardError.RUNTIME_ERROR,
'Reached allegedly unreachable code!',
block.line))
builder.add(Unreachable())
def transform_expression_stmt(builder: IRBuilder, stmt: ExpressionStmt) -> None:
if isinstance(stmt.expr, StrExpr):
# Docstring. Ignore
return
# ExpressionStmts do not need to be coerced like other Expressions, so we shouldn't
# call builder.accept here.
stmt.expr.accept(builder.visitor)
builder.flush_keep_alives()
def transform_return_stmt(builder: IRBuilder, stmt: ReturnStmt) -> None:
if stmt.expr:
retval = builder.accept(stmt.expr)
else:
retval = builder.builder.none()
retval = builder.coerce(retval, builder.ret_types[-1], stmt.line)
builder.nonlocal_control[-1].gen_return(builder, retval, stmt.line)
def transform_assignment_stmt(builder: IRBuilder, stmt: AssignmentStmt) -> None:
lvalues = stmt.lvalues
assert len(lvalues) >= 1
builder.disallow_class_assignments(lvalues, stmt.line)
first_lvalue = lvalues[0]
if stmt.type and isinstance(stmt.rvalue, TempNode):
# This is actually a variable annotation without initializer. Don't generate
# an assignment but we need to call get_assignment_target since it adds a
# name binding as a side effect.
builder.get_assignment_target(first_lvalue, stmt.line)
return
# Special case multiple assignments like 'x, y = e1, e2'.
if (isinstance(first_lvalue, (TupleExpr, ListExpr))
and isinstance(stmt.rvalue, (TupleExpr, ListExpr))
and len(first_lvalue.items) == len(stmt.rvalue.items)
and all(is_simple_lvalue(item) for item in first_lvalue.items)
and len(lvalues) == 1):
temps = []
for right in stmt.rvalue.items:
rvalue_reg = builder.accept(right)
temp = Register(rvalue_reg.type)
builder.assign(temp, rvalue_reg, stmt.line)
temps.append(temp)
for (left, temp) in zip(first_lvalue.items, temps):
assignment_target = builder.get_assignment_target(left)
builder.assign(assignment_target, temp, stmt.line)
return
line = stmt.rvalue.line
rvalue_reg = builder.accept(stmt.rvalue)
if builder.non_function_scope() and stmt.is_final_def:
builder.init_final_static(first_lvalue, rvalue_reg)
for lvalue in lvalues:
target = builder.get_assignment_target(lvalue)
builder.assign(target, rvalue_reg, line)
builder.flush_keep_alives()
def is_simple_lvalue(expr: Expression) -> bool:
return not isinstance(expr, (StarExpr, ListExpr, TupleExpr))
def transform_operator_assignment_stmt(builder: IRBuilder, stmt: OperatorAssignmentStmt) -> None:
"""Operator assignment statement such as x += 1"""
builder.disallow_class_assignments([stmt.lvalue], stmt.line)
if (is_tagged(builder.node_type(stmt.lvalue))
and is_tagged(builder.node_type(stmt.rvalue))
and stmt.op in int_borrow_friendly_op):
can_borrow = (is_borrow_friendly_expr(builder, stmt.rvalue)
and is_borrow_friendly_expr(builder, stmt.lvalue))
else:
can_borrow = False
target = builder.get_assignment_target(stmt.lvalue)
target_value = builder.read(target, stmt.line, can_borrow=can_borrow)
rreg = builder.accept(stmt.rvalue, can_borrow=can_borrow)
# the Python parser strips the '=' from operator assignment statements, so re-add it
op = stmt.op + '='
res = builder.binary_op(target_value, rreg, op, stmt.line)
# usually operator assignments are done in-place
# but when target doesn't support that we need to manually assign
builder.assign(target, res, res.line)
builder.flush_keep_alives()
def transform_import(builder: IRBuilder, node: Import) -> None:
if node.is_mypy_only:
return
globals = builder.load_globals_dict()
for node_id, as_name in node.ids:
builder.gen_import(node_id, node.line)
# Update the globals dict with the appropriate module:
# * For 'import foo.bar as baz' we add 'foo.bar' with the name 'baz'
# * For 'import foo.bar' we add 'foo' with the name 'foo'
# Typically we then ignore these entries and access things directly
# via the module static, but we will use the globals version for modules
# that mypy couldn't find, since it doesn't analyze module references
# from those properly.
# TODO: Don't add local imports to the global namespace
# Miscompiling imports inside of functions, like below in import from.
if as_name:
name = as_name
base = node_id
else:
base = name = node_id.split('.')[0]
obj = builder.get_module(base, node.line)
builder.gen_method_call(
globals, '__setitem__', [builder.load_str(name), obj],
result_type=None, line=node.line)
def transform_import_from(builder: IRBuilder, node: ImportFrom) -> None:
if node.is_mypy_only:
return
module_state = builder.graph[builder.module_name]
if module_state.ancestors is not None and module_state.ancestors:
module_package = module_state.ancestors[0]
elif builder.module_path.endswith("__init__.py"):
module_package = builder.module_name
else:
module_package = ''
id = importlib.util.resolve_name('.' * node.relative + node.id, module_package)
globals = builder.load_globals_dict()
imported_names = [name for name, _ in node.names]
module = builder.gen_import_from(id, globals, imported_names, node.line)
# Copy everything into our module's dict.
# Note that we miscompile import from inside of functions here,
# since that case *shouldn't* load it into the globals dict.
# This probably doesn't matter much and the code runs basically right.
for name, maybe_as_name in node.names:
as_name = maybe_as_name or name
obj = builder.call_c(import_from_op,
[module, builder.load_str(id),
builder.load_str(name), builder.load_str(as_name)],
node.line)
builder.gen_method_call(
globals, '__setitem__', [builder.load_str(as_name), obj],
result_type=None, line=node.line)
def transform_import_all(builder: IRBuilder, node: ImportAll) -> None:
if node.is_mypy_only:
return
builder.gen_import(node.id, node.line)
def transform_if_stmt(builder: IRBuilder, stmt: IfStmt) -> None:
if_body, next = BasicBlock(), BasicBlock()
else_body = BasicBlock() if stmt.else_body else next
# If statements are normalized
assert len(stmt.expr) == 1
process_conditional(builder, stmt.expr[0], if_body, else_body)
builder.activate_block(if_body)
builder.accept(stmt.body[0])
builder.goto(next)
if stmt.else_body:
builder.activate_block(else_body)
builder.accept(stmt.else_body)
builder.goto(next)
builder.activate_block(next)
def transform_while_stmt(builder: IRBuilder, s: WhileStmt) -> None:
body, next, top, else_block = BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock()
normal_loop_exit = else_block if s.else_body is not None else next
builder.push_loop_stack(top, next)
# Split block so that we get a handle to the top of the loop.
builder.goto_and_activate(top)
process_conditional(builder, s.expr, body, normal_loop_exit)
builder.activate_block(body)
builder.accept(s.body)
# Add branch to the top at the end of the body.
builder.goto(top)
builder.pop_loop_stack()
if s.else_body is not None:
builder.activate_block(else_block)
builder.accept(s.else_body)
builder.goto(next)
builder.activate_block(next)
def transform_for_stmt(builder: IRBuilder, s: ForStmt) -> None:
if s.is_async:
builder.error('async for is unimplemented', s.line)
def body() -> None:
builder.accept(s.body)
def else_block() -> None:
assert s.else_body is not None
builder.accept(s.else_body)
for_loop_helper(builder, s.index, s.expr, body,
else_block if s.else_body else None, s.line)
def transform_break_stmt(builder: IRBuilder, node: BreakStmt) -> None:
builder.nonlocal_control[-1].gen_break(builder, node.line)
def transform_continue_stmt(builder: IRBuilder, node: ContinueStmt) -> None:
builder.nonlocal_control[-1].gen_continue(builder, node.line)
def transform_raise_stmt(builder: IRBuilder, s: RaiseStmt) -> None:
if s.expr is None:
builder.call_c(reraise_exception_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
return
exc = builder.accept(s.expr)
builder.call_c(raise_exception_op, [exc], s.line)
builder.add(Unreachable())
def transform_try_except(builder: IRBuilder,
body: GenFunc,
handlers: Sequence[
Tuple[Optional[Expression], Optional[Expression], GenFunc]],
else_body: Optional[GenFunc],
line: int) -> None:
"""Generalized try/except/else handling that takes functions to gen the bodies.
The point of this is to also be able to support with."""
assert handlers, "try needs except"
except_entry, exit_block, cleanup_block = BasicBlock(), BasicBlock(), BasicBlock()
double_except_block = BasicBlock()
# If there is an else block, jump there after the try, otherwise just leave
else_block = BasicBlock() if else_body else exit_block
# Compile the try block with an error handler
builder.builder.push_error_handler(except_entry)
builder.goto_and_activate(BasicBlock())
body()
builder.goto(else_block)
builder.builder.pop_error_handler()
# The error handler catches the error and then checks it
# against the except clauses. We compile the error handler
# itself with an error handler so that it can properly restore
# the *old* exc_info if an exception occurs.
# The exception chaining will be done automatically when the
# exception is raised, based on the exception in exc_info.
builder.builder.push_error_handler(double_except_block)
builder.activate_block(except_entry)
old_exc = builder.maybe_spill(builder.call_c(error_catch_op, [], line))
# Compile the except blocks with the nonlocal control flow overridden to clear exc_info
builder.nonlocal_control.append(
ExceptNonlocalControl(builder.nonlocal_control[-1], old_exc))
# Process the bodies
for type, var, handler_body in handlers:
next_block = None
if type:
next_block, body_block = BasicBlock(), BasicBlock()
matches = builder.call_c(
exc_matches_op, [builder.accept(type)], type.line
)
builder.add(Branch(matches, body_block, next_block, Branch.BOOL))
builder.activate_block(body_block)
if var:
target = builder.get_assignment_target(var)
builder.assign(
target,
builder.call_c(get_exc_value_op, [], var.line),
var.line
)
handler_body()
builder.goto(cleanup_block)
if next_block:
builder.activate_block(next_block)
# Reraise the exception if needed
if next_block:
builder.call_c(reraise_exception_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
builder.nonlocal_control.pop()
builder.builder.pop_error_handler()
# Cleanup for if we leave except through normal control flow:
# restore the saved exc_info information and continue propagating
# the exception if it exists.
builder.activate_block(cleanup_block)
builder.call_c(restore_exc_info_op, [builder.read(old_exc)], line)
builder.goto(exit_block)
# Cleanup for if we leave except through a raised exception:
# restore the saved exc_info information and continue propagating
# the exception.
builder.activate_block(double_except_block)
builder.call_c(restore_exc_info_op, [builder.read(old_exc)], line)
builder.call_c(keep_propagating_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
# If present, compile the else body in the obvious way
if else_body:
builder.activate_block(else_block)
else_body()
builder.goto(exit_block)
builder.activate_block(exit_block)
def transform_try_except_stmt(builder: IRBuilder, t: TryStmt) -> None:
def body() -> None:
builder.accept(t.body)
# Work around scoping woes
def make_handler(body: Block) -> GenFunc:
return lambda: builder.accept(body)
handlers = [(type, var, make_handler(body))
for type, var, body in zip(t.types, t.vars, t.handlers)]
else_body = (lambda: builder.accept(t.else_body)) if t.else_body else None
transform_try_except(builder, body, handlers, else_body, t.line)
def try_finally_try(builder: IRBuilder,
err_handler: BasicBlock,
return_entry: BasicBlock,
main_entry: BasicBlock,
try_body: GenFunc) -> Optional[Register]:
# Compile the try block with an error handler
control = TryFinallyNonlocalControl(return_entry)
builder.builder.push_error_handler(err_handler)
builder.nonlocal_control.append(control)
builder.goto_and_activate(BasicBlock())
try_body()
builder.goto(main_entry)
builder.nonlocal_control.pop()
builder.builder.pop_error_handler()
return control.ret_reg
def try_finally_entry_blocks(builder: IRBuilder,
err_handler: BasicBlock,
return_entry: BasicBlock,
main_entry: BasicBlock,
finally_block: BasicBlock,
ret_reg: Optional[Register]) -> Value:
old_exc = Register(exc_rtuple)
# Entry block for non-exceptional flow
builder.activate_block(main_entry)
if ret_reg:
builder.add(
Assign(
ret_reg,
builder.add(LoadErrorValue(builder.ret_types[-1]))
)
)
builder.goto(return_entry)
builder.activate_block(return_entry)
builder.add(Assign(old_exc, builder.add(LoadErrorValue(exc_rtuple))))
builder.goto(finally_block)
# Entry block for errors
builder.activate_block(err_handler)
if ret_reg:
builder.add(
Assign(
ret_reg,
builder.add(LoadErrorValue(builder.ret_types[-1]))
)
)
builder.add(Assign(old_exc, builder.call_c(error_catch_op, [], -1)))
builder.goto(finally_block)
return old_exc
def try_finally_body(
builder: IRBuilder,
finally_block: BasicBlock,
finally_body: GenFunc,
ret_reg: Optional[Value],
old_exc: Value) -> Tuple[BasicBlock, FinallyNonlocalControl]:
cleanup_block = BasicBlock()
# Compile the finally block with the nonlocal control flow overridden to restore exc_info
builder.builder.push_error_handler(cleanup_block)
finally_control = FinallyNonlocalControl(
builder.nonlocal_control[-1], ret_reg, old_exc)
builder.nonlocal_control.append(finally_control)
builder.activate_block(finally_block)
finally_body()
builder.nonlocal_control.pop()
return cleanup_block, finally_control
def try_finally_resolve_control(builder: IRBuilder,
cleanup_block: BasicBlock,
finally_control: FinallyNonlocalControl,
old_exc: Value,
ret_reg: Optional[Value]) -> BasicBlock:
"""Resolve the control flow out of a finally block.
This means returning if there was a return, propagating
exceptions, break/continue (soon), or just continuing on.
"""
reraise, rest = BasicBlock(), BasicBlock()
builder.add(Branch(old_exc, rest, reraise, Branch.IS_ERROR))
# Reraise the exception if there was one
builder.activate_block(reraise)
builder.call_c(reraise_exception_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
builder.builder.pop_error_handler()
# If there was a return, keep returning
if ret_reg:
builder.activate_block(rest)
return_block, rest = BasicBlock(), BasicBlock()
builder.add(Branch(ret_reg, rest, return_block, Branch.IS_ERROR))
builder.activate_block(return_block)
builder.nonlocal_control[-1].gen_return(builder, ret_reg, -1)
# TODO: handle break/continue
builder.activate_block(rest)
out_block = BasicBlock()
builder.goto(out_block)
# If there was an exception, restore again
builder.activate_block(cleanup_block)
finally_control.gen_cleanup(builder, -1)
builder.call_c(keep_propagating_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
return out_block
def transform_try_finally_stmt(builder: IRBuilder,
try_body: GenFunc,
finally_body: GenFunc) -> None:
"""Generalized try/finally handling that takes functions to gen the bodies.
The point of this is to also be able to support with."""
# Finally is a big pain, because there are so many ways that
# exits can occur. We emit 10+ basic blocks for every finally!
err_handler, main_entry, return_entry, finally_block = (
BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock())
# Compile the body of the try
ret_reg = try_finally_try(
builder, err_handler, return_entry, main_entry, try_body)
# Set up the entry blocks for the finally statement
old_exc = try_finally_entry_blocks(
builder, err_handler, return_entry, main_entry, finally_block, ret_reg)
# Compile the body of the finally
cleanup_block, finally_control = try_finally_body(
builder, finally_block, finally_body, ret_reg, old_exc)
# Resolve the control flow out of the finally block
out_block = try_finally_resolve_control(
builder, cleanup_block, finally_control, old_exc, ret_reg)
builder.activate_block(out_block)
def transform_try_stmt(builder: IRBuilder, t: TryStmt) -> None:
# Our compilation strategy for try/except/else/finally is to
# treat try/except/else and try/finally as separate language
# constructs that we compile separately. When we have a
# try/except/else/finally, we treat the try/except/else as the
# body of a try/finally block.
if t.finally_body:
def transform_try_body() -> None:
if t.handlers:
transform_try_except_stmt(builder, t)
else:
builder.accept(t.body)
body = t.finally_body
transform_try_finally_stmt(builder, transform_try_body, lambda: builder.accept(body))
else:
transform_try_except_stmt(builder, t)
def get_sys_exc_info(builder: IRBuilder) -> List[Value]:
exc_info = builder.call_c(get_exc_info_op, [], -1)
return [builder.add(TupleGet(exc_info, i, -1)) for i in range(3)]
def transform_with(builder: IRBuilder,
expr: Expression,
target: Optional[Lvalue],
body: GenFunc,
line: int) -> None:
# This is basically a straight transcription of the Python code in PEP 343.
# I don't actually understand why a bunch of it is the way it is.
# We could probably optimize the case where the manager is compiled by us,
# but that is not our common case at all, so.
mgr_v = builder.accept(expr)
typ = builder.call_c(type_op, [mgr_v], line)
exit_ = builder.maybe_spill(builder.py_get_attr(typ, '__exit__', line))
value = builder.py_call(
builder.py_get_attr(typ, '__enter__', line), [mgr_v], line
)
mgr = builder.maybe_spill(mgr_v)
exc = builder.maybe_spill_assignable(builder.true())
def try_body() -> None:
if target:
builder.assign(builder.get_assignment_target(target), value, line)
body()
def except_body() -> None:
builder.assign(exc, builder.false(), line)
out_block, reraise_block = BasicBlock(), BasicBlock()
builder.add_bool_branch(
builder.py_call(builder.read(exit_),
[builder.read(mgr)] + get_sys_exc_info(builder), line),
out_block,
reraise_block
)
builder.activate_block(reraise_block)
builder.call_c(reraise_exception_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
builder.activate_block(out_block)
def finally_body() -> None:
out_block, exit_block = BasicBlock(), BasicBlock()
builder.add(
Branch(builder.read(exc), exit_block, out_block, Branch.BOOL)
)
builder.activate_block(exit_block)
none = builder.none_object()
builder.py_call(
builder.read(exit_), [builder.read(mgr), none, none, none], line
)
builder.goto_and_activate(out_block)
transform_try_finally_stmt(
builder,
lambda: transform_try_except(builder,
try_body,
[(None, None, except_body)],
None,
line),
finally_body
)
def transform_with_stmt(builder: IRBuilder, o: WithStmt) -> None:
if o.is_async:
builder.error('async with is unimplemented', o.line)
# Generate separate logic for each expr in it, left to right
def generate(i: int) -> None:
if i >= len(o.expr):
builder.accept(o.body)
else:
transform_with(builder, o.expr[i], o.target[i], lambda: generate(i + 1), o.line)
generate(0)
def transform_assert_stmt(builder: IRBuilder, a: AssertStmt) -> None:
if builder.options.strip_asserts:
return
cond = builder.accept(a.expr)
ok_block, error_block = BasicBlock(), BasicBlock()
builder.add_bool_branch(cond, ok_block, error_block)
builder.activate_block(error_block)
if a.msg is None:
# Special case (for simpler generated code)
builder.add(RaiseStandardError(RaiseStandardError.ASSERTION_ERROR, None, a.line))
elif isinstance(a.msg, StrExpr):
# Another special case
builder.add(RaiseStandardError(RaiseStandardError.ASSERTION_ERROR, a.msg.value,
a.line))
else:
# The general case -- explicitly construct an exception instance
message = builder.accept(a.msg)
exc_type = builder.load_module_attr_by_fullname('builtins.AssertionError', a.line)
exc = builder.py_call(exc_type, [message], a.line)
builder.call_c(raise_exception_op, [exc], a.line)
builder.add(Unreachable())
builder.activate_block(ok_block)
def transform_del_stmt(builder: IRBuilder, o: DelStmt) -> None:
transform_del_item(builder, builder.get_assignment_target(o.expr), o.line)
def transform_del_item(builder: IRBuilder, target: AssignmentTarget, line: int) -> None:
if isinstance(target, AssignmentTargetIndex):
builder.gen_method_call(
target.base,
'__delitem__',
[target.index],
result_type=None,
line=line
)
elif isinstance(target, AssignmentTargetAttr):
if isinstance(target.obj_type, RInstance):
cl = target.obj_type.class_ir
if not cl.is_deletable(target.attr):
builder.error(f'"{target.attr}" cannot be deleted', line)
builder.note(
'Using "__deletable__ = ' +
'[\'<attr>\']" in the class body enables "del obj.<attr>"', line)
key = builder.load_str(target.attr)
builder.call_c(py_delattr_op, [target.obj, key], line)
elif isinstance(target, AssignmentTargetRegister):
# Delete a local by assigning an error value to it, which will
# prompt the insertion of uninit checks.
builder.add(Assign(target.register,
builder.add(LoadErrorValue(target.type, undefines=True))))
elif isinstance(target, AssignmentTargetTuple):
for subtarget in target.items:
transform_del_item(builder, subtarget, line)