| """Transformation for inserting reference 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 |