From b0f57ab136ae31688b39c8ceab90c25c2d18e21c Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 12 Aug 2021 17:39:32 -0400 Subject: [PATCH 01/16] mypyc: cherry picked wip from a year ago --- mypyc/codegen/emitmodule.py | 6 ++++-- mypyc/codegen/literals.py | 38 +++++++++++++++++++++++++++++++++---- mypyc/ir/ops.py | 4 ++-- mypyc/irbuild/expression.py | 34 +++++++++++++++++++++++++++++++++ mypyc/lib-rt/CPy.h | 3 ++- mypyc/lib-rt/misc_ops.c | 19 ++++++++++++++++++- mypyc/primitives/set_ops.py | 2 +- 7 files changed, 95 insertions(+), 11 deletions(-) diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index 005c0f764e9a..b849f94e063c 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -668,6 +668,9 @@ def generate_literal_tables(self) -> None: # Descriptions of tuple literals init_tuple = c_array_initializer(literals.encoded_tuple_values()) self.declare_global("const int []", "CPyLit_Tuple", initializer=init_tuple) + # Descriptions of frozenset literals + init_frozenset = c_array_initializer(literals.encoded_frozenset_values()) + self.declare_global('const int []', 'CPyLit_FrozenSet', initializer=init_frozenset) def generate_export_table(self, decl_emitter: Emitter, code_emitter: Emitter) -> None: """Generate the declaration and definition of the group's export struct. @@ -837,8 +840,7 @@ def generate_globals_init(self, emitter: Emitter) -> None: emitter.emit_line("CPy_Init();") for symbol, fixup in self.simple_inits: emitter.emit_line(f"{symbol} = {fixup};") - - values = "CPyLit_Str, CPyLit_Bytes, CPyLit_Int, CPyLit_Float, CPyLit_Complex, CPyLit_Tuple" + values = "CPyLit_Str, CPyLit_Bytes, CPyLit_Int, CPyLit_Float, CPyLit_Complex, CPyLit_Tuple, CPyLit_FrozenSet" emitter.emit_lines( f"if (CPyStatics_Initialize(CPyStatics, {values}) < 0) {{", "return -1;", "}" ) diff --git a/mypyc/codegen/literals.py b/mypyc/codegen/literals.py index 29957d52101c..de84d5f5bef9 100644 --- a/mypyc/codegen/literals.py +++ b/mypyc/codegen/literals.py @@ -1,12 +1,12 @@ from __future__ import annotations -from typing import Any, Tuple, Union, cast +from typing import Any, FrozenSet, List, Tuple, Union, cast + from typing_extensions import Final -# Supported Python literal types. All tuple items must have supported +# Supported Python literal types. All tuple / frozenset items must have supported # literal types as well, but we can't represent the type precisely. -LiteralValue = Union[str, bytes, int, bool, float, complex, Tuple[object, ...], None] - +LiteralValue = Union[str, bytes, int, bool, float, complex, Tuple[object, ...], FrozenSet[object], None] # Some literals are singletons and handled specially (None, False and True) NUM_SINGLETONS: Final = 3 @@ -23,6 +23,7 @@ def __init__(self) -> None: self.float_literals: dict[float, int] = {} self.complex_literals: dict[complex, int] = {} self.tuple_literals: dict[tuple[object, ...], int] = {} + self.frozenset_literals: dict[frozenset[object], int] = {} def record_literal(self, value: LiteralValue) -> None: """Ensure that the literal value is available in generated code.""" @@ -55,6 +56,12 @@ def record_literal(self, value: LiteralValue) -> None: for item in value: self.record_literal(cast(Any, item)) tuple_literals[value] = len(tuple_literals) + elif isinstance(value, frozenset): + frozenset_literals = self.frozenset_literals + if value not in frozenset_literals: + for item in value: + self.record_literal(cast(Any, item)) + frozenset_literals[value] = len(frozenset_literals) else: assert False, "invalid literal: %r" % value @@ -86,6 +93,9 @@ def literal_index(self, value: LiteralValue) -> int: n += len(self.complex_literals) if isinstance(value, tuple): return n + self.tuple_literals[value] + n += len(self.tuple_literals) + if isinstance(value, frozenset): + return n + self.frozenset_literals[value] assert False, "invalid literal: %r" % value def num_literals(self) -> int: @@ -98,6 +108,7 @@ def num_literals(self) -> int: + len(self.float_literals) + len(self.complex_literals) + len(self.tuple_literals) + + len(self.frozenset_literals) ) # The following methods return the C encodings of literal values @@ -144,6 +155,25 @@ def encoded_tuple_values(self) -> list[str]: result.append(str(index)) return result + def encoded_frozenset_values(self) -> List[str]: + """Encode frozenset values into a C array. + + The format used here is identical to one used by tuples. + """ + values = self.frozenset_literals + value_by_index = {index: value for value, index in values.items()} + set_count = len(values) + result = [] + result.append(str(set_count)) + for i in range(set_count): + value = value_by_index[i] + result.append(str(len(value))) + for item in value: + index = self.literal_index(cast(Any, item)) + result.append(str(index)) + + return result + def _encode_str_values(values: dict[str, int]) -> list[bytes]: value_by_index = {index: value for value, index in values.items()} diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 56a1e6103acf..5b6d4d25641d 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -589,7 +589,7 @@ class LoadLiteral(RegisterOp): This is used to load a static PyObject * value corresponding to a literal of one of the supported types. - Tuple literals must contain only valid literal values as items. + Tuple / frozenset literals must contain only valid literal values as items. NOTE: You can use this to load boxed (Python) int objects. Use Integer to load unboxed, tagged integers or fixed-width, @@ -606,7 +606,7 @@ class LoadLiteral(RegisterOp): def __init__( self, - value: None | str | bytes | bool | int | float | complex | tuple[object, ...], + value: None | str | bytes | bool | int | float | complex | tuple[object, ...] | frozenset[object], rtype: RType, ) -> None: self.value = value diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index f6d488ccac42..190ce8e9a8f9 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -53,6 +53,7 @@ Assign, BasicBlock, LoadAddress, + LoadLiteral, RaiseStandardError, Register, TupleGet, @@ -61,6 +62,7 @@ ) from mypyc.ir.rtypes import ( RTuple, + bool_rprimitive, int_rprimitive, is_int_rprimitive, is_list_rprimitive, @@ -668,6 +670,38 @@ def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value: right = builder.accept(right_expr, can_borrow=True) return builder.compare_tagged(left, right, first_op, e.line) + # x in {...} + # x not in {...} + if (e.operators[0] in ['in', 'not in'] + and len(e.operators) == 1 + and isinstance(e.operands[1], SetExpr)): + negated = e.operators[0] == 'not in' + items = e.operands[1].items + if items: + # Precalculate a frozenset at module top level and use that instead + # of always rebuilding the set literal every evaluation. + # NOTE: this only supports set literals which only contain items + # which can be literals and aren't containers or sequences. + literal_safe = True + literal_values: List[Union[str, complex, bytes, int, float, None]] = [] + for item in items: + if isinstance(item, NameExpr) and item.name == 'None': + literal_safe = literal_safe and True + literal_values.append(None) + elif isinstance(item, (BytesExpr, StrExpr, IntExpr, FloatExpr, ComplexExpr)): + literal_safe = literal_safe and True + literal_values.append(item.value) + else: + literal_safe = False + if literal_safe: + left_operand = builder.accept(e.operands[0]) + set_literal = builder.add(LoadLiteral(frozenset(literal_values), set_rprimitive)) + value = builder.builder.call_c(set_in_op, + [left_operand, set_literal], e.line, bool_rprimitive) + if negated: + value = builder.unary_op(value, 'not', e.line) + return value + # TODO: Don't produce an expression when used in conditional context # All of the trickiness here is due to support for chained conditionals # (`e1 < e2 > e3`, etc). `e1 < e2 > e3` is approximately equivalent to diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index cffbbb3e1666..d9182b8bceaa 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -600,7 +600,8 @@ int CPyStatics_Initialize(PyObject **statics, const char * const *ints, const double *floats, const double *complex_numbers, - const int *tuples); + const int *tuples, + const int *frozensets); PyObject *CPy_Super(PyObject *builtins, PyObject *self); PyObject *CPy_CallReverseOpMethod(PyObject *left, PyObject *right, const char *op, _Py_Identifier *method); diff --git a/mypyc/lib-rt/misc_ops.c b/mypyc/lib-rt/misc_ops.c index 90292ce61073..dcfdb856d9c6 100644 --- a/mypyc/lib-rt/misc_ops.c +++ b/mypyc/lib-rt/misc_ops.c @@ -529,7 +529,8 @@ int CPyStatics_Initialize(PyObject **statics, const char * const *ints, const double *floats, const double *complex_numbers, - const int *tuples) { + const int *tuples, + const int *frozensets) { PyObject **result = statics; // Start with some hard-coded values *result++ = Py_None; @@ -629,6 +630,22 @@ int CPyStatics_Initialize(PyObject **statics, *result++ = obj; } } + if (frozensets) { + int num = *frozensets++; + while (num-- > 0) { + int num_items = *frozensets++; + PyObject *obj = PyFrozenSet_New(NULL); + if (obj == NULL) { + return -1; + } + for (int i = 0; i < num_items; i++) { + PyObject *item = statics[*frozensets++]; + Py_INCREF(item); + PySet_Add(obj, item); + } + *result++ = obj; + } + } return 0; } diff --git a/mypyc/primitives/set_ops.py b/mypyc/primitives/set_ops.py index 801fdad34ea4..fcfb7847dc7d 100644 --- a/mypyc/primitives/set_ops.py +++ b/mypyc/primitives/set_ops.py @@ -54,7 +54,7 @@ ) # item in set -binary_op( +set_in_op = binary_op( name="in", arg_types=[object_rprimitive, set_rprimitive], return_type=c_int_rprimitive, From c8d2ddaddd64c3142d20a7728b72fae781b1c78b Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Wed, 24 Aug 2022 15:08:24 -0400 Subject: [PATCH 02/16] [wip] [mypyc] Precompute set literals for in ops --- mypyc/analysis/ircheck.py | 14 +++++ mypyc/codegen/emitmodule.py | 2 +- mypyc/codegen/literals.py | 56 ++++++++----------- mypyc/ir/ops.py | 10 +++- mypyc/irbuild/expression.py | 96 +++++++++++++++++++++----------- mypyc/test-data/irbuild-set.test | 18 ++++++ 6 files changed, 127 insertions(+), 69 deletions(-) diff --git a/mypyc/analysis/ircheck.py b/mypyc/analysis/ircheck.py index c2cdd073f62e..b960e4a62e39 100644 --- a/mypyc/analysis/ircheck.py +++ b/mypyc/analysis/ircheck.py @@ -248,6 +248,15 @@ def check_tuple_items_valid_literals(self, op: LoadLiteral, t: tuple[object, ... if isinstance(x, tuple): self.check_tuple_items_valid_literals(op, x) + def check_frozenset_items_valid_literals( + self, op: LoadLiteral, s: frozenset[object, ...] + ) -> None: + for x in s: + if x is not None and not isinstance(x, (str, bytes, bool, int, float, complex, tuple)): + self.fail(op, f"Invalid type for item of frozenset literal: {type(x)})") + if isinstance(x, tuple): + self.check_tuple_items_valid_literals(op, x) + def visit_load_literal(self, op: LoadLiteral) -> None: expected_type = None if op.value is None: @@ -267,6 +276,11 @@ def visit_load_literal(self, op: LoadLiteral) -> None: elif isinstance(op.value, tuple): expected_type = "builtins.tuple" self.check_tuple_items_valid_literals(op, op.value) + elif isinstance(op.value, frozenset): + # There's no frozenset_rprimitive type since it'd be pretty useless so we just pretend + # it's a set (when it's really a frozenset). + expected_type = "builtins.set" + self.check_frozenset_items_valid_literals(op, op.value) assert expected_type is not None, "Missed a case for LoadLiteral check" diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index b849f94e063c..20402c4494ac 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -670,7 +670,7 @@ def generate_literal_tables(self) -> None: self.declare_global("const int []", "CPyLit_Tuple", initializer=init_tuple) # Descriptions of frozenset literals init_frozenset = c_array_initializer(literals.encoded_frozenset_values()) - self.declare_global('const int []', 'CPyLit_FrozenSet', initializer=init_frozenset) + self.declare_global("const int []", "CPyLit_FrozenSet", initializer=init_frozenset) def generate_export_table(self, decl_emitter: Emitter, code_emitter: Emitter) -> None: """Generate the declaration and definition of the group's export struct. diff --git a/mypyc/codegen/literals.py b/mypyc/codegen/literals.py index de84d5f5bef9..6beae5dcf075 100644 --- a/mypyc/codegen/literals.py +++ b/mypyc/codegen/literals.py @@ -6,7 +6,9 @@ # Supported Python literal types. All tuple / frozenset items must have supported # literal types as well, but we can't represent the type precisely. -LiteralValue = Union[str, bytes, int, bool, float, complex, Tuple[object, ...], FrozenSet[object], None] +LiteralValue = Union[ + str, bytes, int, bool, float, complex, Tuple[object, ...], FrozenSet[object], None +] # Some literals are singletons and handled specially (None, False and True) NUM_SINGLETONS: Final = 3 @@ -130,48 +132,36 @@ def encoded_complex_values(self) -> list[str]: return _encode_complex_values(self.complex_literals) def encoded_tuple_values(self) -> list[str]: - """Encode tuple values into a C array. + return self._encode_collection_values(self.tuple_literals) + + def encoded_frozenset_values(self) -> List[str]: + return self._encode_collection_values(self.frozenset_literals) + + def _encode_collection_values( + self, values: dict[tuple[object, ...], int] | dict[frozenset[object], int] + ) -> list[str]: + """Encode tuple/frozen values into a C array. The format of the result is like this: - - + + ... - + ... """ - values = self.tuple_literals value_by_index = {index: value for value, index in values.items()} result = [] - num = len(values) - result.append(str(num)) - for i in range(num): - value = value_by_index[i] - result.append(str(len(value))) - for item in value: - index = self.literal_index(cast(Any, item)) - result.append(str(index)) - return result - - def encoded_frozenset_values(self) -> List[str]: - """Encode frozenset values into a C array. - - The format used here is identical to one used by tuples. - """ - values = self.frozenset_literals - value_by_index = {index: value for value, index in values.items()} - set_count = len(values) - result = [] - result.append(str(set_count)) - for i in range(set_count): - value = value_by_index[i] - result.append(str(len(value))) - for item in value: - index = self.literal_index(cast(Any, item)) - result.append(str(index)) - + count = len(values) + result.append(str(count)) + for i in range(count): + value = value_by_index[i] + result.append(str(len(value))) + for item in value: + index = self.literal_index(cast(Any, item)) + result.append(str(index)) return result diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 5b6d4d25641d..22eb32e53ce0 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -606,7 +606,15 @@ class LoadLiteral(RegisterOp): def __init__( self, - value: None | str | bytes | bool | int | float | complex | tuple[object, ...] | frozenset[object], + value: None + | str + | bytes + | bool + | int + | float + | complex + | tuple[object, ...] + | frozenset[object], rtype: RType, ) -> None: self.value = value diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index 190ce8e9a8f9..575fd566e896 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Callable, cast +from typing import Callable, Sequence, Tuple, Union, cast from mypy.nodes import ( ARG_POS, @@ -68,6 +68,7 @@ is_list_rprimitive, is_none_rprimitive, object_rprimitive, + set_rprimitive, ) from mypyc.irbuild.ast_helpers import is_borrow_friendly_expr, process_conditional from mypyc.irbuild.builder import IRBuilder, int_borrow_friendly_op @@ -92,7 +93,7 @@ from mypyc.primitives.list_ops import list_append_op, list_extend_op, list_slice_op from mypyc.primitives.misc_ops import ellipsis_op, get_module_dict_op, new_slice_op, type_op from mypyc.primitives.registry import CFunctionDescription, builtin_names -from mypyc.primitives.set_ops import set_add_op, set_update_op +from mypyc.primitives.set_ops import set_add_op, set_in_op, set_update_op from mypyc.primitives.str_ops import str_slice_op from mypyc.primitives.tuple_ops import list_tuple_op, tuple_slice_op @@ -600,6 +601,48 @@ def transform_conditional_expr(builder: IRBuilder, expr: ConditionalExpr) -> Val return target +def precompute_set_literal(builder: IRBuilder, s: SetExpr) -> Value | None: + """Try to pre-compute a frozenset literal during module initialization. + + Return None if it's not possible. + + Only references to "simple" final variables, tuple literals (with items that + are themselves supported), and other non-container literals are supported. + """ + SupportedValue = Union[str, bytes, bool, int, float, complex, Tuple[object, ...], None] + + def _set_literal_final_values(items: Sequence[Expression]) -> list[SupportedValue] | None: + values: list[SupportedValue] = [] + for item in items: + if isinstance(item, NameExpr) and item.name in ("None", "True", "False"): + if item.name == "None": + values.append(None) + elif item.name == "True": + values.append(True) + elif item.name == "False": + values.append(False) + elif isinstance(item, (NameExpr, MemberExpr)) and isinstance(item.node, Var): + if item.node.is_final: + v = item.node.final_value + if isinstance(v, (str, int, float, bool)): + values.append(v) + elif isinstance(item, (StrExpr, BytesExpr, IntExpr, FloatExpr, ComplexExpr)): + values.append(item.value) + elif isinstance(item, TupleExpr): + t = _set_literal_final_values(item.items) + if t is not None: + values.append(tuple(t)) + + if len(values) != len(items): + return None + return values + + values = _set_literal_final_values(s.items) + if values is not None: + return builder.add(LoadLiteral(frozenset(values), set_rprimitive)) + return None + + def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value: # x in (...)/[...] # x not in (...)/[...] @@ -653,6 +696,23 @@ def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value: else: return builder.true() + # x in {...} + # x not in {...} + if ( + first_op in ("in", "not in") + and len(e.operators) == 1 + and isinstance(e.operands[1], SetExpr) + ): + literal = precompute_set_literal(builder, e.operands[1]) + if literal is not None: + lhs = e.operands[0] + v = builder.builder.call_c( + set_in_op, [builder.accept(lhs), literal], e.line, bool_rprimitive + ) + if first_op == "not in": + return builder.unary_op(v, "not", e.line) + return v + if len(e.operators) == 1: # Special some common simple cases if first_op in ("is", "is not"): @@ -670,38 +730,6 @@ def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value: right = builder.accept(right_expr, can_borrow=True) return builder.compare_tagged(left, right, first_op, e.line) - # x in {...} - # x not in {...} - if (e.operators[0] in ['in', 'not in'] - and len(e.operators) == 1 - and isinstance(e.operands[1], SetExpr)): - negated = e.operators[0] == 'not in' - items = e.operands[1].items - if items: - # Precalculate a frozenset at module top level and use that instead - # of always rebuilding the set literal every evaluation. - # NOTE: this only supports set literals which only contain items - # which can be literals and aren't containers or sequences. - literal_safe = True - literal_values: List[Union[str, complex, bytes, int, float, None]] = [] - for item in items: - if isinstance(item, NameExpr) and item.name == 'None': - literal_safe = literal_safe and True - literal_values.append(None) - elif isinstance(item, (BytesExpr, StrExpr, IntExpr, FloatExpr, ComplexExpr)): - literal_safe = literal_safe and True - literal_values.append(item.value) - else: - literal_safe = False - if literal_safe: - left_operand = builder.accept(e.operands[0]) - set_literal = builder.add(LoadLiteral(frozenset(literal_values), set_rprimitive)) - value = builder.builder.call_c(set_in_op, - [left_operand, set_literal], e.line, bool_rprimitive) - if negated: - value = builder.unary_op(value, 'not', e.line) - return value - # TODO: Don't produce an expression when used in conditional context # All of the trickiness here is due to support for chained conditionals # (`e1 < e2 > e3`, etc). `e1 < e2 > e3` is approximately equivalent to diff --git a/mypyc/test-data/irbuild-set.test b/mypyc/test-data/irbuild-set.test index fec76751c915..8d41bc3665fe 100644 --- a/mypyc/test-data/irbuild-set.test +++ b/mypyc/test-data/irbuild-set.test @@ -655,3 +655,21 @@ L0: r12 = PySet_Add(r0, r11) r13 = r12 >= 0 :: signed return r0 + +[case testOperatorInLiteral] +def f(i: object) -> bool: + return i in {1, 2.7, "daylily", b"daylily", (None, (None,))} +[out] +def f(i): + i :: object + r0 :: set + r1 :: int32 + r2 :: bit + r3 :: bool +L0: + r0 = frozenset({1, 2.7, (None, (None,)), 'daylily'}) + r1 = PySet_Contains(r0, i) + r2 = r1 >= 0 :: signed + r3 = truncate r1: int32 to builtins.bool + return r3 + From 779a59819534097a638ca4c02d1bdb431de864ad Mon Sep 17 00:00:00 2001 From: Richard Si Date: Fri, 6 Jan 2023 12:56:48 -0500 Subject: [PATCH 03/16] Stablize pprint of frozenset literals --- mypyc/ir/pprint.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mypyc/ir/pprint.py b/mypyc/ir/pprint.py index 0ef555f86738..6f3da893baeb 100644 --- a/mypyc/ir/pprint.py +++ b/mypyc/ir/pprint.py @@ -106,7 +106,13 @@ def visit_load_literal(self, op: LoadLiteral) -> str: # it explicit that this is a Python object. if isinstance(op.value, int): prefix = "object " - return self.format("%r = %s%s", op, prefix, repr(op.value)) + + rvalue = repr(op.value) + if isinstance(op.value, frozenset): + # Pretty print frozensets in a hacky but stable way. + sorted_items = sorted(op.value, key=str) + rvalue = "frozenset({" + repr(sorted_items)[1:-1] + "})" + return self.format("%r = %s%s", op, prefix, rvalue) def visit_get_attr(self, op: GetAttr) -> str: return self.format("%r = %s%r.%s", op, self.borrow_prefix(op), op.obj, op.attr) From 7fabcfe64ddf87d015ec2b9a3c3e2bc325a4baad Mon Sep 17 00:00:00 2001 From: Richard Si Date: Fri, 6 Jan 2023 13:15:23 -0500 Subject: [PATCH 04/16] Fix byte literals & clean up precompute_set_literal() --- mypy/nodes.py | 5 ++++ mypyc/irbuild/expression.py | 58 ++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 2b32d5f4f25c..4fe672001c2c 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2,6 +2,7 @@ from __future__ import annotations +import ast import os from abc import abstractmethod from collections import defaultdict @@ -1616,6 +1617,10 @@ def __init__(self, value: str) -> None: def accept(self, visitor: ExpressionVisitor[T]) -> T: return visitor.visit_bytes_expr(self) + @property + def true_value(self) -> bytes: + return ast.literal_eval("b" + repr(self.value)) + class FloatExpr(Expression): """Float literal""" diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index 575fd566e896..a109567a664b 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -601,6 +601,33 @@ def transform_conditional_expr(builder: IRBuilder, expr: ConditionalExpr) -> Val return target +def set_literal_values(items: Sequence[Expression]) -> list[object] | None: + values: list[object] = [] + for item in items: + if isinstance(item, NameExpr) and item.name in ("None", "True", "False"): + if item.name == "None": + values.append(None) + elif item.name == "True": + values.append(True) + elif item.name == "False": + values.append(False) + elif isinstance(item, (NameExpr, MemberExpr)) and isinstance(item.node, Var): + if item.node.is_final: + v = item.node.final_value + if isinstance(v, (str, int, float, bool)): + values.append(v) + elif isinstance(item, (StrExpr, BytesExpr, IntExpr, FloatExpr, ComplexExpr)): + values.append(item.true_value if isinstance(item, BytesExpr) else item.value) + elif isinstance(item, TupleExpr): + t = set_literal_values(item.items) + if t is not None: + values.append(tuple(t)) + + if len(values) != len(items): + return None + return values + + def precompute_set_literal(builder: IRBuilder, s: SetExpr) -> Value | None: """Try to pre-compute a frozenset literal during module initialization. @@ -609,37 +636,10 @@ def precompute_set_literal(builder: IRBuilder, s: SetExpr) -> Value | None: Only references to "simple" final variables, tuple literals (with items that are themselves supported), and other non-container literals are supported. """ - SupportedValue = Union[str, bytes, bool, int, float, complex, Tuple[object, ...], None] - - def _set_literal_final_values(items: Sequence[Expression]) -> list[SupportedValue] | None: - values: list[SupportedValue] = [] - for item in items: - if isinstance(item, NameExpr) and item.name in ("None", "True", "False"): - if item.name == "None": - values.append(None) - elif item.name == "True": - values.append(True) - elif item.name == "False": - values.append(False) - elif isinstance(item, (NameExpr, MemberExpr)) and isinstance(item.node, Var): - if item.node.is_final: - v = item.node.final_value - if isinstance(v, (str, int, float, bool)): - values.append(v) - elif isinstance(item, (StrExpr, BytesExpr, IntExpr, FloatExpr, ComplexExpr)): - values.append(item.value) - elif isinstance(item, TupleExpr): - t = _set_literal_final_values(item.items) - if t is not None: - values.append(tuple(t)) - - if len(values) != len(items): - return None - return values - - values = _set_literal_final_values(s.items) + values = set_literal_values(s.items) if values is not None: return builder.add(LoadLiteral(frozenset(values), set_rprimitive)) + return None From 97faab57e6dcce7db1d6531dfa3dd8cafb690321 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Fri, 6 Jan 2023 13:15:58 -0500 Subject: [PATCH 05/16] [wip] more tests --- mypyc/codegen/emitmodule.py | 1 + mypyc/codegen/literals.py | 2 +- mypyc/test-data/irbuild-set.test | 77 ++++++++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index 20402c4494ac..38b9daec028d 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -840,6 +840,7 @@ def generate_globals_init(self, emitter: Emitter) -> None: emitter.emit_line("CPy_Init();") for symbol, fixup in self.simple_inits: emitter.emit_line(f"{symbol} = {fixup};") + values = "CPyLit_Str, CPyLit_Bytes, CPyLit_Int, CPyLit_Float, CPyLit_Complex, CPyLit_Tuple, CPyLit_FrozenSet" emitter.emit_lines( f"if (CPyStatics_Initialize(CPyStatics, {values}) < 0) {{", "return -1;", "}" diff --git a/mypyc/codegen/literals.py b/mypyc/codegen/literals.py index 6beae5dcf075..3d235b19bcc8 100644 --- a/mypyc/codegen/literals.py +++ b/mypyc/codegen/literals.py @@ -140,7 +140,7 @@ def encoded_frozenset_values(self) -> List[str]: def _encode_collection_values( self, values: dict[tuple[object, ...], int] | dict[frozenset[object], int] ) -> list[str]: - """Encode tuple/frozen values into a C array. + """Encode tuple/frozenset values into a C array. The format of the result is like this: diff --git a/mypyc/test-data/irbuild-set.test b/mypyc/test-data/irbuild-set.test index 8d41bc3665fe..062ecfb1ed39 100644 --- a/mypyc/test-data/irbuild-set.test +++ b/mypyc/test-data/irbuild-set.test @@ -657,19 +657,88 @@ L0: return r0 [case testOperatorInLiteral] -def f(i: object) -> bool: - return i in {1, 2.7, "daylily", b"daylily", (None, (None,))} +from typing import Final + +CONST: Final = 1 +non_const = 10 + +def precomputed(i: object) -> bool: + return i in {1, 2.7, 1j, "daylily", b"daylily", CONST, (None, (None,))} +def not_precomputed(i: int) -> bool: + return i in {non_const} +def not_precomputed2(i: int) -> bool: + return i in {{1}, 2} [out] -def f(i): +def precomputed(i): i :: object r0 :: set r1 :: int32 r2 :: bit r3 :: bool L0: - r0 = frozenset({1, 2.7, (None, (None,)), 'daylily'}) + r0 = frozenset({(None, (None,)), 1, 1j, 2.7, b'daylily', 'daylily'}) r1 = PySet_Contains(r0, i) r2 = r1 >= 0 :: signed r3 = truncate r1: int32 to builtins.bool return r3 +def not_precomputed(i): + i :: int + r0 :: dict + r1 :: str + r2 :: object + r3 :: int + r4 :: set + r5 :: object + r6 :: int32 + r7 :: bit + r8 :: object + r9 :: int32 + r10 :: bit + r11 :: bool +L0: + r0 = __main__.globals :: static + r1 = 'non_const' + r2 = CPyDict_GetItem(r0, r1) + r3 = unbox(int, r2) + r4 = PySet_New(0) + r5 = box(int, r3) + r6 = PySet_Add(r4, r5) + r7 = r6 >= 0 :: signed + r8 = box(int, i) + r9 = PySet_Contains(r4, r8) + r10 = r9 >= 0 :: signed + r11 = truncate r9: int32 to builtins.bool + return r11 +def not_precomputed2(i): + i :: int + r0 :: set + r1 :: object + r2 :: int32 + r3 :: bit + r4 :: set + r5 :: int32 + r6 :: bit + r7 :: object + r8 :: int32 + r9 :: bit + r10 :: object + r11 :: int32 + r12 :: bit + r13 :: bool +L0: + r0 = PySet_New(0) + r1 = object 1 + r2 = PySet_Add(r0, r1) + r3 = r2 >= 0 :: signed + r4 = PySet_New(0) + r5 = PySet_Add(r4, r0) + r6 = r5 >= 0 :: signed + r7 = object 2 + r8 = PySet_Add(r4, r7) + r9 = r8 >= 0 :: signed + r10 = box(int, i) + r11 = PySet_Contains(r4, r10) + r12 = r11 >= 0 :: signed + r13 = truncate r11: int32 to builtins.bool + return r13 From 3ccf95121502e2bf55c4c764903a9b84fbdd1278 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Fri, 6 Jan 2023 17:20:36 -0500 Subject: [PATCH 06/16] Update irbuild and add run tests --- mypyc/test-data/irbuild-set.test | 38 +++++++++++++++++++++++++------- mypyc/test-data/run-sets.test | 27 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/mypyc/test-data/irbuild-set.test b/mypyc/test-data/irbuild-set.test index 062ecfb1ed39..d8fa5c892c4d 100644 --- a/mypyc/test-data/irbuild-set.test +++ b/mypyc/test-data/irbuild-set.test @@ -659,15 +659,18 @@ L0: [case testOperatorInLiteral] from typing import Final -CONST: Final = 1 +CONST: Final = "daylily" non_const = 10 def precomputed(i: object) -> bool: - return i in {1, 2.7, 1j, "daylily", b"daylily", CONST, (None, (None,))} -def not_precomputed(i: int) -> bool: + return i in {1, 2.7, 1j, "foo", b"bar", CONST, (None, (27,))} +def not_precomputed_non_final_name(i: int) -> bool: return i in {non_const} -def not_precomputed2(i: int) -> bool: +def not_precomputed_nested_set(i: int) -> bool: return i in {{1}, 2} +def not_precomputed_add_operation(i: int) -> bool: + # TODO: support this? + return i in {1 + 2} [out] def precomputed(i): i :: object @@ -676,12 +679,12 @@ def precomputed(i): r2 :: bit r3 :: bool L0: - r0 = frozenset({(None, (None,)), 1, 1j, 2.7, b'daylily', 'daylily'}) + r0 = frozenset({(None, (27,)), 1, 1j, 2.7, b'bar', 'daylily', 'foo'}) r1 = PySet_Contains(r0, i) r2 = r1 >= 0 :: signed r3 = truncate r1: int32 to builtins.bool return r3 -def not_precomputed(i): +def not_precomputed_non_final_name(i): i :: int r0 :: dict r1 :: str @@ -709,7 +712,7 @@ L0: r10 = r9 >= 0 :: signed r11 = truncate r9: int32 to builtins.bool return r11 -def not_precomputed2(i): +def not_precomputed_nested_set(i): i :: int r0 :: set r1 :: object @@ -741,4 +744,23 @@ L0: r12 = r11 >= 0 :: signed r13 = truncate r11: int32 to builtins.bool return r13 - +def not_precomputed_add_operation(i): + i :: int + r0 :: set + r1 :: object + r2 :: int32 + r3 :: bit + r4 :: object + r5 :: int32 + r6 :: bit + r7 :: bool +L0: + r0 = PySet_New(0) + r1 = object 3 + r2 = PySet_Add(r0, r1) + r3 = r2 >= 0 :: signed + r4 = box(int, i) + r5 = PySet_Contains(r0, r4) + r6 = r5 >= 0 :: signed + r7 = truncate r5: int32 to builtins.bool + return r7 diff --git a/mypyc/test-data/run-sets.test b/mypyc/test-data/run-sets.test index 98ac92d569b7..85f1df51b7d7 100644 --- a/mypyc/test-data/run-sets.test +++ b/mypyc/test-data/run-sets.test @@ -115,3 +115,30 @@ from native import update s = {1, 2, 3} update(s, [5, 4, 3]) assert s == {1, 2, 3, 4, 5} + +[case testPrecomputedFrozensets] +from typing import Any, Final + +CONST: Final = "CONST" +non_const = "non_const" + +def main_set(item: Any) -> bool: + return item in {None, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST} + +def main_negated_set(item: Any) -> bool: + return item not in {None, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST} + +def non_final_name_set(item: Any) -> bool: + return item in {non_const} + +[file driver.py] +import native +from native import CONST, main_set, main_negated_set, non_final_name_set + +for item in (None, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST): + assert main_set(item), f"{item!r} should be in set_main" + assert not main_negated_set(item), item + +assert non_final_name_set(native.non_const) +native.non_const = "updated" +assert non_final_name_set("updated") From 0036e204b6558eff964bc3177cdf4c3e4378a85e Mon Sep 17 00:00:00 2001 From: Richard Si Date: Fri, 6 Jan 2023 18:26:38 -0500 Subject: [PATCH 07/16] Clean up patch & add boolean to tests --- mypyc/analysis/ircheck.py | 12 ++++---- mypyc/ir/ops.py | 15 ++-------- mypyc/ir/pprint.py | 11 +++++-- mypyc/irbuild/expression.py | 12 ++++---- mypyc/test-data/irbuild-set.test | 50 +++++++++++++++++--------------- mypyc/test-data/run-sets.test | 6 ++-- 6 files changed, 51 insertions(+), 55 deletions(-) diff --git a/mypyc/analysis/ircheck.py b/mypyc/analysis/ircheck.py index 487c979ecbd0..e96c640fa8a1 100644 --- a/mypyc/analysis/ircheck.py +++ b/mypyc/analysis/ircheck.py @@ -252,14 +252,14 @@ def check_tuple_items_valid_literals(self, op: LoadLiteral, t: tuple[object, ... if isinstance(x, tuple): self.check_tuple_items_valid_literals(op, x) - def check_frozenset_items_valid_literals( - self, op: LoadLiteral, s: frozenset[object, ...] - ) -> None: + def check_frozenset_items_valid_literals(self, op: LoadLiteral, s: frozenset[object]) -> None: for x in s: - if x is not None and not isinstance(x, (str, bytes, bool, int, float, complex, tuple)): - self.fail(op, f"Invalid type for item of frozenset literal: {type(x)})") - if isinstance(x, tuple): + if x is None or isinstance(x, (str, bytes, bool, int, float, complex)): + pass + elif isinstance(x, tuple): self.check_tuple_items_valid_literals(op, x) + else: + self.fail(op, f"Invalid type for item of frozenset literal: {type(x)})") def visit_load_literal(self, op: LoadLiteral) -> None: expected_type = None diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 09b51454b5bf..cc6f542c3e23 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -39,6 +39,7 @@ ) if TYPE_CHECKING: + from mypyc.codegen.literals import LiteralValue from mypyc.ir.class_ir import ClassIR from mypyc.ir.func_ir import FuncDecl, FuncIR @@ -603,19 +604,7 @@ class LoadLiteral(RegisterOp): error_kind = ERR_NEVER is_borrowed = True - def __init__( - self, - value: None - | str - | bytes - | bool - | int - | float - | complex - | tuple[object, ...] - | frozenset[object], - rtype: RType, - ) -> None: + def __init__(self, value: LiteralValue, rtype: RType) -> None: self.value = value self.type = rtype diff --git a/mypyc/ir/pprint.py b/mypyc/ir/pprint.py index 447b2468192f..cb9e4a2d2541 100644 --- a/mypyc/ir/pprint.py +++ b/mypyc/ir/pprint.py @@ -109,9 +109,14 @@ def visit_load_literal(self, op: LoadLiteral) -> str: rvalue = repr(op.value) if isinstance(op.value, frozenset): - # Pretty print frozensets in a hacky but stable way. - sorted_items = sorted(op.value, key=str) - rvalue = "frozenset({" + repr(sorted_items)[1:-1] + "})" + # We need to generate a string representation that won't vary + # run-to-run because sets are unordered, otherwise we may get + # spurious irbuild test failures. + # + # Sorting by the item's string representation is a bit of a + # hack, but it's stable and won't cause TypeErrors. + formatted_items = [repr(i) for i in sorted(op.value, key=str)] + rvalue = "frozenset({" + ", ".join(formatted_items) + "})" return self.format("%r = %s%s", op, prefix, rvalue) def visit_get_attr(self, op: GetAttr) -> str: diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index e499b0b2eb15..4e02c304cb02 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -718,15 +718,15 @@ def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value: and len(e.operators) == 1 and isinstance(e.operands[1], SetExpr) ): - literal = precompute_set_literal(builder, e.operands[1]) - if literal is not None: + set_literal = precompute_set_literal(builder, e.operands[1]) + if set_literal is not None: lhs = e.operands[0] - v = builder.builder.call_c( - set_in_op, [builder.accept(lhs), literal], e.line, bool_rprimitive + result = builder.builder.call_c( + set_in_op, [builder.accept(lhs), set_literal], e.line, bool_rprimitive ) if first_op == "not in": - return builder.unary_op(v, "not", e.line) - return v + return builder.unary_op(result, "not", e.line) + return result if len(e.operators) == 1: # Special some common simple cases diff --git a/mypyc/test-data/irbuild-set.test b/mypyc/test-data/irbuild-set.test index d8fa5c892c4d..036708869448 100644 --- a/mypyc/test-data/irbuild-set.test +++ b/mypyc/test-data/irbuild-set.test @@ -663,11 +663,11 @@ CONST: Final = "daylily" non_const = 10 def precomputed(i: object) -> bool: - return i in {1, 2.7, 1j, "foo", b"bar", CONST, (None, (27,))} + return i in {1, 2.7, 1j, "foo", b"bar", CONST, (None, (27,)), (), False} def not_precomputed_non_final_name(i: int) -> bool: return i in {non_const} def not_precomputed_nested_set(i: int) -> bool: - return i in {{1}, 2} + return i in {frozenset({1}), 2} def not_precomputed_add_operation(i: int) -> bool: # TODO: support this? return i in {1 + 2} @@ -679,7 +679,7 @@ def precomputed(i): r2 :: bit r3 :: bool L0: - r0 = frozenset({(None, (27,)), 1, 1j, 2.7, b'bar', 'daylily', 'foo'}) + r0 = frozenset({(), (None, (27,)), 1, 1j, 2.7, False, b'bar', 'daylily', 'foo'}) r1 = PySet_Contains(r0, i) r2 = r1 >= 0 :: signed r3 = truncate r1: int32 to builtins.bool @@ -718,32 +718,34 @@ def not_precomputed_nested_set(i): r1 :: object r2 :: int32 r3 :: bit - r4 :: set - r5 :: int32 - r6 :: bit - r7 :: object - r8 :: int32 - r9 :: bit - r10 :: object - r11 :: int32 - r12 :: bit - r13 :: bool + r4 :: object + r5 :: set + r6 :: int32 + r7 :: bit + r8 :: object + r9 :: int32 + r10 :: bit + r11 :: object + r12 :: int32 + r13 :: bit + r14 :: bool L0: r0 = PySet_New(0) r1 = object 1 r2 = PySet_Add(r0, r1) r3 = r2 >= 0 :: signed - r4 = PySet_New(0) - r5 = PySet_Add(r4, r0) - r6 = r5 >= 0 :: signed - r7 = object 2 - r8 = PySet_Add(r4, r7) - r9 = r8 >= 0 :: signed - r10 = box(int, i) - r11 = PySet_Contains(r4, r10) - r12 = r11 >= 0 :: signed - r13 = truncate r11: int32 to builtins.bool - return r13 + r4 = PyFrozenSet_New(r0) + r5 = PySet_New(0) + r6 = PySet_Add(r5, r4) + r7 = r6 >= 0 :: signed + r8 = object 2 + r9 = PySet_Add(r5, r8) + r10 = r9 >= 0 :: signed + r11 = box(int, i) + r12 = PySet_Contains(r5, r11) + r13 = r12 >= 0 :: signed + r14 = truncate r12: int32 to builtins.bool + return r14 def not_precomputed_add_operation(i): i :: int r0 :: set diff --git a/mypyc/test-data/run-sets.test b/mypyc/test-data/run-sets.test index 85f1df51b7d7..aa1057e57485 100644 --- a/mypyc/test-data/run-sets.test +++ b/mypyc/test-data/run-sets.test @@ -123,10 +123,10 @@ CONST: Final = "CONST" non_const = "non_const" def main_set(item: Any) -> bool: - return item in {None, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST} + return item in {None, False, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST} def main_negated_set(item: Any) -> bool: - return item not in {None, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST} + return item not in {None, False, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST} def non_final_name_set(item: Any) -> bool: return item in {non_const} @@ -135,7 +135,7 @@ def non_final_name_set(item: Any) -> bool: import native from native import CONST, main_set, main_negated_set, non_final_name_set -for item in (None, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST): +for item in (None, False, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST): assert main_set(item), f"{item!r} should be in set_main" assert not main_negated_set(item), item From e1527e2a5f9882100cee2af703e3d15ad777486c Mon Sep 17 00:00:00 2001 From: Richard Si Date: Fri, 6 Jan 2023 20:58:29 -0500 Subject: [PATCH 08/16] Optimize for ... in --- mypyc/irbuild/for_helpers.py | 15 +++- mypyc/test-data/irbuild-set.test | 121 ++++++++++++++++++++++++++++++- mypyc/test-data/run-sets.test | 6 ++ 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/mypyc/irbuild/for_helpers.py b/mypyc/irbuild/for_helpers.py index fc67178af5de..61dbbe960eb2 100644 --- a/mypyc/irbuild/for_helpers.py +++ b/mypyc/irbuild/for_helpers.py @@ -17,6 +17,7 @@ Lvalue, MemberExpr, RefExpr, + SetExpr, TupleExpr, TypeAlias, ) @@ -469,12 +470,22 @@ def make_for_loop_generator( for_dict_gen.init(expr_reg, target_type) return for_dict_gen + iterable_expr_reg: Value | None = None + if isinstance(expr, SetExpr): + # Special case "for x in ". + from mypyc.irbuild.expression import precompute_set_literal + + set_literal = precompute_set_literal(builder, expr) + if set_literal is not None: + iterable_expr_reg = set_literal + # Default to a generic for loop. - expr_reg = builder.accept(expr) + if iterable_expr_reg is None: + iterable_expr_reg = builder.accept(expr) for_obj = ForIterable(builder, index, body_block, loop_exit, line, nested) item_type = builder._analyze_iterable_item_type(expr) item_rtype = builder.type_to_rtype(item_type) - for_obj.init(expr_reg, item_rtype) + for_obj.init(iterable_expr_reg, item_rtype) return for_obj diff --git a/mypyc/test-data/irbuild-set.test b/mypyc/test-data/irbuild-set.test index 036708869448..f2c8bbdfb121 100644 --- a/mypyc/test-data/irbuild-set.test +++ b/mypyc/test-data/irbuild-set.test @@ -656,7 +656,7 @@ L0: r13 = r12 >= 0 :: signed return r0 -[case testOperatorInLiteral] +[case testOperatorInSetLiteral] from typing import Final CONST: Final = "daylily" @@ -766,3 +766,122 @@ L0: r6 = r5 >= 0 :: signed r7 = truncate r5: int32 to builtins.bool return r7 + +[case testForSetLiteral] +from typing import Final + +CONST: Final = 10 +non_const = 20 + +def precomputed() -> None: + for _ in {"None", "True", "False"}: + pass + +def precomputed2() -> None: + for _ in {None, False, 1, 2.0, "4", b"5", (6,), 7j, CONST}: + pass + +def not_precomputed() -> None: + for not_optimized in {1 + 1}: + pass + for not_optimized in {non_const}: + pass + +[out] +def precomputed(): + r0 :: set + r1, r2 :: object + r3 :: str + _ :: object + r4 :: bit +L0: + r0 = frozenset({'False', 'None', 'True'}) + r1 = PyObject_GetIter(r0) +L1: + r2 = PyIter_Next(r1) + if is_error(r2) goto L4 else goto L2 +L2: + r3 = cast(str, r2) + _ = r3 +L3: + goto L1 +L4: + r4 = CPy_NoErrOccured() +L5: + return 1 +def precomputed2(): + r0 :: set + r1, r2, _ :: object + r3 :: bit +L0: + r0 = frozenset({(6,), 1, 10, 2.0, '4', 7j, False, None, b'5'}) + r1 = PyObject_GetIter(r0) +L1: + r2 = PyIter_Next(r1) + if is_error(r2) goto L4 else goto L2 +L2: + _ = r2 +L3: + goto L1 +L4: + r3 = CPy_NoErrOccured() +L5: + return 1 +def not_precomputed(): + r0 :: set + r1 :: object + r2 :: int32 + r3 :: bit + r4, r5 :: object + r6, not_optimized :: int + r7 :: bit + r8 :: dict + r9 :: str + r10 :: object + r11 :: int + r12 :: set + r13 :: object + r14 :: int32 + r15 :: bit + r16, r17 :: object + r18 :: int + r19 :: bit +L0: + r0 = PySet_New(0) + r1 = object 2 + r2 = PySet_Add(r0, r1) + r3 = r2 >= 0 :: signed + r4 = PyObject_GetIter(r0) +L1: + r5 = PyIter_Next(r4) + if is_error(r5) goto L4 else goto L2 +L2: + r6 = unbox(int, r5) + not_optimized = r6 +L3: + goto L1 +L4: + r7 = CPy_NoErrOccured() +L5: + r8 = __main__.globals :: static + r9 = 'non_const' + r10 = CPyDict_GetItem(r8, r9) + r11 = unbox(int, r10) + r12 = PySet_New(0) + r13 = box(int, r11) + r14 = PySet_Add(r12, r13) + r15 = r14 >= 0 :: signed + r16 = PyObject_GetIter(r12) +L6: + r17 = PyIter_Next(r16) + if is_error(r17) goto L9 else goto L7 +L7: + r18 = unbox(int, r17) + not_optimized = r18 +L8: + goto L6 +L9: + r19 = CPy_NoErrOccured() +L10: + return 1 + diff --git a/mypyc/test-data/run-sets.test b/mypyc/test-data/run-sets.test index aa1057e57485..0975e16e2b04 100644 --- a/mypyc/test-data/run-sets.test +++ b/mypyc/test-data/run-sets.test @@ -131,6 +131,10 @@ def main_negated_set(item: Any) -> bool: def non_final_name_set(item: Any) -> bool: return item in {non_const} +s = set() +for i in {None, False, 1, 2.0, "3", b"4", 5j, (6,), CONST}: + s.add(i) + [file driver.py] import native from native import CONST, main_set, main_negated_set, non_final_name_set @@ -142,3 +146,5 @@ for item in (None, False, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST): assert non_final_name_set(native.non_const) native.non_const = "updated" assert non_final_name_set("updated") + +assert not native.s ^ {None, False, 1, 2.0, "3", b"4", 5j, (6,), CONST}, native.s From e3d3045f67b086744151354f9b3c7720b5a1678c Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 7 Jan 2023 14:22:54 -0500 Subject: [PATCH 09/16] Oh look, I found constant_fold_expr() constant_fold_expr() replaces my custom logic for string/integer literals and final references. It even supports folding constant expressions which means even {1 + 2} could be precomputed! This means final references to bool, float, bytes*, and complex values are no longer supported, but that's okay for now. In a later patch, we can add proper support for (some of) these types to constant_fold_expr(). *Also bytes are just in general really annoying to work with (because the true bytes value isn't stored anywhere, you have to convert the string representation to bytes instead... ugh) so I don't really care about final refs to bytes values/literals right now. Byte literals IN the sets are still supported. --- mypy/nodes.py | 5 -- mypyc/irbuild/builder.py | 5 +- mypyc/irbuild/expression.py | 38 ++++++----- mypyc/irbuild/util.py | 10 +++ mypyc/test-data/irbuild-set.test | 104 +++++++++---------------------- 5 files changed, 61 insertions(+), 101 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 940c5f4a065a..80ab787f4a9c 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2,7 +2,6 @@ from __future__ import annotations -import ast import os from abc import abstractmethod from collections import defaultdict @@ -1655,10 +1654,6 @@ def __init__(self, value: str) -> None: def accept(self, visitor: ExpressionVisitor[T]) -> T: return visitor.visit_bytes_expr(self) - @property - def true_value(self) -> bytes: - return ast.literal_eval("b" + repr(self.value)) - class FloatExpr(Expression): """Float literal""" diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index 792697970785..a67f3a1e32d5 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -118,7 +118,7 @@ AssignmentTargetRegister, AssignmentTargetTuple, ) -from mypyc.irbuild.util import is_constant +from mypyc.irbuild.util import bytes_from_str, is_constant from mypyc.options import CompilerOptions from mypyc.primitives.dict_ops import dict_get_item_op, dict_set_item_op from mypyc.primitives.generic_ops import iter_op, next_op, py_setattr_op @@ -296,8 +296,7 @@ def load_bytes_from_str_literal(self, value: str) -> Value: are stored in BytesExpr.value, whose type is 'str' not 'bytes'. Thus we perform a special conversion here. """ - bytes_value = bytes(value, "utf8").decode("unicode-escape").encode("raw-unicode-escape") - return self.builder.load_bytes(bytes_value) + return self.builder.load_bytes(bytes_from_str(value)) def load_int(self, value: int) -> Value: return self.builder.load_int(value) diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index 4e02c304cb02..d4e5a634dbac 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Callable, Sequence, Tuple, Union, cast +from typing import Callable, Sequence, cast from mypy.nodes import ( ARG_POS, @@ -89,6 +89,7 @@ tokenizer_printf_style, ) from mypyc.irbuild.specialize import apply_function_specialization, apply_method_specialization +from mypyc.irbuild.util import bytes_from_str from mypyc.primitives.bytes_ops import bytes_slice_op from mypyc.primitives.dict_ops import dict_get_item_op, dict_new_op, dict_set_item_op from mypyc.primitives.generic_ops import iter_op @@ -616,29 +617,32 @@ def transform_conditional_expr(builder: IRBuilder, expr: ConditionalExpr) -> Val return target -def set_literal_values(items: Sequence[Expression]) -> list[object] | None: +def set_literal_values(builder: IRBuilder, items: Sequence[Expression]) -> list[object] | None: values: list[object] = [] for item in items: - if isinstance(item, NameExpr) and item.name in ("None", "True", "False"): - if item.name == "None": + const_value = constant_fold_expr(builder, item) + if const_value is not None: + values.append(const_value) + continue + + if isinstance(item, RefExpr): + if item.fullname == "builtins.None": values.append(None) - elif item.name == "True": + elif item.fullname == "builtins.True": values.append(True) - elif item.name == "False": + elif item.fullname == "builtins.False": values.append(False) - elif isinstance(item, (NameExpr, MemberExpr)) and isinstance(item.node, Var): - if item.node.is_final: - v = item.node.final_value - if isinstance(v, (str, int, float, bool)): - values.append(v) - elif isinstance(item, (StrExpr, BytesExpr, IntExpr, FloatExpr, ComplexExpr)): - values.append(item.true_value if isinstance(item, BytesExpr) else item.value) + elif isinstance(item, (BytesExpr, FloatExpr, ComplexExpr)): + # constant_fold_expr() doesn't handle these (yet?) + v = bytes_from_str(item.value) if isinstance(item, BytesExpr) else item.value + values.append(v) elif isinstance(item, TupleExpr): - t = set_literal_values(item.items) - if t is not None: - values.append(tuple(t)) + tuple_values = set_literal_values(builder, item.items) + if tuple_values is not None: + values.append(tuple(tuple_values)) if len(values) != len(items): + # Bail if not all items can be converted into values. return None return values @@ -651,7 +655,7 @@ def precompute_set_literal(builder: IRBuilder, s: SetExpr) -> Value | None: Only references to "simple" final variables, tuple literals (with items that are themselves supported), and other non-container literals are supported. """ - values = set_literal_values(s.items) + values = set_literal_values(builder, s.items) if values is not None: return builder.add(LoadLiteral(frozenset(values), set_rprimitive)) diff --git a/mypyc/irbuild/util.py b/mypyc/irbuild/util.py index f50241b96cb3..ed01a59d1214 100644 --- a/mypyc/irbuild/util.py +++ b/mypyc/irbuild/util.py @@ -177,3 +177,13 @@ def is_constant(e: Expression) -> bool: ) ) ) + + +def bytes_from_str(value: str) -> bytes: + """Convert a string representing bytes into actual bytes. + + This is needed because the literal characters of BytesExpr (the + characters inside b'') are stored in BytesExpr.value, whose type is + 'str' not 'bytes'. + """ + return bytes(value, "utf8").decode("unicode-escape").encode("raw-unicode-escape") diff --git a/mypyc/test-data/irbuild-set.test b/mypyc/test-data/irbuild-set.test index f2c8bbdfb121..adb266b3224b 100644 --- a/mypyc/test-data/irbuild-set.test +++ b/mypyc/test-data/irbuild-set.test @@ -663,14 +663,11 @@ CONST: Final = "daylily" non_const = 10 def precomputed(i: object) -> bool: - return i in {1, 2.7, 1j, "foo", b"bar", CONST, (None, (27,)), (), False} + return i in {1, 2.0, 1 +2, 4j, "foo", b"bar", CONST, (None, (27,)), (), False} def not_precomputed_non_final_name(i: int) -> bool: return i in {non_const} def not_precomputed_nested_set(i: int) -> bool: return i in {frozenset({1}), 2} -def not_precomputed_add_operation(i: int) -> bool: - # TODO: support this? - return i in {1 + 2} [out] def precomputed(i): i :: object @@ -679,7 +676,7 @@ def precomputed(i): r2 :: bit r3 :: bool L0: - r0 = frozenset({(), (None, (27,)), 1, 1j, 2.7, False, b'bar', 'daylily', 'foo'}) + r0 = frozenset({(), (None, (27,)), 1, 2.0, 3, 4j, False, b'bar', 'daylily', 'foo'}) r1 = PySet_Contains(r0, i) r2 = r1 >= 0 :: signed r3 = truncate r1: int32 to builtins.bool @@ -746,26 +743,6 @@ L0: r13 = r12 >= 0 :: signed r14 = truncate r12: int32 to builtins.bool return r14 -def not_precomputed_add_operation(i): - i :: int - r0 :: set - r1 :: object - r2 :: int32 - r3 :: bit - r4 :: object - r5 :: int32 - r6 :: bit - r7 :: bool -L0: - r0 = PySet_New(0) - r1 = object 3 - r2 = PySet_Add(r0, r1) - r3 = r2 >= 0 :: signed - r4 = box(int, i) - r5 = PySet_Contains(r0, r4) - r6 = r5 >= 0 :: signed - r7 = truncate r5: int32 to builtins.bool - return r7 [case testForSetLiteral] from typing import Final @@ -778,12 +755,10 @@ def precomputed() -> None: pass def precomputed2() -> None: - for _ in {None, False, 1, 2.0, "4", b"5", (6,), 7j, CONST}: + for _ in {None, False, 1, 2.0, "4", b"5", (6,), 7j, CONST, CONST + 1}: pass def not_precomputed() -> None: - for not_optimized in {1 + 1}: - pass for not_optimized in {non_const}: pass @@ -814,7 +789,7 @@ def precomputed2(): r1, r2, _ :: object r3 :: bit L0: - r0 = frozenset({(6,), 1, 10, 2.0, '4', 7j, False, None, b'5'}) + r0 = frozenset({(6,), 1, 10, 11, 2.0, '4', 7j, False, None, b'5'}) r1 = PyObject_GetIter(r0) L1: r2 = PyIter_Next(r1) @@ -828,60 +803,37 @@ L4: L5: return 1 def not_precomputed(): - r0 :: set - r1 :: object - r2 :: int32 - r3 :: bit - r4, r5 :: object - r6, not_optimized :: int + r0 :: dict + r1 :: str + r2 :: object + r3 :: int + r4 :: set + r5 :: object + r6 :: int32 r7 :: bit - r8 :: dict - r9 :: str - r10 :: object - r11 :: int - r12 :: set - r13 :: object - r14 :: int32 - r15 :: bit - r16, r17 :: object - r18 :: int - r19 :: bit + r8, r9 :: object + r10, not_optimized :: int + r11 :: bit L0: - r0 = PySet_New(0) - r1 = object 2 - r2 = PySet_Add(r0, r1) - r3 = r2 >= 0 :: signed - r4 = PyObject_GetIter(r0) + r0 = __main__.globals :: static + r1 = 'non_const' + r2 = CPyDict_GetItem(r0, r1) + r3 = unbox(int, r2) + r4 = PySet_New(0) + r5 = box(int, r3) + r6 = PySet_Add(r4, r5) + r7 = r6 >= 0 :: signed + r8 = PyObject_GetIter(r4) L1: - r5 = PyIter_Next(r4) - if is_error(r5) goto L4 else goto L2 + r9 = PyIter_Next(r8) + if is_error(r9) goto L4 else goto L2 L2: - r6 = unbox(int, r5) - not_optimized = r6 + r10 = unbox(int, r9) + not_optimized = r10 L3: goto L1 L4: - r7 = CPy_NoErrOccured() + r11 = CPy_NoErrOccured() L5: - r8 = __main__.globals :: static - r9 = 'non_const' - r10 = CPyDict_GetItem(r8, r9) - r11 = unbox(int, r10) - r12 = PySet_New(0) - r13 = box(int, r11) - r14 = PySet_Add(r12, r13) - r15 = r14 >= 0 :: signed - r16 = PyObject_GetIter(r12) -L6: - r17 = PyIter_Next(r16) - if is_error(r17) goto L9 else goto L7 -L7: - r18 = unbox(int, r17) - not_optimized = r18 -L8: - goto L6 -L9: - r19 = CPy_NoErrOccured() -L10: return 1 From 8d68a7b9352a248299875830c00a79b780e7d50d Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 7 Jan 2023 14:41:20 -0500 Subject: [PATCH 10/16] Run flake8/isort --- mypyc/codegen/literals.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mypyc/codegen/literals.py b/mypyc/codegen/literals.py index 3d235b19bcc8..05884b754452 100644 --- a/mypyc/codegen/literals.py +++ b/mypyc/codegen/literals.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import Any, FrozenSet, List, Tuple, Union, cast - from typing_extensions import Final # Supported Python literal types. All tuple / frozenset items must have supported @@ -157,11 +156,11 @@ def _encode_collection_values( count = len(values) result.append(str(count)) for i in range(count): - value = value_by_index[i] - result.append(str(len(value))) - for item in value: - index = self.literal_index(cast(Any, item)) - result.append(str(index)) + value = value_by_index[i] + result.append(str(len(value))) + for item in value: + index = self.literal_index(cast(Any, item)) + result.append(str(index)) return result From 1b75b7de473dd8550f2e17b9c9f04034be6ecfcc Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 7 Jan 2023 15:46:59 -0500 Subject: [PATCH 11/16] I forgot Final is a 3.8+ feature >.< --- mypyc/test-data/irbuild-set.test | 4 ++-- mypyc/test-data/run-sets.test | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mypyc/test-data/irbuild-set.test b/mypyc/test-data/irbuild-set.test index adb266b3224b..c567422abac7 100644 --- a/mypyc/test-data/irbuild-set.test +++ b/mypyc/test-data/irbuild-set.test @@ -657,7 +657,7 @@ L0: return r0 [case testOperatorInSetLiteral] -from typing import Final +from typing_extensions import Final CONST: Final = "daylily" non_const = 10 @@ -745,7 +745,7 @@ L0: return r14 [case testForSetLiteral] -from typing import Final +from typing_extensions import Final CONST: Final = 10 non_const = 20 diff --git a/mypyc/test-data/run-sets.test b/mypyc/test-data/run-sets.test index 0975e16e2b04..586bbdb08305 100644 --- a/mypyc/test-data/run-sets.test +++ b/mypyc/test-data/run-sets.test @@ -117,7 +117,8 @@ update(s, [5, 4, 3]) assert s == {1, 2, 3, 4, 5} [case testPrecomputedFrozensets] -from typing import Any, Final +from typing import Any +from typing_extensions import Final CONST: Final = "CONST" non_const = "non_const" From 1a55fa4ae394671f40dd1426ca6ce62c8b8d52f3 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 7 Jan 2023 16:13:38 -0500 Subject: [PATCH 12/16] Work around mypyc bug ... --- mypyc/codegen/literals.py | 4 ++-- mypyc/irbuild/builder.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mypyc/codegen/literals.py b/mypyc/codegen/literals.py index 05884b754452..ab3d95105a79 100644 --- a/mypyc/codegen/literals.py +++ b/mypyc/codegen/literals.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, FrozenSet, List, Tuple, Union, cast +from typing import Any, Dict, FrozenSet, List, Tuple, Union, cast from typing_extensions import Final # Supported Python literal types. All tuple / frozenset items must have supported @@ -151,7 +151,7 @@ def _encode_collection_values( ... """ - value_by_index = {index: value for value, index in values.items()} + value_by_index = {index: value for value, index in cast(Dict[Any, int], values.items())} result = [] count = len(values) result.append(str(count)) diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index a67f3a1e32d5..c24207ac64ec 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -885,7 +885,7 @@ def get_dict_base_type(self, expr: Expression) -> Instance: This is useful for dict subclasses like SymbolTable. """ target_type = get_proper_type(self.types[expr]) - assert isinstance(target_type, Instance) + assert isinstance(target_type, Instance), target_type dict_base = next(base for base in target_type.type.mro if base.fullname == "builtins.dict") return map_instance_to_supertype(target_type, dict_base) From 3d44293d87fcd838823b7d0ecfab42ce2013f636 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 7 Jan 2023 17:51:18 -0500 Subject: [PATCH 13/16] Work around mypyc bug ... attempt 2 --- mypyc/codegen/literals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypyc/codegen/literals.py b/mypyc/codegen/literals.py index ab3d95105a79..354f9fa28a2b 100644 --- a/mypyc/codegen/literals.py +++ b/mypyc/codegen/literals.py @@ -151,7 +151,8 @@ def _encode_collection_values( ... """ - value_by_index = {index: value for value, index in cast(Dict[Any, int], values.items())} + values = cast(Dict[Any, int], values) # FIXME: https://github.com/mypyc/mypyc/issues/965 + value_by_index = {index: value for value, index in values.items()} result = [] count = len(values) result.append(str(count)) From fefac473e6e974929061751ef030933deea46c33 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 7 Jan 2023 18:12:20 -0500 Subject: [PATCH 14/16] Work around mypyc bug ... attempt 3 I got tired of trying to compile mypy locally (15 minutes and it's still not done), let's see what CI has to say. --- mypyc/codegen/literals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypyc/codegen/literals.py b/mypyc/codegen/literals.py index 354f9fa28a2b..784a8ed27c4e 100644 --- a/mypyc/codegen/literals.py +++ b/mypyc/codegen/literals.py @@ -151,8 +151,8 @@ def _encode_collection_values( ... """ - values = cast(Dict[Any, int], values) # FIXME: https://github.com/mypyc/mypyc/issues/965 - value_by_index = {index: value for value, index in values.items()} + # FIXME: https://github.com/mypyc/mypyc/issues/965 + value_by_index = {index: value for value, index in cast(Dict[Any, int], values).items()} result = [] count = len(values) result.append(str(count)) From 9cdfc98b25058f0bd4c8e45c44aeea97dd46a04e Mon Sep 17 00:00:00 2001 From: Richard Si Date: Mon, 9 Jan 2023 17:16:49 -0500 Subject: [PATCH 15/16] Add error handling + use test_* syntax instead --- mypyc/lib-rt/misc_ops.c | 4 +++- mypyc/test-data/fixtures/ir.py | 2 ++ mypyc/test-data/run-sets.test | 23 +++++++++++------------ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/mypyc/lib-rt/misc_ops.c b/mypyc/lib-rt/misc_ops.c index dca375d62f1f..5fda78704bbc 100644 --- a/mypyc/lib-rt/misc_ops.c +++ b/mypyc/lib-rt/misc_ops.c @@ -647,7 +647,9 @@ int CPyStatics_Initialize(PyObject **statics, for (int i = 0; i < num_items; i++) { PyObject *item = statics[*frozensets++]; Py_INCREF(item); - PySet_Add(obj, item); + if (PySet_Add(obj, item) == -1) { + return -1; + } } *result++ = obj; } diff --git a/mypyc/test-data/fixtures/ir.py b/mypyc/test-data/fixtures/ir.py index 0e437f4597ea..2f3c18e9c731 100644 --- a/mypyc/test-data/fixtures/ir.py +++ b/mypyc/test-data/fixtures/ir.py @@ -221,12 +221,14 @@ def clear(self) -> None: pass def pop(self) -> T: pass def update(self, x: Iterable[S]) -> None: pass def __or__(self, s: Union[Set[S], FrozenSet[S]]) -> Set[Union[T, S]]: ... + def __xor__(self, s: Union[Set[S], FrozenSet[S]]) -> Set[Union[T, S]]: ... class frozenset(Generic[T]): def __init__(self, i: Optional[Iterable[T]] = None) -> None: pass def __iter__(self) -> Iterator[T]: pass def __len__(self) -> int: pass def __or__(self, s: Union[Set[S], FrozenSet[S]]) -> FrozenSet[Union[T, S]]: ... + def __xor__(self, s: Union[Set[S], FrozenSet[S]]) -> FrozenSet[Union[T, S]]: ... class slice: pass diff --git a/mypyc/test-data/run-sets.test b/mypyc/test-data/run-sets.test index 586bbdb08305..56c946933fac 100644 --- a/mypyc/test-data/run-sets.test +++ b/mypyc/test-data/run-sets.test @@ -116,7 +116,7 @@ s = {1, 2, 3} update(s, [5, 4, 3]) assert s == {1, 2, 3, 4, 5} -[case testPrecomputedFrozensets] +[case testPrecomputedFrozenSets] from typing import Any from typing_extensions import Final @@ -136,16 +136,15 @@ s = set() for i in {None, False, 1, 2.0, "3", b"4", 5j, (6,), CONST}: s.add(i) -[file driver.py] -import native -from native import CONST, main_set, main_negated_set, non_final_name_set - -for item in (None, False, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST): - assert main_set(item), f"{item!r} should be in set_main" - assert not main_negated_set(item), item +def test_in_set() -> None: + for item in (None, False, 1, 2.0, "3", b"4", 5j, (6,), ((7,),), (), CONST): + assert main_set(item), f"{item!r} should be in set_main" + assert not main_negated_set(item), item -assert non_final_name_set(native.non_const) -native.non_const = "updated" -assert non_final_name_set("updated") + assert non_final_name_set(non_const) + global non_const + non_const = "updated" + assert non_final_name_set("updated") -assert not native.s ^ {None, False, 1, 2.0, "3", b"4", 5j, (6,), CONST}, native.s +def test_for_set() -> None: + assert not s ^ {None, False, 1, 2.0, "3", b"4", 5j, (6,), CONST}, s From 2f18f30e012e0f1233cf097d25c2c32a00806a5c Mon Sep 17 00:00:00 2001 From: Richard Si Date: Mon, 9 Jan 2023 17:19:38 -0500 Subject: [PATCH 16/16] Improve precompute_set_literal() docstring --- mypyc/irbuild/expression.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index d4e5a634dbac..3f5b795a1436 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -652,8 +652,11 @@ def precompute_set_literal(builder: IRBuilder, s: SetExpr) -> Value | None: Return None if it's not possible. - Only references to "simple" final variables, tuple literals (with items that - are themselves supported), and other non-container literals are supported. + Supported items: + - Anything supported by irbuild.constant_fold.constant_fold_expr() + - None, True, and False + - Float, byte, and complex literals + - Tuple literals with only items listed above """ values = set_literal_values(builder, s.items) if values is not None: