cinderx 2026.1.16.2__cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. __static__/__init__.py +641 -0
  2. __static__/compiler_flags.py +8 -0
  3. __static__/enum.py +160 -0
  4. __static__/native_utils.py +77 -0
  5. __static__/type_code.py +48 -0
  6. __strict__/__init__.py +39 -0
  7. _cinderx.so +0 -0
  8. cinderx/__init__.py +577 -0
  9. cinderx/__pycache__/__init__.cpython-314.pyc +0 -0
  10. cinderx/_asyncio.py +156 -0
  11. cinderx/compileall.py +710 -0
  12. cinderx/compiler/__init__.py +40 -0
  13. cinderx/compiler/__main__.py +137 -0
  14. cinderx/compiler/config.py +7 -0
  15. cinderx/compiler/consts.py +72 -0
  16. cinderx/compiler/debug.py +70 -0
  17. cinderx/compiler/dis_stable.py +283 -0
  18. cinderx/compiler/errors.py +151 -0
  19. cinderx/compiler/flow_graph_optimizer.py +1287 -0
  20. cinderx/compiler/future.py +91 -0
  21. cinderx/compiler/misc.py +32 -0
  22. cinderx/compiler/opcode_cinder.py +18 -0
  23. cinderx/compiler/opcode_static.py +100 -0
  24. cinderx/compiler/opcodebase.py +158 -0
  25. cinderx/compiler/opcodes.py +991 -0
  26. cinderx/compiler/optimizer.py +547 -0
  27. cinderx/compiler/pyassem.py +3711 -0
  28. cinderx/compiler/pycodegen.py +7660 -0
  29. cinderx/compiler/pysourceloader.py +62 -0
  30. cinderx/compiler/static/__init__.py +1404 -0
  31. cinderx/compiler/static/compiler.py +629 -0
  32. cinderx/compiler/static/declaration_visitor.py +335 -0
  33. cinderx/compiler/static/definite_assignment_checker.py +280 -0
  34. cinderx/compiler/static/effects.py +160 -0
  35. cinderx/compiler/static/module_table.py +666 -0
  36. cinderx/compiler/static/type_binder.py +2176 -0
  37. cinderx/compiler/static/types.py +10580 -0
  38. cinderx/compiler/static/util.py +81 -0
  39. cinderx/compiler/static/visitor.py +91 -0
  40. cinderx/compiler/strict/__init__.py +69 -0
  41. cinderx/compiler/strict/class_conflict_checker.py +249 -0
  42. cinderx/compiler/strict/code_gen_base.py +409 -0
  43. cinderx/compiler/strict/common.py +507 -0
  44. cinderx/compiler/strict/compiler.py +352 -0
  45. cinderx/compiler/strict/feature_extractor.py +130 -0
  46. cinderx/compiler/strict/flag_extractor.py +97 -0
  47. cinderx/compiler/strict/loader.py +827 -0
  48. cinderx/compiler/strict/preprocessor.py +11 -0
  49. cinderx/compiler/strict/rewriter/__init__.py +5 -0
  50. cinderx/compiler/strict/rewriter/remove_annotations.py +84 -0
  51. cinderx/compiler/strict/rewriter/rewriter.py +975 -0
  52. cinderx/compiler/strict/runtime.py +77 -0
  53. cinderx/compiler/symbols.py +1754 -0
  54. cinderx/compiler/unparse.py +414 -0
  55. cinderx/compiler/visitor.py +194 -0
  56. cinderx/jit.py +230 -0
  57. cinderx/opcode.py +202 -0
  58. cinderx/static.py +113 -0
  59. cinderx/strictmodule.py +6 -0
  60. cinderx/test_support.py +341 -0
  61. cinderx-2026.1.16.2.dist-info/METADATA +15 -0
  62. cinderx-2026.1.16.2.dist-info/RECORD +68 -0
  63. cinderx-2026.1.16.2.dist-info/WHEEL +6 -0
  64. cinderx-2026.1.16.2.dist-info/licenses/LICENSE +21 -0
  65. cinderx-2026.1.16.2.dist-info/top_level.txt +5 -0
  66. opcodes/__init__.py +0 -0
  67. opcodes/assign_opcode_numbers.py +272 -0
  68. opcodes/cinderx_opcodes.py +121 -0
