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:
IndieStatusBot 2026-04-25 05:00:00 +00:00
commit 902133edd3
4655 changed files with 1342691 additions and 0 deletions

View file

@ -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)

View 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

View file

@ -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)

View file

@ -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)

View 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,
)

View 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)

View 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

View 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

View 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