Module refinery.lib.scripts.analysis.liveness
The backward live-variable worklist, over one control-flow graph and a caller's gen/kill oracle.
A value is live at a point when some path from there may read it before overwriting it. The solver is the standard backward fixpoint and knows nothing about what it is tracking: the domain is whatever hashable thing the oracle returns, which for a language with a binding model is a binding and for one without could be a name. What a language contributes is the two sets per node — what this node reads, and what it definitely overwrites — and nothing else.
The transfer function is deliberately not the textbook one. A node's live-in is
use ∪ (normal_out − kill) ∪ exceptional_out
where the textbook rule subtracts kill from the whole of live-out. The difference is the
exceptional successors: a statement that may throw part-way through has not necessarily performed
its store, so along that edge the store must not mask an earlier one. Subtracting the kill from the
exceptional set too reports a store as dead whose value the handler can still read, which is a
deletion of live code — and the asymmetry is why this rule is stated here once rather than
rediscovered per language.
Expand source code Browse git
"""
The backward live-variable worklist, over one control-flow graph and a caller's gen/kill oracle.
A value is *live* at a point when some path from there may read it before overwriting it. The solver
is the standard backward fixpoint and knows nothing about what it is tracking: the domain is whatever
hashable thing the oracle returns, which for a language with a binding model is a binding and for one
without could be a name. What a language contributes is the two sets per node — what this node reads,
and what it definitely overwrites — and nothing else.
**The transfer function is deliberately not the textbook one.** A node's live-in is
use ∪ (normal_out − kill) ∪ exceptional_out
where the textbook rule subtracts `kill` from the whole of live-out. The difference is the
exceptional successors: a statement that may throw part-way through has not necessarily performed
its store, so along that edge the store must not mask an earlier one. Subtracting the kill from the
exceptional set too reports a store as dead whose value the handler can still read, which is a
deletion of live code — and the asymmetry is why this rule is stated here once rather than
rediscovered per language.
"""
from __future__ import annotations
from typing import Callable, Hashable, TypeVar
from refinery.lib.scripts.analysis.cfg import CfgNode, ControlFlowGraph
_T = TypeVar('_T', bound=Hashable)
#: What a language answers for one control-flow node: what it reads, and what it definitely
#: overwrites. A node whose store is conditional — guarded by a short-circuit, a ternary, a
#: destructuring default — contributes it to neither set, because the kill is what licenses calling
#: an earlier store dead and a conditional one licenses nothing.
NodeSets = Callable[[ControlFlowGraph, CfgNode], tuple[set[_T], set[_T]]]
def solve_liveness(
graph: ControlFlowGraph,
node_sets: NodeSets[_T],
) -> tuple[dict[int, frozenset[_T]], dict[int, frozenset[_T]]]:
"""
The live-in and live-out sets of every node in *graph*, keyed by node identity.
Iterated in reverse graph order until nothing moves, which converges because the sets only grow
and the domain is finite. Reverse order is a heuristic for how many rounds that takes and never
a correctness condition — the fixpoint is the same whichever order the nodes are visited in.
"""
use: dict[int, set[_T]] = {}
kill: dict[int, set[_T]] = {}
normal_successors: dict[int, list[CfgNode]] = {}
exceptional_successors: dict[int, list[CfgNode]] = {}
for node in graph.nodes:
use[id(node)], kill[id(node)] = node_sets(graph, node)
normal_successors[id(node)] = [
successor for successor in node.successors
if not graph.is_exceptional(node, successor)
]
exceptional_successors[id(node)] = [
successor for successor in node.successors
if graph.is_exceptional(node, successor)
]
live_in: dict[int, set[_T]] = {id(node): set() for node in graph.nodes}
live_out: dict[int, set[_T]] = {id(node): set() for node in graph.nodes}
changed = True
while changed:
changed = False
for node in reversed(graph.nodes):
normal: set[_T] = set()
exceptional: set[_T] = set()
for successor in normal_successors[id(node)]:
normal |= live_in[id(successor)]
for successor in exceptional_successors[id(node)]:
exceptional |= live_in[id(successor)]
out = normal | exceptional
inn = use[id(node)] | (normal - kill[id(node)]) | exceptional
if out != live_out[id(node)] or inn != live_in[id(node)]:
live_out[id(node)] = out
live_in[id(node)] = inn
changed = True
return (
{key: frozenset(value) for key, value in live_in.items()},
{key: frozenset(value) for key, value in live_out.items()},
)
Global variables
var NodeSets-
What a language answers for one control-flow node: what it reads, and what it definitely overwrites. A node whose store is conditional — guarded by a short-circuit, a ternary, a destructuring default — contributes it to neither set, because the kill is what licenses calling an earlier store dead and a conditional one licenses nothing.
Functions
def solve_liveness(graph, node_sets)-
The live-in and live-out sets of every node in graph, keyed by node identity.
Iterated in reverse graph order until nothing moves, which converges because the sets only grow and the domain is finite. Reverse order is a heuristic for how many rounds that takes and never a correctness condition — the fixpoint is the same whichever order the nodes are visited in.
Expand source code Browse git
def solve_liveness( graph: ControlFlowGraph, node_sets: NodeSets[_T], ) -> tuple[dict[int, frozenset[_T]], dict[int, frozenset[_T]]]: """ The live-in and live-out sets of every node in *graph*, keyed by node identity. Iterated in reverse graph order until nothing moves, which converges because the sets only grow and the domain is finite. Reverse order is a heuristic for how many rounds that takes and never a correctness condition — the fixpoint is the same whichever order the nodes are visited in. """ use: dict[int, set[_T]] = {} kill: dict[int, set[_T]] = {} normal_successors: dict[int, list[CfgNode]] = {} exceptional_successors: dict[int, list[CfgNode]] = {} for node in graph.nodes: use[id(node)], kill[id(node)] = node_sets(graph, node) normal_successors[id(node)] = [ successor for successor in node.successors if not graph.is_exceptional(node, successor) ] exceptional_successors[id(node)] = [ successor for successor in node.successors if graph.is_exceptional(node, successor) ] live_in: dict[int, set[_T]] = {id(node): set() for node in graph.nodes} live_out: dict[int, set[_T]] = {id(node): set() for node in graph.nodes} changed = True while changed: changed = False for node in reversed(graph.nodes): normal: set[_T] = set() exceptional: set[_T] = set() for successor in normal_successors[id(node)]: normal |= live_in[id(successor)] for successor in exceptional_successors[id(node)]: exceptional |= live_in[id(successor)] out = normal | exceptional inn = use[id(node)] | (normal - kill[id(node)]) | exceptional if out != live_out[id(node)] or inn != live_in[id(node)]: live_out[id(node)] = out live_in[id(node)] = inn changed = True return ( {key: frozenset(value) for key, value in live_in.items()}, {key: frozenset(value) for key, value in live_out.items()}, )