@@ -0,0 +1,547 @@
1
+ # Portions copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ # pyre-strict
4
+
5
+ from __future__ import annotations
6
+
7
+ import ast
8
+ import operator
9
+ from ast import cmpop, Constant, copy_location
10
+ from dataclasses import dataclass
11
+ from typing import Any, Callable, Iterable, Mapping
12
+
13
+ from .visitor import ASTRewriter
14
+
15
+
16
+ class PyLimits:
17
+ MAX_INT_SIZE = 128
18
+ MAX_COLLECTION_SIZE = 256
19
+ MAX_STR_SIZE = 4096
20
+ MAX_TOTAL_ITEMS = 1024
21
+
22
+
23
+ # pyre-fixme[5]: Global annotation cannot contain `Any`.
24
+ UNARY_OPS: Mapping[type[ast.unaryop], Callable[[Any], object]] = {
25
+ ast.Invert: operator.invert,
26
+ ast.Not: operator.not_,
27
+ ast.UAdd: operator.pos,
28
+ ast.USub: operator.neg,
29
+ }
30
+ INVERSE_OPS: Mapping[type[cmpop], type[cmpop]] = {
31
+ ast.Is: ast.IsNot,
32
+ ast.IsNot: ast.Is,
33
+ ast.In: ast.NotIn,
34
+ ast.NotIn: ast.In,
35
+ }
36
+
37
+ BIN_OPS: Mapping[type[ast.operator], Callable[[object, object], object]] = {
38
+ ast.Add: operator.add,
39
+ ast.Sub: operator.sub,
40
+ ast.Mult: lambda lhs, rhs: safe_multiply(lhs, rhs, PyLimits),
41
+ ast.Div: operator.truediv,
42
+ ast.FloorDiv: operator.floordiv,
43
+ ast.Mod: lambda lhs, rhs: safe_mod(lhs, rhs, PyLimits),
44
+ ast.Pow: lambda lhs, rhs: safe_power(lhs, rhs, PyLimits),
45
+ ast.LShift: lambda lhs, rhs: safe_lshift(lhs, rhs, PyLimits),
46
+ ast.RShift: operator.rshift,
47
+ ast.BitOr: operator.or_,
48
+ ast.BitXor: operator.xor,
49
+ ast.BitAnd: operator.and_,
50
+ }
51
+
52
+
53
+ class DefaultLimits:
54
+ MAX_INT_SIZE = 128
55
+ MAX_COLLECTION_SIZE = 20
56
+ MAX_STR_SIZE = 20
57
+ MAX_TOTAL_ITEMS = 1024
58
+
59
+
60
+ LimitsType = type[PyLimits] | type[DefaultLimits]
61
+
62
+
63
+ # pyre-fixme[2]: Parameter annotation cannot be `Any`.
64
+ def safe_lshift(left: Any, right: Any, limits: LimitsType = DefaultLimits) -> object:
65
+ if isinstance(left, int) and isinstance(right, int) and left and right:
66
+ lbits = left.bit_length()
67
+ if (
68
+ right < 0
69
+ or right > limits.MAX_INT_SIZE
70
+ or lbits > limits.MAX_INT_SIZE - right
71
+ ):
72
+ raise OverflowError()
73
+
74
+ return left << right
75
+
76
+
77
+ def check_complexity(obj: object, limit: int) -> int:
78
+ if isinstance(obj, (frozenset, tuple)):
79
+ limit -= len(obj)
80
+ for item in obj:
81
+ limit = check_complexity(item, limit)
82
+ if limit < 0:
83
+ break
84
+
85
+ return limit
86
+
87
+
88
+ # pyre-fixme[2]: Parameter annotation cannot be `Any`.
89
+ def safe_multiply(left: Any, right: Any, limits: LimitsType = DefaultLimits) -> object:
90
+ if isinstance(left, int) and isinstance(right, int) and left and right:
91
+ lbits = left.bit_length()
92
+ rbits = right.bit_length()
93
+ if lbits + rbits > limits.MAX_INT_SIZE:
94
+ raise OverflowError()
95
+ elif isinstance(left, int) and isinstance(right, (tuple, frozenset)):
96
+ rsize = len(right)
97
+ if rsize:
98
+ if left < 0 or left > limits.MAX_COLLECTION_SIZE / rsize:
99
+ raise OverflowError()
100
+ if left:
101
+ if check_complexity(right, limits.MAX_TOTAL_ITEMS // left) < 0:
102
+ raise OverflowError()
103
+ elif isinstance(left, int) and isinstance(right, (str, bytes)):
104
+ rsize = len(right)
105
+ if rsize:
106
+ if left < 0 or left > limits.MAX_STR_SIZE / rsize:
107
+ raise OverflowError()
108
+ elif isinstance(right, int) and isinstance(left, (tuple, frozenset, str, bytes)):
109
+ return safe_multiply(right, left, limits)
110
+
111
+ return left * right
112
+
113
+
114
+ # pyre-fixme[2]: Parameter annotation cannot be `Any`.
115
+ def safe_power(left: Any, right: Any, limits: LimitsType = DefaultLimits) -> object:
116
+ if isinstance(left, int) and isinstance(right, int) and left and right > 0:
117
+ lbits = left.bit_length()
118
+ if lbits > limits.MAX_INT_SIZE / right:
119
+ raise OverflowError()
120
+
121
+ return left**right
122
+
123
+
124
+ # pyre-fixme[2]: Parameter annotation cannot be `Any`.
125
+ def safe_mod(left: Any, right: Any, limits: LimitsType = DefaultLimits) -> object:
126
+ if isinstance(left, (str, bytes)):
127
+ raise OverflowError()
128
+
129
+ return left % right
130
+
131
+
132
+ class AstOptimizer(ASTRewriter):
133
+ def __init__(self, optimize: bool = False, string_anns: bool = False) -> None:
134
+ super().__init__()
135
+ self.optimize = optimize
136
+ self.string_anns = string_anns
137
+
138
+ def skip_field(self, node: ast.AST, field: str) -> bool:
139
+ if self.string_anns:
140
+ if (
141
+ isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
142
+ and field == "returns"
143
+ ):
144
+ return True
145
+ if isinstance(node, ast.arg) and field == "annotation":
146
+ return True
147
+ if isinstance(node, ast.AnnAssign) and field == "annotation":
148
+ return True
149
+ return False
150
+
151
+ def visitUnaryOp(self, node: ast.UnaryOp) -> ast.expr:
152
+ op = self.visit(node.operand)
153
+ if isinstance(op, Constant):
154
+ conv = UNARY_OPS[type(node.op)]
155
+ try:
156
+ return copy_location(Constant(conv(op.value)), node)
157
+ except Exception:
158
+ pass
159
+ elif (
160
+ isinstance(node.op, ast.Not)
161
+ and isinstance(op, ast.Compare)
162
+ and len(op.ops) == 1
163
+ ):
164
+ cmp_op = op.ops[0]
165
+ new_op = INVERSE_OPS.get(type(cmp_op))
166
+ if new_op is not None:
167
+ return self.update_node(op, ops=[new_op()])
168
+
169
+ return self.update_node(node, operand=op)
170
+
171
+ def visitBinOp(self, node: ast.BinOp) -> ast.expr:
172
+ left = self.visit(node.left)
173
+ right = self.visit(node.right)
174
+
175
+ if isinstance(left, Constant) and isinstance(right, Constant):
176
+ handler = BIN_OPS.get(type(node.op))
177
+ if handler is not None:
178
+ try:
179
+ return copy_location(
180
+ Constant(handler(left.value, right.value)), node
181
+ )
182
+ except Exception:
183
+ pass
184
+
185
+ return self.update_node(node, left=left, right=right)
186
+
187
+ def makeConstTuple(self, elts: Iterable[ast.expr]) -> Constant | None:
188
+ if all(isinstance(elt, Constant) for elt in elts):
189
+ # pyre-ignore[16]: each elt is a constant at this point.
190
+ return Constant(tuple(elt.value for elt in elts))
191
+
192
+ return None
193
+
194
+ def visitTuple(self, node: ast.Tuple) -> ast.expr:
195
+ elts = self.walk_list(node.elts)
196
+
197
+ if isinstance(node.ctx, ast.Load):
198
+ # pyre-ignore[6]: Can't type walk_list fully yet.
199
+ res = self.makeConstTuple(elts)
200
+ if res is not None:
201
+ return copy_location(res, node)
202
+
203
+ return self.update_node(node, elts=elts)
204
+
205
+ def visitSubscript(self, node: ast.Subscript) -> ast.expr:
206
+ value = self.visit(node.value)
207
+ slice = self.visit(node.slice)
208
+
209
+ if (
210
+ isinstance(node.ctx, ast.Load)
211
+ and isinstance(value, Constant)
212
+ and isinstance(slice, Constant)
213
+ ):
214
+ try:
215
+ return copy_location(
216
+ Constant(value.value[slice.value]),
217
+ node,
218
+ )
219
+ except Exception:
220
+ pass
221
+
222
+ return self.update_node(node, value=value, slice=slice)
223
+
224
+ def _visitIter(self, node: ast.expr) -> ast.expr:
225
+ if isinstance(node, ast.List):
226
+ elts = self.walk_list(node.elts)
227
+ # pyre-ignore[6]: Can't type walk_list fully yet.
228
+ res = self.makeConstTuple(elts)
229
+ if res is not None:
230
+ return copy_location(res, node)
231
+ if not any(isinstance(e, ast.Starred) for e in elts):
232
+ # pyre-fixme[6]: For 1st argument expected `List[expr]` but got
233
+ # `Sequence[expr]`.
234
+ return copy_location(ast.Tuple(elts=elts, ctx=node.ctx), node)
235
+ return self.update_node(node, elts=elts)
236
+ elif isinstance(node, ast.Set):
237
+ elts = self.walk_list(node.elts)
238
+ # pyre-ignore[6]: Can't type walk_list fully yet.
239
+ res = self.makeConstTuple(elts)
240
+ if res is not None:
241
+ return copy_location(Constant(frozenset(res.value)), node)
242
+
243
+ return self.update_node(node, elts=elts)
244
+
245
+ return self.generic_visit(node)
246
+
247
+ def visitcomprehension(self, node: ast.comprehension) -> ast.comprehension:
248
+ target = self.visit(node.target)
249
+ iter = self.visit(node.iter)
250
+ assert isinstance(iter, ast.expr)
251
+ ifs = self.walk_list(node.ifs)
252
+ iter = self._visitIter(iter)
253
+
254
+ return self.update_node(node, target=target, iter=iter, ifs=ifs)
255
+
256
+ def visitFor(self, node: ast.For) -> ast.For:
257
+ target = self.visit(node.target)
258
+ iter = self.visit(node.iter)
259
+ assert isinstance(iter, ast.expr)
260
+ body = self.walk_list(node.body)
261
+ orelse = self.walk_list(node.orelse)
262
+
263
+ iter = self._visitIter(iter)
264
+ return self.update_node(
265
+ node, target=target, iter=iter, body=body, orelse=orelse
266
+ )
267
+
268
+ def visitCompare(self, node: ast.Compare) -> ast.expr:
269
+ left = self.visit(node.left)
270
+ comparators = self.walk_list(node.comparators)
271
+
272
+ if isinstance(node.ops[-1], (ast.In, ast.NotIn)):
273
+ # pyre-ignore[6]: Can't type walk_list fully yet.
274
+ new_iter = self._visitIter(comparators[-1])
275
+ if new_iter is not None and new_iter is not comparators[-1]:
276
+ comparators = list(comparators)
277
+ comparators[-1] = new_iter
278
+
279
+ return self.update_node(node, left=left, comparators=comparators)
280
+
281
+ def visitName(self, node: ast.Name) -> ast.Name | ast.Constant:
282
+ if node.id == "__debug__":
283
+ return copy_location(Constant(not self.optimize), node)
284
+
285
+ return self.generic_visit(node)
286
+
287
+ def visitNamedExpr(self, node: ast.NamedExpr) -> ast.NamedExpr:
288
+ return self.generic_visit(node)
289
+
290
+
291
+ F_LJUST: int = 1 << 0
292
+ F_SIGN: int = 1 << 1
293
+ F_BLANK: int = 1 << 2
294
+ F_ALT: int = 1 << 3
295
+ F_ZERO: int = 1 << 4
296
+
297
+ FLAG_DICT: dict[str, int] = {
298
+ "-": F_LJUST,
299
+ "+": F_SIGN,
300
+ " ": F_BLANK,
301
+ "#": F_ALT,
302
+ "0": F_ZERO,
303
+ }
304
+ MAXDIGITS = 3
305
+
306
+
307
+ class UnsupportedFormat(Exception):
308
+ pass
309
+
310
+
311
+ @dataclass(frozen=True)
312
+ class FormatInfo:
313
+ spec: str
314
+ flags: int = 0
315
+ width: int | None = None
316
+ prec: int | None = None
317
+
318
+ def as_formatted_value(self, arg: ast.expr) -> ast.FormattedValue | None:
319
+ if self.spec not in "sra":
320
+ return None
321
+
322
+ res = []
323
+ if not (self.flags & F_LJUST) and self.width:
324
+ res.append(">")
325
+ if self.width is not None:
326
+ res.append(str(self.width))
327
+ if self.prec is not None:
328
+ res.append(f".{self.prec}")
329
+
330
+ return copy_location(
331
+ ast.FormattedValue(
332
+ arg,
333
+ ord(self.spec),
334
+ copy_location(ast.Constant("".join(res)), arg) if res else None,
335
+ ),
336
+ arg,
337
+ )
338
+
339
+
340
+ class FormatParser:
341
+ def __init__(self, val: str) -> None:
342
+ self.val = val
343
+ self.size: int = len(val)
344
+ self.pos = 0
345
+
346
+ def next_ch(self) -> str:
347
+ """Gets the next character in the format string, or raises an
348
+ exception if we've run out of characters"""
349
+ if self.pos >= self.size:
350
+ raise UnsupportedFormat()
351
+ self.pos += 1
352
+ return self.val[self.pos]
353
+
354
+ def parse_int(self) -> int:
355
+ """Parses an integer from the format string"""
356
+ res = 0
357
+ digits = 0
358
+ ch = self.val[self.pos]
359
+ while "0" <= ch <= "9":
360
+ res = res * 10 + ord(ch) - ord("0")
361
+ ch = self.next_ch()
362
+ digits += 1
363
+ if digits >= MAXDIGITS:
364
+ raise UnsupportedFormat()
365
+ return res
366
+
367
+ def parse_flags(self, ch: str) -> int:
368
+ """Parse any flags (-, +, " ", #, 0)"""
369
+ flags = 0
370
+ while True:
371
+ flag_val = FLAG_DICT.get(ch)
372
+ if flag_val is not None:
373
+ flags |= flag_val
374
+ ch = self.next_ch()
375
+ else:
376
+ break
377
+ return flags
378
+
379
+ def parse_str(self) -> str:
380
+ """Parses a string component of the format string up to a %"""
381
+ has_percents = False
382
+ start = self.pos
383
+ while self.pos < self.size:
384
+ ch = self.val[self.pos]
385
+ if ch != "%":
386
+ self.pos += 1
387
+ elif self.pos + 1 < self.size and self.val[self.pos + 1] == "%":
388
+ has_percents = True
389
+ self.pos += 2
390
+ else:
391
+ break
392
+
393
+ component = self.val[start : self.pos]
394
+ if has_percents:
395
+ component = component.replace("%%", "%")
396
+ return component
397
+
398
+ def enum_components(self) -> Iterable[str | FormatInfo]:
399
+ """Enumerates the components of the format string and returns a stream
400
+ of interleaved strings and FormatInfo objects"""
401
+ # Parse the string up to the format specifier
402
+ ch = None
403
+ while self.pos < self.size:
404
+ yield self.parse_str()
405
+
406
+ if self.pos == self.size:
407
+ return
408
+
409
+ assert self.val[self.pos] == "%"
410
+
411
+ flags = self.parse_flags(self.next_ch())
412
+
413
+ # Parse width
414
+ width = None
415
+ if "0" <= self.val[self.pos] <= "9":
416
+ width = self.parse_int()
417
+
418
+ prec = None
419
+ if self.val[self.pos] == ".":
420
+ self.next_ch()
421
+ prec = self.parse_int()
422
+
423
+ yield FormatInfo(self.val[self.pos], flags, width, prec)
424
+ self.pos += 1
425
+
426
+
427
+ def enum_format_str_components(val: str) -> Iterable[str | FormatInfo]:
428
+ return FormatParser(val).enum_components()
429
+
430
+
431
+ def set_no_locations(node: ast.expr) -> ast.expr:
432
+ node.lineno = -1
433
+ node.end_lineno = -1
434
+ node.col_offset = -1
435
+ node.end_col_offset = -1
436
+ return node
437
+
438
+
439
+ class AstOptimizer312(AstOptimizer):
440
+ def visitBinOp(self, node: ast.BinOp) -> ast.expr:
441
+ res = super().visitBinOp(node)
442
+
443
+ if (
444
+ not isinstance(res, ast.BinOp)
445
+ or not isinstance(res.op, ast.Mod)
446
+ or not isinstance(res.left, ast.Constant)
447
+ or not isinstance(res.left.value, str)
448
+ or not isinstance(res.right, ast.Tuple)
449
+ or any(isinstance(e, ast.Starred) for e in res.right.elts)
450
+ ):
451
+ return res
452
+
453
+ return self.optimize_format(res)
454
+
455
+ def optimize_format(self, node: ast.BinOp) -> ast.expr:
456
+ left = node.left
457
+ right = node.right
458
+ assert isinstance(left, ast.Constant)
459
+ assert isinstance(right, ast.Tuple)
460
+ assert isinstance(left.value, str)
461
+
462
+ try:
463
+ seq = []
464
+ cnt = 0
465
+ for item in enum_format_str_components(left.value):
466
+ if isinstance(item, str):
467
+ if item:
468
+ seq.append(set_no_locations(ast.Constant(item)))
469
+ continue
470
+
471
+ if cnt >= len(right.elts):
472
+ # More format units than items.
473
+ return node
474
+
475
+ formatted = item.as_formatted_value(right.elts[cnt])
476
+ if formatted is None:
477
+ return node
478
+ seq.append(formatted)
479
+ cnt += 1
480
+
481
+ if cnt < len(right.elts):
482
+ # More items than format units.
483
+ return node
484
+
485
+ return copy_location(ast.JoinedStr(seq), node)
486
+
487
+ except UnsupportedFormat:
488
+ return node
489
+
490
+
491
+ class AstOptimizer314(AstOptimizer312):
492
+ def has_starred(self, e: ast.Tuple) -> bool:
493
+ return any(isinstance(e, ast.Starred) for e in e.elts)
494
+
495
+ def visitUnaryOp(self, node: ast.UnaryOp) -> ast.expr:
496
+ op = self.visit(node.operand)
497
+ return self.update_node(node, operand=op)
498
+
499
+ def visitBinOp(self, node: ast.BinOp) -> ast.expr:
500
+ lhs = self.visit(node.left)
501
+ rhs = self.visit(node.right)
502
+ if (
503
+ isinstance(lhs, ast.Constant)
504
+ and isinstance(rhs, ast.Tuple)
505
+ and isinstance(lhs.value, str)
506
+ and not self.has_starred(rhs)
507
+ ):
508
+ return self.optimize_format(self.update_node(node, left=lhs, right=rhs))
509
+
510
+ return self.update_node(node, left=lhs, right=rhs)
511
+
512
+ def visitSubscript(self, node: ast.Subscript) -> ast.expr:
513
+ value = self.visit(node.value)
514
+ slice = self.visit(node.slice)
515
+
516
+ return self.update_node(node, value=value, slice=slice)
517
+
518
+ def visitMatchValue(self, node: ast.MatchValue) -> ast.MatchValue:
519
+ return self.update_node(node, value=self.fold_const_match_patterns(node.value))
520
+
521
+ def visitMatchMapping(self, node: ast.MatchMapping) -> ast.MatchMapping:
522
+ keys = [self.fold_const_match_patterns(key) for key in node.keys]
523
+ patterns = self.walk_list(node.patterns)
524
+ return self.update_node(node, keys=keys, patterns=patterns)
525
+
526
+ def fold_const_match_patterns(self, node: ast.expr) -> ast.expr:
527
+ if isinstance(node, ast.UnaryOp):
528
+ if isinstance(node.op, ast.USub) and isinstance(node.operand, ast.Constant):
529
+ return super().visitUnaryOp(node)
530
+ elif isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub)):
531
+ if isinstance(node.right, ast.Constant):
532
+ node = self.update_node(
533
+ node, left=self.fold_const_match_patterns(node.left)
534
+ )
535
+ if isinstance(node.left, ast.Constant):
536
+ return super().visitBinOp(node)
537
+
538
+ return node
539
+
540
+ def visitTuple(self, node: ast.Tuple) -> ast.expr:
541
+ elts = self.walk_list(node.elts)
542
+
543
+ return self.update_node(node, elts=elts)
544
+
545
+ def _visitIter(self, node: ast.expr) -> ast.expr:
546
+ # This optimization has been removed in 3.14
547
+ return node