pulse-framework 0.1.62__py3-none-any.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 (126) hide show
  1. pulse/__init__.py +1493 -0
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +1086 -0
  4. pulse/channel.py +607 -0
  5. pulse/cli/__init__.py +0 -0
  6. pulse/cli/cmd.py +575 -0
  7. pulse/cli/dependencies.py +181 -0
  8. pulse/cli/folder_lock.py +134 -0
  9. pulse/cli/helpers.py +271 -0
  10. pulse/cli/logging.py +102 -0
  11. pulse/cli/models.py +35 -0
  12. pulse/cli/packages.py +262 -0
  13. pulse/cli/processes.py +292 -0
  14. pulse/cli/secrets.py +39 -0
  15. pulse/cli/uvicorn_log_config.py +87 -0
  16. pulse/code_analysis.py +38 -0
  17. pulse/codegen/__init__.py +0 -0
  18. pulse/codegen/codegen.py +359 -0
  19. pulse/codegen/templates/__init__.py +0 -0
  20. pulse/codegen/templates/layout.py +106 -0
  21. pulse/codegen/templates/route.py +345 -0
  22. pulse/codegen/templates/routes_ts.py +42 -0
  23. pulse/codegen/utils.py +20 -0
  24. pulse/component.py +237 -0
  25. pulse/components/__init__.py +0 -0
  26. pulse/components/for_.py +83 -0
  27. pulse/components/if_.py +86 -0
  28. pulse/components/react_router.py +94 -0
  29. pulse/context.py +108 -0
  30. pulse/cookies.py +322 -0
  31. pulse/decorators.py +344 -0
  32. pulse/dom/__init__.py +0 -0
  33. pulse/dom/elements.py +1024 -0
  34. pulse/dom/events.py +445 -0
  35. pulse/dom/props.py +1250 -0
  36. pulse/dom/svg.py +0 -0
  37. pulse/dom/tags.py +328 -0
  38. pulse/dom/tags.pyi +480 -0
  39. pulse/env.py +178 -0
  40. pulse/form.py +538 -0
  41. pulse/helpers.py +541 -0
  42. pulse/hooks/__init__.py +0 -0
  43. pulse/hooks/core.py +452 -0
  44. pulse/hooks/effects.py +88 -0
  45. pulse/hooks/init.py +668 -0
  46. pulse/hooks/runtime.py +464 -0
  47. pulse/hooks/setup.py +254 -0
  48. pulse/hooks/stable.py +138 -0
  49. pulse/hooks/state.py +192 -0
  50. pulse/js/__init__.py +125 -0
  51. pulse/js/__init__.pyi +115 -0
  52. pulse/js/_types.py +299 -0
  53. pulse/js/array.py +339 -0
  54. pulse/js/console.py +50 -0
  55. pulse/js/date.py +119 -0
  56. pulse/js/document.py +145 -0
  57. pulse/js/error.py +140 -0
  58. pulse/js/json.py +66 -0
  59. pulse/js/map.py +97 -0
  60. pulse/js/math.py +69 -0
  61. pulse/js/navigator.py +79 -0
  62. pulse/js/number.py +57 -0
  63. pulse/js/obj.py +81 -0
  64. pulse/js/object.py +172 -0
  65. pulse/js/promise.py +172 -0
  66. pulse/js/pulse.py +115 -0
  67. pulse/js/react.py +495 -0
  68. pulse/js/regexp.py +57 -0
  69. pulse/js/set.py +124 -0
  70. pulse/js/string.py +38 -0
  71. pulse/js/weakmap.py +53 -0
  72. pulse/js/weakset.py +48 -0
  73. pulse/js/window.py +205 -0
  74. pulse/messages.py +202 -0
  75. pulse/middleware.py +471 -0
  76. pulse/plugin.py +96 -0
  77. pulse/proxy.py +242 -0
  78. pulse/py.typed +0 -0
  79. pulse/queries/__init__.py +0 -0
  80. pulse/queries/client.py +609 -0
  81. pulse/queries/common.py +101 -0
  82. pulse/queries/effect.py +55 -0
  83. pulse/queries/infinite_query.py +1418 -0
  84. pulse/queries/mutation.py +295 -0
  85. pulse/queries/protocol.py +136 -0
  86. pulse/queries/query.py +1314 -0
  87. pulse/queries/store.py +120 -0
  88. pulse/react_component.py +88 -0
  89. pulse/reactive.py +1208 -0
  90. pulse/reactive_extensions.py +1172 -0
  91. pulse/render_session.py +768 -0
  92. pulse/renderer.py +584 -0
  93. pulse/request.py +205 -0
  94. pulse/routing.py +598 -0
  95. pulse/serializer.py +279 -0
  96. pulse/state.py +556 -0
  97. pulse/test_helpers.py +15 -0
  98. pulse/transpiler/__init__.py +111 -0
  99. pulse/transpiler/assets.py +81 -0
  100. pulse/transpiler/builtins.py +1029 -0
  101. pulse/transpiler/dynamic_import.py +130 -0
  102. pulse/transpiler/emit_context.py +49 -0
  103. pulse/transpiler/errors.py +96 -0
  104. pulse/transpiler/function.py +611 -0
  105. pulse/transpiler/id.py +18 -0
  106. pulse/transpiler/imports.py +341 -0
  107. pulse/transpiler/js_module.py +336 -0
  108. pulse/transpiler/modules/__init__.py +33 -0
  109. pulse/transpiler/modules/asyncio.py +57 -0
  110. pulse/transpiler/modules/json.py +24 -0
  111. pulse/transpiler/modules/math.py +265 -0
  112. pulse/transpiler/modules/pulse/__init__.py +5 -0
  113. pulse/transpiler/modules/pulse/tags.py +250 -0
  114. pulse/transpiler/modules/typing.py +63 -0
  115. pulse/transpiler/nodes.py +1987 -0
  116. pulse/transpiler/py_module.py +135 -0
  117. pulse/transpiler/transpiler.py +1100 -0
  118. pulse/transpiler/vdom.py +256 -0
  119. pulse/types/__init__.py +0 -0
  120. pulse/types/event_handler.py +50 -0
  121. pulse/user_session.py +386 -0
  122. pulse/version.py +69 -0
  123. pulse_framework-0.1.62.dist-info/METADATA +198 -0
  124. pulse_framework-0.1.62.dist-info/RECORD +126 -0
  125. pulse_framework-0.1.62.dist-info/WHEEL +4 -0
  126. pulse_framework-0.1.62.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1029 @@
