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