pulse-framework 0.1.44__py3-none-any.whl → 0.1.47__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 (80) hide show
  1. pulse/__init__.py +10 -24
  2. pulse/app.py +3 -25
  3. pulse/codegen/codegen.py +43 -88
  4. pulse/codegen/js.py +35 -5
  5. pulse/codegen/templates/route.py +341 -254
  6. pulse/form.py +1 -1
  7. pulse/helpers.py +40 -8
  8. pulse/hooks/core.py +2 -2
  9. pulse/hooks/effects.py +1 -1
  10. pulse/hooks/init.py +2 -1
  11. pulse/hooks/setup.py +1 -1
  12. pulse/hooks/stable.py +2 -2
  13. pulse/hooks/states.py +2 -2
  14. pulse/html/props.py +3 -2
  15. pulse/html/tags.py +135 -0
  16. pulse/html/tags.pyi +4 -0
  17. pulse/js/__init__.py +110 -0
  18. pulse/js/__init__.pyi +95 -0
  19. pulse/js/_types.py +297 -0
  20. pulse/js/array.py +253 -0
  21. pulse/js/console.py +47 -0
  22. pulse/js/date.py +113 -0
  23. pulse/js/document.py +138 -0
  24. pulse/js/error.py +139 -0
  25. pulse/js/json.py +62 -0
  26. pulse/js/map.py +84 -0
  27. pulse/js/math.py +66 -0
  28. pulse/js/navigator.py +76 -0
  29. pulse/js/number.py +54 -0
  30. pulse/js/object.py +173 -0
  31. pulse/js/promise.py +150 -0
  32. pulse/js/regexp.py +54 -0
  33. pulse/js/set.py +109 -0
  34. pulse/js/string.py +35 -0
  35. pulse/js/weakmap.py +50 -0
  36. pulse/js/weakset.py +45 -0
  37. pulse/js/window.py +199 -0
  38. pulse/messages.py +22 -3
  39. pulse/queries/client.py +7 -7
  40. pulse/queries/effect.py +16 -0
  41. pulse/queries/infinite_query.py +138 -29
  42. pulse/queries/mutation.py +1 -15
  43. pulse/queries/protocol.py +136 -0
  44. pulse/queries/query.py +610 -174
  45. pulse/queries/store.py +11 -14
  46. pulse/react_component.py +167 -14
  47. pulse/reactive.py +19 -1
  48. pulse/reactive_extensions.py +5 -5
  49. pulse/render_session.py +185 -59
  50. pulse/renderer.py +80 -158
  51. pulse/routing.py +1 -18
  52. pulse/transpiler/__init__.py +131 -0
  53. pulse/transpiler/builtins.py +731 -0
  54. pulse/transpiler/constants.py +110 -0
  55. pulse/transpiler/context.py +26 -0
  56. pulse/transpiler/errors.py +2 -0
  57. pulse/transpiler/function.py +250 -0
  58. pulse/transpiler/ids.py +16 -0
  59. pulse/transpiler/imports.py +409 -0
  60. pulse/transpiler/js_module.py +274 -0
  61. pulse/transpiler/modules/__init__.py +30 -0
  62. pulse/transpiler/modules/asyncio.py +38 -0
  63. pulse/transpiler/modules/json.py +20 -0
  64. pulse/transpiler/modules/math.py +320 -0
  65. pulse/transpiler/modules/re.py +466 -0
  66. pulse/transpiler/modules/tags.py +268 -0
  67. pulse/transpiler/modules/typing.py +59 -0
  68. pulse/transpiler/nodes.py +1216 -0
  69. pulse/transpiler/py_module.py +119 -0
  70. pulse/transpiler/transpiler.py +938 -0
  71. pulse/transpiler/utils.py +4 -0
  72. pulse/types/event_handler.py +3 -2
  73. pulse/vdom.py +212 -13
  74. {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/METADATA +1 -1
  75. pulse_framework-0.1.47.dist-info/RECORD +119 -0
  76. pulse/codegen/imports.py +0 -204
  77. pulse/css.py +0 -155
  78. pulse_framework-0.1.44.dist-info/RECORD +0 -79
  79. {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/WHEEL +0 -0
  80. {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.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