Skip to content

Commit

Permalink
Checkpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
mpage committed Nov 16, 2024
1 parent 32eacba commit 012519c
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 75 deletions.
62 changes: 18 additions & 44 deletions Lib/test/test_generated_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import os
import sys
import tempfile
import textwrap
import unittest

from io import StringIO
from test import support
from test import test_tools

Expand All @@ -30,9 +32,11 @@ def skip_if_different_mount_drives():
test_tools.skip_if_missing("cases_generator")
with test_tools.imports_under_tool("cases_generator"):
from analyzer import analyze_forest, StackItem
from cwriter import CWriter
import parser
from stack import get_deeper_stack, get_stack_hwm, Local, Stack, StackError
import tier1_generator
import opcode_metadata_generator
import optimizer_generator


Expand Down Expand Up @@ -118,58 +122,28 @@ def test_XXX(self):
print(s.top_offset.to_c())


class TestGetHWM(unittest.TestCase):
def check(self, src, expected_vars):
analysis = analyze_forest(parse_src(src))
hwm = get_stack_hwm(analysis.instructions["OP"])
hwm_vars = [loc.item.name for loc in hwm.variables]
self.assertEqual(hwm_vars, expected_vars)
class TestGenerateMaxStackEffect(unittest.TestCase):
def check(self, input, output):
analysis = analyze_forest(parse_src(input))
buf = StringIO()
writer = CWriter(buf, 0, False)
opcode_metadata_generator.generate_max_stack_effect_function(analysis, writer)
buf.seek(0)
self.assertIn(output.strip(), buf.read())

def test_inst(self):
src = """
input = """
inst(OP, (a -- b, c)) {
SPAM();
}
"""
self.check(src, ["b", "c"])

def test_uops(self):
src = """
op(OP0, (a -- b, c)) {
SPAM();
}
op(OP1, (b, c -- x)) {
SPAM();
}
op(OP2, (x -- x, y if (oparg))) {
SPAM();
}
macro(OP) = OP0 + OP1 + OP2;
"""
self.check(src, ["b", "c"])

def test_incompatible_stacks(self):
src = """
op(OP0, (a -- b, c if (oparg & 1))) {
SPAM();
}
op(OP1, (b, c -- x)) {
SPAM();
}
op(OP2, (x -- x, y if (oparg & 2))) {
SPAM();
output = """
case OP: {
*effect = 1;
return 0;
}
macro(OP) = OP0 + OP1 + OP2;
"""
with self.assertRaisesRegex(StackError,
"Cannot determine stack hwm for OP"):
self.check(src, [])
self.check(input, output)


class TestGeneratedCases(unittest.TestCase):
Expand Down
114 changes: 92 additions & 22 deletions Tools/cases_generator/opcode_metadata_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
cflags,
)
from cwriter import CWriter
from dataclasses import dataclass
from typing import TextIO
from stack import get_stack_effect, get_stack_hwm, get_deeper_stack
from stack import Stack, get_stack_effect, get_stack_effects, get_stack_hwm, get_deeper_stack

# Constants used instead of size for macro expansions.
# Note: 1, 2, 4 must match actual cache entry sizes.
Expand Down Expand Up @@ -94,8 +95,6 @@ def generate_stack_effect_functions(analysis: Analysis, out: CWriter) -> None:

def add(inst: Instruction | PseudoInstruction) -> None:
stack = get_stack_effect(inst)
hwm = get_stack_hwm(inst)
print(f"{inst.name} - {hwm}")
popped = (-stack.base_offset).to_c()
pushed = (stack.top_offset - stack.base_offset).to_c()
popped_data.append((inst.name, popped))
Expand All @@ -106,30 +105,101 @@ def add(inst: Instruction | PseudoInstruction) -> None:
for pseudo in analysis.pseudos.values():
add(pseudo)

def analyze_family(family):
hwm_inst = family.members[0]
hwm = get_stack_hwm(hwm_inst)
for inst in family.members[1:]:
inst_hwm = get_stack_hwm(inst)
deeper = get_deeper_stack(hwm, inst_hwm)
if deeper is None:
print(f"INCOMPATIBLE: {family.name}")
print(f"{inst.name} {inst_hwm.top_offset.to_c()}")
print(f"{hwm_inst.name} {hwm.top_offset.to_c()}")
return
elif deeper is not hwm:
hwm = deeper
hwm_inst = isnt
print(f"fam={name} hwm_inst={hwm_inst.name} hwm={hwm.top_offset.to_c()}")

for name, family in analysis.families.items():
analyze_family(family)

emit_stack_effect_function(out, "popped", sorted(popped_data))
emit_stack_effect_function(out, "pushed", sorted(pushed_data))

generate_max_stack_effect_function(analysis, out)


