From 2319e42b4766b8d91e4c85f333fbefe2c9311326 Mon Sep 17 00:00:00 2001 From: jrmccluskey Date: Wed, 14 Oct 2026 14:41:31 +0000 Subject: [PATCH] Update Trivial Inference for Python 3.15 --- sdks/python/apache_beam/typehints/opcodes.py | 26 ++++++++++++++++++- .../typehints/trivial_inference.py | 23 ++++++++++++---- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/sdks/python/apache_beam/typehints/opcodes.py b/sdks/python/apache_beam/typehints/opcodes.py index 53eabdadc4af..c5dda817d0d2 100644 --- a/sdks/python/apache_beam/typehints/opcodes.py +++ b/sdks/python/apache_beam/typehints/opcodes.py @@ -67,8 +67,13 @@ if sys.version_info >= (3, 14): _NB_SUBSCR_OPCODE = [op[0] for op in opcode._nb_ops].index('NB_SUBSCR') + # Table of values indexed by the arg of LOAD_COMMON_CONSTANT. In 3.15 this + # was expanded to include common literals such as None, '', True, False, + # and -1, which are no longer loaded via LOAD_CONST. + _COMMON_CONSTANTS = tuple(getattr(opcode, '_common_constants', ())) else: _NB_SUBSCR_OPCODE = -1 + _COMMON_CONSTANTS = () def pop_one(state, unused_arg): @@ -107,7 +112,11 @@ def end_for(state, unused_arg): def end_send(state, unused_arg): - del state.stack[-2] + if sys.version_info >= (3, 15): + # The receiver is a two-slot (iter, index_or_null) pair in 3.15. + del state.stack[-3:-1] + else: + del state.stack[-2] def copy(state, arg): @@ -151,6 +160,14 @@ def unary_convert(state, unused_arg): def get_iter(state, unused_arg): state.stack.append(Iterable[element_type(state.stack.pop())]) + if sys.version_info >= (3, 15): + # In 3.15 the iteration state occupies two stack slots: the iterator (or + # the iterable itself for "virtual" iteration over sequences) and either + # NULL or a tagged integer index. FOR_ITER, POP_ITER, END_SEND, etc. all + # operate on this pair. The second slot is irrelevant for type inference + # but must be tracked so stack-relative args (e.g. LIST_APPEND, SWAP) + # stay correct. + state.stack.append(Any) _NUMERIC_PROMOTION_LADDER = [bool, int, float, complex] @@ -506,6 +523,13 @@ def load_small_int(state, arg): state.stack.append(Const(arg)) +def load_common_constant(state, arg): + if arg < len(_COMMON_CONSTANTS): + state.stack.append(Const(_COMMON_CONSTANTS[arg])) + else: + state.stack.append(Any) + + store_map = pop_two diff --git a/sdks/python/apache_beam/typehints/trivial_inference.py b/sdks/python/apache_beam/typehints/trivial_inference.py index 69edfc309281..e34dbb98fbbc 100644 --- a/sdks/python/apache_beam/typehints/trivial_inference.py +++ b/sdks/python/apache_beam/typehints/trivial_inference.py @@ -578,7 +578,11 @@ def infer_return_type_func(f, input_types, debug=False, depth=0): else: # Handle comprehensions always having an arg of 0 for CALL # See https://github.com/python/cpython/issues/102403 for context. - if (pop_count == 1 and last_real_opname == 'GET_ITER' and + # In 3.15 the GET_ITER for a generator expression's iterable moved + # into the generator's own code object, so the CALL is no longer + # preceded by a GET_ITER in the calling frame. + if (pop_count == 1 and + (last_real_opname == 'GET_ITER' or sys.version_info >= (3, 15)) and len(state.stack) > 1 and isinstance(state.stack[-2], Const) and getattr(state.stack[-2].value, '__name__', None) in ('', '', '', '')): @@ -692,11 +696,20 @@ def infer_return_type_func(f, input_types, debug=False, depth=0): # so jump 4 more bytes. jmp += 4 jmp_state = state.copy() - jmp_state.stack.pop() - state.stack.append(element_type(state.stack[-1])) + if sys.version_info >= (3, 15): + # In 3.15 the iterator occupies two stack slots (iter, index_or_null) + # and both are popped when the iterator is exhausted. + jmp_state.stack[-2:] = [] + state.stack.append(element_type(state.stack[-2])) + else: + jmp_state.stack.pop() + state.stack.append(element_type(state.stack[-1])) elif opname == 'POP_ITER': - # Introduced in 3.14. - state.stack.pop() + # Introduced in 3.14. Pops (iter, index_or_null) in 3.15. + if sys.version_info >= (3, 15): + state.stack[-2:] = [] + else: + state.stack.pop() elif opname == 'COPY_FREE_VARS': # Helps with calling closures, but since we aren't executing # them we can treat this as a no-op