feat: indie status page MVP -- FastAPI + SQLite
- 8 DB models (services, incidents, monitors, subscribers, etc.) - Full CRUD API for services, incidents, monitors - Public status page with live data - Incident detail page with timeline - API key authentication - Uptime monitoring scheduler - 13 tests passing - TECHNICAL_DESIGN.md with full spec
This commit is contained in:
commit
902133edd3
4655 changed files with 1342691 additions and 0 deletions
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,94 @@
|
|||
"""Simple copy propagation optimization.
|
||||
|
||||
Example input:
|
||||
|
||||
x = f()
|
||||
y = x
|
||||
|
||||
The register x is redundant and we can directly assign its value to y:
|
||||
|
||||
y = f()
|
||||
|
||||
This can optimize away registers that are assigned to once.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from mypyc.ir.func_ir import FuncIR
|
||||
from mypyc.ir.ops import Assign, AssignMulti, LoadAddress, LoadErrorValue, Register, Value
|
||||
from mypyc.irbuild.ll_builder import LowLevelIRBuilder
|
||||
from mypyc.options import CompilerOptions
|
||||
from mypyc.sametype import is_same_type
|
||||
from mypyc.transform.ir_transform import IRTransform
|
||||
|
||||
|
||||
def do_copy_propagation(fn: FuncIR, options: CompilerOptions) -> None:
|
||||
"""Perform copy propagation optimization for fn."""
|
||||
|
||||
# Anything with an assignment count >1 will not be optimized
|
||||
# here, as it would be require data flow analysis and we want to
|
||||
# keep this simple and fast, at least until we've made data flow
|
||||
# analysis much faster.
|
||||
counts: dict[Value, int] = {}
|
||||
replacements: dict[Value, Value] = {}
|
||||
for arg in fn.arg_regs:
|
||||
# Arguments are always assigned to initially
|
||||
counts[arg] = 1
|
||||
|
||||
for block in fn.blocks:
|
||||
for op in block.ops:
|
||||
if isinstance(op, Assign):
|
||||
c = counts.get(op.dest, 0)
|
||||
counts[op.dest] = c + 1
|
||||
# Does this look like a supported assignment?
|
||||
# TODO: Something needs LoadErrorValue assignments to be preserved?
|
||||
if (
|
||||
c == 0
|
||||
and is_same_type(op.dest.type, op.src.type)
|
||||
and not isinstance(op.src, LoadErrorValue)
|
||||
):
|
||||
replacements[op.dest] = op.src
|
||||
elif c == 1:
|
||||
# Too many assignments -- don't replace this one
|
||||
replacements.pop(op.dest, 0)
|
||||
elif isinstance(op, AssignMulti):
|
||||
# Copy propagation not supported for AssignMulti destinations
|
||||
counts[op.dest] = 2
|
||||
replacements.pop(op.dest, 0)
|
||||
elif isinstance(op, LoadAddress):
|
||||
# We don't support taking the address of an arbitrary Value,
|
||||
# so we'll need to preserve the operands of LoadAddress.
|
||||
if isinstance(op.src, Register):
|
||||
counts[op.src] = 2
|
||||
replacements.pop(op.src, 0)
|
||||
|
||||
# Follow chains of propagation with more than one assignment.
|
||||
for src, dst in list(replacements.items()):
|
||||
if counts.get(dst, 0) > 1:
|
||||
# Not supported
|
||||
del replacements[src]
|
||||
else:
|
||||
while dst in replacements:
|
||||
dst = replacements[dst]
|
||||
if counts.get(dst, 0) > 1:
|
||||
# Not supported
|
||||
del replacements[src]
|
||||
if src in replacements:
|
||||
replacements[src] = dst
|
||||
|
||||
builder = LowLevelIRBuilder(None, options)
|
||||
transform = CopyPropagationTransform(builder, replacements)
|
||||
transform.transform_blocks(fn.blocks)
|
||||
fn.blocks = builder.blocks
|
||||
|
||||
|
||||
class CopyPropagationTransform(IRTransform):
|
||||
def __init__(self, builder: LowLevelIRBuilder, map: dict[Value, Value]) -> None:
|
||||
super().__init__(builder)
|
||||
self.op_map.update(map)
|
||||
self.removed = set(map)
|
||||
|
||||
def visit_assign(self, op: Assign) -> Value | None:
|
||||
if op.dest in self.removed:
|
||||
return None
|
||||
return self.add(op)
|
||||
Binary file not shown.
192
venv/lib/python3.11/site-packages/mypyc/transform/exceptions.py
Normal file
192
venv/lib/python3.11/site-packages/mypyc/transform/exceptions.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
"""Transform that inserts error checks after opcodes.
|
||||
|
||||
When initially building the IR, the code doesn't perform error checks
|
||||
for exceptions. This module is used to insert all required error checks
|
||||
afterwards. Each Op describes how it indicates an error condition (if
|
||||
at all).
|
||||
|
||||
We need to split basic blocks on each error check since branches can
|
||||
only be placed at the end of a basic block.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from mypyc.ir.func_ir import FuncIR
|
||||
from mypyc.ir.ops import (
|
||||
ERR_ALWAYS,
|
||||
ERR_FALSE,
|
||||
ERR_MAGIC,
|
||||
ERR_MAGIC_OVERLAPPING,
|
||||
ERR_NEVER,
|
||||
NO_TRACEBACK_LINE_NO,
|
||||
BasicBlock,
|
||||
Branch,
|
||||
CallC,
|
||||
ComparisonOp,
|
||||
Float,
|
||||
GetAttr,
|
||||
Integer,
|
||||
LoadErrorValue,
|
||||
Op,
|
||||
RegisterOp,
|
||||
Return,
|
||||
SetAttr,
|
||||
TupleGet,
|
||||
Value,
|
||||
)
|
||||
from mypyc.ir.rtypes import RTuple, bool_rprimitive, is_float_rprimitive
|
||||
from mypyc.primitives.exc_ops import err_occurred_op
|
||||
from mypyc.primitives.registry import CFunctionDescription
|
||||
|
||||
|
||||
def insert_exception_handling(ir: FuncIR, strict_traceback_checks: bool) -> None:
|
||||
# Generate error block if any ops may raise an exception. If an op
|
||||
# fails without its own error handler, we'll branch to this
|
||||
# block. The block just returns an error value.
|
||||
error_label: BasicBlock | None = None
|
||||
for block in ir.blocks:
|
||||
adjust_error_kinds(block)
|
||||
if error_label is None and any(op.can_raise() for op in block.ops):
|
||||
error_label = add_default_handler_block(ir)
|
||||
if error_label:
|
||||
ir.blocks = split_blocks_at_errors(
|
||||
ir.blocks, error_label, ir.traceback_name, strict_traceback_checks
|
||||
)
|
||||
|
||||
|
||||
def add_default_handler_block(ir: FuncIR) -> BasicBlock:
|
||||
block = BasicBlock()
|
||||
ir.blocks.append(block)
|
||||
op = LoadErrorValue(ir.ret_type)
|
||||
block.ops.append(op)
|
||||
block.ops.append(Return(op))
|
||||
return block
|
||||
|
||||
|
||||
def split_blocks_at_errors(
|
||||
blocks: list[BasicBlock],
|
||||
default_error_handler: BasicBlock,
|
||||
func_name: str | None,
|
||||
strict_traceback_checks: bool,
|
||||
) -> list[BasicBlock]:
|
||||
new_blocks: list[BasicBlock] = []
|
||||
|
||||
# First split blocks on ops that may raise.
|
||||
for block in blocks:
|
||||
ops = block.ops
|
||||
block.ops = []
|
||||
cur_block = block
|
||||
new_blocks.append(cur_block)
|
||||
|
||||
# If the block has an error handler specified, use it. Otherwise
|
||||
# fall back to the default.
|
||||
error_label = block.error_handler or default_error_handler
|
||||
block.error_handler = None
|
||||
|
||||
for op in ops:
|
||||
target: Value = op
|
||||
cur_block.ops.append(op)
|
||||
if isinstance(op, RegisterOp) and op.error_kind != ERR_NEVER:
|
||||
# Split
|
||||
new_block = BasicBlock()
|
||||
new_blocks.append(new_block)
|
||||
|
||||
if op.error_kind == ERR_MAGIC:
|
||||
# Op returns an error value on error that depends on result RType.
|
||||
variant = Branch.IS_ERROR
|
||||
negated = False
|
||||
elif op.error_kind == ERR_FALSE:
|
||||
# Op returns a C false value on error.
|
||||
variant = Branch.BOOL
|
||||
negated = True
|
||||
elif op.error_kind == ERR_ALWAYS:
|
||||
variant = Branch.BOOL
|
||||
negated = True
|
||||
# this is a hack to represent the always fail
|
||||
# semantics, using a temporary bool with value false
|
||||
target = Integer(0, bool_rprimitive)
|
||||
elif op.error_kind == ERR_MAGIC_OVERLAPPING:
|
||||
comp = insert_overlapping_error_value_check(cur_block.ops, target)
|
||||
new_block2 = BasicBlock()
|
||||
new_blocks.append(new_block2)
|
||||
branch = Branch(
|
||||
comp,
|
||||
true_label=new_block2,
|
||||
false_label=new_block,
|
||||
op=Branch.BOOL,
|
||||
rare=True,
|
||||
)
|
||||
cur_block.ops.append(branch)
|
||||
cur_block = new_block2
|
||||
target = primitive_call(err_occurred_op, [], target.line)
|
||||
cur_block.ops.append(target)
|
||||
variant = Branch.IS_ERROR
|
||||
negated = True
|
||||
else:
|
||||
assert False, "unknown error kind %d" % op.error_kind
|
||||
|
||||
# Void ops can't generate errors since error is always
|
||||
# indicated by a special value stored in a register.
|
||||
if op.error_kind != ERR_ALWAYS:
|
||||
assert not op.is_void, "void op generating errors?"
|
||||
|
||||
branch = Branch(
|
||||
target, true_label=error_label, false_label=new_block, op=variant, line=op.line
|
||||
)
|
||||
branch.negated = negated
|
||||
if op.line != NO_TRACEBACK_LINE_NO and func_name is not None:
|
||||
if strict_traceback_checks:
|
||||
assert (
|
||||
op.line >= 0
|
||||
), f"Cannot add a traceback entry with a negative line number for op {op}"
|
||||
branch.traceback_entry = (func_name, op.line)
|
||||
cur_block.ops.append(branch)
|
||||
cur_block = new_block
|
||||
|
||||
return new_blocks
|
||||
|
||||
|
||||
def primitive_call(desc: CFunctionDescription, args: list[Value], line: int) -> CallC:
|
||||
return CallC(
|
||||
desc.c_function_name,
|
||||
[],
|
||||
desc.return_type,
|
||||
desc.steals,
|
||||
desc.is_borrowed,
|
||||
desc.error_kind,
|
||||
line,
|
||||
dependencies=desc.dependencies,
|
||||
)
|
||||
|
||||
|
||||
def adjust_error_kinds(block: BasicBlock) -> None:
|
||||
"""Infer more precise error_kind attributes for ops.
|
||||
|
||||
We have access here to more information than what was available
|
||||
when the IR was initially built.
|
||||
"""
|
||||
for op in block.ops:
|
||||
if isinstance(op, GetAttr):
|
||||
if op.class_type.class_ir.is_always_defined(op.attr):
|
||||
op.error_kind = ERR_NEVER
|
||||
if isinstance(op, SetAttr):
|
||||
if op.class_type.class_ir.is_always_defined(op.attr):
|
||||
op.error_kind = ERR_NEVER
|
||||
|
||||
|
||||
def insert_overlapping_error_value_check(ops: list[Op], target: Value) -> ComparisonOp:
|
||||
"""Append to ops to check for an overlapping error value."""
|
||||
typ = target.type
|
||||
if isinstance(typ, RTuple):
|
||||
item = TupleGet(target, 0)
|
||||
ops.append(item)
|
||||
return insert_overlapping_error_value_check(ops, item)
|
||||
else:
|
||||
errvalue: Value
|
||||
if is_float_rprimitive(target.type):
|
||||
errvalue = Float(float(typ.c_undefined))
|
||||
else:
|
||||
errvalue = Integer(int(typ.c_undefined), rtype=typ)
|
||||
op = ComparisonOp(target, errvalue, ComparisonOp.EQ)
|
||||
ops.append(op)
|
||||
return op
|
||||
Binary file not shown.
|
|
@ -0,0 +1,107 @@
|
|||
"""Bool register elimination optimization.
|
||||
|
||||
Example input:
|
||||
|
||||
L1:
|
||||
r0 = f()
|
||||
b = r0
|
||||
goto L3
|
||||
L2:
|
||||
r1 = g()
|
||||
b = r1
|
||||
goto L3
|
||||
L3:
|
||||
if b goto L4 else goto L5
|
||||
|
||||
The register b is redundant and we replace the assignments with two copies of
|
||||
the branch in L3:
|
||||
|
||||
L1:
|
||||
r0 = f()
|
||||
if r0 goto L4 else goto L5
|
||||
L2:
|
||||
r1 = g()
|
||||
if r1 goto L4 else goto L5
|
||||
|
||||
This helps generate simpler IR for tagged integers comparisons, for example.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from mypyc.ir.func_ir import FuncIR
|
||||
from mypyc.ir.ops import Assign, BasicBlock, Branch, Goto, Register, Unreachable
|
||||
from mypyc.irbuild.ll_builder import LowLevelIRBuilder
|
||||
from mypyc.options import CompilerOptions
|
||||
from mypyc.transform.ir_transform import IRTransform
|
||||
|
||||
|
||||
def do_flag_elimination(fn: FuncIR, options: CompilerOptions) -> None:
|
||||
# Find registers that are used exactly once as source, and in a branch.
|
||||
counts: dict[Register, int] = {}
|
||||
branches: dict[Register, Branch] = {}
|
||||
labels: dict[Register, BasicBlock] = {}
|
||||
for block in fn.blocks:
|
||||
for i, op in enumerate(block.ops):
|
||||
for src in op.sources():
|
||||
if isinstance(src, Register):
|
||||
counts[src] = counts.get(src, 0) + 1
|
||||
if i == 0 and isinstance(op, Branch) and isinstance(op.value, Register):
|
||||
branches[op.value] = op
|
||||
labels[op.value] = block
|
||||
|
||||
# Based on these we can find the candidate registers.
|
||||
candidates: set[Register] = {
|
||||
r for r in branches if counts.get(r, 0) == 1 and r not in fn.arg_regs
|
||||
}
|
||||
|
||||
# Remove candidates with invalid assignments.
|
||||
for block in fn.blocks:
|
||||
for i, op in enumerate(block.ops):
|
||||
if isinstance(op, Assign) and op.dest in candidates:
|
||||
next_op = block.ops[i + 1]
|
||||
if not (isinstance(next_op, Goto) and next_op.label is labels[op.dest]):
|
||||
# Not right
|
||||
candidates.remove(op.dest)
|
||||
|
||||
builder = LowLevelIRBuilder(None, options)
|
||||
transform = FlagEliminationTransform(
|
||||
builder, {x: y for x, y in branches.items() if x in candidates}
|
||||
)
|
||||
transform.transform_blocks(fn.blocks)
|
||||
fn.blocks = builder.blocks
|
||||
|
||||
|
||||
class FlagEliminationTransform(IRTransform):
|
||||
def __init__(self, builder: LowLevelIRBuilder, branch_map: dict[Register, Branch]) -> None:
|
||||
super().__init__(builder)
|
||||
self.branch_map = branch_map
|
||||
self.branches = set(branch_map.values())
|
||||
|
||||
def visit_assign(self, op: Assign) -> None:
|
||||
if old_branch := self.branch_map.get(op.dest):
|
||||
# Replace assignment with a copy of the old branch, which is in a
|
||||
# separate basic block. The old branch will be deleted in visit_branch.
|
||||
new_branch = Branch(
|
||||
op.src,
|
||||
old_branch.true,
|
||||
old_branch.false,
|
||||
old_branch.op,
|
||||
old_branch.line,
|
||||
rare=old_branch.rare,
|
||||
)
|
||||
new_branch.negated = old_branch.negated
|
||||
new_branch.traceback_entry = old_branch.traceback_entry
|
||||
self.add(new_branch)
|
||||
else:
|
||||
self.add(op)
|
||||
|
||||
def visit_goto(self, op: Goto) -> None:
|
||||
# This is a no-op if basic block already terminated
|
||||
self.builder.goto(op.label)
|
||||
|
||||
def visit_branch(self, op: Branch) -> None:
|
||||
if op in self.branches:
|
||||
# This branch is optimized away
|
||||
self.add(Unreachable())
|
||||
else:
|
||||
self.add(op)
|
||||
Binary file not shown.
|
|
@ -0,0 +1,385 @@
|
|||
"""Helpers for implementing generic IR to IR transforms."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from mypyc.ir.ops import (
|
||||
Assign,
|
||||
AssignMulti,
|
||||
BasicBlock,
|
||||
Box,
|
||||
Branch,
|
||||
Call,
|
||||
CallC,
|
||||
Cast,
|
||||
ComparisonOp,
|
||||
DecRef,
|
||||
Extend,
|
||||
FloatComparisonOp,
|
||||
FloatNeg,
|
||||
FloatOp,
|
||||
GetAttr,
|
||||
GetElement,
|
||||
GetElementPtr,
|
||||
Goto,
|
||||
IncRef,
|
||||
InitStatic,
|
||||
IntOp,
|
||||
KeepAlive,
|
||||
LoadAddress,
|
||||
LoadErrorValue,
|
||||
LoadGlobal,
|
||||
LoadLiteral,
|
||||
LoadMem,
|
||||
LoadStatic,
|
||||
MethodCall,
|
||||
Op,
|
||||
OpVisitor,
|
||||
PrimitiveOp,
|
||||
RaiseStandardError,
|
||||
Return,
|
||||
SetAttr,
|
||||
SetElement,
|
||||
SetMem,
|
||||
Truncate,
|
||||
TupleGet,
|
||||
TupleSet,
|
||||
Unborrow,
|
||||
Unbox,
|
||||
Unreachable,
|
||||
Value,
|
||||
)
|
||||
from mypyc.irbuild.ll_builder import LowLevelIRBuilder
|
||||
|
||||
|
||||
class IRTransform(OpVisitor[Value | None]):
|
||||
"""Identity transform.
|
||||
|
||||
Subclass and override to perform changes to IR.
|
||||
|
||||
Subclass IRTransform and override any OpVisitor visit_* methods
|
||||
that perform any IR changes. The default implementations implement
|
||||
an identity transform.
|
||||
|
||||
A visit method can return None to remove ops. In this case the
|
||||
transform must ensure that no op uses the original removed op
|
||||
as a source after the transform.
|
||||
|
||||
You can retain old BasicBlock and op references in ops. The transform
|
||||
will automatically patch these for you as needed.
|
||||
"""
|
||||
|
||||
def __init__(self, builder: LowLevelIRBuilder) -> None:
|
||||
self.builder = builder
|
||||
# Subclasses add additional op mappings here. A None value indicates
|
||||
# that the op/register is deleted.
|
||||
self.op_map: dict[Value, Value | None] = {}
|
||||
|
||||
def transform_blocks(self, blocks: list[BasicBlock]) -> None:
|
||||
"""Transform basic blocks that represent a single function.
|
||||
|
||||
The result of the transform will be collected at self.builder.blocks.
|
||||
"""
|
||||
block_map: dict[BasicBlock, BasicBlock] = {}
|
||||
op_map = self.op_map
|
||||
empties = set()
|
||||
for block in blocks:
|
||||
new_block = BasicBlock()
|
||||
block_map[block] = new_block
|
||||
self.builder.activate_block(new_block)
|
||||
new_block.error_handler = block.error_handler
|
||||
for op in block.ops:
|
||||
new_op = op.accept(self)
|
||||
if new_op is not op:
|
||||
op_map[op] = new_op
|
||||
# A transform can produce empty blocks which can be removed.
|
||||
if is_empty_block(new_block) and not is_empty_block(block):
|
||||
empties.add(new_block)
|
||||
self.builder.blocks = [block for block in self.builder.blocks if block not in empties]
|
||||
# Update all op/block references to point to the transformed ones.
|
||||
patcher = PatchVisitor(op_map, block_map)
|
||||
for block in self.builder.blocks:
|
||||
for op in block.ops:
|
||||
op.accept(patcher)
|
||||
if block.error_handler is not None:
|
||||
block.error_handler = block_map.get(block.error_handler, block.error_handler)
|
||||
|
||||
def add(self, op: Op) -> Value:
|
||||
return self.builder.add(op)
|
||||
|
||||
def visit_goto(self, op: Goto) -> None:
|
||||
self.add(op)
|
||||
|
||||
def visit_branch(self, op: Branch) -> None:
|
||||
self.add(op)
|
||||
|
||||
def visit_return(self, op: Return) -> None:
|
||||
self.add(op)
|
||||
|
||||
def visit_unreachable(self, op: Unreachable) -> None:
|
||||
self.add(op)
|
||||
|
||||
def visit_assign(self, op: Assign) -> Value | None:
|
||||
if op.src in self.op_map and self.op_map[op.src] is None:
|
||||
# Special case: allow removing register initialization assignments
|
||||
return None
|
||||
return self.add(op)
|
||||
|
||||
def visit_assign_multi(self, op: AssignMulti) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_load_error_value(self, op: LoadErrorValue) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_load_literal(self, op: LoadLiteral) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_get_attr(self, op: GetAttr) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_set_attr(self, op: SetAttr) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_load_static(self, op: LoadStatic) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_init_static(self, op: InitStatic) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_tuple_get(self, op: TupleGet) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_tuple_set(self, op: TupleSet) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_inc_ref(self, op: IncRef) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_dec_ref(self, op: DecRef) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_call(self, op: Call) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_method_call(self, op: MethodCall) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_cast(self, op: Cast) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_box(self, op: Box) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_unbox(self, op: Unbox) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_raise_standard_error(self, op: RaiseStandardError) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_call_c(self, op: CallC) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_primitive_op(self, op: PrimitiveOp) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_truncate(self, op: Truncate) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_extend(self, op: Extend) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_load_global(self, op: LoadGlobal) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_int_op(self, op: IntOp) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_comparison_op(self, op: ComparisonOp) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_float_op(self, op: FloatOp) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_float_neg(self, op: FloatNeg) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_float_comparison_op(self, op: FloatComparisonOp) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_load_mem(self, op: LoadMem) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_set_mem(self, op: SetMem) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_get_element(self, op: GetElement) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_get_element_ptr(self, op: GetElementPtr) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_set_element(self, op: SetElement) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_load_address(self, op: LoadAddress) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_keep_alive(self, op: KeepAlive) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
def visit_unborrow(self, op: Unborrow) -> Value | None:
|
||||
return self.add(op)
|
||||
|
||||
|
||||
class PatchVisitor(OpVisitor[None]):
|
||||
def __init__(
|
||||
self, op_map: dict[Value, Value | None], block_map: dict[BasicBlock, BasicBlock]
|
||||
) -> None:
|
||||
self.op_map: Final = op_map
|
||||
self.block_map: Final = block_map
|
||||
|
||||
def fix_op(self, op: Value) -> Value:
|
||||
new = self.op_map.get(op, op)
|
||||
assert new is not None, "use of removed op"
|
||||
return new
|
||||
|
||||
def fix_block(self, block: BasicBlock) -> BasicBlock:
|
||||
return self.block_map.get(block, block)
|
||||
|
||||
def visit_goto(self, op: Goto) -> None:
|
||||
op.label = self.fix_block(op.label)
|
||||
|
||||
def visit_branch(self, op: Branch) -> None:
|
||||
op.value = self.fix_op(op.value)
|
||||
op.true = self.fix_block(op.true)
|
||||
op.false = self.fix_block(op.false)
|
||||
|
||||
def visit_return(self, op: Return) -> None:
|
||||
op.value = self.fix_op(op.value)
|
||||
|
||||
def visit_unreachable(self, op: Unreachable) -> None:
|
||||
pass
|
||||
|
||||
def visit_assign(self, op: Assign) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_assign_multi(self, op: AssignMulti) -> None:
|
||||
op.src = [self.fix_op(s) for s in op.src]
|
||||
|
||||
def visit_load_error_value(self, op: LoadErrorValue) -> None:
|
||||
pass
|
||||
|
||||
def visit_load_literal(self, op: LoadLiteral) -> None:
|
||||
pass
|
||||
|
||||
def visit_get_attr(self, op: GetAttr) -> None:
|
||||
op.obj = self.fix_op(op.obj)
|
||||
|
||||
def visit_set_attr(self, op: SetAttr) -> None:
|
||||
op.obj = self.fix_op(op.obj)
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_load_static(self, op: LoadStatic) -> None:
|
||||
pass
|
||||
|
||||
def visit_init_static(self, op: InitStatic) -> None:
|
||||
op.value = self.fix_op(op.value)
|
||||
|
||||
def visit_tuple_get(self, op: TupleGet) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_tuple_set(self, op: TupleSet) -> None:
|
||||
op.items = [self.fix_op(item) for item in op.items]
|
||||
|
||||
def visit_inc_ref(self, op: IncRef) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_dec_ref(self, op: DecRef) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_call(self, op: Call) -> None:
|
||||
op.args = [self.fix_op(arg) for arg in op.args]
|
||||
|
||||
def visit_method_call(self, op: MethodCall) -> None:
|
||||
op.obj = self.fix_op(op.obj)
|
||||
op.args = [self.fix_op(arg) for arg in op.args]
|
||||
|
||||
def visit_cast(self, op: Cast) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_box(self, op: Box) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_unbox(self, op: Unbox) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_raise_standard_error(self, op: RaiseStandardError) -> None:
|
||||
if isinstance(op.value, Value):
|
||||
op.value = self.fix_op(op.value)
|
||||
|
||||
def visit_call_c(self, op: CallC) -> None:
|
||||
op.args = [self.fix_op(arg) for arg in op.args]
|
||||
|
||||
def visit_primitive_op(self, op: PrimitiveOp) -> None:
|
||||
op.args = [self.fix_op(arg) for arg in op.args]
|
||||
|
||||
def visit_truncate(self, op: Truncate) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_extend(self, op: Extend) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_load_global(self, op: LoadGlobal) -> None:
|
||||
pass
|
||||
|
||||
def visit_int_op(self, op: IntOp) -> None:
|
||||
op.lhs = self.fix_op(op.lhs)
|
||||
op.rhs = self.fix_op(op.rhs)
|
||||
|
||||
def visit_comparison_op(self, op: ComparisonOp) -> None:
|
||||
op.lhs = self.fix_op(op.lhs)
|
||||
op.rhs = self.fix_op(op.rhs)
|
||||
|
||||
def visit_float_op(self, op: FloatOp) -> None:
|
||||
op.lhs = self.fix_op(op.lhs)
|
||||
op.rhs = self.fix_op(op.rhs)
|
||||
|
||||
def visit_float_neg(self, op: FloatNeg) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_float_comparison_op(self, op: FloatComparisonOp) -> None:
|
||||
op.lhs = self.fix_op(op.lhs)
|
||||
op.rhs = self.fix_op(op.rhs)
|
||||
|
||||
def visit_load_mem(self, op: LoadMem) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_set_mem(self, op: SetMem) -> None:
|
||||
op.dest = self.fix_op(op.dest)
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_get_element(self, op: GetElement) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_get_element_ptr(self, op: GetElementPtr) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_set_element(self, op: SetElement) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
def visit_load_address(self, op: LoadAddress) -> None:
|
||||
if isinstance(op.src, LoadStatic):
|
||||
new = self.fix_op(op.src)
|
||||
assert isinstance(new, LoadStatic), new
|
||||
op.src = new
|
||||
|
||||
def visit_keep_alive(self, op: KeepAlive) -> None:
|
||||
op.src = [self.fix_op(s) for s in op.src]
|
||||
|
||||
def visit_unborrow(self, op: Unborrow) -> None:
|
||||
op.src = self.fix_op(op.src)
|
||||
|
||||
|
||||
def is_empty_block(block: BasicBlock) -> bool:
|
||||
return len(block.ops) == 1 and isinstance(block.ops[0], Unreachable)
|
||||
Binary file not shown.
158
venv/lib/python3.11/site-packages/mypyc/transform/log_trace.py
Normal file
158
venv/lib/python3.11/site-packages/mypyc/transform/log_trace.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""This optional pass adds logging of various executed operations.
|
||||
|
||||
Some subset of the executed operations are logged to the mypyc_trace.txt file.
|
||||
|
||||
This is useful for performance analysis. For example, it's possible
|
||||
to identify how frequently various primitive functions are called,
|
||||
and in which code locations they are called.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from mypyc.ir.func_ir import FuncIR
|
||||
from mypyc.ir.ops import (
|
||||
Box,
|
||||
Call,
|
||||
CallC,
|
||||
Cast,
|
||||
CString,
|
||||
DecRef,
|
||||
GetAttr,
|
||||
IncRef,
|
||||
LoadLiteral,
|
||||
LoadStatic,
|
||||
Op,
|
||||
PrimitiveOp,
|
||||
SetAttr,
|
||||
Unbox,
|
||||
Value,
|
||||
)
|
||||
from mypyc.ir.rtypes import none_rprimitive
|
||||
from mypyc.irbuild.ll_builder import LowLevelIRBuilder
|
||||
from mypyc.options import CompilerOptions
|
||||
from mypyc.primitives.misc_ops import log_trace_event
|
||||
from mypyc.transform.ir_transform import IRTransform
|
||||
|
||||
|
||||
def insert_event_trace_logging(fn: FuncIR, options: CompilerOptions) -> None:
|
||||
builder = LowLevelIRBuilder(None, options)
|
||||
transform = LogTraceEventTransform(builder, fn.decl.fullname)
|
||||
transform.transform_blocks(fn.blocks)
|
||||
fn.blocks = builder.blocks
|
||||
|
||||
|
||||
def get_load_global_name(op: CallC) -> str | None:
|
||||
name = op.function_name
|
||||
if name == "CPyDict_GetItem":
|
||||
arg = op.args[0]
|
||||
if (
|
||||
isinstance(arg, LoadStatic)
|
||||
and arg.namespace == "static"
|
||||
and arg.identifier == "globals"
|
||||
and isinstance(op.args[1], LoadLiteral)
|
||||
):
|
||||
return str(op.args[1].value)
|
||||
return None
|
||||
|
||||
|
||||
# These primitives perform an implicit IncRef for the return value. Only some of the most common ones
|
||||
# are included, and mostly ops that could be switched to use borrowing in some contexts.
|
||||
primitives_that_inc_ref: Final = {
|
||||
"list_get_item_unsafe",
|
||||
"CPyList_GetItemShort",
|
||||
"CPyDict_GetWithNone",
|
||||
"CPyList_GetItem",
|
||||
"CPyDict_GetItem",
|
||||
"CPyList_PopLast",
|
||||
}
|
||||
|
||||
|
||||
class LogTraceEventTransform(IRTransform):
|
||||
def __init__(self, builder: LowLevelIRBuilder, fullname: str) -> None:
|
||||
super().__init__(builder)
|
||||
self.fullname = fullname.encode("utf-8")
|
||||
|
||||
def visit_call(self, op: Call) -> Value:
|
||||
# TODO: Use different op name when constructing an instance
|
||||
return self.log(op, "call", op.fn.fullname)
|
||||
|
||||
def visit_primitive_op(self, op: PrimitiveOp) -> Value:
|
||||
value = self.log(op, "primitive_op", op.desc.name)
|
||||
if op.desc.name in primitives_that_inc_ref:
|
||||
self.log_inc_ref(value)
|
||||
return value
|
||||
|
||||
def visit_call_c(self, op: CallC) -> Value:
|
||||
if global_name := get_load_global_name(op):
|
||||
return self.log(op, "globals_dict_get_item", global_name)
|
||||
|
||||
func_name = op.function_name
|
||||
if func_name == "PyObject_Vectorcall" and isinstance(op.args[0], CallC):
|
||||
if global_name := get_load_global_name(op.args[0]):
|
||||
return self.log(op, "python_call_global", global_name)
|
||||
elif func_name == "CPyObject_GetAttr" and isinstance(op.args[1], LoadLiteral):
|
||||
return self.log(op, "python_get_attr", str(op.args[1].value))
|
||||
elif func_name == "PyObject_VectorcallMethod" and isinstance(op.args[0], LoadLiteral):
|
||||
return self.log(op, "python_call_method", str(op.args[0].value))
|
||||
|
||||
value = self.log(op, "call_c", func_name)
|
||||
if func_name in primitives_that_inc_ref:
|
||||
self.log_inc_ref(value)
|
||||
return value
|
||||
|
||||
def visit_get_attr(self, op: GetAttr) -> Value:
|
||||
value = self.log(op, "get_attr", f"{op.class_type.name}.{op.attr}")
|
||||
if not op.is_borrowed and op.type.is_refcounted:
|
||||
self.log_inc_ref(op)
|
||||
return value
|
||||
|
||||
def visit_set_attr(self, op: SetAttr) -> Value:
|
||||
name = "set_attr" if not op.is_init else "set_attr_init"
|
||||
return self.log(op, name, f"{op.class_type.name}.{op.attr}")
|
||||
|
||||
def visit_box(self, op: Box) -> Value:
|
||||
if op.src.type is none_rprimitive:
|
||||
# Boxing 'None' is a very quick operation, so we don't log it.
|
||||
return self.add(op)
|
||||
else:
|
||||
return self.log(op, "box", str(op.src.type))
|
||||
|
||||
def visit_unbox(self, op: Unbox) -> Value:
|
||||
return self.log(op, "unbox", str(op.type))
|
||||
|
||||
def visit_cast(self, op: Cast) -> Value | None:
|
||||
value = self.log(op, "cast", str(op.type))
|
||||
if not op.is_borrowed:
|
||||
self.log_inc_ref(value)
|
||||
return value
|
||||
|
||||
def visit_inc_ref(self, op: IncRef) -> Value:
|
||||
return self.log(op, "inc_ref", str(op.src.type))
|
||||
|
||||
def visit_dec_ref(self, op: DecRef) -> Value:
|
||||
return self.log(op, "dec_ref", str(op.src.type))
|
||||
|
||||
def log_inc_ref(self, value: Value) -> None:
|
||||
self.log_event("inc_ref", str(value.type), value.line)
|
||||
|
||||
def log(self, op: Op, name: str, details: str) -> Value:
|
||||
self.log_event(name, details, op.line)
|
||||
return self.add(op)
|
||||
|
||||
def log_event(self, name: str, details: str, line: int) -> None:
|
||||
if line >= 0:
|
||||
line_str = str(line)
|
||||
else:
|
||||
line_str = ""
|
||||
self.builder.primitive_op(
|
||||
log_trace_event,
|
||||
[
|
||||
CString(self.fullname),
|
||||
CString(line_str.encode("ascii")),
|
||||
CString(name.encode("utf-8")),
|
||||
CString(details.encode("utf-8")),
|
||||
],
|
||||
line,
|
||||
)
|
||||
Binary file not shown.
35
venv/lib/python3.11/site-packages/mypyc/transform/lower.py
Normal file
35
venv/lib/python3.11/site-packages/mypyc/transform/lower.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""Transform IR to lower-level ops.
|
||||
|
||||
Higher-level ops are used in earlier compiler passes, as they make
|
||||
various analyses, optimizations and transforms easier to implement.
|
||||
Later passes use lower-level ops, as they are easier to generate code
|
||||
from, and they help with lower-level optimizations.
|
||||
|
||||
Lowering of various primitive ops is implemented in the mypyc.lower
|
||||
package.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from mypyc.ir.func_ir import FuncIR
|
||||
from mypyc.ir.ops import PrimitiveOp, Value
|
||||
from mypyc.irbuild.ll_builder import LowLevelIRBuilder
|
||||
from mypyc.lower.registry import lowering_registry
|
||||
from mypyc.options import CompilerOptions
|
||||
from mypyc.transform.ir_transform import IRTransform
|
||||
|
||||
|
||||
def lower_ir(ir: FuncIR, options: CompilerOptions) -> None:
|
||||
builder = LowLevelIRBuilder(None, options)
|
||||
visitor = LoweringVisitor(builder)
|
||||
visitor.transform_blocks(ir.blocks)
|
||||
ir.blocks = builder.blocks
|
||||
|
||||
|
||||
class LoweringVisitor(IRTransform):
|
||||
def visit_primitive_op(self, op: PrimitiveOp) -> Value | None:
|
||||
# The lowering implementation functions of various primitive ops are stored
|
||||
# in a registry, which is populated using function decorators. The name
|
||||
# of op (such as "int_eq") is used as the key.
|
||||
lower_fn = lowering_registry[op.desc.name]
|
||||
return lower_fn(self.builder, op.args, op.line)
|
||||
Binary file not shown.
298
venv/lib/python3.11/site-packages/mypyc/transform/refcount.py
Normal file
298
venv/lib/python3.11/site-packages/mypyc/transform/refcount.py
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
"""Transformation for inserting refrecence count inc/dec opcodes.
|
||||
|
||||
This transformation happens towards the end of compilation. Before this
|
||||
transformation, reference count management is not explicitly handled at all.
|
||||
By postponing this pass, the previous passes are simpler as they don't have
|
||||
to update reference count opcodes.
|
||||
|
||||
The approach is to decrement reference counts soon after a value is no
|
||||
longer live, to quickly free memory (and call __del__ methods), though
|
||||
there are no strict guarantees -- other than that local variables are
|
||||
freed before return from a function.
|
||||
|
||||
Function arguments are a little special. They are initially considered
|
||||
'borrowed' from the caller and their reference counts don't need to be
|
||||
decremented before returning. An assignment to a borrowed value turns it
|
||||
into a regular, owned reference that needs to freed before return.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
from mypyc.analysis.dataflow import (
|
||||
AnalysisDict,
|
||||
analyze_borrowed_arguments,
|
||||
analyze_live_regs,
|
||||
analyze_must_defined_regs,
|
||||
cleanup_cfg,
|
||||
get_cfg,
|
||||
)
|
||||
from mypyc.ir.func_ir import FuncIR, all_values
|
||||
from mypyc.ir.ops import (
|
||||
Assign,
|
||||
BasicBlock,
|
||||
Branch,
|
||||
CallC,
|
||||
ControlOp,
|
||||
DecRef,
|
||||
Goto,
|
||||
IncRef,
|
||||
Integer,
|
||||
KeepAlive,
|
||||
LoadAddress,
|
||||
Op,
|
||||
Register,
|
||||
RegisterOp,
|
||||
Undef,
|
||||
Value,
|
||||
)
|
||||
|
||||
Decs = tuple[tuple[Value, bool], ...]
|
||||
Incs = tuple[Value, ...]
|
||||
|
||||
# A cache of basic blocks that decrement and increment specific values
|
||||
# and then jump to some target block. This lets us cut down on how
|
||||
# much code we generate in some circumstances.
|
||||
BlockCache = dict[tuple[BasicBlock, Decs, Incs], BasicBlock]
|
||||
|
||||
|
||||
def insert_ref_count_opcodes(ir: FuncIR) -> None:
|
||||
"""Insert reference count inc/dec opcodes to a function.
|
||||
|
||||
This is the entry point to this module.
|
||||
"""
|
||||
cfg = get_cfg(ir.blocks)
|
||||
values = all_values(ir.arg_regs, ir.blocks)
|
||||
|
||||
borrowed = {value for value in values if value.is_borrowed}
|
||||
args: set[Value] = set(ir.arg_regs)
|
||||
live = analyze_live_regs(ir.blocks, cfg)
|
||||
borrow = analyze_borrowed_arguments(ir.blocks, cfg, borrowed)
|
||||
defined = analyze_must_defined_regs(ir.blocks, cfg, args, values, strict_errors=True)
|
||||
ordering = make_value_ordering(ir)
|
||||
cache: BlockCache = {}
|
||||
for block in ir.blocks.copy():
|
||||
if isinstance(block.ops[-1], (Branch, Goto)):
|
||||
insert_branch_inc_and_decrefs(
|
||||
block,
|
||||
cache,
|
||||
ir.blocks,
|
||||
live.before,
|
||||
borrow.before,
|
||||
borrow.after,
|
||||
defined.after,
|
||||
ordering,
|
||||
)
|
||||
transform_block(block, live.before, live.after, borrow.before, defined.after)
|
||||
|
||||
cleanup_cfg(ir.blocks)
|
||||
|
||||
|
||||
def is_maybe_undefined(post_must_defined: set[Value], src: Value) -> bool:
|
||||
return (isinstance(src, Register) and src not in post_must_defined) or (
|
||||
isinstance(src, CallC) and src.returns_null
|
||||
)
|
||||
|
||||
|
||||
def maybe_append_dec_ref(
|
||||
ops: list[Op], dest: Value, defined: AnalysisDict[Value], key: tuple[BasicBlock, int]
|
||||
) -> None:
|
||||
if dest.type.is_refcounted and not isinstance(dest, (Integer, Undef)):
|
||||
ops.append(DecRef(dest, is_xdec=is_maybe_undefined(defined[key], dest)))
|
||||
|
||||
|
||||
def maybe_append_inc_ref(ops: list[Op], dest: Value) -> None:
|
||||
if dest.type.is_refcounted:
|
||||
ops.append(IncRef(dest))
|
||||
|
||||
|
||||
def transform_block(
|
||||
block: BasicBlock,
|
||||
pre_live: AnalysisDict[Value],
|
||||
post_live: AnalysisDict[Value],
|
||||
pre_borrow: AnalysisDict[Value],
|
||||
post_must_defined: AnalysisDict[Value],
|
||||
) -> None:
|
||||
old_ops = block.ops
|
||||
ops: list[Op] = []
|
||||
for i, op in enumerate(old_ops):
|
||||
key = (block, i)
|
||||
|
||||
assert op not in pre_live[key]
|
||||
dest = op.dest if isinstance(op, Assign) else op
|
||||
stolen = op.stolen()
|
||||
|
||||
# Incref any references that are being stolen that stay live, were borrowed,
|
||||
# or are stolen more than once by this operation.
|
||||
for j, src in enumerate(stolen):
|
||||
if src in post_live[key] or src in pre_borrow[key] or src in stolen[:j]:
|
||||
maybe_append_inc_ref(ops, src)
|
||||
# For assignments to registers that were already live,
|
||||
# decref the old value.
|
||||
if dest not in pre_borrow[key] and dest in pre_live[key]:
|
||||
assert isinstance(op, Assign), op
|
||||
maybe_append_dec_ref(ops, dest, post_must_defined, key)
|
||||
|
||||
# Strip KeepAlive. Its only purpose is to help with this transform.
|
||||
if not isinstance(op, KeepAlive):
|
||||
ops.append(op)
|
||||
|
||||
# Control ops don't have any space to insert ops after them, so
|
||||
# their inc/decrefs get inserted by insert_branch_inc_and_decrefs.
|
||||
if isinstance(op, ControlOp):
|
||||
continue
|
||||
|
||||
for src in op.unique_sources():
|
||||
# Decrement source that won't be live afterwards.
|
||||
if src not in post_live[key] and src not in pre_borrow[key] and src not in stolen:
|
||||
maybe_append_dec_ref(ops, src, post_must_defined, key)
|
||||
# Decrement the destination if it is dead after the op and
|
||||
# wasn't a borrowed RegisterOp
|
||||
if (
|
||||
not dest.is_void
|
||||
and dest not in post_live[key]
|
||||
and not (isinstance(op, RegisterOp) and dest.is_borrowed)
|
||||
):
|
||||
maybe_append_dec_ref(ops, dest, post_must_defined, key)
|
||||
block.ops = ops
|
||||
|
||||
|
||||
def insert_branch_inc_and_decrefs(
|
||||
block: BasicBlock,
|
||||
cache: BlockCache,
|
||||
blocks: list[BasicBlock],
|
||||
pre_live: AnalysisDict[Value],
|
||||
pre_borrow: AnalysisDict[Value],
|
||||
post_borrow: AnalysisDict[Value],
|
||||
post_must_defined: AnalysisDict[Value],
|
||||
ordering: dict[Value, int],
|
||||
) -> None:
|
||||
"""Insert inc_refs and/or dec_refs after a branch/goto.
|
||||
|
||||
Add dec_refs for registers that become dead after a branch.
|
||||
Add inc_refs for registers that become unborrowed after a branch or goto.
|
||||
|
||||
Branches are special as the true and false targets may have a different
|
||||
live and borrowed register sets. Add new blocks before the true/false target
|
||||
blocks that tweak reference counts.
|
||||
|
||||
Example where we need to add an inc_ref:
|
||||
|
||||
def f(a: int) -> None
|
||||
if a:
|
||||
a = 1
|
||||
return a # a is borrowed if condition is false and unborrowed if true
|
||||
"""
|
||||
prev_key = (block, len(block.ops) - 1)
|
||||
source_live_regs = pre_live[prev_key]
|
||||
source_borrowed = post_borrow[prev_key]
|
||||
source_defined = post_must_defined[prev_key]
|
||||
|
||||
term = block.terminator
|
||||
for i, target in enumerate(term.targets()):
|
||||
# HAX: After we've checked against an error value the value we must not touch the
|
||||
# refcount since it will be a null pointer. The correct way to do this would be
|
||||
# to perform data flow analysis on whether a value can be null (or is always
|
||||
# null).
|
||||
omitted: Iterable[Value]
|
||||
if isinstance(term, Branch) and term.op == Branch.IS_ERROR and i == 0:
|
||||
omitted = (term.value,)
|
||||
else:
|
||||
omitted = ()
|
||||
|
||||
decs = after_branch_decrefs(
|
||||
target, pre_live, source_defined, source_borrowed, source_live_regs, ordering, omitted
|
||||
)
|
||||
incs = after_branch_increfs(target, pre_live, pre_borrow, source_borrowed, ordering)
|
||||
term.set_target(i, add_block(decs, incs, cache, blocks, target))
|
||||
|
||||
|
||||
def after_branch_decrefs(
|
||||
label: BasicBlock,
|
||||
pre_live: AnalysisDict[Value],
|
||||
source_defined: set[Value],
|
||||
source_borrowed: set[Value],
|
||||
source_live_regs: set[Value],
|
||||
ordering: dict[Value, int],
|
||||
omitted: Iterable[Value],
|
||||
) -> tuple[tuple[Value, bool], ...]:
|
||||
target_pre_live = pre_live[label, 0]
|
||||
decref = source_live_regs - target_pre_live - source_borrowed
|
||||
if decref:
|
||||
return tuple(
|
||||
(reg, is_maybe_undefined(source_defined, reg))
|
||||
for reg in sorted(decref, key=lambda r: ordering[r])
|
||||
if reg.type.is_refcounted and reg not in omitted
|
||||
)
|
||||
return ()
|
||||
|
||||
|
||||
def after_branch_increfs(
|
||||
label: BasicBlock,
|
||||
pre_live: AnalysisDict[Value],
|
||||
pre_borrow: AnalysisDict[Value],
|
||||
source_borrowed: set[Value],
|
||||
ordering: dict[Value, int],
|
||||
) -> tuple[Value, ...]:
|
||||
target_pre_live = pre_live[label, 0]
|
||||
target_borrowed = pre_borrow[label, 0]
|
||||
incref = (source_borrowed - target_borrowed) & target_pre_live
|
||||
if incref:
|
||||
return tuple(
|
||||
reg for reg in sorted(incref, key=lambda r: ordering[r]) if reg.type.is_refcounted
|
||||
)
|
||||
return ()
|
||||
|
||||
|
||||
def add_block(
|
||||
decs: Decs, incs: Incs, cache: BlockCache, blocks: list[BasicBlock], label: BasicBlock
|
||||
) -> BasicBlock:
|
||||
if not decs and not incs:
|
||||
return label
|
||||
|
||||
# TODO: be able to share *partial* results
|
||||
if (label, decs, incs) in cache:
|
||||
return cache[label, decs, incs]
|
||||
|
||||
block = BasicBlock()
|
||||
blocks.append(block)
|
||||
block.ops.extend(DecRef(reg, is_xdec=xdec) for reg, xdec in decs)
|
||||
block.ops.extend(IncRef(reg) for reg in incs)
|
||||
block.ops.append(Goto(label))
|
||||
cache[label, decs, incs] = block
|
||||
return block
|
||||
|
||||
|
||||
def make_value_ordering(ir: FuncIR) -> dict[Value, int]:
|
||||
"""Create a ordering of values that allows them to be sorted.
|
||||
|
||||
This omits registers that are only ever read.
|
||||
"""
|
||||
# TODO: Never initialized values??
|
||||
result: dict[Value, int] = {}
|
||||
n = 0
|
||||
|
||||
for arg in ir.arg_regs:
|
||||
result[arg] = n
|
||||
n += 1
|
||||
|
||||
for block in ir.blocks:
|
||||
for op in block.ops:
|
||||
if (
|
||||
isinstance(op, LoadAddress)
|
||||
and isinstance(op.src, Register)
|
||||
and op.src not in result
|
||||
):
|
||||
# Taking the address of a register allows initialization.
|
||||
result[op.src] = n
|
||||
n += 1
|
||||
if isinstance(op, Assign):
|
||||
if op.dest not in result:
|
||||
result[op.dest] = n
|
||||
n += 1
|
||||
elif op not in result:
|
||||
result[op] = n
|
||||
n += 1
|
||||
|
||||
return result
|
||||
Binary file not shown.
113
venv/lib/python3.11/site-packages/mypyc/transform/spill.py
Normal file
113
venv/lib/python3.11/site-packages/mypyc/transform/spill.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
"""Insert spills for values that are live across yields."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from mypyc.analysis.dataflow import AnalysisResult, analyze_live_regs, get_cfg
|
||||
from mypyc.common import TEMP_ATTR_NAME
|
||||
from mypyc.ir.class_ir import ClassIR
|
||||
from mypyc.ir.func_ir import FuncIR
|
||||
from mypyc.ir.ops import (
|
||||
BasicBlock,
|
||||
Branch,
|
||||
DecRef,
|
||||
GetAttr,
|
||||
IncRef,
|
||||
LoadErrorValue,
|
||||
Register,
|
||||
SetAttr,
|
||||
Value,
|
||||
)
|
||||
|
||||
|
||||
def insert_spills(ir: FuncIR, env: ClassIR) -> None:
|
||||
cfg = get_cfg(ir.blocks, use_yields=True)
|
||||
live = analyze_live_regs(ir.blocks, cfg)
|
||||
entry_live = live.before[ir.blocks[0], 0]
|
||||
|
||||
entry_live = {op for op in entry_live if not (isinstance(op, Register) and op.is_arg)}
|
||||
# TODO: Actually for now, no Registers at all -- we keep the manual spills
|
||||
entry_live = {op for op in entry_live if not isinstance(op, Register)}
|
||||
|
||||
ir.blocks = spill_regs(ir.blocks, env, entry_live, live, ir.arg_regs[0])
|
||||
|
||||
|
||||
def spill_regs(
|
||||
blocks: list[BasicBlock],
|
||||
env: ClassIR,
|
||||
to_spill: set[Value],
|
||||
live: AnalysisResult[Value],
|
||||
self_reg: Register,
|
||||
) -> list[BasicBlock]:
|
||||
env_reg: Value
|
||||
for op in blocks[0].ops:
|
||||
if isinstance(op, GetAttr) and op.attr == "__mypyc_env__":
|
||||
env_reg = op
|
||||
break
|
||||
else:
|
||||
# Environment has been merged into generator object
|
||||
env_reg = self_reg
|
||||
|
||||
spill_locs = {}
|
||||
for i, val in enumerate(to_spill):
|
||||
name = f"{TEMP_ATTR_NAME}2_{i}"
|
||||
env.attributes[name] = val.type
|
||||
if val.type.error_overlap:
|
||||
# We can safely treat as always initialized, since the type has no pointers.
|
||||
# This way we also don't need to manage the defined attribute bitfield.
|
||||
env._always_initialized_attrs.add(name)
|
||||
spill_locs[val] = name
|
||||
|
||||
for block in blocks:
|
||||
ops = block.ops
|
||||
block.ops = []
|
||||
|
||||
for i, op in enumerate(ops):
|
||||
to_decref = []
|
||||
|
||||
if isinstance(op, IncRef) and op.src in spill_locs:
|
||||
raise AssertionError("not sure what to do with an incref of a spill...")
|
||||
if isinstance(op, DecRef) and op.src in spill_locs:
|
||||
# When we decref a spilled value, we turn that into
|
||||
# NULLing out the attribute, but only if the spilled
|
||||
# value is not live *when we include yields in the
|
||||
# CFG*. (The original decrefs are computed without that.)
|
||||
#
|
||||
# We also skip a decref is the env register is not
|
||||
# live. That should only happen when an exception is
|
||||
# being raised, so everything should be handled there.
|
||||
if op.src not in live.after[block, i] and env_reg in live.after[block, i]:
|
||||
# Skip the DecRef but null out the spilled location
|
||||
null = LoadErrorValue(op.src.type)
|
||||
block.ops.extend([null, SetAttr(env_reg, spill_locs[op.src], null, op.line)])
|
||||
continue
|
||||
|
||||
if (
|
||||
any(src in spill_locs for src in op.sources())
|
||||
# N.B: IS_ERROR should be before a spill happens
|
||||
# XXX: but could we have a regular branch?
|
||||
and not (isinstance(op, Branch) and op.op == Branch.IS_ERROR)
|
||||
):
|
||||
new_sources: list[Value] = []
|
||||
stolen = op.stolen()
|
||||
for src in op.sources():
|
||||
if src in spill_locs:
|
||||
read = GetAttr(env_reg, spill_locs[src], op.line)
|
||||
block.ops.append(read)
|
||||
new_sources.append(read)
|
||||
if src.type.is_refcounted and src not in stolen:
|
||||
to_decref.append(read)
|
||||
else:
|
||||
new_sources.append(src)
|
||||
|
||||
op.set_sources(new_sources)
|
||||
|
||||
block.ops.append(op)
|
||||
|
||||
for dec in to_decref:
|
||||
block.ops.append(DecRef(dec))
|
||||
|
||||
if op in spill_locs:
|
||||
# XXX: could we set uninit?
|
||||
block.ops.append(SetAttr(env_reg, spill_locs[op], op, op.line))
|
||||
|
||||
return blocks
|
||||
Binary file not shown.
199
venv/lib/python3.11/site-packages/mypyc/transform/uninit.py
Normal file
199
venv/lib/python3.11/site-packages/mypyc/transform/uninit.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
"""Insert checks for uninitialized values."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from mypyc.analysis.dataflow import AnalysisDict, analyze_must_defined_regs, cleanup_cfg, get_cfg
|
||||
from mypyc.common import BITMAP_BITS
|
||||
from mypyc.ir.func_ir import FuncIR, all_values
|
||||
from mypyc.ir.ops import (
|
||||
Assign,
|
||||
BasicBlock,
|
||||
Branch,
|
||||
ComparisonOp,
|
||||
Integer,
|
||||
IntOp,
|
||||
LoadAddress,
|
||||
LoadErrorValue,
|
||||
Op,
|
||||
RaiseStandardError,
|
||||
Register,
|
||||
Unreachable,
|
||||
Value,
|
||||
)
|
||||
from mypyc.ir.rtypes import bitmap_rprimitive
|
||||
|
||||
|
||||
def insert_uninit_checks(ir: FuncIR, strict_traceback_checks: bool) -> None:
|
||||
# Remove dead blocks from the CFG, which helps avoid spurious
|
||||
# checks due to unused error handling blocks.
|
||||
cleanup_cfg(ir.blocks)
|
||||
|
||||
cfg = get_cfg(ir.blocks)
|
||||
must_defined = analyze_must_defined_regs(
|
||||
ir.blocks, cfg, set(ir.arg_regs), all_values(ir.arg_regs, ir.blocks)
|
||||
)
|
||||
|
||||
ir.blocks = split_blocks_at_uninits(ir.blocks, must_defined.before, strict_traceback_checks)
|
||||
|
||||
|
||||
def split_blocks_at_uninits(
|
||||
blocks: list[BasicBlock], pre_must_defined: AnalysisDict[Value], strict_traceback_checks: bool
|
||||
) -> list[BasicBlock]:
|
||||
new_blocks: list[BasicBlock] = []
|
||||
|
||||
init_registers = []
|
||||
init_registers_set = set()
|
||||
bitmap_registers: list[Register] = [] # Init status bitmaps
|
||||
bitmap_backed: list[Register] = [] # These use bitmaps to track init status
|
||||
|
||||
# First split blocks on ops that may raise.
|
||||
for block in blocks:
|
||||
ops = block.ops
|
||||
block.ops = []
|
||||
cur_block = block
|
||||
new_blocks.append(cur_block)
|
||||
|
||||
for i, op in enumerate(ops):
|
||||
defined = pre_must_defined[block, i]
|
||||
for src in op.unique_sources():
|
||||
# If a register operand is not guaranteed to be
|
||||
# initialized is an operand to something other than a
|
||||
# check that it is defined, insert a check.
|
||||
|
||||
# Note that for register operand in a LoadAddress op,
|
||||
# we should be able to use it without initialization
|
||||
# as we may need to use its address to update itself
|
||||
if (
|
||||
isinstance(src, Register)
|
||||
and src not in defined
|
||||
and not (isinstance(op, Branch) and op.op == Branch.IS_ERROR)
|
||||
and not isinstance(op, LoadAddress)
|
||||
):
|
||||
if src not in init_registers_set:
|
||||
init_registers.append(src)
|
||||
init_registers_set.add(src)
|
||||
|
||||
# XXX: if src.name is empty, it should be a
|
||||
# temp... and it should be OK??
|
||||
if not src.name:
|
||||
continue
|
||||
|
||||
new_block, error_block = BasicBlock(), BasicBlock()
|
||||
new_block.error_handler = error_block.error_handler = cur_block.error_handler
|
||||
new_blocks += [error_block, new_block]
|
||||
|
||||
if not src.type.error_overlap:
|
||||
cur_block.ops.append(
|
||||
Branch(
|
||||
src,
|
||||
true_label=error_block,
|
||||
false_label=new_block,
|
||||
op=Branch.IS_ERROR,
|
||||
line=op.line,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# We need to use bitmap for this one.
|
||||
check_for_uninit_using_bitmap(
|
||||
cur_block.ops,
|
||||
src,
|
||||
bitmap_registers,
|
||||
bitmap_backed,
|
||||
error_block,
|
||||
new_block,
|
||||
op.line,
|
||||
)
|
||||
|
||||
if strict_traceback_checks:
|
||||
assert (
|
||||
op.line >= 0
|
||||
), f"Cannot raise an error with a negative line number for op {op}"
|
||||
raise_std = RaiseStandardError(
|
||||
RaiseStandardError.UNBOUND_LOCAL_ERROR,
|
||||
f'local variable "{src.name}" referenced before assignment',
|
||||
op.line,
|
||||
)
|
||||
error_block.ops.append(raise_std)
|
||||
error_block.ops.append(Unreachable())
|
||||
cur_block = new_block
|
||||
cur_block.ops.append(op)
|
||||
|
||||
if bitmap_backed:
|
||||
update_register_assignments_to_set_bitmap(new_blocks, bitmap_registers, bitmap_backed)
|
||||
|
||||
if init_registers:
|
||||
new_ops: list[Op] = []
|
||||
for reg in init_registers:
|
||||
err = LoadErrorValue(reg.type, undefines=True)
|
||||
new_ops.append(err)
|
||||
new_ops.append(Assign(reg, err))
|
||||
for reg in bitmap_registers:
|
||||
new_ops.append(Assign(reg, Integer(0, bitmap_rprimitive)))
|
||||
new_blocks[0].ops[0:0] = new_ops
|
||||
|
||||
return new_blocks
|
||||
|
||||
|
||||
def check_for_uninit_using_bitmap(
|
||||
ops: list[Op],
|
||||
src: Register,
|
||||
bitmap_registers: list[Register],
|
||||
bitmap_backed: list[Register],
|
||||
error_block: BasicBlock,
|
||||
ok_block: BasicBlock,
|
||||
line: int,
|
||||
) -> None:
|
||||
"""Check if src is defined using a bitmap.
|
||||
|
||||
Modifies ops, bitmap_registers and bitmap_backed.
|
||||
"""
|
||||
if src not in bitmap_backed:
|
||||
# Set up a new bitmap backed register.
|
||||
bitmap_backed.append(src)
|
||||
n = (len(bitmap_backed) - 1) // BITMAP_BITS
|
||||
if len(bitmap_registers) <= n:
|
||||
bitmap_registers.append(Register(bitmap_rprimitive, f"__locals_bitmap{n}"))
|
||||
|
||||
index = bitmap_backed.index(src)
|
||||
masked = IntOp(
|
||||
bitmap_rprimitive,
|
||||
bitmap_registers[index // BITMAP_BITS],
|
||||
Integer(1 << (index & (BITMAP_BITS - 1)), bitmap_rprimitive),
|
||||
IntOp.AND,
|
||||
line,
|
||||
)
|
||||
ops.append(masked)
|
||||
chk = ComparisonOp(masked, Integer(0, bitmap_rprimitive), ComparisonOp.EQ)
|
||||
ops.append(chk)
|
||||
ops.append(Branch(chk, error_block, ok_block, Branch.BOOL))
|
||||
|
||||
|
||||
def update_register_assignments_to_set_bitmap(
|
||||
blocks: list[BasicBlock], bitmap_registers: list[Register], bitmap_backed: list[Register]
|
||||
) -> None:
|
||||
"""Update some assignments to registers to also set a bit in a bitmap.
|
||||
|
||||
The bitmaps are used to track if a local variable has been assigned to.
|
||||
|
||||
Modifies blocks.
|
||||
"""
|
||||
for block in blocks:
|
||||
if any(isinstance(op, Assign) and op.dest in bitmap_backed for op in block.ops):
|
||||
new_ops: list[Op] = []
|
||||
for op in block.ops:
|
||||
if isinstance(op, Assign) and op.dest in bitmap_backed:
|
||||
index = bitmap_backed.index(op.dest)
|
||||
new_ops.append(op)
|
||||
reg = bitmap_registers[index // BITMAP_BITS]
|
||||
new = IntOp(
|
||||
bitmap_rprimitive,
|
||||
reg,
|
||||
Integer(1 << (index & (BITMAP_BITS - 1)), bitmap_rprimitive),
|
||||
IntOp.OR,
|
||||
op.line,
|
||||
)
|
||||
new_ops.append(new)
|
||||
new_ops.append(Assign(reg, new))
|
||||
else:
|
||||
new_ops.append(op)
|
||||
block.ops = new_ops
|
||||
Loading…
Add table
Add a link
Reference in a new issue