Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion sdks/python/apache_beam/typehints/opcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -506,6 +523,13 @@ def load_small_int(state, arg):
state.stack.append(Const(arg))


def load_common_constant(state, arg):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this called anywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't have a direct call but will be used when the typehint tests run on 3.15. The functions defined in opcodes.py get imported in infer_return_type_func() and placed into a dictionary, then invoked through that dict entry rather than calling the function directly. This is the general convention for any operation that doesn't do more complex behavior like modifying the program counter

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks

if arg < len(_COMMON_CONSTANTS):
state.stack.append(Const(_COMMON_CONSTANTS[arg]))
else:
state.stack.append(Any)


store_map = pop_two


Expand Down
23 changes: 18 additions & 5 deletions sdks/python/apache_beam/typehints/trivial_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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>')):
Expand Down Expand Up @@ -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
Expand Down
Loading