diff --git a/Lib/test/test_generated_cases.py b/Lib/test/test_generated_cases.py index 98bc8ecc3f7ce6..d0e988c3e8fb87 100644 --- a/Lib/test/test_generated_cases.py +++ b/Lib/test/test_generated_cases.py @@ -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 @@ -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 @@ -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): diff --git a/Tools/cases_generator/opcode_metadata_generator.py b/Tools/cases_generator/opcode_metadata_generator.py index 021e519d26e970..a658bd946603e0 100644 --- a/Tools/cases_generator/opcode_metadata_generator.py +++ b/Tools/cases_generator/opcode_metadata_generator.py @@ -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. @@ -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)) @@ -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: diff --git a/Tools/cases_generator/stack.py b/Tools/cases_generator/stack.py index ce3ad57706298e..bfabc98c3d65cd 100644 --- a/Tools/cases_generator/stack.py +++ b/Tools/cases_generator/stack.py @@ -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): @@ -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