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 77c2dacd1c1 More precise binary operation inference. (#34305)
77c2dacd1c1 is described below
commit 77c2dacd1c1e3797feb38cbee09c93c275f3067e
Author: Robert Bradshaw <[email protected]>
AuthorDate: Wed Mar 19 07:23:29 2025 -0700
More precise binary operation inference. (#34305)
* More precise binary operation inference.
Handle numeric promotion, sequence multiplication, and fix division for
Python 3.11+.
* Fix older python versions.
They have the opcode module, but no _nb_ops.
* Restore fix for 3.9 and 3.10.
* yapf
---
sdks/python/apache_beam/typehints/opcodes.py | 40 +++++++++++++++++-----
.../typehints/trivial_inference_test.py | 16 +++++++++
2 files changed, 48 insertions(+), 8 deletions(-)
diff --git a/sdks/python/apache_beam/typehints/opcodes.py
b/sdks/python/apache_beam/typehints/opcodes.py
index cd78e397f25..e6fdcdba8e4 100644
--- a/sdks/python/apache_beam/typehints/opcodes.py
+++ b/sdks/python/apache_beam/typehints/opcodes.py
@@ -54,6 +54,15 @@ from apache_beam.typehints.typehints import Union
# method on a C-implemented type will do.
_MethodDescriptorType = type(str.upper)
+if sys.version_info >= (3, 11):
+ import opcode
+ _div_binop_args = frozenset([
+ ix for (ix, (argname, _)) in enumerate(opcode._nb_ops)
+ if 'TRUE_DIVIDE' in argname
+ ])
+else:
+ _div_binop_args = frozenset()
+
def pop_one(state, unused_arg):
del state.stack[-1:]
@@ -137,13 +146,32 @@ def get_iter(state, unused_arg):
state.stack.append(Iterable[element_type(state.stack.pop())])
-def symmetric_binary_op(state, unused_arg):
+_NUMERIC_PROMOTION_LADDER = [bool, int, float, complex]
+
+
+def symmetric_binary_op(state, arg, is_true_div=None):
# TODO(robertwb): This may not be entirely correct...
b, a = Const.unwrap(state.stack.pop()), Const.unwrap(state.stack.pop())
if a == b:
- state.stack.append(a)
+ if a is int and b is int and (arg in _div_binop_args or is_true_div):
+ state.stack.append(float)
+ else:
+ state.stack.append(a)
elif type(a) == type(b) and isinstance(a, typehints.SequenceTypeConstraint):
state.stack.append(type(a)(union(element_type(a), element_type(b))))
+ # Technically these next two will be errors for anything but multiplication,
+ # but that's OK.
+ elif a is int and (b in (bytes, str) or
+ isinstance(b, typehints.SequenceTypeConstraint)):
+ state.stack.append(b)
+ elif b is int and (a in (bytes, str) or
+ isinstance(a, typehints.SequenceTypeConstraint)):
+ state.stack.append(a)
+ elif a in _NUMERIC_PROMOTION_LADDER and b in _NUMERIC_PROMOTION_LADDER:
+ state.stack.append(
+ _NUMERIC_PROMOTION_LADDER[max(
+ _NUMERIC_PROMOTION_LADDER.index(a),
+ _NUMERIC_PROMOTION_LADDER.index(b))])
else:
state.stack.append(Any)
@@ -155,12 +183,8 @@ binary_divide = inplace_divide = symmetric_binary_op
binary_floor_divide = inplace_floor_divide = symmetric_binary_op
-def binary_true_divide(state, unused_arg):
- u = union(state.stack.pop(), state.stack.pop)
- if u == int:
- state.stack.append(float)
- else:
- state.stack.append(u)
+def binary_true_divide(state, arg):
+ return symmetric_binary_op(state, arg, True)
inplace_true_divide = binary_true_divide
diff --git a/sdks/python/apache_beam/typehints/trivial_inference_test.py
b/sdks/python/apache_beam/typehints/trivial_inference_test.py
index 9cea6d5794d..c8b59c7ccbf 100644
--- a/sdks/python/apache_beam/typehints/trivial_inference_test.py
+++ b/sdks/python/apache_beam/typehints/trivial_inference_test.py
@@ -244,6 +244,22 @@ class TrivialInferenceTest(unittest.TestCase):
lambda a,
b: a + b, [typehints.List[int], typehints.List[str]])
+ def testBinOpPromotion(self):
+ self.assertReturnType(int, lambda a, b: a + b, [int, bool])
+ self.assertReturnType(float, lambda a, b: a + b, [int, float])
+ self.assertReturnType(complex, lambda a, b: a + b, [int, complex])
+
+ def testBinOpSequenceMul(self):
+ self.assertReturnType(str, lambda a, b: a * b, [int, str])
+ self.assertReturnType(bytes, lambda a, b: a * b, [bytes, int])
+ self.assertReturnType(
+ typehints.List[float], lambda a, b: a * b, [int,
typehints.List[float]])
+
+ def testDiv(self):
+ # We only support Python 3 now.
+ self.assertReturnType(float, lambda a, b: a / b, [int, int])
+ self.assertReturnType(int, lambda a, b: a // b, [int, int])
+
def testCall(self):
f = lambda x, *args: x
self.assertReturnType(