From dc9ad65acadb6f7c513774f169a474dd7fab6bad Mon Sep 17 00:00:00 2001 From: Joel Flores Date: Sun, 18 Oct 2026 04:51:38 -0500 Subject: [PATCH] fix stack effects of make_function, build_slice, and format_value operands --- pytest/test_stack_effect.py | 24 ++++++++++++++++++++++++ xdis/cross_dis.py | 18 ++++++++++++------ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/pytest/test_stack_effect.py b/pytest/test_stack_effect.py index 9adb02b4..754ad08b 100644 --- a/pytest/test_stack_effect.py +++ b/pytest/test_stack_effect.py @@ -175,5 +175,29 @@ def test_one(xdis_args, dis_args, has_arg: bool) -> None: return +def test_stack_effect_operand_dependent() -> None: + """Check stack effects of MAKE_FUNCTION, FORMAT_VALUE, and BUILD_SLICE + for operands whose bits change how many items are popped. + """ + for version in ((3, 6), (3, 10)): + opc = get_opcode(version, False) + for oparg, effect in ((11, -4), (12, -3), (15, -5)): + assert xstack_effect(opc.opmap["MAKE_FUNCTION"], opc, oparg) == effect + + for version in ((3, 11), (3, 12)): + opc = get_opcode(version, False) + for oparg, effect in ((3, -2), (4, -1), (8, -1), (9, -2), (10, -2), (11, -3), (15, -4)): + assert xstack_effect(opc.opmap["MAKE_FUNCTION"], opc, oparg) == effect + + for version in ((3, 6), (3, 12)): + opc = get_opcode(version, False) + assert xstack_effect(opc.opmap["FORMAT_VALUE"], opc, 0) == 0 + assert xstack_effect(opc.opmap["FORMAT_VALUE"], opc, 4) == -1 + + for version in ((3, 6), (3, 13)): + opc = get_opcode(version, False) + assert xstack_effect(opc.opmap["BUILD_SLICE"], opc, 3) == -2 + + if __name__ == "__main__": test_stack_effect_fixed() diff --git a/xdis/cross_dis.py b/xdis/cross_dis.py index 5c34a2b4..e75ff137 100644 --- a/xdis/cross_dis.py +++ b/xdis/cross_dis.py @@ -508,21 +508,27 @@ def xstack_effect(opcode, opc, oparg: int = 0, jump=None): "BUILD_TUPLE", ) and version_tuple >= (3, 12): return 1 - oparg - elif opname in ("BUILD_SLICE") and version_tuple <= (2, 7): + elif opname in ("BUILD_SLICE") and version_tuple < (3, 14): + # this was 2.7 only, but 3.0 through 3.13 also pop the step in a[x:y:z] + # (3.14 changed BUILD_SLICE and its opcode table already handles it) return -2 if oparg == 3 else -1 + elif opname == "FORMAT_VALUE" and version_tuple >= (3, 6): + # f"{x:>10}" also pops the format spec; the table only counted the value + return -1 if oparg & 0x04 else 0 elif opname == "LOAD_ATTR" and version_tuple >= (3, 12): return 1 if oparg & 1 else 0 elif opname == "MAKE_FUNCTION": if version_tuple >= (3, 5): - if 0 <= oparg <= 10: + # was 10, but a nested function with defaults, kwdefaults, and a closure is 11 (15 with annotations) + if 0 <= oparg <= (10 if version_tuple == (3, 5) else 15): if version_tuple == (3, 5): return [-1, -2, -3, -3, -2, -3, -3, -4, -2, -3, -3, -4][oparg] elif (3, 6) <= version_tuple < (3, 11): - return [-1, -2, -2, -3, -2, -3, -3, -4, -2, -3, -3, -4][oparg] - elif 0 <= oparg <= 2: - return [0, -1, -1][oparg] + return [-1, -2, -2, -3, -2, -3, -3, -4, -2, -3, -3, -4, -3, -4, -4, -5][oparg] else: - return None + # only flags 0-2 were handled, so closures and annotations gave None; + # this is the 3.6-3.10 table plus one since 3.11 no longer pops the qualname + return [0, -1, -1, -2, -1, -2, -2, -3, -1, -2, -2, -3, -2, -3, -3, -4][oparg] else: return None elif opname in ("CALL", "INSTRUMENTED_CALL") and version_tuple >= (3, 12):