This is an automated email from the ASF dual-hosted git repository.

jrmccluskey pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 84b62218f1e Update Trivial Inference for Python 3.15 (#40578)
84b62218f1e is described below

commit 84b62218f1e7adecd7791c2dabe27fef2a8d277f
Author: Jack McCluskey <[email protected]>
AuthorDate: Thu Oct 15 09:55:00 2026 -0400

    Update Trivial Inference for Python 3.15 (#40578)
---
 sdks/python/apache_beam/typehints/opcodes.py       | 26 +++++++++++++++++++++-
 .../apache_beam/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 53eabdadc4a..c5dda817d0d 100644
--- a/sdks/python/apache_beam/typehints/opcodes.py
+++ b/sdks/python/apache_beam/typehints/opcodes.py
@@ -67,8 +67,13 @@ else:
 
 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 69edfc30928..e34dbb98fbb 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 ('<listcomp>', '<dictcomp>', '<setcomp>', '<genexpr>')):
@@ -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

Reply via email to