1
+ """Python builtin functions and methods -> JavaScript equivalents.
2
+
3
+ This module provides transpilation for Python builtins to JavaScript.
4
+ Builtin methods use runtime type checks when the type is not statically known.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import builtins
10
+ from abc import ABC, abstractmethod
11
+ from typing import TYPE_CHECKING, Any, override
12
+
13
+ from pulse.transpiler.errors import TranspileError
14
+ from pulse.transpiler.nodes import (
15
+ Array,
16
+ Arrow,
17
+ Binary,
18
+ Call,
19
+ Expr,
20
+ Identifier,
21
+ Literal,
22
+ Member,
23
+ New,
24
+ Spread,
25
+ Subscript,
26
+ Template,
27
+ Ternary,
28
+ Throw,
29
+ Transformer,
30
+ Unary,
31
+ Undefined,
32
+ transformer,
33
+ )
34
+
35
+ if TYPE_CHECKING:
36
+ from pulse.transpiler.transpiler import Transpiler
37
+
38
+
39
+ # =============================================================================
40
+ # Builtin Function Transpilers
41
+ # =============================================================================
42
+
43
+
44
+ @transformer("print")
45
+ def emit_print(*args: Any, ctx: Transpiler) -> Expr:
46
+ """print(*args) -> console.log(...)"""
47
+ return Call(Member(Identifier("console"), "log"), [ctx.emit_expr(a) for a in args])
48
+
49
+
50
+ @transformer("len")
51
+ def emit_len(x: Any, *, ctx: Transpiler) -> Expr:
52
+ """len(x) -> x.length ?? x.size"""
53
+ x = ctx.emit_expr(x)
54
+ return Binary(Member(x, "length"), "??", Member(x, "size"))
55
+
56
+
57
+ @transformer("min")
58
+ def emit_min(*args: Any, ctx: Transpiler) -> Expr:
59
+ """min(*args) -> Math.min(...)"""
60
+ if builtins.len(args) == 0:
61
+ raise TranspileError("min() expects at least one argument")
62
+ if builtins.len(args) == 1:
63
+ iterable = ctx.emit_expr(args[0])
64
+ return Call(
65
+ Member(Identifier("Math"), "min"),
66
+ [Spread(Call(Member(Identifier("Array"), "from"), [iterable]))],
67
+ )
68
+ return Call(Member(Identifier("Math"), "min"), [ctx.emit_expr(a) for a in args])
69
+
70
+
71
+ @transformer("max")
72
+ def emit_max(*args: Any, ctx: Transpiler) -> Expr:
73
+ """max(*args) -> Math.max(...)"""
74
+ if builtins.len(args) == 0:
75
+ raise TranspileError("max() expects at least one argument")
76
+ if builtins.len(args) == 1:
77
+ iterable = ctx.emit_expr(args[0])
78
+ return Call(
79
+ Member(Identifier("Math"), "max"),
80
+ [Spread(Call(Member(Identifier("Array"), "from"), [iterable]))],
81
+ )
82
+ return Call(Member(Identifier("Math"), "max"), [ctx.emit_expr(a) for a in args])
83
+
84
+
85
+ @transformer("abs")
86
+ def emit_abs(x: Any, *, ctx: Transpiler) -> Expr:
87
+ """abs(x) -> Math.abs(x)"""
88
+ return Call(Member(Identifier("Math"), "abs"), [ctx.emit_expr(x)])
89
+
90
+
91
+ @transformer("round")
92
+ def emit_round(number: Any, ndigits: Any = None, *, ctx: Transpiler) -> Expr:
93
+ """round(number, ndigits=None) -> Math.round(...) or toFixed(...)"""
94
+ number = ctx.emit_expr(number)
95
+ if ndigits is None:
96
+ return Call(Member(Identifier("Math"), "round"), [number])
97
+ # With ndigits: Number(Number(x).toFixed(ndigits)) to keep numeric semantics
98
+ return Call(
99
+ Identifier("Number"),
100
+ [
101
+ Call(
102
+ Member(Call(Identifier("Number"), [number]), "toFixed"),
103
+ [ctx.emit_expr(ndigits)],
104
+ )
105
+ ],
106
+ )
107
+
108
+
109
+ @transformer("str")
110
+ def emit_str(x: Any, *, ctx: Transpiler) -> Expr:
111
+ """str(x) -> String(x)"""
112
+ return Call(Identifier("String"), [ctx.emit_expr(x)])
113
+
114
+
115
+ @transformer("int")
116
+ def emit_int(*args: Any, ctx: Transpiler) -> Expr:
117
+ """int(x) or int(x, base) -> parseInt(...)"""
118
+ if builtins.len(args) == 1:
119
+ return Call(Identifier("parseInt"), [ctx.emit_expr(args[0])])
120
+ if builtins.len(args) == 2:
121
+ return Call(
122
+ Identifier("parseInt"), [ctx.emit_expr(args[0]), ctx.emit_expr(args[1])]
123
+ )
124
+ raise TranspileError("int() expects one or two arguments")
125
+
126
+
127
+ @transformer("float")
128
+ def emit_float(x: Any, *, ctx: Transpiler) -> Expr:
129
+ """float(x) -> parseFloat(x)"""
130
+ return Call(Identifier("parseFloat"), [ctx.emit_expr(x)])
131
+
132
+
133
+ @transformer("list")
134
+ def emit_list(x: Any, *, ctx: Transpiler) -> Expr:
135
+ """list(x) -> Array.from(x)"""
136
+ return Call(Member(Identifier("Array"), "from"), [ctx.emit_expr(x)])
137
+
138
+
139
+ @transformer("bool")
140
+ def emit_bool(x: Any, *, ctx: Transpiler) -> Expr:
141
+ """bool(x) -> Boolean(x)"""
142
+ return Call(Identifier("Boolean"), [ctx.emit_expr(x)])
143
+
144
+
145
+ @transformer("set")
146
+ def emit_set(*args: Any, ctx: Transpiler) -> Expr:
147
+ """set() or set(iterable) -> new Set([iterable])"""
148
+ if builtins.len(args) == 0:
149
+ return New(Identifier("Set"), [])
150
+ if builtins.len(args) == 1:
151
+ return New(Identifier("Set"), [ctx.emit_expr(args[0])])
152
+ raise TranspileError("set() expects at most one argument")
153
+
154
+
155
+ @transformer("tuple")
156
+ def emit_tuple(*args: Any, ctx: Transpiler) -> Expr:
157
+ """tuple() or tuple(iterable) -> Array.from(iterable)"""
158
+ if builtins.len(args) == 0:
159
+ return Array([])
160
+ if builtins.len(args) == 1:
161
+ return Call(Member(Identifier("Array"), "from"), [ctx.emit_expr(args[0])])
162
+ raise TranspileError("tuple() expects at most one argument")
163
+
164
+
165
+ @transformer("dict")
166
+ def emit_dict(*args: Any, ctx: Transpiler) -> Expr:
167
+ """dict() or dict(iterable) -> new Map([iterable])"""
168
+ if builtins.len(args) == 0:
169
+ return New(Identifier("Map"), [])
170
+ if builtins.len(args) == 1:
171
+ return New(Identifier("Map"), [ctx.emit_expr(args[0])])
172
+ raise TranspileError("dict() expects at most one argument")
173
+
174
+
175
+ @transformer("filter")
176
+ def emit_filter(*args: Any, ctx: Transpiler) -> Expr:
177
+ """filter(func, iterable) -> iterable.filter(func)"""
178
+ if not (1 <= builtins.len(args) <= 2):
179
+ raise TranspileError("filter() expects one or two arguments")
180
+ if builtins.len(args) == 1:
181
+ # filter(iterable) - filter truthy values
182
+ iterable = ctx.emit_expr(args[0])
183
+ predicate = Arrow(["v"], Identifier("v"))
184
+ return Call(Member(iterable, "filter"), [predicate])
185
+ func, iterable = ctx.emit_expr(args[0]), ctx.emit_expr(args[1])
186
+ # filter(None, iterable) means filter truthy
187
+ if builtins.isinstance(func, (Literal, Undefined)) and (
188
+ builtins.isinstance(func, Undefined)
189
+ or (builtins.isinstance(func, Literal) and func.value is None)
190
+ ):
191
+ func = Arrow(["v"], Identifier("v"))
192
+ return Call(Member(iterable, "filter"), [func])
193
+
194
+
195
+ @transformer("map")
196
+ def emit_map(func: Any, iterable: Any, *, ctx: Transpiler) -> Expr:
197
+ """map(func, iterable) -> iterable.map(func)"""
198
+ return Call(Member(ctx.emit_expr(iterable), "map"), [ctx.emit_expr(func)])
199
+
200
+
201
+ @transformer("reversed")
202
+ def emit_reversed(iterable: Any, *, ctx: Transpiler) -> Expr:
203
+ """reversed(iterable) -> iterable.slice().reverse()"""
204
+ return Call(
205
+ Member(Call(Member(ctx.emit_expr(iterable), "slice"), []), "reverse"), []
206
+ )
207
+
208
+
209
+ @transformer("enumerate")
210
+ def emit_enumerate(iterable: Any, start: Any = None, *, ctx: Transpiler) -> Expr:
211
+ """enumerate(iterable, start=0) -> iterable.map((v, i) => [i + start, v])"""
212
+ base = Literal(0) if start is None else ctx.emit_expr(start)
213
+ return Call(
214
+ Member(ctx.emit_expr(iterable), "map"),
215
+ [
216
+ Arrow(
217
+ ["v", "i"],
218
+ Array([Binary(Identifier("i"), "+", base), Identifier("v")]),
219
+ )
220
+ ],
221
+ )
222
+
223
+
224
+ @transformer("range")
225
+ def emit_range(*args: Any, ctx: Transpiler) -> Expr:
226
+ """range(stop) or range(start, stop[, step]) -> Array.from(...)"""
227
+ if not (1 <= builtins.len(args) <= 3):
228
+ raise TranspileError("range() expects 1 to 3 arguments")
229
+ if builtins.len(args) == 1:
230
+ stop = ctx.emit_expr(args[0])
231
+ length = Call(Member(Identifier("Math"), "max"), [Literal(0), stop])
232
+ return Call(
233
+ Member(Identifier("Array"), "from"),
234
+ [Call(Member(New(Identifier("Array"), [length]), "keys"), [])],
235
+ )
236
+ start = ctx.emit_expr(args[0])
237
+ stop = ctx.emit_expr(args[1])
238
+ step = ctx.emit_expr(args[2]) if builtins.len(args) == 3 else Literal(1)
239
+ # count = max(0, ceil((stop - start) / step))
240
+ diff = Binary(stop, "-", start)
241
+ div = Binary(diff, "/", step)
242
+ ceil = Call(Member(Identifier("Math"), "ceil"), [div])
243
+ count = Call(Member(Identifier("Math"), "max"), [Literal(0), ceil])
244
+ # Array.from(new Array(count).keys(), i => start + i * step)
245
+ return Call(
246
+ Member(Identifier("Array"), "from"),
247
+ [
248
+ Call(Member(New(Identifier("Array"), [count]), "keys"), []),
249
+ Arrow(["i"], Binary(start, "+", Binary(Identifier("i"), "*", step))),
250
+ ],
251
+ )
252
+
253
+
254
+ @transformer("sorted")
255
+ def emit_sorted(
256
+ *args: Any, key: Any = None, reverse: Any = None, ctx: Transpiler
257
+ ) -> Expr:
258
+ """sorted(iterable, key=None, reverse=False) -> iterable.slice().sort(...)"""
259
+ if builtins.len(args) != 1:
260
+ raise TranspileError("sorted() expects exactly one positional argument")
261
+ iterable = ctx.emit_expr(args[0])
262
+ clone = Call(Member(iterable, "slice"), [])
263
+ # comparator: (a, b) => (a > b) - (a < b) or with key
264
+ if key is None:
265
+ cmp_expr = Binary(
266
+ Binary(Identifier("a"), ">", Identifier("b")),
267
+ "-",
268
+ Binary(Identifier("a"), "<", Identifier("b")),
269
+ )
270
+ else:
271
+ key_js = ctx.emit_expr(key)
272
+ cmp_expr = Binary(
273
+ Binary(
274
+ Call(key_js, [Identifier("a")]),
275
+ ">",
276
+ Call(key_js, [Identifier("b")]),
277
+ ),
278
+ "-",
279
+ Binary(
280
+ Call(key_js, [Identifier("a")]),
281
+ "<",
282
+ Call(key_js, [Identifier("b")]),
283
+ ),
284
+ )
285
+ sort_call = Call(Member(clone, "sort"), [Arrow(["a", "b"], cmp_expr)])
286
+ if reverse is None:
287
+ return sort_call
288
+ return Ternary(
289
+ ctx.emit_expr(reverse), Call(Member(sort_call, "reverse"), []), sort_call
290
+ )
291
+
292
+
293
+ @transformer("zip")
294
+ def emit_zip(*args: Any, ctx: Transpiler) -> Expr:
295
+ """zip(*iterables) -> Array.from(...) with paired elements"""
296
+ if builtins.len(args) == 0:
297
+ return Array([])
298
+
299
+ js_args = [ctx.emit_expr(a) for a in args]
300
+
301
+ def length_of(x: Expr) -> Expr:
302
+ return Member(x, "length")
303
+
304
+ min_len = length_of(js_args[0])
305
+ for it in js_args[1:]:
306
+ min_len = Call(Member(Identifier("Math"), "min"), [min_len, length_of(it)])
307
+
308
+ elems = [Subscript(arg, Identifier("i")) for arg in js_args]
309
+ make_pair = Arrow(["i"], Array(elems))
310
+ return Call(
311
+ Member(Identifier("Array"), "from"),
312
+ [Call(Member(New(Identifier("Array"), [min_len]), "keys"), []), make_pair],
313
+ )
314
+
315
+
316
+ @transformer("pow")
317
+ def emit_pow(base: Any, exp: Any, *, ctx: Transpiler) -> Expr:
318
+ """pow(base, exp) -> Math.pow(base, exp)"""
319
+ return Call(
320
+ Member(Identifier("Math"), "pow"), [ctx.emit_expr(base), ctx.emit_expr(exp)]
321
+ )
322
+
323
+
324
+ @transformer("chr")
325
+ def emit_chr(x: Any, *, ctx: Transpiler) -> Expr:
326
+ """chr(x) -> String.fromCharCode(x)"""
327
+ return Call(Member(Identifier("String"), "fromCharCode"), [ctx.emit_expr(x)])
328
+
329
+
330
+ @transformer("ord")
331
+ def emit_ord(x: Any, *, ctx: Transpiler) -> Expr:
332
+ """ord(x) -> x.charCodeAt(0)"""
333
+ return Call(Member(ctx.emit_expr(x), "charCodeAt"), [Literal(0)])
334
+
335
+
336
+ @transformer("any")
337
+ def emit_any(x: Any, *, ctx: Transpiler) -> Expr:
338
+ """any(iterable) -> iterable.some(v => v)"""
339
+ x = ctx.emit_expr(x)
340
+ # Optimization: if x is a map call, use .some directly
341
+ if (
342
+ builtins.isinstance(x, Call)
343
+ and builtins.isinstance(x.callee, Member)
344
+ and x.callee.prop == "map"
345
+ and x.args
346
+ ):
347
+ return Call(Member(x.callee.obj, "some"), [x.args[0]])
348
+ return Call(Member(x, "some"), [Arrow(["v"], Identifier("v"))])
349
+
350
+
351
+ @transformer("all")
352
+ def emit_all(x: Any, *, ctx: Transpiler) -> Expr:
353
+ """all(iterable) -> iterable.every(v => v)"""
354
+ x = ctx.emit_expr(x)
355
+ # Optimization: if x is a map call, use .every directly
356
+ if (
357
+ builtins.isinstance(x, Call)
358
+ and builtins.isinstance(x.callee, Member)
359
+ and x.callee.prop == "map"
360
+ and x.args
361
+ ):
362
+ return Call(Member(x.callee.obj, "every"), [x.args[0]])
363
+ return Call(Member(x, "every"), [Arrow(["v"], Identifier("v"))])
364
+
365
+
366
+ @transformer("sum")
367
+ def emit_sum(*args: Any, ctx: Transpiler) -> Expr:
368
+ """sum(iterable, start=0) -> iterable.reduce((a, b) => a + b, start)"""
369
+ if not (1 <= builtins.len(args) <= 2):
370
+ raise TranspileError("sum() expects one or two arguments")
371
+ start = ctx.emit_expr(args[1]) if builtins.len(args) == 2 else Literal(0)
372
+ base = ctx.emit_expr(args[0])
373
+ reducer = Arrow(["a", "b"], Binary(Identifier("a"), "+", Identifier("b")))
374
+ return Call(Member(base, "reduce"), [reducer, start])
375
+
376
+
377
+ @transformer("divmod")
378
+ def emit_divmod(x: Any, y: Any, *, ctx: Transpiler) -> Expr:
379
+ """divmod(x, y) -> [Math.floor(x / y), x - Math.floor(x / y) * y]"""
380
+ x, y = ctx.emit_expr(x), ctx.emit_expr(y)
381
+ q = Call(Member(Identifier("Math"), "floor"), [Binary(x, "/", y)])
382
+ r = Binary(x, "-", Binary(q, "*", y))
383
+ return Array([q, r])
384
+
385
+
386
+ @transformer("isinstance")
387
+ def emit_isinstance(*args: Any, ctx: Transpiler) -> Expr:
388
+ """isinstance is not directly supported in v2; raise error."""
389
+ raise TranspileError("isinstance() is not supported in JavaScript transpilation")
390
+
391
+
392
+ @transformer("Exception")
393
+ def emit_exception(*args: Any, ctx: Transpiler) -> Expr:
394
+ """Exception(msg) -> new Error(msg)"""
395
+ return New(Identifier("Error"), [ctx.emit_expr(a) for a in args])
396
+
397
+
398
+ @transformer("ValueError")
399
+ def emit_value_error(*args: Any, ctx: Transpiler) -> Expr:
400
+ """ValueError(msg) -> new Error(msg)"""
401
+ return New(Identifier("Error"), [ctx.emit_expr(a) for a in args])
402
+
403
+
404
+ @transformer("TypeError")
405
+ def emit_type_error(*args: Any, ctx: Transpiler) -> Expr:
406
+ """TypeError(msg) -> new TypeError(msg)"""
407
+ return New(Identifier("TypeError"), [ctx.emit_expr(a) for a in args])
408
+
409
+
410
+ @transformer("RuntimeError")
411
+ def emit_runtime_error(*args: Any, ctx: Transpiler) -> Expr:
412
+ """RuntimeError(msg) -> new Error(msg)"""
413
+ return New(Identifier("Error"), [ctx.emit_expr(a) for a in args])
414
+
415
+
416
+ # Registry of builtin transformers
417
+ # Note: @transformer decorator returns Transformer but lies about the type
418
+ # for ergonomic reasons. These are all Transformer instances at runtime.
419
+ BUILTINS: dict[str, Transformer[Any]] = dict(
420
+ print=emit_print,
421
+ len=emit_len,
422
+ min=emit_min,
423
+ max=emit_max,
424
+ abs=emit_abs,
425
+ round=emit_round,
426
+ str=emit_str,
427
+ int=emit_int,
428
+ float=emit_float,
429
+ list=emit_list,
430
+ bool=emit_bool,
431
+ set=emit_set,
432
+ tuple=emit_tuple,
433
+ dict=emit_dict,
434
+ filter=emit_filter,
435
+ map=emit_map,
436
+ reversed=emit_reversed,
437
+ enumerate=emit_enumerate,
438
+ range=emit_range,
439
+ sorted=emit_sorted,
440
+ zip=emit_zip,
441
+ pow=emit_pow,
442
+ chr=emit_chr,
443
+ ord=emit_ord,
444
+ any=emit_any,
445
+ all=emit_all,
446
+ sum=emit_sum,
447
+ divmod=emit_divmod,
448
+ isinstance=emit_isinstance,
449
+ # Exception types
450
+ Exception=emit_exception,
451
+ ValueError=emit_value_error,
452
+ TypeError=emit_type_error,
453
+ RuntimeError=emit_runtime_error,
454
+ ) # pyright: ignore[reportAssignmentType]
455
+
456
+
457
+ # =============================================================================
458
+ # Builtin Method Transpilation
459
+ # =============================================================================
460
+ #
461
+ # Methods are organized into classes by type (StringMethods, ListMethods, etc.).
462
+ # Each class contains methods that transpile Python methods to their JS equivalents.
463
+ #
464
+ # Methods return None to fall through to the default method call (when no
465
+ # transformation is needed).
466
+
467
+
468
+ class BuiltinMethods(ABC):
469
+ """Abstract base class for type-specific method transpilation."""
470
+
471
+ def __init__(self, obj: Expr) -> None:
472
+ self.this: Expr = obj
473
+
474
+ @classmethod
475
+ @abstractmethod
476
+ def __runtime_check__(cls, expr: Expr) -> Expr:
477
+ """Return a JS expression that checks if expr is this type at runtime."""
478
+ ...
479
+
480
+ @classmethod
481
+ @abstractmethod
482
+ def __methods__(cls) -> builtins.set[str]:
483
+ """Return the set of method names this class handles."""
484
+ ...
485
+
486
+
487
+ class StringMethods(BuiltinMethods):
488
+ """String method transpilation."""
489
+
490
+ @classmethod
491
+ @override
492
+ def __runtime_check__(cls, expr: Expr) -> Expr:
493
+ return Binary(Unary("typeof", expr), "===", Literal("string"))
494
+
495
+ @classmethod
496
+ @override
497
+ def __methods__(cls) -> set[str]:
498
+ return STR_METHODS
499
+
500
+ def lower(self) -> Expr:
501
+ """str.lower() -> str.toLowerCase()"""
502
+ return Call(Member(self.this, "toLowerCase"), [])
503
+
504
+ def upper(self) -> Expr:
505
+ """str.upper() -> str.toUpperCase()"""
506
+ return Call(Member(self.this, "toUpperCase"), [])
507
+
508
+ def strip(self) -> Expr:
509
+ """str.strip() -> str.trim()"""
510
+ return Call(Member(self.this, "trim"), [])
511
+
512
+ def lstrip(self) -> Expr:
513
+ """str.lstrip() -> str.trimStart()"""
514
+ return Call(Member(self.this, "trimStart"), [])
515
+
516
+ def rstrip(self) -> Expr:
517
+ """str.rstrip() -> str.trimEnd()"""
518
+ return Call(Member(self.this, "trimEnd"), [])
519
+
520
+ def zfill(self, width: Expr) -> Expr:
521
+ """str.zfill(width) -> str.padStart(width, '0')"""
522
+ return Call(Member(self.this, "padStart"), [width, Literal("0")])
523
+
524
+ def startswith(self, prefix: Expr) -> Expr:
525
+ """str.startswith(prefix) -> str.startsWith(prefix)"""
526
+ return Call(Member(self.this, "startsWith"), [prefix])
527
+
528
+ def endswith(self, suffix: Expr) -> Expr:
529
+ """str.endswith(suffix) -> str.endsWith(suffix)"""
530
+ return Call(Member(self.this, "endsWith"), [suffix])
531
+
532
+ def replace(self, old: Expr, new: Expr) -> Expr:
533
+ """str.replace(old, new) -> str.replaceAll(old, new)"""
534
+ return Call(Member(self.this, "replaceAll"), [old, new])
535
+
536
+ def capitalize(self) -> Expr:
537
+ """str.capitalize() -> str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()"""
538
+ left = Call(
539
+ Member(Call(Member(self.this, "charAt"), [Literal(0)]), "toUpperCase"), []
540
+ )
541
+ right = Call(
542
+ Member(Call(Member(self.this, "slice"), [Literal(1)]), "toLowerCase"), []
543
+ )
544
+ return Binary(left, "+", right)
545
+
546
+ def split(self, sep: Expr | None = None) -> Expr | None:
547
+ """str.split(sep) -> str.split(sep) or special whitespace handling.
548
+
549
+ Python's split() without args splits on whitespace and removes empties:
550
+ "a b".split() -> ["a", "b"]
551
+
552
+ JavaScript's split() without args returns the whole string:
553
+ "a b".split() -> ["a b"]
554
+
555
+ Fix: str.trim().split(/\\s+/)
556
+ """
557
+ if sep is None:
558
+ # Python's default: split on whitespace and filter empties
559
+ trimmed = Call(Member(self.this, "trim"), [])
560
+ return Call(Member(trimmed, "split"), [Identifier(r"/\s+/")])
561
+ return None # Fall through for explicit separator
562
+
563
+ def join(self, iterable: Expr) -> Expr:
564
+ """str.join(iterable) -> iterable.join(str)"""
565
+ return Call(Member(iterable, "join"), [self.this])
566
+
567
+ def find(self, sub: Expr) -> Expr:
568
+ """str.find(sub) -> str.indexOf(sub)"""
569
+ return Call(Member(self.this, "indexOf"), [sub])
570
+
571
+ def rfind(self, sub: Expr) -> Expr:
572
+ """str.rfind(sub) -> str.lastIndexOf(sub)"""
573
+ return Call(Member(self.this, "lastIndexOf"), [sub])
574
+
575
+ def count(self, sub: Expr) -> Expr:
576
+ """str.count(sub) -> (str.split(sub).length - 1)"""
577
+ return Binary(
578
+ Member(Call(Member(self.this, "split"), [sub]), "length"),
579
+ "-",
580
+ Literal(1),
581
+ )
582
+
583
+ def isdigit(self) -> Expr:
584
+ r"""str.isdigit() -> /^\d+$/.test(str)"""
585
+ return Call(
586
+ Member(Identifier("/^\\d+$/"), "test"),
587
+ [self.this],
588
+ )
589
+
590
+ def isalpha(self) -> Expr:
591
+ r"""str.isalpha() -> /^[a-zA-Z]+$/.test(str)"""
592
+ return Call(
593
+ Member(Identifier("/^[a-zA-Z]+$/"), "test"),
594
+ [self.this],
595
+ )
596
+
597
+ def isalnum(self) -> Expr:
598
+ r"""str.isalnum() -> /^[a-zA-Z0-9]+$/.test(str)"""
599
+ return Call(
600
+ Member(Identifier("/^[a-zA-Z0-9]+$/"), "test"),
601
+ [self.this],
602
+ )
603
+
604
+
605
+ STR_METHODS = {k for k in StringMethods.__dict__ if not k.startswith("_")}
606
+
607
+
608
+ class ListMethods(BuiltinMethods):
609
+ """List method transpilation."""
610
+
611
+ @classmethod
612
+ @override
613
+ def __runtime_check__(cls, expr: Expr) -> Expr:
614
+ return Call(Member(Identifier("Array"), "isArray"), [expr])
615
+
616
+ @classmethod
617
+ @override
618
+ def __methods__(cls) -> set[str]:
619
+ return LIST_METHODS
620
+
621
+ def append(self, value: Expr) -> Expr:
622
+ """list.append(value) -> (list.push(value), undefined)[1]"""
623
+ # Returns undefined to match Python's None return
624
+ return Subscript(
625
+ Array([Call(Member(self.this, "push"), [value]), Undefined()]),
626
+ Literal(1),
627
+ )
628
+
629
+ def extend(self, iterable: Expr) -> Expr:
630
+ """list.extend(iterable) -> (list.push(...iterable), undefined)[1]"""
631
+ return Subscript(
632
+ Array([Call(Member(self.this, "push"), [Spread(iterable)]), Undefined()]),
633
+ Literal(1),
634
+ )
635
+
636
+ def pop(self, index: Expr | None = None) -> Expr | None:
637
+ """list.pop() or list.pop(index)"""
638
+ if index is None:
639
+ return None # Fall through to default .pop()
640
+ return Subscript(
641
+ Call(Member(self.this, "splice"), [index, Literal(1)]), Literal(0)
642
+ )
643
+
644
+ def copy(self) -> Expr:
645
+ """list.copy() -> list.slice()"""
646
+ return Call(Member(self.this, "slice"), [])
647
+
648
+ def count(self, value: Expr) -> Expr:
649
+ """list.count(value) -> list.filter(v => v === value).length"""
650
+ return Member(
651
+ Call(
652
+ Member(self.this, "filter"),
653
+ [Arrow(["v"], Binary(Identifier("v"), "===", value))],
654
+ ),
655
+ "length",
656
+ )
657
+
658
+ def index(self, value: Expr) -> Expr:
659
+ """list.index(value) -> list.indexOf(value)"""
660
+ return Call(Member(self.this, "indexOf"), [value])
661
+
662
+ def reverse(self) -> Expr:
663
+ """list.reverse() -> (list.reverse(), undefined)[1]"""
664
+ return Subscript(
665
+ Array([Call(Member(self.this, "reverse"), []), Undefined()]),
666
+ Literal(1),
667
+ )
668
+
669
+ def sort(self) -> Expr:
670
+ """list.sort() -> (list.sort(), undefined)[1]"""
671
+ return Subscript(
672
+ Array([Call(Member(self.this, "sort"), []), Undefined()]),
673
+ Literal(1),
674
+ )
675
+
676
+ def clear(self) -> Expr:
677
+ """list.clear() -> (list.length = 0, undefined)[1]"""
678
+ # Setting length to 0 clears the array
679
+ return Subscript(
680
+ Array([Binary(Member(self.this, "length"), "=", Literal(0)), Undefined()]),
681
+ Literal(1),
682
+ )
683
+
684
+ def insert(self, index: Expr, value: Expr) -> Expr:
685
+ """list.insert(index, value) -> (list.splice(index, 0, value), undefined)[1]"""
686
+ return Subscript(
687
+ Array(
688
+ [
689
+ Call(Member(self.this, "splice"), [index, Literal(0), value]),
690
+ Undefined(),
691
+ ]
692
+ ),
693
+ Literal(1),
694
+ )
695
+
696
+ def remove(self, value: Expr) -> Expr:
697
+ """list.remove(value) -> safe removal with error on not found.
698
+
699
+ Python raises ValueError if value not in list. We generate:
700
+ (($i) => $i < 0 ? (() => { throw new Error(...) })() : list.splice($i, 1))(list.indexOf(value))
701
+ """
702
+ idx = Identifier("$i")
703
+ index_call = Call(Member(self.this, "indexOf"), [value])
704
+ # IIFE that throws using Arrow with statement body
705
+ throw_iife = Call(
706
+ Arrow(
707
+ [],
708
+ [
709
+ Throw(
710
+ New(
711
+ Identifier("Error"),
712
+ [Literal("list.remove(x): x not in list")],
713
+ )
714
+ )
715
+ ],
716
+ ),
717
+ [],
718
+ )
719
+ safe_splice = Ternary(
720
+ Binary(idx, "<", Literal(0)),
721
+ throw_iife,
722
+ Call(Member(self.this, "splice"), [idx, Literal(1)]),
723
+ )
724
+ return Call(Arrow(["$i"], safe_splice), [index_call])
725
+
726
+
727
+ LIST_METHODS = {k for k in ListMethods.__dict__ if not k.startswith("_")}
728
+
729
+
730
+ class DictMethods(BuiltinMethods):
731
+ """Dict (Map) method transpilation."""
732
+
733
+ @classmethod
734
+ @override
735
+ def __runtime_check__(cls, expr: Expr) -> Expr:
736
+ return Binary(expr, "instanceof", Identifier("Map"))
737
+
738
+ @classmethod
739
+ @override
740
+ def __methods__(cls) -> set[str]:
741
+ return DICT_METHODS
742
+
743
+ def get(self, key: Expr, default: Expr | None = None) -> Expr | None:
744
+ """dict.get(key, default) -> dict.get(key) ?? default"""
745
+ if default is None:
746
+ return None # Fall through to default .get()
747
+ return Binary(Call(Member(self.this, "get"), [key]), "??", default)
748
+
749
+ def keys(self) -> Expr:
750
+ """dict.keys() -> [...dict.keys()]"""
751
+ return Array([Spread(Call(Member(self.this, "keys"), []))])
752
+
753
+ def values(self) -> Expr:
754
+ """dict.values() -> [...dict.values()]"""
755
+ return Array([Spread(Call(Member(self.this, "values"), []))])
756
+
757
+ def items(self) -> Expr:
758
+ """dict.items() -> [...dict.entries()]"""
759
+ return Array([Spread(Call(Member(self.this, "entries"), []))])
760
+
761
+ def copy(self) -> Expr:
762
+ """dict.copy() -> new Map(dict.entries())"""
763
+ return New(Identifier("Map"), [Call(Member(self.this, "entries"), [])])
764
+
765
+ def clear(self) -> Expr | None:
766
+ """dict.clear() doesn't need transformation."""
767
+ return None
768
+
769
+ def pop(self, key: Expr, default: Expr | None = None) -> Expr:
770
+ """dict.pop(key, default) -> complex expression to get and delete"""
771
+ # (v => (dict.delete(key), v))(dict.get(key) ?? default)
772
+ get_val = Call(Member(self.this, "get"), [key])
773
+ if default is not None:
774
+ get_val = Binary(get_val, "??", default)
775
+ delete_call = Call(Member(self.this, "delete"), [key])
776
+ return Call(
777
+ Arrow(
778
+ ["$v"], Subscript(Array([delete_call, Identifier("$v")]), Literal(1))
779
+ ),
780
+ [get_val],
781
+ )
782
+
783
+ def update(self, other: Expr) -> Expr:
784
+ """dict.update(other) -> other.forEach((v, k) => dict.set(k, v))"""
785
+ return Call(
786
+ Member(other, "forEach"),
787
+ [
788
+ Arrow(
789
+ ["$v", "$k"],
790
+ Call(
791
+ Member(self.this, "set"), [Identifier("$k"), Identifier("$v")]
792
+ ),
793
+ )
794
+ ],
795
+ )
796
+
797
+ def setdefault(self, key: Expr, default: Expr | None = None) -> Expr:
798
+ """dict.setdefault(key, default) -> dict.has(key) ? dict.get(key) : (dict.set(key, default), default)[1]"""
799
+ default_val = default if default is not None else Literal(None)
800
+ return Ternary(
801
+ Call(Member(self.this, "has"), [key]),
802
+ Call(Member(self.this, "get"), [key]),
803
+ Subscript(
804
+ Array(
805
+ [Call(Member(self.this, "set"), [key, default_val]), default_val]
806
+ ),
807
+ Literal(1),
808
+ ),
809
+ )
810
+
811
+
812
+ DICT_METHODS = {k for k in DictMethods.__dict__ if not k.startswith("_")}
813
+
814
+
815
+ class SetMethods(BuiltinMethods):
816
+ """Set method transpilation."""
817
+
818
+ @classmethod
819
+ @override
820
+ def __runtime_check__(cls, expr: Expr) -> Expr:
821
+ return Binary(expr, "instanceof", Identifier("Set"))
822
+
823
+ @classmethod
824
+ @override
825
+ def __methods__(cls) -> set[str]:
826
+ return SET_METHODS
827
+
828
+ def add(self, value: Expr) -> Expr | None:
829
+ """set.add() doesn't need transformation."""
830
+ return None
831
+
832
+ def remove(self, value: Expr) -> Expr:
833
+ """set.remove(value) -> set.delete(value)"""
834
+ return Call(Member(self.this, "delete"), [value])
835
+
836
+ def discard(self, value: Expr) -> Expr:
837
+ """set.discard(value) -> set.delete(value)"""
838
+ return Call(Member(self.this, "delete"), [value])
839
+
840
+ def clear(self) -> Expr | None:
841
+ """set.clear() doesn't need transformation."""
842
+ return None
843
+
844
+ def copy(self) -> Expr:
845
+ """set.copy() -> new Set(set)"""
846
+ return New(Identifier("Set"), [self.this])
847
+
848
+ def pop(self) -> Expr:
849
+ """set.pop() -> (v => (set.delete(v), v))(set.values().next().value)"""
850
+ get_first = Member(
851
+ Call(Member(Call(Member(self.this, "values"), []), "next"), []), "value"
852
+ )
853
+ delete_call = Call(Member(self.this, "delete"), [Identifier("$v")])
854
+ return Call(
855
+ Arrow(
856
+ ["$v"], Subscript(Array([delete_call, Identifier("$v")]), Literal(1))
857
+ ),
858
+ [get_first],
859
+ )
860
+
861
+ def update(self, other: Expr) -> Expr:
862
+ """set.update(other) -> other.forEach(v => set.add(v))"""
863
+ return Call(
864
+ Member(other, "forEach"),
865
+ [Arrow(["$v"], Call(Member(self.this, "add"), [Identifier("$v")]))],
866
+ )
867
+
868
+ def intersection(self, other: Expr) -> Expr:
869
+ """set.intersection(other) -> new Set([...set].filter(x => other.has(x)))"""
870
+ filtered = Call(
871
+ Member(Array([Spread(self.this)]), "filter"),
872
+ [Arrow(["$x"], Call(Member(other, "has"), [Identifier("$x")]))],
873
+ )
874
+ return New(Identifier("Set"), [filtered])
875
+
876
+ def union(self, other: Expr) -> Expr:
877
+ """set.union(other) -> new Set([...set, ...other])"""
878
+ return New(
879
+ Identifier("Set"),
880
+ [Array([Spread(self.this), Spread(other)])],
881
+ )
882
+
883
+ def difference(self, other: Expr) -> Expr:
884
+ """set.difference(other) -> new Set([...set].filter(x => !other.has(x)))"""
885
+ filtered = Call(
886
+ Member(Array([Spread(self.this)]), "filter"),
887
+ [
888
+ Arrow(
889
+ ["$x"],
890
+ Unary("!", Call(Member(other, "has"), [Identifier("$x")])),
891
+ )
892
+ ],
893
+ )
894
+ return New(Identifier("Set"), [filtered])
895
+
896
+ def issubset(self, other: Expr) -> Expr:
897
+ """set.issubset(other) -> [...set].every(x => other.has(x))"""
898
+ return Call(
899
+ Member(Array([Spread(self.this)]), "every"),
900
+ [Arrow(["$x"], Call(Member(other, "has"), [Identifier("$x")]))],
901
+ )
902
+
903
+ def issuperset(self, other: Expr) -> Expr:
904
+ """set.issuperset(other) -> [...other].every(x => set.has(x))"""
905
+ return Call(
906
+ Member(Array([Spread(other)]), "every"),
907
+ [Arrow(["$x"], Call(Member(self.this, "has"), [Identifier("$x")]))],
908
+ )
909
+
910
+
911
+ SET_METHODS = {k for k in SetMethods.__dict__ if not k.startswith("_")}
912
+
913
+
914
+ # Collect all known method names for quick lookup
915
+ ALL_METHODS = STR_METHODS | LIST_METHODS | DICT_METHODS | SET_METHODS
916
+
917
+ # Method classes in priority order (higher priority = later in list = outermost ternary)
918
+ # We prefer string/list semantics first, then set, then dict.
919
+ METHOD_CLASSES: builtins.list[builtins.type[BuiltinMethods]] = [
920
+ DictMethods,
921
+ SetMethods,
922
+ ListMethods,
923
+ StringMethods,
924
+ ]
925
+
926
+
927
+ def _try_dispatch_method(
928
+ cls: builtins.type[BuiltinMethods],
929
+ obj: Expr,
930
+ method: str,
931
+ args: list[Expr],
932
+ kwargs: builtins.dict[builtins.str, Expr] | None = None,
933
+ ) -> Expr | None:
934
+ """Try to dispatch a method call to a specific builtin class.
935
+
936
+ Returns the transformed expression, or None if the method returns None
937
+ (fall through to default) or if dispatch fails.
938
+ """
939
+ if method not in cls.__methods__():
940
+ return None
941
+
942
+ try:
943
+ handler = cls(obj)
944
+ method_fn = builtins.getattr(handler, method, None)
945
+ if method_fn is None:
946
+ return None
947
+ if kwargs:
948
+ return method_fn(*args, **kwargs)
949
+ return method_fn(*args)
950
+ except TypeError:
951
+ return None
952
+
953
+
954
+ def emit_method(
955
+ obj: Expr,
956
+ method: str,
957
+ args: list[Expr],
958
+ kwargs: builtins.dict[builtins.str, Expr] | None = None,
959
+ ) -> Expr | None:
960
+ """Emit a method call, handling Python builtin methods.
961
+
962
+ For known literal types (Literal str, Template, Array, New Set/Map),
963
+ dispatches directly without runtime checks.
964
+
965
+ For unknown types, builds a ternary chain that checks types at runtime
966
+ and dispatches to the appropriate method implementation.
967
+
968
+ Returns:
969
+ Expr if the method should be transpiled specially
970
+ None if the method should be emitted as a regular method call
971
+ """
972
+ if method not in ALL_METHODS:
973
+ return None
974
+
975
+ # Fast path: known literal types - dispatch directly without runtime checks
976
+ if builtins.isinstance(obj, Literal) and builtins.isinstance(obj.value, str):
977
+ if method in StringMethods.__methods__():
978
+ result = _try_dispatch_method(StringMethods, obj, method, args, kwargs)
979
+ if result is not None:
980
+ return result
981
+ return None
982
+
983
+ if builtins.isinstance(obj, Template):
984
+ if method in StringMethods.__methods__():
985
+ result = _try_dispatch_method(StringMethods, obj, method, args, kwargs)
986
+ if result is not None:
987
+ return result
988
+ return None
989
+
990
+ if builtins.isinstance(obj, Array):
991
+ if method in ListMethods.__methods__():
992
+ result = _try_dispatch_method(ListMethods, obj, method, args, kwargs)
993
+ if result is not None:
994
+ return result
995
+ return None
996
+
997
+ # Fast path: new Set(...) and new Map(...) are known types
998
+ if builtins.isinstance(obj, New) and builtins.isinstance(obj.ctor, Identifier):
999
+ if obj.ctor.name == "Set" and method in SetMethods.__methods__():
1000
+ result = _try_dispatch_method(SetMethods, obj, method, args, kwargs)
1001
+ if result is not None:
1002
+ return result
1003
+ return None
1004
+ if obj.ctor.name == "Map" and method in DictMethods.__methods__():
1005
+ result = _try_dispatch_method(DictMethods, obj, method, args, kwargs)
1006
+ if result is not None:
1007
+ return result
1008
+ return None
1009
+
1010
+ # Slow path: unknown type - build ternary chain with runtime type checks
1011
+ # Start with the default fallback (regular method call)
1012
+ default_expr = Call(Member(obj, method), args)
1013
+ expr: Expr = default_expr
1014
+
1015
+ # Apply in increasing priority so that later (higher priority) wrappers
1016
+ # end up outermost in the final expression.
1017
+ for cls in METHOD_CLASSES:
1018
+ if method not in cls.__methods__():
1019
+ continue
1020
+
1021
+ dispatch_expr = _try_dispatch_method(cls, obj, method, args, kwargs)
1022
+ if dispatch_expr is not None:
1023
+ expr = Ternary(cls.__runtime_check__(obj), dispatch_expr, expr)
1024
+
1025
+ # If we built ternaries, return them; otherwise return None to fall through
1026
+ if expr is not default_expr:
1027
+ return expr
1028
+
1029
+ return None