pulse-framework 0.1.46__py3-none-any.whl → 0.1.48__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 +9 -23
- pulse/app.py +6 -25
- pulse/cli/processes.py +1 -0
- pulse/codegen/codegen.py +43 -88
- pulse/codegen/js.py +35 -5
- pulse/codegen/templates/route.py +341 -254
- pulse/form.py +1 -1
- pulse/helpers.py +51 -27
- pulse/hooks/core.py +2 -2
- pulse/hooks/effects.py +1 -1
- pulse/hooks/init.py +2 -1
- pulse/hooks/setup.py +1 -1
- pulse/hooks/stable.py +2 -2
- pulse/hooks/states.py +2 -2
- pulse/html/props.py +3 -2
- pulse/html/tags.py +135 -0
- pulse/html/tags.pyi +4 -0
- pulse/js/__init__.py +110 -0
- pulse/js/__init__.pyi +95 -0
- pulse/js/_types.py +297 -0
- pulse/js/array.py +253 -0
- pulse/js/console.py +47 -0
- pulse/js/date.py +113 -0
- pulse/js/document.py +138 -0
- pulse/js/error.py +139 -0
- pulse/js/json.py +62 -0
- pulse/js/map.py +84 -0
- pulse/js/math.py +66 -0
- pulse/js/navigator.py +76 -0
- pulse/js/number.py +54 -0
- pulse/js/object.py +173 -0
- pulse/js/promise.py +150 -0
- pulse/js/regexp.py +54 -0
- pulse/js/set.py +109 -0
- pulse/js/string.py +35 -0
- pulse/js/weakmap.py +50 -0
- pulse/js/weakset.py +45 -0
- pulse/js/window.py +199 -0
- pulse/messages.py +22 -3
- pulse/proxy.py +21 -8
- pulse/react_component.py +167 -14
- pulse/reactive_extensions.py +5 -5
- pulse/render_session.py +144 -34
- pulse/renderer.py +80 -115
- pulse/routing.py +1 -18
- pulse/transpiler/__init__.py +131 -0
- pulse/transpiler/builtins.py +731 -0
- pulse/transpiler/constants.py +110 -0
- pulse/transpiler/context.py +26 -0
- pulse/transpiler/errors.py +2 -0
- pulse/transpiler/function.py +250 -0
- pulse/transpiler/ids.py +16 -0
- pulse/transpiler/imports.py +409 -0
- pulse/transpiler/js_module.py +274 -0
- pulse/transpiler/modules/__init__.py +30 -0
- pulse/transpiler/modules/asyncio.py +38 -0
- pulse/transpiler/modules/json.py +20 -0
- pulse/transpiler/modules/math.py +320 -0
- pulse/transpiler/modules/re.py +466 -0
- pulse/transpiler/modules/tags.py +268 -0
- pulse/transpiler/modules/typing.py +59 -0
- pulse/transpiler/nodes.py +1216 -0
- pulse/transpiler/py_module.py +119 -0
- pulse/transpiler/transpiler.py +938 -0
- pulse/transpiler/utils.py +4 -0
- pulse/vdom.py +112 -6
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/METADATA +1 -1
- pulse_framework-0.1.48.dist-info/RECORD +119 -0
- pulse/codegen/imports.py +0 -204
- pulse/css.py +0 -155
- pulse_framework-0.1.46.dist-info/RECORD +0 -80
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python builtin functions -> JavaScript equivalents for v2 transpiler.
|
|
3
|
+
|
|
4
|
+
This module provides transpilation for Python builtins to JavaScript.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import builtins
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import Any, cast, override
|
|
12
|
+
|
|
13
|
+
from pulse.transpiler.errors import JSCompilationError
|
|
14
|
+
from pulse.transpiler.nodes import (
|
|
15
|
+
JSArray,
|
|
16
|
+
JSArrowFunction,
|
|
17
|
+
JSBinary,
|
|
18
|
+
JSCall,
|
|
19
|
+
JSComma,
|
|
20
|
+
JSExpr,
|
|
21
|
+
JSIdentifier,
|
|
22
|
+
JSMember,
|
|
23
|
+
JSMemberCall,
|
|
24
|
+
JSNew,
|
|
25
|
+
JSNumber,
|
|
26
|
+
JSSpread,
|
|
27
|
+
JSString,
|
|
28
|
+
JSSubscript,
|
|
29
|
+
JSTemplate,
|
|
30
|
+
JSTertiary,
|
|
31
|
+
JSTransformer,
|
|
32
|
+
JSUnary,
|
|
33
|
+
JSUndefined,
|
|
34
|
+
js_transformer,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@js_transformer("print")
|
|
39
|
+
def transform_print(*args: Any) -> JSExpr:
|
|
40
|
+
"""print(*args) -> console.log(...)"""
|
|
41
|
+
return JSMemberCall(JSIdentifier("console"), "log", [JSExpr.of(a) for a in args])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@js_transformer("len")
|
|
45
|
+
def transform_len(x: Any) -> JSExpr:
|
|
46
|
+
"""len(x) -> x.length ?? x.size"""
|
|
47
|
+
# .length for strings/arrays, .size for sets/maps
|
|
48
|
+
x = JSExpr.of(x)
|
|
49
|
+
return JSBinary(JSMember(x, "length"), "??", JSMember(x, "size"))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@js_transformer("min")
|
|
53
|
+
def transform_min(*args: Any) -> JSExpr:
|
|
54
|
+
"""min(*args) -> Math.min(...)"""
|
|
55
|
+
return JSMemberCall(JSIdentifier("Math"), "min", [JSExpr.of(a) for a in args])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@js_transformer("max")
|
|
59
|
+
def transform_max(*args: Any) -> JSExpr:
|
|
60
|
+
"""max(*args) -> Math.max(...)"""
|
|
61
|
+
return JSMemberCall(JSIdentifier("Math"), "max", [JSExpr.of(a) for a in args])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@js_transformer("abs")
|
|
65
|
+
def transform_abs(x: Any) -> JSExpr:
|
|
66
|
+
"""abs(x) -> Math.abs(x)"""
|
|
67
|
+
return JSMemberCall(JSIdentifier("Math"), "abs", [JSExpr.of(x)])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@js_transformer("round")
|
|
71
|
+
def transform_round(number: Any, ndigits: Any = None) -> JSExpr:
|
|
72
|
+
"""round(number, ndigits=None) -> Math.round(...) or toFixed(...)"""
|
|
73
|
+
number = JSExpr.of(number)
|
|
74
|
+
if ndigits is None:
|
|
75
|
+
return JSCall(JSIdentifier("Math.round"), [number])
|
|
76
|
+
# With ndigits: Number(x).toFixed(ndigits) for positive, complex for negative
|
|
77
|
+
# For simplicity, assume positive ndigits (most common case)
|
|
78
|
+
return JSMemberCall(
|
|
79
|
+
JSCall(JSIdentifier("Number"), [number]), "toFixed", [JSExpr.of(ndigits)]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@js_transformer("str")
|
|
84
|
+
def transform_str(x: Any) -> JSExpr:
|
|
85
|
+
"""str(x) -> String(x)"""
|
|
86
|
+
return JSCall(JSIdentifier("String"), [JSExpr.of(x)])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@js_transformer("int")
|
|
90
|
+
def transform_int(*args: Any) -> JSExpr:
|
|
91
|
+
"""int(x) or int(x, base) -> parseInt(...)"""
|
|
92
|
+
if builtins.len(args) == 1:
|
|
93
|
+
return JSCall(JSIdentifier("parseInt"), [JSExpr.of(args[0])])
|
|
94
|
+
if builtins.len(args) == 2:
|
|
95
|
+
return JSCall(
|
|
96
|
+
JSIdentifier("parseInt"), [JSExpr.of(args[0]), JSExpr.of(args[1])]
|
|
97
|
+
)
|
|
98
|
+
raise JSCompilationError("int() expects one or two arguments")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@js_transformer("float")
|
|
102
|
+
def transform_float(x: Any) -> JSExpr:
|
|
103
|
+
"""float(x) -> parseFloat(x)"""
|
|
104
|
+
return JSCall(JSIdentifier("parseFloat"), [JSExpr.of(x)])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@js_transformer("list")
|
|
108
|
+
def transform_list(x: Any) -> JSExpr:
|
|
109
|
+
"""list(x) -> Array.from(x)"""
|
|
110
|
+
return JSCall(JSMember(JSIdentifier("Array"), "from"), [JSExpr.of(x)])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@js_transformer("bool")
|
|
114
|
+
def transform_bool(x: Any) -> JSExpr:
|
|
115
|
+
"""bool(x) -> Boolean(x)"""
|
|
116
|
+
return JSCall(JSIdentifier("Boolean"), [JSExpr.of(x)])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@js_transformer("set")
|
|
120
|
+
def transform_set(*args: Any) -> JSExpr:
|
|
121
|
+
"""set() or set(iterable) -> new Set([iterable])"""
|
|
122
|
+
if builtins.len(args) == 0:
|
|
123
|
+
return JSNew(JSIdentifier("Set"), [])
|
|
124
|
+
if builtins.len(args) == 1:
|
|
125
|
+
return JSNew(JSIdentifier("Set"), [JSExpr.of(args[0])])
|
|
126
|
+
raise JSCompilationError("set() expects at most one argument")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@js_transformer("tuple")
|
|
130
|
+
def transform_tuple(*args: Any) -> JSExpr:
|
|
131
|
+
"""tuple() or tuple(iterable) -> Array.from(iterable)"""
|
|
132
|
+
if builtins.len(args) == 0:
|
|
133
|
+
return JSArray([])
|
|
134
|
+
if builtins.len(args) == 1:
|
|
135
|
+
return JSCall(JSMember(JSIdentifier("Array"), "from"), [JSExpr.of(args[0])])
|
|
136
|
+
raise JSCompilationError("tuple() expects at most one argument")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@js_transformer("dict")
|
|
140
|
+
def transform_dict(*args: Any) -> JSExpr:
|
|
141
|
+
"""dict() or dict(iterable) -> new Map([iterable])"""
|
|
142
|
+
if builtins.len(args) == 0:
|
|
143
|
+
return JSNew(JSIdentifier("Map"), [])
|
|
144
|
+
if builtins.len(args) == 1:
|
|
145
|
+
return JSNew(JSIdentifier("Map"), [JSExpr.of(args[0])])
|
|
146
|
+
raise JSCompilationError("dict() expects at most one argument")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@js_transformer("filter")
|
|
150
|
+
def transform_filter(*args: Any) -> JSExpr:
|
|
151
|
+
"""filter(func, iterable) -> iterable.filter(func)"""
|
|
152
|
+
if not (1 <= builtins.len(args) <= 2):
|
|
153
|
+
raise JSCompilationError("filter() expects one or two arguments")
|
|
154
|
+
if builtins.len(args) == 1:
|
|
155
|
+
# filter(iterable) - filter truthy values
|
|
156
|
+
iterable = JSExpr.of(args[0])
|
|
157
|
+
predicate = JSArrowFunction("v", JSIdentifier("v"))
|
|
158
|
+
return JSMemberCall(iterable, "filter", [predicate])
|
|
159
|
+
func, iterable = JSExpr.of(args[0]), JSExpr.of(args[1])
|
|
160
|
+
# filter(None, iterable) means filter truthy
|
|
161
|
+
if builtins.isinstance(func, JSUndefined):
|
|
162
|
+
func = JSArrowFunction("v", JSIdentifier("v"))
|
|
163
|
+
return JSMemberCall(iterable, "filter", [func])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@js_transformer("map")
|
|
167
|
+
def transform_map(func: Any, iterable: Any) -> JSExpr:
|
|
168
|
+
"""map(func, iterable) -> iterable.map(func)"""
|
|
169
|
+
return JSMemberCall(JSExpr.of(iterable), "map", [JSExpr.of(func)])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@js_transformer("reversed")
|
|
173
|
+
def transform_reversed(iterable: Any) -> JSExpr:
|
|
174
|
+
"""reversed(iterable) -> iterable.slice().reverse()"""
|
|
175
|
+
return JSMemberCall(JSMemberCall(JSExpr.of(iterable), "slice", []), "reverse", [])
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@js_transformer("enumerate")
|
|
179
|
+
def transform_enumerate(iterable: Any, start: Any = None) -> JSExpr:
|
|
180
|
+
"""enumerate(iterable, start=0) -> iterable.map((v, i) => [i + start, v])"""
|
|
181
|
+
base = JSNumber(0) if start is None else JSExpr.of(start)
|
|
182
|
+
return JSMemberCall(
|
|
183
|
+
JSExpr.of(iterable),
|
|
184
|
+
"map",
|
|
185
|
+
[
|
|
186
|
+
JSArrowFunction(
|
|
187
|
+
"(v, i)",
|
|
188
|
+
JSArray([JSBinary(JSIdentifier("i"), "+", base), JSIdentifier("v")]),
|
|
189
|
+
)
|
|
190
|
+
],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@js_transformer("range")
|
|
195
|
+
def transform_range(*args: Any) -> JSExpr:
|
|
196
|
+
"""range(stop) or range(start, stop[, step]) -> Array.from(...)"""
|
|
197
|
+
if not (1 <= builtins.len(args) <= 3):
|
|
198
|
+
raise JSCompilationError("range() expects 1 to 3 arguments")
|
|
199
|
+
if builtins.len(args) == 1:
|
|
200
|
+
stop = JSExpr.of(args[0])
|
|
201
|
+
length = JSMemberCall(JSIdentifier("Math"), "max", [JSNumber(0), stop])
|
|
202
|
+
return JSCall(
|
|
203
|
+
JSMember(JSIdentifier("Array"), "from"),
|
|
204
|
+
[JSMemberCall(JSNew(JSIdentifier("Array"), [length]), "keys", [])],
|
|
205
|
+
)
|
|
206
|
+
start = JSExpr.of(args[0])
|
|
207
|
+
stop = JSExpr.of(args[1])
|
|
208
|
+
step = JSExpr.of(args[2]) if builtins.len(args) == 3 else JSNumber(1)
|
|
209
|
+
# count = max(0, ceil((stop - start) / step))
|
|
210
|
+
diff = JSBinary(stop, "-", start)
|
|
211
|
+
div = JSBinary(diff, "/", step)
|
|
212
|
+
ceil = JSMemberCall(JSIdentifier("Math"), "ceil", [div])
|
|
213
|
+
count = JSMemberCall(JSIdentifier("Math"), "max", [JSNumber(0), ceil])
|
|
214
|
+
# Array.from(new Array(count).keys(), i => start + i * step)
|
|
215
|
+
return JSCall(
|
|
216
|
+
JSMember(JSIdentifier("Array"), "from"),
|
|
217
|
+
[
|
|
218
|
+
JSMemberCall(JSNew(JSIdentifier("Array"), [count]), "keys", []),
|
|
219
|
+
JSArrowFunction(
|
|
220
|
+
"i", JSBinary(start, "+", JSBinary(JSIdentifier("i"), "*", step))
|
|
221
|
+
),
|
|
222
|
+
],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@js_transformer("sorted")
|
|
227
|
+
def transform_sorted(*args: Any, key: Any = None, reverse: Any = None) -> JSExpr:
|
|
228
|
+
"""sorted(iterable, key=None, reverse=False) -> iterable.slice().sort(...)"""
|
|
229
|
+
if builtins.len(args) != 1:
|
|
230
|
+
raise JSCompilationError("sorted() expects exactly one positional argument")
|
|
231
|
+
iterable = JSExpr.of(args[0])
|
|
232
|
+
clone = JSMemberCall(iterable, "slice", [])
|
|
233
|
+
# comparator: (a, b) => (a > b) - (a < b) or with key
|
|
234
|
+
if key is None:
|
|
235
|
+
cmp_expr = JSBinary(
|
|
236
|
+
JSBinary(JSIdentifier("a"), ">", JSIdentifier("b")),
|
|
237
|
+
"-",
|
|
238
|
+
JSBinary(JSIdentifier("a"), "<", JSIdentifier("b")),
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
key_js = JSExpr.of(key)
|
|
242
|
+
cmp_expr = JSBinary(
|
|
243
|
+
JSBinary(
|
|
244
|
+
JSCall(key_js, [JSIdentifier("a")]),
|
|
245
|
+
">",
|
|
246
|
+
JSCall(key_js, [JSIdentifier("b")]),
|
|
247
|
+
),
|
|
248
|
+
"-",
|
|
249
|
+
JSBinary(
|
|
250
|
+
JSCall(key_js, [JSIdentifier("a")]),
|
|
251
|
+
"<",
|
|
252
|
+
JSCall(key_js, [JSIdentifier("b")]),
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
sort_call = JSMemberCall(clone, "sort", [JSArrowFunction("(a, b)", cmp_expr)])
|
|
256
|
+
if reverse is None:
|
|
257
|
+
return sort_call
|
|
258
|
+
return JSTertiary(
|
|
259
|
+
JSExpr.of(reverse), JSMemberCall(sort_call, "reverse", []), sort_call
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@js_transformer("zip")
|
|
264
|
+
def transform_zip(*args: Any) -> JSExpr:
|
|
265
|
+
"""zip(*iterables) -> Array.from(...) with paired elements"""
|
|
266
|
+
if builtins.len(args) == 0:
|
|
267
|
+
return JSArray([])
|
|
268
|
+
|
|
269
|
+
js_args = [JSExpr.of(a) for a in args]
|
|
270
|
+
|
|
271
|
+
def length_of(x: JSExpr) -> JSExpr:
|
|
272
|
+
return JSMember(x, "length")
|
|
273
|
+
|
|
274
|
+
min_len = length_of(js_args[0])
|
|
275
|
+
for it in js_args[1:]:
|
|
276
|
+
min_len = JSMemberCall(JSIdentifier("Math"), "min", [min_len, length_of(it)])
|
|
277
|
+
|
|
278
|
+
elems = [JSSubscript(arg, JSIdentifier("i")) for arg in js_args]
|
|
279
|
+
make_pair = JSArrowFunction("i", JSArray(elems))
|
|
280
|
+
return JSCall(
|
|
281
|
+
JSMember(JSIdentifier("Array"), "from"),
|
|
282
|
+
[JSMemberCall(JSNew(JSIdentifier("Array"), [min_len]), "keys", []), make_pair],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@js_transformer("pow")
|
|
287
|
+
def transform_pow(*args: Any) -> JSExpr:
|
|
288
|
+
"""pow(base, exp) -> Math.pow(base, exp)"""
|
|
289
|
+
if builtins.len(args) != 2:
|
|
290
|
+
raise JSCompilationError("pow() expects exactly two arguments")
|
|
291
|
+
return JSMemberCall(
|
|
292
|
+
JSIdentifier("Math"), "pow", [JSExpr.of(args[0]), JSExpr.of(args[1])]
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@js_transformer("chr")
|
|
297
|
+
def transform_chr(x: Any) -> JSExpr:
|
|
298
|
+
"""chr(x) -> String.fromCharCode(x)"""
|
|
299
|
+
return JSMemberCall(JSIdentifier("String"), "fromCharCode", [JSExpr.of(x)])
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@js_transformer("ord")
|
|
303
|
+
def transform_ord(x: Any) -> JSExpr:
|
|
304
|
+
"""ord(x) -> x.charCodeAt(0)"""
|
|
305
|
+
return JSMemberCall(JSExpr.of(x), "charCodeAt", [JSNumber(0)])
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@js_transformer("any")
|
|
309
|
+
def transform_any(x: Any) -> JSExpr:
|
|
310
|
+
"""any(iterable) -> iterable.some(v => v)"""
|
|
311
|
+
x = JSExpr.of(x)
|
|
312
|
+
# Optimization: if x is a map call, use .some directly
|
|
313
|
+
if builtins.isinstance(x, JSMemberCall) and x.method == "map" and x.args:
|
|
314
|
+
return JSMemberCall(x.obj, "some", [x.args[0]])
|
|
315
|
+
return JSMemberCall(x, "some", [JSArrowFunction("v", JSIdentifier("v"))])
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@js_transformer("all")
|
|
319
|
+
def transform_all(x: Any) -> JSExpr:
|
|
320
|
+
"""all(iterable) -> iterable.every(v => v)"""
|
|
321
|
+
x = JSExpr.of(x)
|
|
322
|
+
# Optimization: if x is a map call, use .every directly
|
|
323
|
+
if builtins.isinstance(x, JSMemberCall) and x.method == "map" and x.args:
|
|
324
|
+
return JSMemberCall(x.obj, "every", [x.args[0]])
|
|
325
|
+
return JSMemberCall(x, "every", [JSArrowFunction("v", JSIdentifier("v"))])
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@js_transformer("sum")
|
|
329
|
+
def transform_sum(*args: Any) -> JSExpr:
|
|
330
|
+
"""sum(iterable, start=0) -> iterable.reduce((a, b) => a + b, start)"""
|
|
331
|
+
if not (1 <= builtins.len(args) <= 2):
|
|
332
|
+
raise JSCompilationError("sum() expects one or two arguments")
|
|
333
|
+
start = JSExpr.of(args[1]) if builtins.len(args) == 2 else JSNumber(0)
|
|
334
|
+
base = JSExpr.of(args[0])
|
|
335
|
+
reducer = JSArrowFunction(
|
|
336
|
+
"(a, b)", JSBinary(JSIdentifier("a"), "+", JSIdentifier("b"))
|
|
337
|
+
)
|
|
338
|
+
return JSMemberCall(base, "reduce", [reducer, start])
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@js_transformer("divmod")
|
|
342
|
+
def transform_divmod(x: Any, y: Any) -> JSExpr:
|
|
343
|
+
"""divmod(x, y) -> [Math.floor(x / y), x - Math.floor(x / y) * y]"""
|
|
344
|
+
x, y = JSExpr.of(x), JSExpr.of(y)
|
|
345
|
+
q = JSMemberCall(JSIdentifier("Math"), "floor", [JSBinary(x, "/", y)])
|
|
346
|
+
r = JSBinary(x, "-", JSBinary(q, "*", y))
|
|
347
|
+
return JSArray([q, r])
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@js_transformer("isinstance")
|
|
351
|
+
def transform_isinstance(*args: Any) -> JSExpr:
|
|
352
|
+
"""isinstance is not directly supported in v2; raise error."""
|
|
353
|
+
raise JSCompilationError(
|
|
354
|
+
"isinstance() is not supported in JavaScript transpilation"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# Registry of builtin transformers
|
|
359
|
+
BUILTINS = cast(
|
|
360
|
+
builtins.dict[builtins.str, JSTransformer],
|
|
361
|
+
{
|
|
362
|
+
"print": transform_print,
|
|
363
|
+
"len": transform_len,
|
|
364
|
+
"min": transform_min,
|
|
365
|
+
"max": transform_max,
|
|
366
|
+
"abs": transform_abs,
|
|
367
|
+
"round": transform_round,
|
|
368
|
+
"str": transform_str,
|
|
369
|
+
"int": transform_int,
|
|
370
|
+
"float": transform_float,
|
|
371
|
+
"list": transform_list,
|
|
372
|
+
"bool": transform_bool,
|
|
373
|
+
"set": transform_set,
|
|
374
|
+
"tuple": transform_tuple,
|
|
375
|
+
"dict": transform_dict,
|
|
376
|
+
"filter": transform_filter,
|
|
377
|
+
"map": transform_map,
|
|
378
|
+
"reversed": transform_reversed,
|
|
379
|
+
"enumerate": transform_enumerate,
|
|
380
|
+
"range": transform_range,
|
|
381
|
+
"sorted": transform_sorted,
|
|
382
|
+
"zip": transform_zip,
|
|
383
|
+
"pow": transform_pow,
|
|
384
|
+
"chr": transform_chr,
|
|
385
|
+
"ord": transform_ord,
|
|
386
|
+
"any": transform_any,
|
|
387
|
+
"all": transform_all,
|
|
388
|
+
"sum": transform_sum,
|
|
389
|
+
"divmod": transform_divmod,
|
|
390
|
+
"isinstance": transform_isinstance,
|
|
391
|
+
},
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# =============================================================================
|
|
396
|
+
# Builtin Method Transpilation
|
|
397
|
+
# =============================================================================
|
|
398
|
+
#
|
|
399
|
+
# Methods are organized into classes by type (StringMethods, ListMethods, etc.).
|
|
400
|
+
# Each class contains methods that transpile Python methods to their JS equivalents.
|
|
401
|
+
#
|
|
402
|
+
# Methods return None to fall through to the default method call (when no
|
|
403
|
+
# transformation is needed).
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class BuiltinMethods(ABC):
|
|
407
|
+
"""Abstract base class for type-specific method transpilation."""
|
|
408
|
+
|
|
409
|
+
def __init__(self, obj: JSExpr) -> None:
|
|
410
|
+
self.this: JSExpr = obj
|
|
411
|
+
|
|
412
|
+
@classmethod
|
|
413
|
+
@abstractmethod
|
|
414
|
+
def __runtime_check__(cls, expr: JSExpr) -> JSExpr:
|
|
415
|
+
"""Return a JS expression that checks if expr is this type at runtime."""
|
|
416
|
+
...
|
|
417
|
+
|
|
418
|
+
@classmethod
|
|
419
|
+
@abstractmethod
|
|
420
|
+
def __methods__(cls) -> builtins.set[str]:
|
|
421
|
+
"""Return the set of method names this class handles."""
|
|
422
|
+
...
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class StringMethods(BuiltinMethods):
|
|
426
|
+
"""String method transpilation."""
|
|
427
|
+
|
|
428
|
+
@classmethod
|
|
429
|
+
@override
|
|
430
|
+
def __runtime_check__(cls, expr: JSExpr) -> JSExpr:
|
|
431
|
+
return JSBinary(JSUnary("typeof", expr), "===", JSString("string"))
|
|
432
|
+
|
|
433
|
+
@classmethod
|
|
434
|
+
@override
|
|
435
|
+
def __methods__(cls) -> set[str]:
|
|
436
|
+
return STR_METHODS
|
|
437
|
+
|
|
438
|
+
def lower(self) -> JSExpr:
|
|
439
|
+
"""str.lower() -> str.toLowerCase()"""
|
|
440
|
+
return JSMemberCall(self.this, "toLowerCase", [])
|
|
441
|
+
|
|
442
|
+
def upper(self) -> JSExpr:
|
|
443
|
+
"""str.upper() -> str.toUpperCase()"""
|
|
444
|
+
return JSMemberCall(self.this, "toUpperCase", [])
|
|
445
|
+
|
|
446
|
+
def strip(self) -> JSExpr:
|
|
447
|
+
"""str.strip() -> str.trim()"""
|
|
448
|
+
return JSMemberCall(self.this, "trim", [])
|
|
449
|
+
|
|
450
|
+
def lstrip(self) -> JSExpr:
|
|
451
|
+
"""str.lstrip() -> str.trimStart()"""
|
|
452
|
+
return JSMemberCall(self.this, "trimStart", [])
|
|
453
|
+
|
|
454
|
+
def rstrip(self) -> JSExpr:
|
|
455
|
+
"""str.rstrip() -> str.trimEnd()"""
|
|
456
|
+
return JSMemberCall(self.this, "trimEnd", [])
|
|
457
|
+
|
|
458
|
+
def zfill(self, width: JSExpr) -> JSExpr:
|
|
459
|
+
"""str.zfill(width) -> str.padStart(width, '0')"""
|
|
460
|
+
return JSMemberCall(self.this, "padStart", [width, JSString("0")])
|
|
461
|
+
|
|
462
|
+
def startswith(self, prefix: JSExpr) -> JSExpr:
|
|
463
|
+
"""str.startswith(prefix) -> str.startsWith(prefix)"""
|
|
464
|
+
return JSMemberCall(self.this, "startsWith", [prefix])
|
|
465
|
+
|
|
466
|
+
def endswith(self, suffix: JSExpr) -> JSExpr:
|
|
467
|
+
"""str.endswith(suffix) -> str.endsWith(suffix)"""
|
|
468
|
+
return JSMemberCall(self.this, "endsWith", [suffix])
|
|
469
|
+
|
|
470
|
+
def replace(self, old: JSExpr, new: JSExpr) -> JSExpr:
|
|
471
|
+
"""str.replace(old, new) -> str.replaceAll(old, new)"""
|
|
472
|
+
return JSMemberCall(self.this, "replaceAll", [old, new])
|
|
473
|
+
|
|
474
|
+
def capitalize(self) -> JSExpr:
|
|
475
|
+
"""str.capitalize() -> str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()"""
|
|
476
|
+
left = JSMemberCall(
|
|
477
|
+
JSMemberCall(self.this, "charAt", [JSNumber(0)]), "toUpperCase", []
|
|
478
|
+
)
|
|
479
|
+
right = JSMemberCall(
|
|
480
|
+
JSMemberCall(self.this, "slice", [JSNumber(1)]), "toLowerCase", []
|
|
481
|
+
)
|
|
482
|
+
return JSBinary(left, "+", right)
|
|
483
|
+
|
|
484
|
+
def split(self, sep: JSExpr) -> JSExpr | None:
|
|
485
|
+
"""str.split() doesn't need transformation."""
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
def join(self, iterable: JSExpr) -> JSExpr:
|
|
489
|
+
"""str.join(iterable) -> iterable.join(str)"""
|
|
490
|
+
return JSMemberCall(iterable, "join", [self.this])
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
STR_METHODS = {k for k in StringMethods.__dict__ if not k.startswith("_")}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class ListMethods(BuiltinMethods):
|
|
497
|
+
"""List method transpilation."""
|
|
498
|
+
|
|
499
|
+
@classmethod
|
|
500
|
+
@override
|
|
501
|
+
def __runtime_check__(cls, expr: JSExpr) -> JSExpr:
|
|
502
|
+
return JSMemberCall(JSIdentifier("Array"), "isArray", [expr])
|
|
503
|
+
|
|
504
|
+
@classmethod
|
|
505
|
+
@override
|
|
506
|
+
def __methods__(cls) -> set[str]:
|
|
507
|
+
return LIST_METHODS
|
|
508
|
+
|
|
509
|
+
def append(self, value: JSExpr) -> JSExpr:
|
|
510
|
+
"""list.append(value) -> (list.push(value), undefined)"""
|
|
511
|
+
return JSComma([JSMemberCall(self.this, "push", [value]), JSUndefined()])
|
|
512
|
+
|
|
513
|
+
def extend(self, iterable: JSExpr) -> JSExpr:
|
|
514
|
+
"""list.extend(iterable) -> (list.push(...iterable), undefined)"""
|
|
515
|
+
return JSComma(
|
|
516
|
+
[JSMemberCall(self.this, "push", [JSSpread(iterable)]), JSUndefined()]
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
def pop(self, index: JSExpr | None = None) -> JSExpr | None:
|
|
520
|
+
"""list.pop() or list.pop(index)"""
|
|
521
|
+
if index is None:
|
|
522
|
+
return None # Fall through to default .pop()
|
|
523
|
+
return JSSubscript(
|
|
524
|
+
JSMemberCall(self.this, "splice", [index, JSNumber(1)]), JSNumber(0)
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
def copy(self) -> JSExpr:
|
|
528
|
+
"""list.copy() -> list.slice()"""
|
|
529
|
+
return JSMemberCall(self.this, "slice", [])
|
|
530
|
+
|
|
531
|
+
def count(self, value: JSExpr) -> JSExpr:
|
|
532
|
+
"""list.count(value) -> list.filter(v => v === value).length"""
|
|
533
|
+
return JSMember(
|
|
534
|
+
JSMemberCall(
|
|
535
|
+
self.this,
|
|
536
|
+
"filter",
|
|
537
|
+
[JSArrowFunction("v", JSBinary(JSIdentifier("v"), "===", value))],
|
|
538
|
+
),
|
|
539
|
+
"length",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
def index(self, value: JSExpr) -> JSExpr:
|
|
543
|
+
"""list.index(value) -> list.indexOf(value)"""
|
|
544
|
+
return JSMemberCall(self.this, "indexOf", [value])
|
|
545
|
+
|
|
546
|
+
def reverse(self) -> JSExpr:
|
|
547
|
+
"""list.reverse() -> (list.reverse(), undefined)"""
|
|
548
|
+
return JSComma([JSMemberCall(self.this, "reverse", []), JSUndefined()])
|
|
549
|
+
|
|
550
|
+
def sort(self) -> JSExpr:
|
|
551
|
+
"""list.sort() -> (list.sort(), undefined)"""
|
|
552
|
+
return JSComma([JSMemberCall(self.this, "sort", []), JSUndefined()])
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
LIST_METHODS = {k for k in ListMethods.__dict__ if not k.startswith("_")}
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class DictMethods(BuiltinMethods):
|
|
559
|
+
"""Dict (Map) method transpilation."""
|
|
560
|
+
|
|
561
|
+
@classmethod
|
|
562
|
+
@override
|
|
563
|
+
def __runtime_check__(cls, expr: JSExpr) -> JSExpr:
|
|
564
|
+
return JSBinary(expr, "instanceof", JSIdentifier("Map"))
|
|
565
|
+
|
|
566
|
+
@classmethod
|
|
567
|
+
@override
|
|
568
|
+
def __methods__(cls) -> set[str]:
|
|
569
|
+
return DICT_METHODS
|
|
570
|
+
|
|
571
|
+
def get(self, key: JSExpr, default: JSExpr | None = None) -> JSExpr | None:
|
|
572
|
+
"""dict.get(key, default) -> dict.get(key) ?? default"""
|
|
573
|
+
if default is None:
|
|
574
|
+
return None # Fall through to default .get()
|
|
575
|
+
return JSBinary(JSMemberCall(self.this, "get", [key]), "??", default)
|
|
576
|
+
|
|
577
|
+
def keys(self) -> JSExpr:
|
|
578
|
+
"""dict.keys() -> [...dict.keys()]"""
|
|
579
|
+
return JSArray([JSSpread(JSMemberCall(self.this, "keys", []))])
|
|
580
|
+
|
|
581
|
+
def values(self) -> JSExpr:
|
|
582
|
+
"""dict.values() -> [...dict.values()]"""
|
|
583
|
+
return JSArray([JSSpread(JSMemberCall(self.this, "values", []))])
|
|
584
|
+
|
|
585
|
+
def items(self) -> JSExpr:
|
|
586
|
+
"""dict.items() -> [...dict.entries()]"""
|
|
587
|
+
return JSArray([JSSpread(JSMemberCall(self.this, "entries", []))])
|
|
588
|
+
|
|
589
|
+
def copy(self) -> JSExpr:
|
|
590
|
+
"""dict.copy() -> new Map(dict.entries())"""
|
|
591
|
+
return JSNew(JSIdentifier("Map"), [JSMemberCall(self.this, "entries", [])])
|
|
592
|
+
|
|
593
|
+
def clear(self) -> JSExpr | None:
|
|
594
|
+
"""dict.clear() doesn't need transformation."""
|
|
595
|
+
return None
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
DICT_METHODS = {k for k in DictMethods.__dict__ if not k.startswith("_")}
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class SetMethods(BuiltinMethods):
|
|
602
|
+
"""Set method transpilation."""
|
|
603
|
+
|
|
604
|
+
@classmethod
|
|
605
|
+
@override
|
|
606
|
+
def __runtime_check__(cls, expr: JSExpr) -> JSExpr:
|
|
607
|
+
return JSBinary(expr, "instanceof", JSIdentifier("Set"))
|
|
608
|
+
|
|
609
|
+
@classmethod
|
|
610
|
+
@override
|
|
611
|
+
def __methods__(cls) -> set[str]:
|
|
612
|
+
return SET_METHODS
|
|
613
|
+
|
|
614
|
+
def add(self, value: JSExpr) -> JSExpr | None:
|
|
615
|
+
"""set.add() doesn't need transformation."""
|
|
616
|
+
return None
|
|
617
|
+
|
|
618
|
+
def remove(self, value: JSExpr) -> JSExpr:
|
|
619
|
+
"""set.remove(value) -> set.delete(value)"""
|
|
620
|
+
return JSMemberCall(self.this, "delete", [value])
|
|
621
|
+
|
|
622
|
+
def discard(self, value: JSExpr) -> JSExpr:
|
|
623
|
+
"""set.discard(value) -> set.delete(value)"""
|
|
624
|
+
return JSMemberCall(self.this, "delete", [value])
|
|
625
|
+
|
|
626
|
+
def clear(self) -> JSExpr | None:
|
|
627
|
+
"""set.clear() doesn't need transformation."""
|
|
628
|
+
return None
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
SET_METHODS = {k for k in SetMethods.__dict__ if not k.startswith("_")}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
# Collect all known method names for quick lookup
|
|
635
|
+
ALL_METHODS = STR_METHODS | LIST_METHODS | DICT_METHODS | SET_METHODS
|
|
636
|
+
|
|
637
|
+
# Method classes in priority order (higher priority = later in list = outermost ternary)
|
|
638
|
+
# We prefer string/list semantics first, then set, then dict.
|
|
639
|
+
METHOD_CLASSES: list[type[BuiltinMethods]] = [
|
|
640
|
+
DictMethods,
|
|
641
|
+
SetMethods,
|
|
642
|
+
ListMethods,
|
|
643
|
+
StringMethods,
|
|
644
|
+
]
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _try_dispatch_method(
|
|
648
|
+
cls: type[BuiltinMethods], obj: JSExpr, method: str, args: list[JSExpr]
|
|
649
|
+
) -> JSExpr | None:
|
|
650
|
+
"""Try to dispatch a method call to a specific builtin class.
|
|
651
|
+
|
|
652
|
+
Returns the transformed expression, or None if the method returns None
|
|
653
|
+
(fall through to default) or if dispatch fails.
|
|
654
|
+
"""
|
|
655
|
+
if method not in cls.__methods__():
|
|
656
|
+
return None
|
|
657
|
+
|
|
658
|
+
try:
|
|
659
|
+
handler = cls(obj)
|
|
660
|
+
method_fn = getattr(handler, method, None)
|
|
661
|
+
if method_fn is None:
|
|
662
|
+
return None
|
|
663
|
+
return method_fn(*args)
|
|
664
|
+
except TypeError:
|
|
665
|
+
return None
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def emit_method(obj: JSExpr, method: str, args: list[JSExpr]) -> JSExpr | None:
|
|
669
|
+
"""Emit a method call, handling Python builtin methods.
|
|
670
|
+
|
|
671
|
+
For known literal types (JSString, JSTemplate, JSArray, JSNew Set/Map),
|
|
672
|
+
dispatches directly without runtime checks.
|
|
673
|
+
|
|
674
|
+
For unknown types, builds a ternary chain that checks types at runtime
|
|
675
|
+
and dispatches to the appropriate method implementation.
|
|
676
|
+
|
|
677
|
+
Returns:
|
|
678
|
+
JSExpr if the method should be transpiled specially
|
|
679
|
+
None if the method should be emitted as a regular method call
|
|
680
|
+
"""
|
|
681
|
+
if method not in ALL_METHODS:
|
|
682
|
+
return None
|
|
683
|
+
|
|
684
|
+
# Fast path: known literal types - dispatch directly without runtime checks
|
|
685
|
+
if builtins.isinstance(obj, (JSString, JSTemplate)):
|
|
686
|
+
if method in StringMethods.__methods__():
|
|
687
|
+
result = _try_dispatch_method(StringMethods, obj, method, args)
|
|
688
|
+
if result is not None:
|
|
689
|
+
return result
|
|
690
|
+
return None
|
|
691
|
+
|
|
692
|
+
if builtins.isinstance(obj, JSArray):
|
|
693
|
+
if method in ListMethods.__methods__():
|
|
694
|
+
result = _try_dispatch_method(ListMethods, obj, method, args)
|
|
695
|
+
if result is not None:
|
|
696
|
+
return result
|
|
697
|
+
return None
|
|
698
|
+
|
|
699
|
+
# Fast path: new Set(...) and new Map(...) are known types
|
|
700
|
+
if builtins.isinstance(obj, JSNew) and builtins.isinstance(obj.ctor, JSIdentifier):
|
|
701
|
+
if obj.ctor.name == "Set" and method in SetMethods.__methods__():
|
|
702
|
+
result = _try_dispatch_method(SetMethods, obj, method, args)
|
|
703
|
+
if result is not None:
|
|
704
|
+
return result
|
|
705
|
+
return None
|
|
706
|
+
if obj.ctor.name == "Map" and method in DictMethods.__methods__():
|
|
707
|
+
result = _try_dispatch_method(DictMethods, obj, method, args)
|
|
708
|
+
if result is not None:
|
|
709
|
+
return result
|
|
710
|
+
return None
|
|
711
|
+
|
|
712
|
+
# Slow path: unknown type - build ternary chain with runtime type checks
|
|
713
|
+
# Start with the default fallback (regular method call)
|
|
714
|
+
default_expr = JSMemberCall(obj, method, args)
|
|
715
|
+
expr: JSExpr = default_expr
|
|
716
|
+
|
|
717
|
+
# Apply in increasing priority so that later (higher priority) wrappers
|
|
718
|
+
# end up outermost in the final expression.
|
|
719
|
+
for cls in METHOD_CLASSES:
|
|
720
|
+
if method not in cls.__methods__():
|
|
721
|
+
continue
|
|
722
|
+
|
|
723
|
+
dispatch_expr = _try_dispatch_method(cls, obj, method, args)
|
|
724
|
+
if dispatch_expr is not None:
|
|
725
|
+
expr = JSTertiary(cls.__runtime_check__(obj), dispatch_expr, expr)
|
|
726
|
+
|
|
727
|
+
# If we built ternaries, return them; otherwise return None to fall through
|
|
728
|
+
if expr is not default_expr:
|
|
729
|
+
return expr
|
|
730
|
+
|
|
731
|
+
return None
|