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.
- pulse/__init__.py +1493 -0
- pulse/_examples.py +29 -0
- pulse/app.py +1086 -0
- pulse/channel.py +607 -0
- pulse/cli/__init__.py +0 -0
- pulse/cli/cmd.py +575 -0
- pulse/cli/dependencies.py +181 -0
- pulse/cli/folder_lock.py +134 -0
- pulse/cli/helpers.py +271 -0
- pulse/cli/logging.py +102 -0
- pulse/cli/models.py +35 -0
- pulse/cli/packages.py +262 -0
- pulse/cli/processes.py +292 -0
- pulse/cli/secrets.py +39 -0
- pulse/cli/uvicorn_log_config.py +87 -0
- pulse/code_analysis.py +38 -0
- pulse/codegen/__init__.py +0 -0
- pulse/codegen/codegen.py +359 -0
- pulse/codegen/templates/__init__.py +0 -0
- pulse/codegen/templates/layout.py +106 -0
- pulse/codegen/templates/route.py +345 -0
- pulse/codegen/templates/routes_ts.py +42 -0
- pulse/codegen/utils.py +20 -0
- pulse/component.py +237 -0
- pulse/components/__init__.py +0 -0
- pulse/components/for_.py +83 -0
- pulse/components/if_.py +86 -0
- pulse/components/react_router.py +94 -0
- pulse/context.py +108 -0
- pulse/cookies.py +322 -0
- pulse/decorators.py +344 -0
- pulse/dom/__init__.py +0 -0
- pulse/dom/elements.py +1024 -0
- pulse/dom/events.py +445 -0
- pulse/dom/props.py +1250 -0
- pulse/dom/svg.py +0 -0
- pulse/dom/tags.py +328 -0
- pulse/dom/tags.pyi +480 -0
- pulse/env.py +178 -0
- pulse/form.py +538 -0
- pulse/helpers.py +541 -0
- pulse/hooks/__init__.py +0 -0
- pulse/hooks/core.py +452 -0
- pulse/hooks/effects.py +88 -0
- pulse/hooks/init.py +668 -0
- pulse/hooks/runtime.py +464 -0
- pulse/hooks/setup.py +254 -0
- pulse/hooks/stable.py +138 -0
- pulse/hooks/state.py +192 -0
- pulse/js/__init__.py +125 -0
- pulse/js/__init__.pyi +115 -0
- pulse/js/_types.py +299 -0
- pulse/js/array.py +339 -0
- pulse/js/console.py +50 -0
- pulse/js/date.py +119 -0
- pulse/js/document.py +145 -0
- pulse/js/error.py +140 -0
- pulse/js/json.py +66 -0
- pulse/js/map.py +97 -0
- pulse/js/math.py +69 -0
- pulse/js/navigator.py +79 -0
- pulse/js/number.py +57 -0
- pulse/js/obj.py +81 -0
- pulse/js/object.py +172 -0
- pulse/js/promise.py +172 -0
- pulse/js/pulse.py +115 -0
- pulse/js/react.py +495 -0
- pulse/js/regexp.py +57 -0
- pulse/js/set.py +124 -0
- pulse/js/string.py +38 -0
- pulse/js/weakmap.py +53 -0
- pulse/js/weakset.py +48 -0
- pulse/js/window.py +205 -0
- pulse/messages.py +202 -0
- pulse/middleware.py +471 -0
- pulse/plugin.py +96 -0
- pulse/proxy.py +242 -0
- pulse/py.typed +0 -0
- pulse/queries/__init__.py +0 -0
- pulse/queries/client.py +609 -0
- pulse/queries/common.py +101 -0
- pulse/queries/effect.py +55 -0
- pulse/queries/infinite_query.py +1418 -0
- pulse/queries/mutation.py +295 -0
- pulse/queries/protocol.py +136 -0
- pulse/queries/query.py +1314 -0
- pulse/queries/store.py +120 -0
- pulse/react_component.py +88 -0
- pulse/reactive.py +1208 -0
- pulse/reactive_extensions.py +1172 -0
- pulse/render_session.py +768 -0
- pulse/renderer.py +584 -0
- pulse/request.py +205 -0
- pulse/routing.py +598 -0
- pulse/serializer.py +279 -0
- pulse/state.py +556 -0
- pulse/test_helpers.py +15 -0
- pulse/transpiler/__init__.py +111 -0
- pulse/transpiler/assets.py +81 -0
- pulse/transpiler/builtins.py +1029 -0
- pulse/transpiler/dynamic_import.py +130 -0
- pulse/transpiler/emit_context.py +49 -0
- pulse/transpiler/errors.py +96 -0
- pulse/transpiler/function.py +611 -0
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +341 -0
- pulse/transpiler/js_module.py +336 -0
- pulse/transpiler/modules/__init__.py +33 -0
- pulse/transpiler/modules/asyncio.py +57 -0
- pulse/transpiler/modules/json.py +24 -0
- pulse/transpiler/modules/math.py +265 -0
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +250 -0
- pulse/transpiler/modules/typing.py +63 -0
- pulse/transpiler/nodes.py +1987 -0
- pulse/transpiler/py_module.py +135 -0
- pulse/transpiler/transpiler.py +1100 -0
- pulse/transpiler/vdom.py +256 -0
- pulse/types/__init__.py +0 -0
- pulse/types/event_handler.py +50 -0
- pulse/user_session.py +386 -0
- pulse/version.py +69 -0
- pulse_framework-0.1.62.dist-info/METADATA +198 -0
- pulse_framework-0.1.62.dist-info/RECORD +126 -0
- pulse_framework-0.1.62.dist-info/WHEEL +4 -0
- 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
|