def emit_max_stack_effect_function(
out: CWriter, effects: list[tuple[str, list[str]]]
) -> None:
out.emit("extern int _PyOpcode_max_stack_effect(int opcode, int oparg, int *effect);\n")
out.emit("#ifdef NEED_OPCODE_METADATA\n")
out.emit(f"int _PyOpcode_max_stack_effect(int opcode, int oparg, int *effect) {{\n")
out.emit("switch(opcode) {\n")
for name, exprs in effects:
out.emit(f"case {name}: {{\n")
if len(exprs) == 1:
out.emit(f" *effect = {exprs[0]};\n")
out.emit(f" return 0;\n")
else:
assert len(exprs) > 1
out.emit(f" int max_eff = Py_MAX({exprs[0]}, {exprs[1]});\n")
for expr in exprs[2:]:
out.emit(f" max_eff = Py_MAX(max_eff, {expr});\n")
out.emit(f" *effect = max_eff;\n")
out.emit(f" return 0;\n")
out.emit("}\n")
out.emit("default:\n")
out.emit(" return -1;\n")
out.emit("}\n")
out.emit("}\n\n")
out.emit("#endif\n\n")


@dataclass
class MaxStackEffectSet:
int_effect: int
cond_effects: set[str]

def __init__(self) -> None:
self.int_effect = 0
self.cond_effects = set()

def update(self, other: MaxStackEffectSet) -> None:
self.int_effect = max(self.int_effect, other.int_effect)
self.cond_effects.update(other.cond_effects)


def generate_max_stack_effect_function(analysis: Analysis, out: CWriter) -> None:
"""Generate a function that returns the maximum stack effect of an
instruction while it is executing.
Specialized instructions may have a greater stack effect during instruction
execution than the net stack effect of the instruction if uops pass
values on the stack.
"""
effects: dict[str, MaxStackEffectSet] = {}

def add(inst: Instruction | PseudoInstruction) -> None:
inst_effect = MaxStackEffectSet()
for stack in get_stack_effects(inst):
popped = stack.base_offset
pushed = stack.top_offset - stack.base_offset
popped_int, pushed_int = popped.as_int(), pushed.as_int()
if popped_int is not None and pushed_int is not None:
int_effect = popped_int + pushed_int
if int_effect > inst_effect.int_effect:
inst_effect.int_effect = int_effect
else:
inst_effect.cond_effects.add(f"({popped.to_c()}) + ({pushed.to_c()})")
effects[inst.name] = inst_effect

# Collect unique stack effects for each instruction
for inst in analysis.instructions.values():
add(inst)
for pseudo in analysis.pseudos.values():
add(pseudo)

# Merge the effects of all specializations in a family into the generic
# instruction
for family in analysis.families.values():
for inst in family.members:
effects[family.name].update(effects[inst.name])

data: list[tuple[str, list[str]]] = []
for name, effects in sorted(effects.items(), key=lambda kv: kv[0]):
exprs = []
if effects.int_effect is not None:
exprs.append(str(effects.int_effect))
exprs.extend(sorted(effects.cond_effects))
data.append((name, exprs))
emit_max_stack_effect_function(out, data)


def generate_is_pseudo(analysis: Analysis, out: CWriter) -> None:

"""Write the IS_PSEUDO_INSTR macro"""
out.emit("\n\n#define IS_PSEUDO_INSTR(OP) ( \\\n")
for op in analysis.pseudos:
Expand Down
38 changes: 29 additions & 9 deletions Tools/cases_generator/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,18 +390,37 @@ def merge(self, other: "Stack", out: CWriter) -> None:
self.align(other, out)


def stacks(inst: Instruction | PseudoInstruction) -> Iterator[StackEffect]:
if isinstance(inst, Instruction):
for uop in inst.parts:
if isinstance(uop, Uop):
yield uop.stack
else:
assert isinstance(inst, PseudoInstruction)
yield inst.stack


def get_stack_effect(inst: Instruction | PseudoInstruction) -> Stack:
stack = Stack()
for s in stacks(inst):
locals: dict[str, Local] = {}
for var in reversed(s.inputs):
_, local = stack.pop(var)
if var.name != "unused":
locals[local.name] = local
for var in s.outputs:
if var.name in locals:
local = locals[var.name]
else:
local = Local.unused(var)
stack.push(local)
return stack

def stacks(inst: Instruction | PseudoInstruction) -> Iterator[StackEffect]:
if isinstance(inst, Instruction):
for uop in inst.parts:
if isinstance(uop, Uop):
yield uop.stack
else:
assert isinstance(inst, PseudoInstruction)
yield inst.stack

def get_stack_effects(inst: Instruction | PseudoInstruction) -> list[Stack]:
"""Returns a list of stack effects after each uop"""
result = []
stack = Stack()
for s in stacks(inst):
locals: dict[str, Local] = {}
for var in reversed(s.inputs):
Expand All @@ -414,7 +433,8 @@ def stacks(inst: Instruction | PseudoInstruction) -> Iterator[StackEffect]:
else:
local = Local.unused(var)
stack.push(local)
return stack
result.append(stack.copy())
return result


@dataclass
Expand Down

0 comments on commit 012519c

Please sign in to comment.