cpython/Tools/cases_generator/stacking.py

401 lines
13 KiB
Python

import dataclasses
import typing
from formatting import (
Formatter,
UNUSED,
maybe_parenthesize,
parenthesize_cond,
)
from instructions import (
ActiveCacheEffect,
Instruction,
MacroInstruction,
Component,
Tiers,
TIER_ONE,
)
from parsing import StackEffect, CacheEffect, Family
@dataclasses.dataclass
class StackOffset:
"""Represent the stack offset for a PEEK or POKE.
- At stack_pointer[0], deep and high are both empty.
(Note that that is an invalid stack reference.)
- Below stack top, only deep is non-empty.
- Above stack top, only high is non-empty.
- In complex cases, both deep and high may be non-empty.
All this would be much simpler if all stack entries were the same
size, but with conditional and array effects, they aren't.
The offsets are each represented by a list of StackEffect objects.
The name in the StackEffects is unused.
"""
deep: list[StackEffect] = dataclasses.field(default_factory=list)
high: list[StackEffect] = dataclasses.field(default_factory=list)
def clone(self) -> "StackOffset":
return StackOffset(list(self.deep), list(self.high))
def negate(self) -> "StackOffset":
return StackOffset(list(self.high), list(self.deep))
def deeper(self, eff: StackEffect) -> None:
if eff in self.high:
self.high.remove(eff)
else:
self.deep.append(eff)
def higher(self, eff: StackEffect) -> None:
if eff in self.deep:
self.deep.remove(eff)
else:
self.high.append(eff)
def as_terms(self) -> list[tuple[str, str]]:
num = 0
terms: list[tuple[str, str]] = []
for eff in self.deep:
if eff.size:
terms.append(("-", maybe_parenthesize(eff.size)))
elif eff.cond and eff.cond != "1":
terms.append(("-", f"({parenthesize_cond(eff.cond)} ? 1 : 0)"))
elif eff.cond != "0":
num -= 1
for eff in self.high:
if eff.size:
terms.append(("+", maybe_parenthesize(eff.size)))
elif eff.cond and eff.cond != "1":
terms.append(("+", f"({parenthesize_cond(eff.cond)} ? 1 : 0)"))
elif eff.cond != "0":
num += 1
if num < 0:
terms.insert(0, ("-", str(-num)))
elif num > 0:
terms.append(("+", str(num)))
return terms
def as_index(self) -> str:
terms = self.as_terms()
return make_index(terms)
def make_index(terms: list[tuple[str, str]]) -> str:
# Produce an index expression from the terms honoring PEP 8,
# surrounding binary ops with spaces but not unary minus
index = ""
for sign, term in terms:
if index:
index += f" {sign} {term}"
elif sign == "+":
index = term
else:
index = sign + term
return index or "0"
@dataclasses.dataclass
class StackItem:
offset: StackOffset
effect: StackEffect
def as_variable(self, lax: bool = False) -> str:
"""Return e.g. stack_pointer[-1]."""
terms = self.offset.as_terms()
if self.effect.size:
terms.insert(0, ("+", "stack_pointer"))
index = make_index(terms)
if self.effect.size:
res = index
else:
res = f"stack_pointer[{index}]"
if not lax:
# Check that we're not reading or writing above stack top.
# Skip this for output variable initialization (lax=True).
assert (
self.effect in self.offset.deep and not self.offset.high
), f"Push or pop above current stack level: {res}"
return res
@dataclasses.dataclass
class CopyEffect:
src: StackEffect
dst: StackEffect
class EffectManager:
"""Manage stack effects and offsets for an instruction."""
instr: Instruction
active_caches: list[ActiveCacheEffect]
peeks: list[StackItem]
pokes: list[StackItem]
copies: list[CopyEffect] # See merge()
# Track offsets from stack pointer
min_offset: StackOffset
final_offset: StackOffset
def __init__(
self,
instr: Instruction,
active_caches: list[ActiveCacheEffect],
pred: "EffectManager | None" = None,
):
self.instr = instr
self.active_caches = active_caches
self.peeks = []
self.pokes = []
self.copies = []
self.final_offset = pred.final_offset.clone() if pred else StackOffset()
for eff in reversed(instr.input_effects):
self.final_offset.deeper(eff)
self.peeks.append(StackItem(offset=self.final_offset.clone(), effect=eff))
self.min_offset = self.final_offset.clone()
for eff in instr.output_effects:
self.pokes.append(StackItem(offset=self.final_offset.clone(), effect=eff))
self.final_offset.higher(eff)
if pred:
# Replace push(x) + pop(y) with copy(x, y).
# Check that the sources and destinations are disjoint.
sources: set[str] = set()
destinations: set[str] = set()
while (
pred.pokes
and self.peeks
and pred.pokes[-1].effect == self.peeks[-1].effect
):
src = pred.pokes.pop(-1).effect
dst = self.peeks.pop(0).effect
pred.final_offset.deeper(src)
if dst.name != UNUSED:
destinations.add(dst.name)
if dst.name != src.name:
sources.add(src.name)
self.copies.append(CopyEffect(src, dst))
# TODO: Turn this into an error (pass an Analyzer instance?)
assert sources & destinations == set(), (
pred.instr.name,
self.instr.name,
sources,
destinations,
)
def adjust_deeper(self, eff: StackEffect) -> None:
for peek in self.peeks:
peek.offset.deeper(eff)
for poke in self.pokes:
poke.offset.deeper(eff)
self.min_offset.deeper(eff)
self.final_offset.deeper(eff)
def adjust_higher(self, eff: StackEffect) -> None:
for peek in self.peeks:
peek.offset.higher(eff)
for poke in self.pokes:
poke.offset.higher(eff)
self.min_offset.higher(eff)
self.final_offset.higher(eff)
def adjust(self, offset: StackOffset) -> None:
for down in offset.deep:
self.adjust_deeper(down)
for up in offset.high:
self.adjust_higher(up)
def adjust_inverse(self, offset: StackOffset) -> None:
for down in offset.deep:
self.adjust_higher(down)
for up in offset.high:
self.adjust_deeper(up)
def collect_vars(self) -> dict[str, StackEffect]:
"""Collect all variables, skipping unused ones."""
vars: dict[str, StackEffect] = {}
def add(eff: StackEffect) -> None:
if eff.name != UNUSED:
if eff.name in vars:
# TODO: Make this an error
assert vars[eff.name] == eff, (
self.instr.name,
eff.name,
vars[eff.name],
eff,
)
else:
vars[eff.name] = eff
for copy in self.copies:
add(copy.src)
add(copy.dst)
for peek in self.peeks:
add(peek.effect)
for poke in self.pokes:
add(poke.effect)
return vars
def less_than(a: StackOffset, b: StackOffset) -> bool:
# TODO: Handle more cases
if a.high != b.high:
return False
return a.deep[: len(b.deep)] == b.deep
def get_managers(parts: list[Component]) -> list[EffectManager]:
managers: list[EffectManager] = []
pred: EffectManager | None = None
for part in parts:
mgr = EffectManager(part.instr, part.active_caches, pred)
managers.append(mgr)
pred = mgr
return managers
def get_stack_effect_info_for_macro(mac: MacroInstruction) -> tuple[str, str]:
"""Get the stack effect info for a macro instruction.
Returns a tuple (popped, pushed) where each is a string giving a
symbolic expression for the number of values popped/pushed.
"""
parts = [part for part in mac.parts if isinstance(part, Component)]
managers = get_managers(parts)
popped = StackOffset()
for mgr in managers:
if less_than(mgr.min_offset, popped):
popped = mgr.min_offset.clone()
# Compute pushed = final - popped
pushed = managers[-1].final_offset.clone()
for effect in popped.deep:
pushed.higher(effect)
for effect in popped.high:
pushed.deeper(effect)
return popped.negate().as_index(), pushed.as_index()
def write_single_instr(
instr: Instruction, out: Formatter, tier: Tiers = TIER_ONE
) -> None:
try:
write_components(
[Component(instr, instr.active_caches)],
out,
tier,
)
except AssertionError as err:
raise AssertionError(f"Error writing instruction {instr.name}") from err
def write_macro_instr(
mac: MacroInstruction, out: Formatter, family: Family | None
) -> None:
parts = [part for part in mac.parts if isinstance(part, Component)]
cache_adjust = 0
for part in mac.parts:
match part:
case CacheEffect(size=size):
cache_adjust += size
case Component(instr=instr):
cache_adjust += instr.cache_offset
case _:
typing.assert_never(part)
out.emit("")
with out.block(f"TARGET({mac.name})"):
if mac.predicted:
out.emit(f"PREDICTED({mac.name});")
out.static_assert_family_size(mac.name, family, cache_adjust)
try:
write_components(parts, out, TIER_ONE)
except AssertionError as err:
raise AssertionError(f"Error writing macro {mac.name}") from err
if cache_adjust:
out.emit(f"next_instr += {cache_adjust};")
out.emit("DISPATCH();")
def write_components(
parts: list[Component],
out: Formatter,
tier: Tiers,
) -> None:
managers = get_managers(parts)
all_vars: dict[str, StackEffect] = {}
for mgr in managers:
for name, eff in mgr.collect_vars().items():
if name in all_vars:
# TODO: Turn this into an error -- variable conflict
assert all_vars[name] == eff, (
name,
mgr.instr.name,
all_vars[name],
eff,
)
else:
all_vars[name] = eff
# Declare all variables
for name, eff in all_vars.items():
out.declare(eff, None)
for mgr in managers:
if len(parts) > 1:
out.emit(f"// {mgr.instr.name}")
for copy in mgr.copies:
if copy.src.name != copy.dst.name:
out.assign(copy.dst, copy.src)
for peek in mgr.peeks:
out.assign(
peek.effect,
StackEffect(
peek.as_variable(),
peek.effect.type,
peek.effect.cond,
peek.effect.size,
),
)
# Initialize array outputs
for poke in mgr.pokes:
if poke.effect.size and poke.effect.name not in mgr.instr.unmoved_names:
out.assign(
poke.effect,
StackEffect(
poke.as_variable(lax=True),
poke.effect.type,
poke.effect.cond,
poke.effect.size,
),
)
if len(parts) == 1:
mgr.instr.write_body(out, 0, mgr.active_caches, tier)
else:
with out.block(""):
mgr.instr.write_body(out, -4, mgr.active_caches, tier)
if mgr is managers[-1]:
out.stack_adjust(mgr.final_offset.deep, mgr.final_offset.high)
# Use clone() since adjust_inverse() mutates final_offset
mgr.adjust_inverse(mgr.final_offset.clone())
for poke in mgr.pokes:
if not poke.effect.size and poke.effect.name not in mgr.instr.unmoved_names:
out.assign(
StackEffect(
poke.as_variable(),
poke.effect.type,
poke.effect.cond,
poke.effect.size,
),
poke.effect,
)