Module refinery.lib.scripts.ps1.analysis.assignment
Definite assignment for PowerShell locals: a forward must-analysis over the per-body control-flow graphs answering whether a store to a name has certainly completed before a read of it runs. A store counts only along an edge its statement leaves by completing, and only where the statement certainly completes: where nothing takes an error, 5.1 reports it and carries on along the very edge a completed statement leaves by, so the edge alone does not say that the store was made.
Expand source code Browse git
"""
Definite assignment for PowerShell locals: a forward must-analysis over the per-body control-flow
graphs answering whether a store to a name has certainly completed before a read of it runs. A store
counts only along an edge its statement leaves by completing, and only where the statement certainly
completes: where nothing takes an error, 5.1 reports it and carries on along the very edge a
completed statement leaves by, so the edge alone does not say that the store was made.
"""
from __future__ import annotations
from typing import Callable, NamedTuple
from refinery.lib.scripts import Node
from refinery.lib.scripts.analysis.cfg import CfgNode, ControlFlowGraph, ControlFlowModel
from refinery.lib.scripts.ps1.model import (
Ps1AssignmentExpression,
Ps1CastExpression,
Ps1ExpressionStatement,
Ps1ScopeModifier,
Ps1UnaryExpression,
Ps1Variable,
)
def _plain_local(variable: Node | None) -> str | None:
"""
The name *variable* reads or writes where it is a plain local: no splat, no drive, and no scope
other than the local one.
"""
if not isinstance(variable, Ps1Variable) or variable.splatted or variable.drive:
return None
if variable.scope not in (Ps1ScopeModifier.NONE, Ps1ScopeModifier.LOCAL):
return None
return variable.name.lower()
def _store_of(element: Node | None) -> tuple[str, Node] | None:
"""
The plain local the graph element *element* stores into, through a cast on the target or by an
increment, beside the expression making the store, or `None` where it stores into none.
"""
if isinstance(element, Ps1ExpressionStatement):
element = element.expression
if isinstance(element, Ps1AssignmentExpression):
target = element.target
while isinstance(target, Ps1CastExpression):
target = target.operand
name = _plain_local(target)
elif isinstance(element, Ps1UnaryExpression) and element.operator in ('++', '--'):
name = _plain_local(element.operand)
else:
return None
return None if name is None or element is None else (name, element)
class _Facts(NamedTuple):
"""
The solution for one graph: a bit per name some node stores and the graph's owner reads, and for
each node control reaches, the names certainly stored on every run that arrives there.
"""
bits: dict[str, int]
state: dict[int, int]
class Ps1DefiniteAssignment:
"""
Whether a store to the name of a read certainly completed before the read, over the graphs of
*control_flow*, solved once per graph on the first question about it. A name *may_be_removed*
names is never certainly stored: a removal between the store and the read leaves it unset.
*completes* says whether the expression making a store certainly completes where it stands,
given a test that answers for a read whether a store to its name certainly completed before the
store; a store counts only where it does. The test is read off the state this solves, so a
store of a name read after a store that certainly completed counts as well.
"""
def __init__(
self,
control_flow: ControlFlowModel,
may_be_removed: Callable[[str], bool],
completes: Callable[[Node, Callable[[Node], bool]], bool],
):
self._flow = control_flow
self._may_be_removed = may_be_removed
self._completes = completes
self._facts: dict[int, _Facts] = {}
def assigned_before(self, read: Node) -> bool:
"""
Whether a store to the plain local *read* reads certainly completed, in the body *read*
stands in, before *read* runs.
"""
name = _plain_local(read)
if name is None or self._may_be_removed(name):
return False
located = self._flow.locate(read)
if located is None:
return False
graph, node = located
facts = self._facts.get(id(graph))
if facts is None:
facts = self._facts[id(graph)] = self._solve(graph)
bit = facts.bits.get(name)
return bit is not None and bool(facts.state.get(id(node), 0) & bit)
def _solve(self, graph: ControlFlowGraph) -> _Facts:
stored: dict[int, tuple[str, Node]] = {}
for node in graph.nodes:
store = _store_of(node.element)
if store is not None:
stored[id(node)] = store
read: set[str] = set()
for inner in graph.owner.walk():
if isinstance(inner, Ps1Variable) and (local := _plain_local(inner)) is not None:
read.add(local)
bits: dict[str, int] = {}
for name, _ in stored.values():
if name in read and name not in bits:
bits[name] = 1 << len(bits)
stores = {key: (bits[name], made) for key, (name, made) in stored.items() if name in bits}
if not stores:
return _Facts({}, {})
may_be_removed = self._may_be_removed
completes = self._completes
decided: dict[tuple[int, int], int] = {}
def gen(node: CfgNode, before: int) -> int:
store = stores.get(id(node))
if store is None:
return 0
key = (id(node), before)
found = decided.get(key)
if found is None:
bit, made = store
def assigned(read: Node) -> bool:
name = _plain_local(read)
return name is not None and not may_be_removed(name) and bool(before & bits.get(name, 0))
found = decided[key] = bit if completes(made, assigned) else 0
return found
top = (1 << len(bits)) - 1
state: dict[int, int] = {id(node): top for node in graph.nodes}
state[id(graph.entry)] = 0
reachable: set[int] = set()
frontier: list[CfgNode] = [graph.entry]
while frontier:
node = frontier.pop()
if id(node) in reachable:
continue
reachable.add(id(node))
frontier.extend(node.successors)
changed = True
while changed:
changed = False
for node in graph.nodes:
if node is graph.entry:
continue
met = -1
for pred in node.predecessors:
out = state[id(pred)]
if id(pred) in stores and graph.completes_into(pred, node):
out |= gen(pred, out)
met &= out
if met == -1:
met = 0
if met != state[id(node)]:
state[id(node)] = met
changed = True
return _Facts(bits, {key: value for key, value in state.items() if key in reachable})
Classes
class Ps1DefiniteAssignment (control_flow, may_be_removed, completes)-
Whether a store to the name of a read certainly completed before the read, over the graphs of control_flow, solved once per graph on the first question about it. A name may_be_removed names is never certainly stored: a removal between the store and the read leaves it unset.
completes says whether the expression making a store certainly completes where it stands, given a test that answers for a read whether a store to its name certainly completed before the store; a store counts only where it does. The test is read off the state this solves, so a store of a name read after a store that certainly completed counts as well.
Expand source code Browse git
class Ps1DefiniteAssignment: """ Whether a store to the name of a read certainly completed before the read, over the graphs of *control_flow*, solved once per graph on the first question about it. A name *may_be_removed* names is never certainly stored: a removal between the store and the read leaves it unset. *completes* says whether the expression making a store certainly completes where it stands, given a test that answers for a read whether a store to its name certainly completed before the store; a store counts only where it does. The test is read off the state this solves, so a store of a name read after a store that certainly completed counts as well. """ def __init__( self, control_flow: ControlFlowModel, may_be_removed: Callable[[str], bool], completes: Callable[[Node, Callable[[Node], bool]], bool], ): self._flow = control_flow self._may_be_removed = may_be_removed self._completes = completes self._facts: dict[int, _Facts] = {} def assigned_before(self, read: Node) -> bool: """ Whether a store to the plain local *read* reads certainly completed, in the body *read* stands in, before *read* runs. """ name = _plain_local(read) if name is None or self._may_be_removed(name): return False located = self._flow.locate(read) if located is None: return False graph, node = located facts = self._facts.get(id(graph)) if facts is None: facts = self._facts[id(graph)] = self._solve(graph) bit = facts.bits.get(name) return bit is not None and bool(facts.state.get(id(node), 0) & bit) def _solve(self, graph: ControlFlowGraph) -> _Facts: stored: dict[int, tuple[str, Node]] = {} for node in graph.nodes: store = _store_of(node.element) if store is not None: stored[id(node)] = store read: set[str] = set() for inner in graph.owner.walk(): if isinstance(inner, Ps1Variable) and (local := _plain_local(inner)) is not None: read.add(local) bits: dict[str, int] = {} for name, _ in stored.values(): if name in read and name not in bits: bits[name] = 1 << len(bits) stores = {key: (bits[name], made) for key, (name, made) in stored.items() if name in bits} if not stores: return _Facts({}, {}) may_be_removed = self._may_be_removed completes = self._completes decided: dict[tuple[int, int], int] = {} def gen(node: CfgNode, before: int) -> int: store = stores.get(id(node)) if store is None: return 0 key = (id(node), before) found = decided.get(key) if found is None: bit, made = store def assigned(read: Node) -> bool: name = _plain_local(read) return name is not None and not may_be_removed(name) and bool(before & bits.get(name, 0)) found = decided[key] = bit if completes(made, assigned) else 0 return found top = (1 << len(bits)) - 1 state: dict[int, int] = {id(node): top for node in graph.nodes} state[id(graph.entry)] = 0 reachable: set[int] = set() frontier: list[CfgNode] = [graph.entry] while frontier: node = frontier.pop() if id(node) in reachable: continue reachable.add(id(node)) frontier.extend(node.successors) changed = True while changed: changed = False for node in graph.nodes: if node is graph.entry: continue met = -1 for pred in node.predecessors: out = state[id(pred)] if id(pred) in stores and graph.completes_into(pred, node): out |= gen(pred, out) met &= out if met == -1: met = 0 if met != state[id(node)]: state[id(node)] = met changed = True return _Facts(bits, {key: value for key, value in state.items() if key in reachable})Methods
def assigned_before(self, read)-
Whether a store to the plain local read reads certainly completed, in the body read stands in, before read runs.
Expand source code Browse git
def assigned_before(self, read: Node) -> bool: """ Whether a store to the plain local *read* reads certainly completed, in the body *read* stands in, before *read* runs. """ name = _plain_local(read) if name is None or self._may_be_removed(name): return False located = self._flow.locate(read) if located is None: return False graph, node = located facts = self._facts.get(id(graph)) if facts is None: facts = self._facts[id(graph)] = self._solve(graph) bit = facts.bits.get(name) return bit is not None and bool(facts.state.get(id(node), 0) & bit)