pulse-framework 0.1.50__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.
- pulse/__init__.py +542 -562
- pulse/_examples.py +29 -0
- pulse/app.py +0 -14
- pulse/cli/cmd.py +96 -80
- pulse/cli/dependencies.py +10 -41
- pulse/cli/folder_lock.py +3 -3
- pulse/cli/helpers.py +40 -67
- pulse/cli/logging.py +102 -0
- pulse/cli/packages.py +16 -0
- pulse/cli/processes.py +40 -23
- pulse/codegen/codegen.py +70 -35
- pulse/codegen/js.py +2 -4
- pulse/codegen/templates/route.py +94 -146
- pulse/component.py +115 -0
- pulse/components/for_.py +1 -1
- pulse/components/if_.py +1 -1
- pulse/components/react_router.py +16 -22
- pulse/{html → dom}/events.py +1 -1
- pulse/{html → dom}/props.py +6 -6
- pulse/{html → dom}/tags.py +11 -11
- pulse/dom/tags.pyi +480 -0
- pulse/form.py +7 -6
- pulse/hooks/init.py +1 -13
- pulse/js/__init__.py +37 -41
- pulse/js/__init__.pyi +22 -2
- pulse/js/_types.py +5 -3
- pulse/js/array.py +121 -38
- pulse/js/console.py +9 -9
- pulse/js/date.py +22 -19
- pulse/js/document.py +8 -4
- pulse/js/error.py +12 -14
- pulse/js/json.py +4 -3
- pulse/js/map.py +17 -7
- pulse/js/math.py +2 -2
- pulse/js/navigator.py +4 -4
- pulse/js/number.py +8 -8
- pulse/js/object.py +9 -13
- pulse/js/promise.py +25 -9
- pulse/js/regexp.py +6 -6
- pulse/js/set.py +20 -8
- pulse/js/string.py +7 -7
- pulse/js/weakmap.py +6 -6
- pulse/js/weakset.py +6 -6
- pulse/js/window.py +17 -14
- pulse/messages.py +1 -4
- pulse/react_component.py +3 -999
- pulse/render_session.py +74 -66
- pulse/renderer.py +311 -238
- pulse/routing.py +1 -10
- pulse/serializer.py +11 -1
- pulse/transpiler/__init__.py +84 -114
- pulse/transpiler/builtins.py +661 -343
- pulse/transpiler/errors.py +78 -2
- pulse/transpiler/function.py +463 -133
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +230 -325
- pulse/transpiler/js_module.py +218 -209
- pulse/transpiler/modules/__init__.py +16 -13
- pulse/transpiler/modules/asyncio.py +45 -26
- pulse/transpiler/modules/json.py +12 -8
- pulse/transpiler/modules/math.py +161 -216
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +231 -0
- pulse/transpiler/modules/typing.py +33 -28
- pulse/transpiler/nodes.py +1607 -923
- pulse/transpiler/py_module.py +118 -95
- pulse/transpiler/react_component.py +51 -0
- pulse/transpiler/transpiler.py +593 -437
- pulse/transpiler/vdom.py +255 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/METADATA +1 -1
- pulse_framework-0.1.52.dist-info/RECORD +120 -0
- pulse/html/tags.pyi +0 -470
- pulse/transpiler/constants.py +0 -110
- pulse/transpiler/context.py +0 -26
- pulse/transpiler/ids.py +0 -16
- pulse/transpiler/modules/re.py +0 -466
- pulse/transpiler/modules/tags.py +0 -268
- pulse/transpiler/utils.py +0 -4
- pulse/vdom.py +0 -667
- pulse_framework-0.1.50.dist-info/RECORD +0 -119
- /pulse/{html → dom}/__init__.py +0 -0
- /pulse/{html → dom}/elements.py +0 -0
- /pulse/{html → dom}/svg.py +0 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
pulse/transpiler/transpiler.py
CHANGED
|
@@ -1,93 +1,129 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Python -> JavaScript transpiler
|
|
2
|
+
Python -> JavaScript transpiler using v2 nodes.
|
|
3
3
|
|
|
4
|
-
Transpiles a restricted subset of Python into
|
|
5
|
-
|
|
6
|
-
- Python syntax -> JS syntax conversion
|
|
7
|
-
- Python builtin functions -> JS equivalents
|
|
8
|
-
- Python builtin methods (str, list, dict, set) -> JS equivalents
|
|
9
|
-
|
|
10
|
-
This transpiler is designed for use with @javascript decorated functions.
|
|
4
|
+
Transpiles a restricted subset of Python into v2 Expr/Stmt AST nodes.
|
|
5
|
+
Dependencies are resolved through a dict[str, Expr] mapping.
|
|
11
6
|
"""
|
|
12
7
|
|
|
13
8
|
from __future__ import annotations
|
|
14
9
|
|
|
15
10
|
import ast
|
|
16
11
|
import re
|
|
17
|
-
from collections.abc import Callable
|
|
12
|
+
from collections.abc import Callable, Mapping
|
|
18
13
|
from typing import Any
|
|
19
14
|
|
|
20
|
-
from pulse.transpiler.
|
|
15
|
+
from pulse.transpiler.builtins import BUILTINS, emit_method
|
|
16
|
+
from pulse.transpiler.errors import TranspileError
|
|
21
17
|
from pulse.transpiler.nodes import (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
JSSingleStmt,
|
|
50
|
-
JSSpread,
|
|
51
|
-
JSStmt,
|
|
52
|
-
JSStmtExpr,
|
|
53
|
-
JSString,
|
|
54
|
-
JSSubscript,
|
|
55
|
-
JSTemplate,
|
|
56
|
-
JSTertiary,
|
|
57
|
-
JSUnary,
|
|
58
|
-
JSUndefined,
|
|
59
|
-
JSWhile,
|
|
18
|
+
Array,
|
|
19
|
+
Arrow,
|
|
20
|
+
Assign,
|
|
21
|
+
Binary,
|
|
22
|
+
Block,
|
|
23
|
+
Break,
|
|
24
|
+
Call,
|
|
25
|
+
Continue,
|
|
26
|
+
Expr,
|
|
27
|
+
ExprStmt,
|
|
28
|
+
ForOf,
|
|
29
|
+
Function,
|
|
30
|
+
Identifier,
|
|
31
|
+
If,
|
|
32
|
+
Literal,
|
|
33
|
+
Member,
|
|
34
|
+
Return,
|
|
35
|
+
Spread,
|
|
36
|
+
Stmt,
|
|
37
|
+
StmtSequence,
|
|
38
|
+
Subscript,
|
|
39
|
+
Template,
|
|
40
|
+
Ternary,
|
|
41
|
+
Throw,
|
|
42
|
+
TryStmt,
|
|
43
|
+
Unary,
|
|
44
|
+
While,
|
|
60
45
|
)
|
|
61
46
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
47
|
+
ALLOWED_BINOPS: dict[type[ast.operator], str] = {
|
|
48
|
+
ast.Add: "+",
|
|
49
|
+
ast.Sub: "-",
|
|
50
|
+
ast.Mult: "*",
|
|
51
|
+
ast.Div: "/",
|
|
52
|
+
ast.Mod: "%",
|
|
53
|
+
ast.Pow: "**",
|
|
54
|
+
# Bitwise operators
|
|
55
|
+
ast.BitAnd: "&",
|
|
56
|
+
ast.BitOr: "|",
|
|
57
|
+
ast.BitXor: "^",
|
|
58
|
+
ast.LShift: "<<",
|
|
59
|
+
ast.RShift: ">>",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ALLOWED_UNOPS: dict[type[ast.unaryop], str] = {
|
|
63
|
+
ast.UAdd: "+",
|
|
64
|
+
ast.USub: "-",
|
|
65
|
+
ast.Not: "!",
|
|
66
|
+
ast.Invert: "~", # Bitwise NOT
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ALLOWED_CMPOPS: dict[type[ast.cmpop], str] = {
|
|
70
|
+
ast.Eq: "===",
|
|
71
|
+
ast.NotEq: "!==",
|
|
72
|
+
ast.Lt: "<",
|
|
73
|
+
ast.LtE: "<=",
|
|
74
|
+
ast.Gt: ">",
|
|
75
|
+
ast.GtE: ">=",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Transpiler:
|
|
80
|
+
"""Transpile Python AST to v2 Expr/Stmt AST nodes.
|
|
81
|
+
|
|
82
|
+
Takes a function definition and a dictionary of dependencies.
|
|
83
|
+
Dependencies are substituted when their names are referenced.
|
|
84
|
+
|
|
85
|
+
Dependencies are Expr instances. Expr subclasses can override:
|
|
86
|
+
- transpile_call: custom call behavior (e.g., JSX components)
|
|
87
|
+
- transpile_getattr: custom attribute access
|
|
88
|
+
- transpile_subscript: custom subscript behavior
|
|
68
89
|
"""
|
|
69
90
|
|
|
70
91
|
fndef: ast.FunctionDef | ast.AsyncFunctionDef
|
|
71
92
|
args: list[str]
|
|
72
|
-
deps:
|
|
93
|
+
deps: Mapping[str, Expr]
|
|
73
94
|
locals: set[str]
|
|
95
|
+
jsx: bool
|
|
74
96
|
_temp_counter: int
|
|
75
|
-
_is_async: bool
|
|
76
97
|
|
|
77
98
|
def __init__(
|
|
78
99
|
self,
|
|
79
100
|
fndef: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
80
|
-
|
|
81
|
-
|
|
101
|
+
deps: Mapping[str, Expr],
|
|
102
|
+
*,
|
|
103
|
+
jsx: bool = False,
|
|
82
104
|
) -> None:
|
|
83
105
|
self.fndef = fndef
|
|
106
|
+
# Collect all argument names (regular, vararg, kwonly, kwarg)
|
|
107
|
+
args: list[str] = [arg.arg for arg in fndef.args.args]
|
|
108
|
+
if fndef.args.vararg:
|
|
109
|
+
args.append(fndef.args.vararg.arg)
|
|
110
|
+
args.extend(arg.arg for arg in fndef.args.kwonlyargs)
|
|
111
|
+
if fndef.args.kwarg:
|
|
112
|
+
args.append(fndef.args.kwarg.arg)
|
|
84
113
|
self.args = args
|
|
85
114
|
self.deps = deps
|
|
86
|
-
|
|
87
|
-
self.locals = set(args)
|
|
115
|
+
self.jsx = jsx
|
|
116
|
+
self.locals = set(self.args)
|
|
88
117
|
self._temp_counter = 0
|
|
89
|
-
|
|
90
|
-
|
|
118
|
+
self.init_temp_counter()
|
|
119
|
+
|
|
120
|
+
def init_temp_counter(self) -> None:
|
|
121
|
+
"""Initialize temp counter to avoid collisions with args or globals."""
|
|
122
|
+
all_names = set(self.args) | set(self.deps.keys())
|
|
123
|
+
counter = 0
|
|
124
|
+
while f"$tmp{counter}" in all_names:
|
|
125
|
+
counter += 1
|
|
126
|
+
self._temp_counter = counter
|
|
91
127
|
|
|
92
128
|
def _fresh_temp(self) -> str:
|
|
93
129
|
"""Generate a fresh temporary variable name."""
|
|
@@ -95,82 +131,141 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
95
131
|
self._temp_counter += 1
|
|
96
132
|
return name
|
|
97
133
|
|
|
98
|
-
def _is_string_expr(self, expr: JSExpr) -> bool:
|
|
99
|
-
"""Check if an expression is known to produce a string."""
|
|
100
|
-
# Check common patterns that produce strings
|
|
101
|
-
if isinstance(expr, JSString):
|
|
102
|
-
return True
|
|
103
|
-
if isinstance(expr, JSCall) and isinstance(expr.callee, JSIdentifier):
|
|
104
|
-
return expr.callee.name == "String"
|
|
105
|
-
if isinstance(expr, JSMemberCall):
|
|
106
|
-
# Methods that return strings
|
|
107
|
-
return expr.method in (
|
|
108
|
-
"toFixed",
|
|
109
|
-
"toExponential",
|
|
110
|
-
"toString",
|
|
111
|
-
"toUpperCase",
|
|
112
|
-
"toLowerCase",
|
|
113
|
-
"trim",
|
|
114
|
-
"padStart",
|
|
115
|
-
"padEnd",
|
|
116
|
-
)
|
|
117
|
-
return False
|
|
118
|
-
|
|
119
134
|
# --- Entrypoint ---------------------------------------------------------
|
|
120
|
-
def transpile(self, name: str | None = None) -> JSFunctionDef:
|
|
121
|
-
"""Transpile the function definition to a JS function.
|
|
122
135
|
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
def transpile(self) -> Function | Arrow:
|
|
137
|
+
"""Transpile the function to a Function or Arrow node.
|
|
138
|
+
|
|
139
|
+
For single-expression functions (or single return), produces Arrow:
|
|
140
|
+
(params) => expr
|
|
141
|
+
|
|
142
|
+
For multi-statement functions, produces Function:
|
|
143
|
+
function(params) { ... }
|
|
144
|
+
|
|
145
|
+
For JSX functions, produces Function with destructured props parameter:
|
|
146
|
+
function({param1, param2 = default}) { ... }
|
|
125
147
|
"""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
body = self.fndef.body
|
|
149
|
+
|
|
150
|
+
# Skip docstrings
|
|
151
|
+
if (
|
|
152
|
+
body
|
|
153
|
+
and isinstance(body[0], ast.Expr)
|
|
154
|
+
and isinstance(body[0].value, ast.Constant)
|
|
155
|
+
and isinstance(body[0].value.value, str)
|
|
156
|
+
):
|
|
157
|
+
body = body[1:]
|
|
158
|
+
|
|
159
|
+
# Arrow optimizations (only for non-JSX)
|
|
160
|
+
if not self.jsx:
|
|
161
|
+
if not body:
|
|
162
|
+
return Arrow(self.args, Literal(None))
|
|
163
|
+
|
|
164
|
+
if len(body) == 1:
|
|
165
|
+
stmt = body[0]
|
|
166
|
+
if isinstance(stmt, ast.Return):
|
|
167
|
+
expr = self.emit_expr(stmt.value)
|
|
168
|
+
return Arrow(self.args, expr)
|
|
169
|
+
if isinstance(stmt, ast.Expr):
|
|
170
|
+
expr = self.emit_expr(stmt.value)
|
|
171
|
+
return Arrow(self.args, expr)
|
|
172
|
+
|
|
173
|
+
# General case: Function (for JSX or multi-statement)
|
|
174
|
+
stmts = [self.emit_stmt(s) for s in body]
|
|
175
|
+
is_async = isinstance(self.fndef, ast.AsyncFunctionDef)
|
|
176
|
+
args = [self._jsx_args()] if self.jsx else self.args
|
|
177
|
+
return Function(args, stmts, is_async=is_async)
|
|
178
|
+
|
|
179
|
+
def _jsx_args(self) -> str:
|
|
180
|
+
"""Build a destructured props parameter for JSX functions.
|
|
181
|
+
|
|
182
|
+
React components receive a single props object, so parameters
|
|
183
|
+
are emitted as a destructuring pattern: {param1, param2 = default, ...}
|
|
184
|
+
"""
|
|
185
|
+
args = self.fndef.args
|
|
186
|
+
destructure_parts: list[str] = []
|
|
187
|
+
default_out: list[str] = []
|
|
188
|
+
|
|
189
|
+
# Regular arguments (may have defaults at the end)
|
|
190
|
+
num_defaults = len(args.defaults)
|
|
191
|
+
num_args = len(args.args)
|
|
192
|
+
for i, arg in enumerate(args.args):
|
|
193
|
+
param_name = arg.arg
|
|
194
|
+
# Defaults align to the right: if we have 3 args and 1 default,
|
|
195
|
+
# the default is for args[2], not args[0]
|
|
196
|
+
default_idx = i - (num_args - num_defaults)
|
|
197
|
+
if default_idx >= 0:
|
|
198
|
+
# Has a default value
|
|
199
|
+
default_node = args.defaults[default_idx]
|
|
200
|
+
default_expr = self.emit_expr(default_node)
|
|
201
|
+
default_out.clear()
|
|
202
|
+
default_expr.emit(default_out)
|
|
203
|
+
destructure_parts.append(f"{param_name} = {''.join(default_out)}")
|
|
204
|
+
else:
|
|
205
|
+
# No default
|
|
206
|
+
destructure_parts.append(param_name)
|
|
207
|
+
|
|
208
|
+
# *args (VAR_POSITIONAL)
|
|
209
|
+
if args.vararg:
|
|
210
|
+
destructure_parts.append(args.vararg.arg)
|
|
211
|
+
|
|
212
|
+
# Keyword-only arguments
|
|
213
|
+
for i, arg in enumerate(args.kwonlyargs):
|
|
214
|
+
param_name = arg.arg
|
|
215
|
+
default_node = args.kw_defaults[i]
|
|
216
|
+
if default_node is not None:
|
|
217
|
+
# Has a default value
|
|
218
|
+
default_expr = self.emit_expr(default_node)
|
|
219
|
+
default_out.clear()
|
|
220
|
+
default_expr.emit(default_out)
|
|
221
|
+
destructure_parts.append(f"{param_name} = {''.join(default_out)}")
|
|
222
|
+
else:
|
|
223
|
+
# No default
|
|
224
|
+
destructure_parts.append(param_name)
|
|
225
|
+
|
|
226
|
+
# **kwargs (VAR_KEYWORD)
|
|
227
|
+
if args.kwarg:
|
|
228
|
+
destructure_parts.append(f"...{args.kwarg.arg}")
|
|
229
|
+
|
|
230
|
+
return "{" + ", ".join(destructure_parts) + "}"
|
|
141
231
|
|
|
142
232
|
# --- Statements ----------------------------------------------------------
|
|
143
|
-
|
|
233
|
+
|
|
234
|
+
def emit_stmt(self, node: ast.stmt) -> Stmt:
|
|
144
235
|
"""Emit a statement."""
|
|
145
236
|
if isinstance(node, ast.Return):
|
|
146
|
-
|
|
237
|
+
value = self.emit_expr(node.value) if node.value else None
|
|
238
|
+
return Return(value)
|
|
147
239
|
|
|
148
240
|
if isinstance(node, ast.Break):
|
|
149
|
-
return
|
|
241
|
+
return Break()
|
|
150
242
|
|
|
151
243
|
if isinstance(node, ast.Continue):
|
|
152
|
-
return
|
|
244
|
+
return Continue()
|
|
153
245
|
|
|
154
246
|
if isinstance(node, ast.Pass):
|
|
155
|
-
# Pass is a no-op, emit empty
|
|
156
|
-
return
|
|
247
|
+
# Pass is a no-op, emit empty block
|
|
248
|
+
return Block([])
|
|
157
249
|
|
|
158
250
|
if isinstance(node, ast.AugAssign):
|
|
159
251
|
if not isinstance(node.target, ast.Name):
|
|
160
|
-
raise
|
|
252
|
+
raise TranspileError(
|
|
253
|
+
"Only simple augmented assignments supported", node=node
|
|
254
|
+
)
|
|
161
255
|
target = node.target.id
|
|
162
256
|
op_type = type(node.op)
|
|
163
257
|
if op_type not in ALLOWED_BINOPS:
|
|
164
|
-
raise
|
|
165
|
-
f"Unsupported augmented assignment operator: {op_type.__name__}"
|
|
258
|
+
raise TranspileError(
|
|
259
|
+
f"Unsupported augmented assignment operator: {op_type.__name__}",
|
|
260
|
+
node=node,
|
|
166
261
|
)
|
|
167
262
|
value_expr = self.emit_expr(node.value)
|
|
168
|
-
return
|
|
263
|
+
return Assign(target, value_expr, op=ALLOWED_BINOPS[op_type])
|
|
169
264
|
|
|
170
265
|
if isinstance(node, ast.Assign):
|
|
171
266
|
if len(node.targets) != 1:
|
|
172
|
-
raise
|
|
173
|
-
"Multiple assignment targets
|
|
267
|
+
raise TranspileError(
|
|
268
|
+
"Multiple assignment targets not supported", node=node
|
|
174
269
|
)
|
|
175
270
|
target_node = node.targets[0]
|
|
176
271
|
|
|
@@ -179,47 +274,44 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
179
274
|
return self._emit_unpacking_assign(target_node, node.value)
|
|
180
275
|
|
|
181
276
|
if not isinstance(target_node, ast.Name):
|
|
182
|
-
raise
|
|
183
|
-
"Only simple assignments to local names
|
|
277
|
+
raise TranspileError(
|
|
278
|
+
"Only simple assignments to local names supported", node=node
|
|
184
279
|
)
|
|
185
280
|
|
|
186
281
|
target = target_node.id
|
|
187
282
|
value_expr = self.emit_expr(node.value)
|
|
188
283
|
|
|
189
284
|
if target in self.locals:
|
|
190
|
-
return
|
|
285
|
+
return Assign(target, value_expr)
|
|
191
286
|
else:
|
|
192
287
|
self.locals.add(target)
|
|
193
|
-
return
|
|
288
|
+
return Assign(target, value_expr, declare="let")
|
|
194
289
|
|
|
195
290
|
if isinstance(node, ast.AnnAssign):
|
|
196
291
|
if not isinstance(node.target, ast.Name):
|
|
197
|
-
raise
|
|
292
|
+
raise TranspileError("Only simple annotated assignments supported")
|
|
198
293
|
target = node.target.id
|
|
199
|
-
value =
|
|
294
|
+
value = Literal(None) if node.value is None else self.emit_expr(node.value)
|
|
200
295
|
if target in self.locals:
|
|
201
|
-
return
|
|
296
|
+
return Assign(target, value)
|
|
202
297
|
else:
|
|
203
298
|
self.locals.add(target)
|
|
204
|
-
return
|
|
299
|
+
return Assign(target, value, declare="let")
|
|
205
300
|
|
|
206
301
|
if isinstance(node, ast.If):
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return
|
|
302
|
+
cond = self.emit_expr(node.test)
|
|
303
|
+
then = [self.emit_stmt(s) for s in node.body]
|
|
304
|
+
else_ = [self.emit_stmt(s) for s in node.orelse]
|
|
305
|
+
return If(cond, then, else_)
|
|
211
306
|
|
|
212
307
|
if isinstance(node, ast.Expr):
|
|
213
308
|
expr = self.emit_expr(node.value)
|
|
214
|
-
|
|
215
|
-
if isinstance(expr, JSStmtExpr):
|
|
216
|
-
return expr.stmt
|
|
217
|
-
return JSSingleStmt(expr)
|
|
309
|
+
return ExprStmt(expr)
|
|
218
310
|
|
|
219
311
|
if isinstance(node, ast.While):
|
|
220
|
-
|
|
312
|
+
cond = self.emit_expr(node.test)
|
|
221
313
|
body = [self.emit_stmt(s) for s in node.body]
|
|
222
|
-
return
|
|
314
|
+
return While(cond, body)
|
|
223
315
|
|
|
224
316
|
if isinstance(node, ast.For):
|
|
225
317
|
return self._emit_for_loop(node)
|
|
@@ -227,93 +319,138 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
227
319
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
228
320
|
return self._emit_nested_function(node)
|
|
229
321
|
|
|
230
|
-
|
|
322
|
+
if isinstance(node, ast.Try):
|
|
323
|
+
return self._emit_try(node)
|
|
324
|
+
|
|
325
|
+
if isinstance(node, ast.Raise):
|
|
326
|
+
return self._emit_raise(node)
|
|
327
|
+
|
|
328
|
+
raise TranspileError(f"Unsupported statement: {type(node).__name__}", node=node)
|
|
231
329
|
|
|
232
330
|
def _emit_unpacking_assign(
|
|
233
331
|
self, target: ast.Tuple | ast.List, value: ast.expr
|
|
234
|
-
) ->
|
|
332
|
+
) -> Stmt:
|
|
235
333
|
"""Emit unpacking assignment: a, b, c = expr"""
|
|
236
334
|
elements = target.elts
|
|
237
335
|
if not elements or not all(isinstance(e, ast.Name) for e in elements):
|
|
238
|
-
raise
|
|
336
|
+
raise TranspileError("Unpacking only supported for simple variables")
|
|
239
337
|
|
|
240
338
|
tmp_name = self._fresh_temp()
|
|
241
339
|
value_expr = self.emit_expr(value)
|
|
242
|
-
stmts: list[
|
|
340
|
+
stmts: list[Stmt] = [Assign(tmp_name, value_expr, declare="const")]
|
|
243
341
|
|
|
244
342
|
for idx, e in enumerate(elements):
|
|
245
343
|
assert isinstance(e, ast.Name)
|
|
246
344
|
name = e.id
|
|
247
|
-
sub =
|
|
345
|
+
sub = Subscript(Identifier(tmp_name), Literal(idx))
|
|
248
346
|
if name in self.locals:
|
|
249
|
-
stmts.append(
|
|
347
|
+
stmts.append(Assign(name, sub))
|
|
250
348
|
else:
|
|
251
349
|
self.locals.add(name)
|
|
252
|
-
stmts.append(
|
|
350
|
+
stmts.append(Assign(name, sub, declare="let"))
|
|
253
351
|
|
|
254
|
-
return
|
|
352
|
+
return StmtSequence(stmts)
|
|
255
353
|
|
|
256
|
-
def _emit_for_loop(self, node: ast.For) ->
|
|
354
|
+
def _emit_for_loop(self, node: ast.For) -> Stmt:
|
|
257
355
|
"""Emit a for loop."""
|
|
258
356
|
# Handle tuple unpacking in for target
|
|
259
357
|
if isinstance(node.target, (ast.Tuple, ast.List)):
|
|
260
358
|
names: list[str] = []
|
|
261
359
|
for e in node.target.elts:
|
|
262
360
|
if not isinstance(e, ast.Name):
|
|
263
|
-
raise
|
|
361
|
+
raise TranspileError(
|
|
264
362
|
"Only simple name targets supported in for-loop unpacking"
|
|
265
363
|
)
|
|
266
364
|
names.append(e.id)
|
|
267
365
|
self.locals.add(e.id)
|
|
268
366
|
iter_expr = self.emit_expr(node.iter)
|
|
269
367
|
body = [self.emit_stmt(s) for s in node.body]
|
|
270
|
-
|
|
368
|
+
# Use array pattern for destructuring
|
|
369
|
+
target = f"[{', '.join(names)}]"
|
|
370
|
+
return ForOf(target, iter_expr, body)
|
|
271
371
|
|
|
272
372
|
if not isinstance(node.target, ast.Name):
|
|
273
|
-
raise
|
|
373
|
+
raise TranspileError("Only simple name targets supported in for-loops")
|
|
274
374
|
|
|
275
375
|
target = node.target.id
|
|
276
376
|
self.locals.add(target)
|
|
277
377
|
iter_expr = self.emit_expr(node.iter)
|
|
278
378
|
body = [self.emit_stmt(s) for s in node.body]
|
|
279
|
-
return
|
|
379
|
+
return ForOf(target, iter_expr, body)
|
|
280
380
|
|
|
281
381
|
def _emit_nested_function(
|
|
282
382
|
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
283
|
-
) ->
|
|
383
|
+
) -> Stmt:
|
|
284
384
|
"""Emit a nested function definition."""
|
|
285
385
|
name = node.name
|
|
286
386
|
params = [arg.arg for arg in node.args.args]
|
|
287
387
|
|
|
288
|
-
# Save current locals and extend with params
|
|
388
|
+
# Save current locals and extend with params
|
|
289
389
|
saved_locals = set(self.locals)
|
|
290
390
|
self.locals.update(params)
|
|
291
391
|
|
|
292
392
|
# Skip docstrings and emit body
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
393
|
+
body_stmts = node.body
|
|
394
|
+
if (
|
|
395
|
+
body_stmts
|
|
396
|
+
and isinstance(body_stmts[0], ast.Expr)
|
|
397
|
+
and isinstance(body_stmts[0].value, ast.Constant)
|
|
398
|
+
and isinstance(body_stmts[0].value.value, str)
|
|
399
|
+
):
|
|
400
|
+
body_stmts = body_stmts[1:]
|
|
401
|
+
|
|
402
|
+
stmts: list[Stmt] = [self.emit_stmt(s) for s in body_stmts]
|
|
303
403
|
|
|
304
404
|
# Restore outer locals and add function name
|
|
305
405
|
self.locals = saved_locals
|
|
306
406
|
self.locals.add(name)
|
|
307
407
|
|
|
308
408
|
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
309
|
-
fn =
|
|
310
|
-
return
|
|
409
|
+
fn = Function(params, stmts, is_async=is_async)
|
|
410
|
+
return Assign(name, fn, declare="const")
|
|
411
|
+
|
|
412
|
+
def _emit_try(self, node: ast.Try) -> Stmt:
|
|
413
|
+
"""Emit a try/except/finally statement."""
|
|
414
|
+
body = [self.emit_stmt(s) for s in node.body]
|
|
415
|
+
|
|
416
|
+
# Handle except handlers - JS only supports single catch
|
|
417
|
+
catch_param: str | None = None
|
|
418
|
+
catch_body: list[Stmt] | None = None
|
|
419
|
+
|
|
420
|
+
if node.handlers:
|
|
421
|
+
if len(node.handlers) > 1:
|
|
422
|
+
raise TranspileError(
|
|
423
|
+
"Multiple except clauses not supported; JS only has one catch block",
|
|
424
|
+
node=node.handlers[1],
|
|
425
|
+
)
|
|
426
|
+
handler = node.handlers[0]
|
|
427
|
+
if handler.name:
|
|
428
|
+
catch_param = handler.name
|
|
429
|
+
self.locals.add(catch_param)
|
|
430
|
+
catch_body = [self.emit_stmt(s) for s in handler.body]
|
|
431
|
+
|
|
432
|
+
# Handle finally
|
|
433
|
+
finally_body: list[Stmt] | None = None
|
|
434
|
+
if node.finalbody:
|
|
435
|
+
finally_body = [self.emit_stmt(s) for s in node.finalbody]
|
|
436
|
+
|
|
437
|
+
return TryStmt(body, catch_param, catch_body, finally_body)
|
|
438
|
+
|
|
439
|
+
def _emit_raise(self, node: ast.Raise) -> Stmt:
|
|
440
|
+
"""Emit a raise statement as throw."""
|
|
441
|
+
if node.exc is None:
|
|
442
|
+
raise TranspileError(
|
|
443
|
+
"Bare raise not supported; use explicit 'raise e' instead", node=node
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
return Throw(self.emit_expr(node.exc))
|
|
311
447
|
|
|
312
448
|
# --- Expressions ---------------------------------------------------------
|
|
313
|
-
|
|
449
|
+
|
|
450
|
+
def emit_expr(self, node: ast.expr | None) -> Expr:
|
|
314
451
|
"""Emit an expression."""
|
|
315
452
|
if node is None:
|
|
316
|
-
return
|
|
453
|
+
return Literal(None)
|
|
317
454
|
|
|
318
455
|
if isinstance(node, ast.Constant):
|
|
319
456
|
return self._emit_constant(node)
|
|
@@ -328,8 +465,9 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
328
465
|
return self._emit_dict(node)
|
|
329
466
|
|
|
330
467
|
if isinstance(node, ast.Set):
|
|
331
|
-
return
|
|
332
|
-
|
|
468
|
+
return Call(
|
|
469
|
+
Identifier("Set"),
|
|
470
|
+
[Array([self.emit_expr(e) for e in node.elts])],
|
|
333
471
|
)
|
|
334
472
|
|
|
335
473
|
if isinstance(node, ast.BinOp):
|
|
@@ -339,17 +477,17 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
339
477
|
return self._emit_unaryop(node)
|
|
340
478
|
|
|
341
479
|
if isinstance(node, ast.BoolOp):
|
|
342
|
-
|
|
343
|
-
return JSLogicalChain(op, [self.emit_expr(v) for v in node.values])
|
|
480
|
+
return self._emit_boolop(node)
|
|
344
481
|
|
|
345
482
|
if isinstance(node, ast.Compare):
|
|
346
483
|
return self._emit_compare(node)
|
|
347
484
|
|
|
348
485
|
if isinstance(node, ast.IfExp):
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
486
|
+
return Ternary(
|
|
487
|
+
self.emit_expr(node.test),
|
|
488
|
+
self.emit_expr(node.body),
|
|
489
|
+
self.emit_expr(node.orelse),
|
|
490
|
+
)
|
|
353
491
|
|
|
354
492
|
if isinstance(node, ast.Call):
|
|
355
493
|
return self._emit_call(node)
|
|
@@ -377,113 +515,134 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
377
515
|
arr = self._emit_comprehension_chain(
|
|
378
516
|
node.generators, lambda: self.emit_expr(node.elt)
|
|
379
517
|
)
|
|
380
|
-
return
|
|
518
|
+
return Call(Identifier("Set"), [arr])
|
|
381
519
|
|
|
382
520
|
if isinstance(node, ast.DictComp):
|
|
383
521
|
pairs = self._emit_comprehension_chain(
|
|
384
522
|
node.generators,
|
|
385
|
-
lambda:
|
|
523
|
+
lambda: Array([self.emit_expr(node.key), self.emit_expr(node.value)]),
|
|
386
524
|
)
|
|
387
|
-
return
|
|
525
|
+
return Call(Identifier("Map"), [pairs])
|
|
388
526
|
|
|
389
527
|
if isinstance(node, ast.Lambda):
|
|
390
528
|
return self._emit_lambda(node)
|
|
391
529
|
|
|
392
530
|
if isinstance(node, ast.Starred):
|
|
393
|
-
return
|
|
531
|
+
return Spread(self.emit_expr(node.value))
|
|
394
532
|
|
|
395
533
|
if isinstance(node, ast.Await):
|
|
396
|
-
|
|
397
|
-
self._is_async = True
|
|
398
|
-
return JSAwait(self.emit_expr(node.value))
|
|
534
|
+
return Unary("await", self.emit_expr(node.value))
|
|
399
535
|
|
|
400
|
-
raise
|
|
536
|
+
raise TranspileError(
|
|
537
|
+
f"Unsupported expression: {type(node).__name__}", node=node
|
|
538
|
+
)
|
|
401
539
|
|
|
402
|
-
def _emit_constant(self, node: ast.Constant) ->
|
|
540
|
+
def _emit_constant(self, node: ast.Constant) -> Expr:
|
|
403
541
|
"""Emit a constant value."""
|
|
404
542
|
v = node.value
|
|
405
543
|
if isinstance(v, str):
|
|
406
544
|
# Use template literals for strings with Unicode line separators
|
|
407
545
|
if "\u2028" in v or "\u2029" in v:
|
|
408
|
-
return
|
|
409
|
-
return
|
|
546
|
+
return Template([v])
|
|
547
|
+
return Literal(v)
|
|
410
548
|
if v is None:
|
|
411
|
-
return
|
|
412
|
-
if v
|
|
413
|
-
return
|
|
414
|
-
if v is False:
|
|
415
|
-
return JSBoolean(False)
|
|
549
|
+
return Literal(None)
|
|
550
|
+
if isinstance(v, bool):
|
|
551
|
+
return Literal(v)
|
|
416
552
|
if isinstance(v, (int, float)):
|
|
417
|
-
return
|
|
418
|
-
raise
|
|
553
|
+
return Literal(v)
|
|
554
|
+
raise TranspileError(f"Unsupported constant type: {type(v).__name__}")
|
|
419
555
|
|
|
420
|
-
def _emit_name(self, node: ast.Name) ->
|
|
421
|
-
"""Emit a name reference.
|
|
422
|
-
|
|
423
|
-
All dependencies are JSExpr subclasses. Behavior is encoded in hooks.
|
|
424
|
-
"""
|
|
556
|
+
def _emit_name(self, node: ast.Name) -> Expr:
|
|
557
|
+
"""Emit a name reference."""
|
|
425
558
|
name = node.id
|
|
426
559
|
|
|
427
|
-
# Check deps first
|
|
560
|
+
# Check deps first
|
|
428
561
|
if name in self.deps:
|
|
429
562
|
return self.deps[name]
|
|
430
563
|
|
|
431
564
|
# Local variable
|
|
432
565
|
if name in self.locals:
|
|
433
|
-
return
|
|
566
|
+
return Identifier(name)
|
|
567
|
+
|
|
568
|
+
# Check builtins
|
|
569
|
+
if name in BUILTINS:
|
|
570
|
+
return BUILTINS[name]
|
|
434
571
|
|
|
435
|
-
raise
|
|
572
|
+
raise TranspileError(f"Unbound name referenced: {name}", node=node)
|
|
436
573
|
|
|
437
|
-
def _emit_list_or_tuple(self, node: ast.List | ast.Tuple) ->
|
|
574
|
+
def _emit_list_or_tuple(self, node: ast.List | ast.Tuple) -> Expr:
|
|
438
575
|
"""Emit a list or tuple literal."""
|
|
439
|
-
parts: list[
|
|
576
|
+
parts: list[Expr] = []
|
|
440
577
|
for e in node.elts:
|
|
441
578
|
if isinstance(e, ast.Starred):
|
|
442
|
-
parts.append(
|
|
579
|
+
parts.append(Spread(self.emit_expr(e.value)))
|
|
443
580
|
else:
|
|
444
581
|
parts.append(self.emit_expr(e))
|
|
445
|
-
return
|
|
582
|
+
return Array(parts)
|
|
446
583
|
|
|
447
|
-
def _emit_dict(self, node: ast.Dict) ->
|
|
584
|
+
def _emit_dict(self, node: ast.Dict) -> Expr:
|
|
448
585
|
"""Emit a dict literal as new Map([...])."""
|
|
449
|
-
entries: list[
|
|
586
|
+
entries: list[Expr] = []
|
|
450
587
|
for k, v in zip(node.keys, node.values, strict=False):
|
|
451
588
|
if k is None:
|
|
452
589
|
# Spread merge
|
|
453
590
|
vexpr = self.emit_expr(v)
|
|
454
|
-
is_map =
|
|
455
|
-
map_entries =
|
|
456
|
-
obj_entries =
|
|
457
|
-
|
|
458
|
-
)
|
|
459
|
-
entries.append(JSSpread(JSTertiary(is_map, map_entries, obj_entries)))
|
|
591
|
+
is_map = Binary(vexpr, "instanceof", Identifier("Map"))
|
|
592
|
+
map_entries = Call(Member(vexpr, "entries"), [])
|
|
593
|
+
obj_entries = Call(Member(Identifier("Object"), "entries"), [vexpr])
|
|
594
|
+
entries.append(Spread(Ternary(is_map, map_entries, obj_entries)))
|
|
460
595
|
continue
|
|
461
596
|
key_expr = self.emit_expr(k)
|
|
462
597
|
val_expr = self.emit_expr(v)
|
|
463
|
-
entries.append(
|
|
464
|
-
return
|
|
598
|
+
entries.append(Array([key_expr, val_expr]))
|
|
599
|
+
return Call(Identifier("Map"), [Array(entries)])
|
|
465
600
|
|
|
466
|
-
def _emit_binop(self, node: ast.BinOp) ->
|
|
601
|
+
def _emit_binop(self, node: ast.BinOp) -> Expr:
|
|
467
602
|
"""Emit a binary operation."""
|
|
468
603
|
op = type(node.op)
|
|
604
|
+
|
|
605
|
+
# Special case: floor division -> Math.floor(x / y)
|
|
606
|
+
if op is ast.FloorDiv:
|
|
607
|
+
left = self.emit_expr(node.left)
|
|
608
|
+
right = self.emit_expr(node.right)
|
|
609
|
+
return Call(
|
|
610
|
+
Member(Identifier("Math"), "floor"),
|
|
611
|
+
[Binary(left, "/", right)],
|
|
612
|
+
)
|
|
613
|
+
|
|
469
614
|
if op not in ALLOWED_BINOPS:
|
|
470
|
-
raise
|
|
615
|
+
raise TranspileError(
|
|
616
|
+
f"Unsupported binary operator: {op.__name__}", node=node
|
|
617
|
+
)
|
|
471
618
|
left = self.emit_expr(node.left)
|
|
472
619
|
right = self.emit_expr(node.right)
|
|
473
|
-
return
|
|
620
|
+
return Binary(left, ALLOWED_BINOPS[op], right)
|
|
474
621
|
|
|
475
|
-
def _emit_unaryop(self, node: ast.UnaryOp) ->
|
|
622
|
+
def _emit_unaryop(self, node: ast.UnaryOp) -> Expr:
|
|
476
623
|
"""Emit a unary operation."""
|
|
477
624
|
op = type(node.op)
|
|
478
625
|
if op not in ALLOWED_UNOPS:
|
|
479
|
-
raise
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
626
|
+
raise TranspileError(
|
|
627
|
+
f"Unsupported unary operator: {op.__name__}", node=node
|
|
628
|
+
)
|
|
629
|
+
return Unary(ALLOWED_UNOPS[op], self.emit_expr(node.operand))
|
|
630
|
+
|
|
631
|
+
def _emit_boolop(self, node: ast.BoolOp) -> Expr:
|
|
632
|
+
"""Emit a boolean operation (and/or chain)."""
|
|
633
|
+
op = "&&" if isinstance(node.op, ast.And) else "||"
|
|
634
|
+
values = [self.emit_expr(v) for v in node.values]
|
|
635
|
+
# Build binary chain: a && b && c -> Binary(Binary(a, &&, b), &&, c)
|
|
636
|
+
result = values[0]
|
|
637
|
+
for v in values[1:]:
|
|
638
|
+
result = Binary(result, op, v)
|
|
639
|
+
return result
|
|
640
|
+
|
|
641
|
+
def _emit_compare(self, node: ast.Compare) -> Expr:
|
|
483
642
|
"""Emit a comparison expression."""
|
|
484
643
|
operands: list[ast.expr] = [node.left, *node.comparators]
|
|
485
|
-
exprs: list[
|
|
486
|
-
cmp_parts: list[
|
|
644
|
+
exprs: list[Expr] = [self.emit_expr(e) for e in operands]
|
|
645
|
+
cmp_parts: list[Expr] = []
|
|
487
646
|
|
|
488
647
|
for i, op in enumerate(node.ops):
|
|
489
648
|
left_node = operands[i]
|
|
@@ -496,16 +655,21 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
496
655
|
|
|
497
656
|
if len(cmp_parts) == 1:
|
|
498
657
|
return cmp_parts[0]
|
|
499
|
-
|
|
658
|
+
|
|
659
|
+
# Chain with &&
|
|
660
|
+
result = cmp_parts[0]
|
|
661
|
+
for v in cmp_parts[1:]:
|
|
662
|
+
result = Binary(result, "&&", v)
|
|
663
|
+
return result
|
|
500
664
|
|
|
501
665
|
def _build_comparison(
|
|
502
666
|
self,
|
|
503
|
-
left_expr:
|
|
667
|
+
left_expr: Expr,
|
|
504
668
|
left_node: ast.expr,
|
|
505
669
|
op: ast.cmpop,
|
|
506
|
-
right_expr:
|
|
670
|
+
right_expr: Expr,
|
|
507
671
|
right_node: ast.expr,
|
|
508
|
-
) ->
|
|
672
|
+
) -> Expr:
|
|
509
673
|
"""Build a single comparison."""
|
|
510
674
|
# Identity comparisons
|
|
511
675
|
if isinstance(op, (ast.Is, ast.IsNot)):
|
|
@@ -515,8 +679,8 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
515
679
|
isinstance(left_node, ast.Constant) and left_node.value is None
|
|
516
680
|
):
|
|
517
681
|
expr = right_expr if isinstance(left_node, ast.Constant) else left_expr
|
|
518
|
-
return
|
|
519
|
-
return
|
|
682
|
+
return Binary(expr, "!=" if is_not else "==", Literal(None))
|
|
683
|
+
return Binary(left_expr, "!==" if is_not else "===", right_expr)
|
|
520
684
|
|
|
521
685
|
# Membership tests
|
|
522
686
|
if isinstance(op, (ast.In, ast.NotIn)):
|
|
@@ -527,140 +691,111 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
527
691
|
# Standard comparisons
|
|
528
692
|
op_type = type(op)
|
|
529
693
|
if op_type not in ALLOWED_CMPOPS:
|
|
530
|
-
raise
|
|
531
|
-
|
|
532
|
-
)
|
|
533
|
-
return JSBinary(left_expr, ALLOWED_CMPOPS[op_type], right_expr)
|
|
694
|
+
raise TranspileError(f"Unsupported comparison operator: {op_type.__name__}")
|
|
695
|
+
return Binary(left_expr, ALLOWED_CMPOPS[op_type], right_expr)
|
|
534
696
|
|
|
535
|
-
def _build_membership_test(
|
|
536
|
-
self, item: JSExpr, container: JSExpr, negate: bool
|
|
537
|
-
) -> JSExpr:
|
|
697
|
+
def _build_membership_test(self, item: Expr, container: Expr, negate: bool) -> Expr:
|
|
538
698
|
"""Build a membership test (in / not in)."""
|
|
539
|
-
is_string =
|
|
540
|
-
is_array =
|
|
541
|
-
is_set =
|
|
542
|
-
is_map =
|
|
699
|
+
is_string = Binary(Unary("typeof", container), "===", Literal("string"))
|
|
700
|
+
is_array = Call(Member(Identifier("Array"), "isArray"), [container])
|
|
701
|
+
is_set = Binary(container, "instanceof", Identifier("Set"))
|
|
702
|
+
is_map = Binary(container, "instanceof", Identifier("Map"))
|
|
543
703
|
|
|
544
|
-
is_array_or_string =
|
|
545
|
-
is_set_or_map =
|
|
704
|
+
is_array_or_string = Binary(is_array, "||", is_string)
|
|
705
|
+
is_set_or_map = Binary(is_set, "||", is_map)
|
|
546
706
|
|
|
547
|
-
has_array_or_string =
|
|
548
|
-
has_set_or_map =
|
|
549
|
-
has_obj =
|
|
707
|
+
has_array_or_string = Call(Member(container, "includes"), [item])
|
|
708
|
+
has_set_or_map = Call(Member(container, "has"), [item])
|
|
709
|
+
has_obj = Binary(item, "in", container)
|
|
550
710
|
|
|
551
|
-
membership_expr =
|
|
711
|
+
membership_expr = Ternary(
|
|
552
712
|
is_array_or_string,
|
|
553
713
|
has_array_or_string,
|
|
554
|
-
|
|
714
|
+
Ternary(is_set_or_map, has_set_or_map, has_obj),
|
|
555
715
|
)
|
|
556
716
|
|
|
557
717
|
if negate:
|
|
558
|
-
return
|
|
718
|
+
return Unary("!", membership_expr)
|
|
559
719
|
return membership_expr
|
|
560
720
|
|
|
561
|
-
def _emit_call(self, node: ast.Call) ->
|
|
562
|
-
"""Emit a function call.
|
|
721
|
+
def _emit_call(self, node: ast.Call) -> Expr:
|
|
722
|
+
"""Emit a function call."""
|
|
723
|
+
# Collect args and kwargs as raw AST values
|
|
724
|
+
args_raw = list(node.args)
|
|
725
|
+
kwargs_raw: dict[str, Any] = {}
|
|
726
|
+
for kw in node.keywords:
|
|
727
|
+
if kw.arg is None:
|
|
728
|
+
raise TranspileError(
|
|
729
|
+
"Spread props (**kwargs) not yet supported in v2 transpiler"
|
|
730
|
+
)
|
|
731
|
+
kwargs_raw[kw.arg] = kw.value
|
|
563
732
|
|
|
564
|
-
|
|
565
|
-
emit_call receives raw Python values (JSExpr instances from emit_expr),
|
|
566
|
-
and decides what to convert using JSExpr.of().
|
|
567
|
-
"""
|
|
568
|
-
# Handle typing.cast: ignore type argument, return value unchanged
|
|
569
|
-
# Must short-circuit before evaluating args to avoid transpiling type annotations
|
|
570
|
-
if isinstance(node.func, ast.Name) and node.func.id == "cast":
|
|
571
|
-
if len(node.args) >= 2:
|
|
572
|
-
return self.emit_expr(node.args[1])
|
|
573
|
-
raise JSCompilationError("typing.cast requires two arguments")
|
|
574
|
-
|
|
575
|
-
# Emit args as JSExpr (they're already the transpiled form)
|
|
576
|
-
args: list[Any] = [self.emit_expr(a) for a in node.args]
|
|
577
|
-
kwargs = self._build_kwargs(node)
|
|
578
|
-
|
|
579
|
-
# Method call: obj.method(args) -> obj.emit_getattr(method).emit_call(args)
|
|
733
|
+
# Method call: obj.method(args) - try builtin method dispatch
|
|
580
734
|
if isinstance(node.func, ast.Attribute):
|
|
581
735
|
obj = self.emit_expr(node.func.value)
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
736
|
+
method = node.func.attr
|
|
737
|
+
args: list[Expr] = [self.emit_expr(a) for a in args_raw]
|
|
738
|
+
kwargs: dict[str, Expr] = {
|
|
739
|
+
k: self.emit_expr(v) for k, v in kwargs_raw.items()
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
# Try builtin method handling with runtime checks
|
|
743
|
+
result = emit_method(obj, method, args, kwargs)
|
|
744
|
+
if result is not None:
|
|
745
|
+
return result
|
|
746
|
+
|
|
747
|
+
# IMPORTANT: derive method expr via transpile_getattr
|
|
748
|
+
method_expr = obj.transpile_getattr(method, self)
|
|
749
|
+
return method_expr.transpile_call(args_raw, kwargs_raw, self)
|
|
750
|
+
|
|
751
|
+
# Function call (or other callable expr)
|
|
586
752
|
callee = self.emit_expr(node.func)
|
|
587
|
-
return callee.
|
|
588
|
-
|
|
589
|
-
def _build_kwargs(self, node: ast.Call) -> dict[str, Any]:
|
|
590
|
-
"""Build kwargs dict from AST Call node.
|
|
591
|
-
|
|
592
|
-
Returns a dict mapping:
|
|
593
|
-
- "propName" -> JSExpr for named kwargs (as raw values)
|
|
594
|
-
- "$spread{N}" -> JSSpread(expr) for **spread kwargs
|
|
595
|
-
|
|
596
|
-
Dict order is preserved (Python 3.7+), so iteration order matches source order.
|
|
597
|
-
Uses $ prefix for spreads since it's not a valid Python identifier.
|
|
598
|
-
"""
|
|
599
|
-
kwargs: dict[str, Any] = {}
|
|
600
|
-
spread_count = 0
|
|
601
|
-
|
|
602
|
-
for kw in node.keywords:
|
|
603
|
-
if kw.arg is None:
|
|
604
|
-
# **kwargs spread - use invalid Python identifier to avoid conflicts
|
|
605
|
-
kwargs[f"$spread{spread_count}"] = JSSpread(self.emit_expr(kw.value))
|
|
606
|
-
spread_count += 1
|
|
607
|
-
else:
|
|
608
|
-
kwargs[kw.arg] = self.emit_expr(kw.value)
|
|
609
|
-
return kwargs
|
|
610
|
-
|
|
611
|
-
def _emit_attribute(self, node: ast.Attribute) -> JSExpr:
|
|
612
|
-
"""Emit an attribute access.
|
|
753
|
+
return callee.transpile_call(args_raw, kwargs_raw, self)
|
|
613
754
|
|
|
614
|
-
|
|
615
|
-
"""
|
|
755
|
+
def _emit_attribute(self, node: ast.Attribute) -> Expr:
|
|
756
|
+
"""Emit an attribute access."""
|
|
616
757
|
value = self.emit_expr(node.value)
|
|
617
|
-
|
|
758
|
+
# Delegate to Expr.transpile_getattr (default returns Member)
|
|
759
|
+
return value.transpile_getattr(node.attr, self)
|
|
618
760
|
|
|
619
|
-
def _emit_subscript(self, node: ast.Subscript) ->
|
|
761
|
+
def _emit_subscript(self, node: ast.Subscript) -> Expr:
|
|
620
762
|
"""Emit a subscript expression."""
|
|
621
763
|
value = self.emit_expr(node.value)
|
|
622
764
|
|
|
623
|
-
# Slice handling
|
|
765
|
+
# Slice handling
|
|
624
766
|
if isinstance(node.slice, ast.Slice):
|
|
625
767
|
return self._emit_slice(value, node.slice)
|
|
626
768
|
|
|
627
|
-
# Negative index: use .at()
|
|
769
|
+
# Negative index: use .at()
|
|
628
770
|
if isinstance(node.slice, ast.UnaryOp) and isinstance(node.slice.op, ast.USub):
|
|
629
771
|
idx_expr = self.emit_expr(node.slice.operand)
|
|
630
|
-
return
|
|
631
|
-
|
|
632
|
-
# Collect indices - tuple means multiple indices like x[a, b, c]
|
|
633
|
-
# Pass as raw values (JSExpr instances) to emit_subscript
|
|
634
|
-
if isinstance(node.slice, ast.Tuple):
|
|
635
|
-
indices: list[Any] = [self.emit_expr(e) for e in node.slice.elts]
|
|
636
|
-
else:
|
|
637
|
-
indices = [self.emit_expr(node.slice)]
|
|
772
|
+
return Call(Member(value, "at"), [Unary("-", idx_expr)])
|
|
638
773
|
|
|
639
|
-
#
|
|
640
|
-
return value.
|
|
774
|
+
# Delegate to Expr.transpile_subscript (default returns Subscript)
|
|
775
|
+
return value.transpile_subscript(node.slice, self)
|
|
641
776
|
|
|
642
|
-
def _emit_slice(self, value:
|
|
777
|
+
def _emit_slice(self, value: Expr, slice_node: ast.Slice) -> Expr:
|
|
643
778
|
"""Emit a slice operation."""
|
|
644
779
|
if slice_node.step is not None:
|
|
645
|
-
raise
|
|
780
|
+
raise TranspileError("Slice steps are not supported")
|
|
646
781
|
|
|
647
782
|
lower = slice_node.lower
|
|
648
783
|
upper = slice_node.upper
|
|
649
784
|
|
|
650
785
|
if lower is None and upper is None:
|
|
651
|
-
return
|
|
786
|
+
return Call(Member(value, "slice"), [])
|
|
652
787
|
elif lower is None:
|
|
653
|
-
return
|
|
788
|
+
return Call(Member(value, "slice"), [Literal(0), self.emit_expr(upper)])
|
|
654
789
|
elif upper is None:
|
|
655
|
-
return
|
|
790
|
+
return Call(Member(value, "slice"), [self.emit_expr(lower)])
|
|
656
791
|
else:
|
|
657
|
-
return
|
|
658
|
-
value, "slice", [self.emit_expr(lower), self.emit_expr(upper)]
|
|
792
|
+
return Call(
|
|
793
|
+
Member(value, "slice"), [self.emit_expr(lower), self.emit_expr(upper)]
|
|
659
794
|
)
|
|
660
795
|
|
|
661
|
-
def _emit_fstring(self, node: ast.JoinedStr) ->
|
|
796
|
+
def _emit_fstring(self, node: ast.JoinedStr) -> Expr:
|
|
662
797
|
"""Emit an f-string as a template literal."""
|
|
663
|
-
parts: list[str |
|
|
798
|
+
parts: list[str | Expr] = []
|
|
664
799
|
for part in node.values:
|
|
665
800
|
if isinstance(part, ast.Constant) and isinstance(part.value, str):
|
|
666
801
|
parts.append(part.value)
|
|
@@ -668,65 +803,53 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
668
803
|
expr = self.emit_expr(part.value)
|
|
669
804
|
# Handle conversion flags: !s, !r, !a
|
|
670
805
|
if part.conversion == ord("s"):
|
|
671
|
-
expr =
|
|
806
|
+
expr = Call(Identifier("String"), [expr])
|
|
672
807
|
elif part.conversion == ord("r"):
|
|
673
|
-
expr =
|
|
808
|
+
expr = Call(Member(Identifier("JSON"), "stringify"), [expr])
|
|
674
809
|
elif part.conversion == ord("a"):
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
# Handle format_spec (it's always a JoinedStr in practice)
|
|
810
|
+
expr = Call(Member(Identifier("JSON"), "stringify"), [expr])
|
|
811
|
+
# Handle format_spec
|
|
678
812
|
if part.format_spec is not None:
|
|
679
813
|
if not isinstance(part.format_spec, ast.JoinedStr):
|
|
680
|
-
raise
|
|
814
|
+
raise TranspileError("Format spec must be a JoinedStr")
|
|
681
815
|
expr = self._apply_format_spec(expr, part.format_spec)
|
|
682
816
|
parts.append(expr)
|
|
683
817
|
else:
|
|
684
|
-
raise
|
|
818
|
+
raise TranspileError(
|
|
685
819
|
f"Unsupported f-string component: {type(part).__name__}"
|
|
686
820
|
)
|
|
687
|
-
return
|
|
688
|
-
|
|
689
|
-
def _apply_format_spec(self, expr:
|
|
690
|
-
"""Apply a Python format spec to an expression.
|
|
691
|
-
|
|
692
|
-
Supports common format specs:
|
|
693
|
-
- .Nf: N decimal places (float) -> .toFixed(N)
|
|
694
|
-
- 0Nd: zero-padded integer, width N -> String(...).padStart(N, '0')
|
|
695
|
-
- >N: right-align, width N -> String(...).padStart(N)
|
|
696
|
-
- <N: left-align, width N -> String(...).padEnd(N)
|
|
697
|
-
- ^N: center, width N -> custom centering
|
|
698
|
-
- #x, #o, #b: hex/octal/binary with prefix
|
|
699
|
-
- +.Nf: with sign prefix
|
|
700
|
-
"""
|
|
701
|
-
# Extract the format spec string (it's a JoinedStr but usually just one constant)
|
|
821
|
+
return Template(parts)
|
|
822
|
+
|
|
823
|
+
def _apply_format_spec(self, expr: Expr, format_spec: ast.JoinedStr) -> Expr:
|
|
824
|
+
"""Apply a Python format spec to an expression."""
|
|
702
825
|
if len(format_spec.values) != 1:
|
|
703
|
-
raise
|
|
826
|
+
raise TranspileError("Dynamic format specs not supported")
|
|
704
827
|
spec_part = format_spec.values[0]
|
|
705
828
|
if not isinstance(spec_part, ast.Constant) or not isinstance(
|
|
706
829
|
spec_part.value, str
|
|
707
830
|
):
|
|
708
|
-
raise
|
|
831
|
+
raise TranspileError("Dynamic format specs not supported")
|
|
709
832
|
|
|
710
833
|
spec = spec_part.value
|
|
711
834
|
return self._parse_and_apply_format(expr, spec)
|
|
712
835
|
|
|
713
|
-
def _parse_and_apply_format(self, expr:
|
|
836
|
+
def _parse_and_apply_format(self, expr: Expr, spec: str) -> Expr:
|
|
714
837
|
"""Parse a format spec string and apply it to expr."""
|
|
715
838
|
if not spec:
|
|
716
839
|
return expr
|
|
717
840
|
|
|
718
|
-
# Parse Python format spec
|
|
841
|
+
# Parse Python format spec
|
|
719
842
|
pattern = r"^([^<>=^]?[<>=^])?([+\- ])?([#])?(0)?(\d+)?([,_])?(\.(\d+))?([bcdeEfFgGnosxX%])?$"
|
|
720
843
|
match = re.match(pattern, spec)
|
|
721
844
|
if not match:
|
|
722
|
-
raise
|
|
845
|
+
raise TranspileError(f"Unsupported format spec: {spec!r}")
|
|
723
846
|
|
|
724
847
|
align_part = match.group(1) or ""
|
|
725
848
|
sign = match.group(2) or ""
|
|
726
|
-
alt_form = match.group(3)
|
|
727
|
-
zero_pad = match.group(4)
|
|
849
|
+
alt_form = match.group(3)
|
|
850
|
+
zero_pad = match.group(4)
|
|
728
851
|
width_str = match.group(5)
|
|
729
|
-
|
|
852
|
+
grouping = match.group(6) or ""
|
|
730
853
|
precision_str = match.group(8)
|
|
731
854
|
type_char = match.group(9) or ""
|
|
732
855
|
|
|
@@ -746,126 +869,145 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
746
869
|
|
|
747
870
|
# Handle type conversions first
|
|
748
871
|
if type_char in ("f", "F"):
|
|
749
|
-
# Float with precision
|
|
750
872
|
prec = precision if precision is not None else 6
|
|
751
|
-
expr =
|
|
873
|
+
expr = Call(Member(expr, "toFixed"), [Literal(prec)])
|
|
752
874
|
if sign == "+":
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
JSBinary(JSString("+"), "+", expr),
|
|
875
|
+
expr = Ternary(
|
|
876
|
+
Binary(expr, ">=", Literal(0)),
|
|
877
|
+
Binary(Literal("+"), "+", expr),
|
|
757
878
|
expr,
|
|
758
879
|
)
|
|
759
880
|
elif type_char == "d":
|
|
760
|
-
# Integer - convert to string for padding (only if we need padding later)
|
|
761
881
|
if width is not None:
|
|
762
|
-
expr =
|
|
882
|
+
expr = Call(Identifier("String"), [expr])
|
|
763
883
|
elif type_char == "x":
|
|
764
|
-
|
|
765
|
-
base_expr = JSMemberCall(expr, "toString", [JSNumber(16)])
|
|
884
|
+
base_expr = Call(Member(expr, "toString"), [Literal(16)])
|
|
766
885
|
if alt_form:
|
|
767
|
-
expr =
|
|
886
|
+
expr = Binary(Literal("0x"), "+", base_expr)
|
|
768
887
|
else:
|
|
769
888
|
expr = base_expr
|
|
770
889
|
elif type_char == "X":
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
JSMemberCall(expr, "toString", [JSNumber(16)]), "toUpperCase", []
|
|
890
|
+
base_expr = Call(
|
|
891
|
+
Member(Call(Member(expr, "toString"), [Literal(16)]), "toUpperCase"), []
|
|
774
892
|
)
|
|
775
893
|
if alt_form:
|
|
776
|
-
expr =
|
|
894
|
+
expr = Binary(Literal("0x"), "+", base_expr)
|
|
777
895
|
else:
|
|
778
896
|
expr = base_expr
|
|
779
897
|
elif type_char == "o":
|
|
780
|
-
|
|
781
|
-
base_expr = JSMemberCall(expr, "toString", [JSNumber(8)])
|
|
898
|
+
base_expr = Call(Member(expr, "toString"), [Literal(8)])
|
|
782
899
|
if alt_form:
|
|
783
|
-
expr =
|
|
900
|
+
expr = Binary(Literal("0o"), "+", base_expr)
|
|
784
901
|
else:
|
|
785
902
|
expr = base_expr
|
|
786
903
|
elif type_char == "b":
|
|
787
|
-
|
|
788
|
-
base_expr = JSMemberCall(expr, "toString", [JSNumber(2)])
|
|
904
|
+
base_expr = Call(Member(expr, "toString"), [Literal(2)])
|
|
789
905
|
if alt_form:
|
|
790
|
-
expr =
|
|
906
|
+
expr = Binary(Literal("0b"), "+", base_expr)
|
|
791
907
|
else:
|
|
792
908
|
expr = base_expr
|
|
793
909
|
elif type_char == "e":
|
|
794
|
-
# Exponential notation lowercase
|
|
795
910
|
prec = precision if precision is not None else 6
|
|
796
|
-
expr =
|
|
911
|
+
expr = Call(Member(expr, "toExponential"), [Literal(prec)])
|
|
797
912
|
elif type_char == "E":
|
|
798
|
-
# Exponential notation uppercase
|
|
799
913
|
prec = precision if precision is not None else 6
|
|
800
|
-
expr =
|
|
801
|
-
|
|
914
|
+
expr = Call(
|
|
915
|
+
Member(
|
|
916
|
+
Call(Member(expr, "toExponential"), [Literal(prec)]), "toUpperCase"
|
|
917
|
+
),
|
|
918
|
+
[],
|
|
802
919
|
)
|
|
920
|
+
elif type_char == "g":
|
|
921
|
+
# General format: uses toPrecision
|
|
922
|
+
prec = precision if precision is not None else 6
|
|
923
|
+
expr = Call(Member(expr, "toPrecision"), [Literal(prec)])
|
|
924
|
+
elif type_char == "G":
|
|
925
|
+
# General format uppercase
|
|
926
|
+
prec = precision if precision is not None else 6
|
|
927
|
+
expr = Call(
|
|
928
|
+
Member(
|
|
929
|
+
Call(Member(expr, "toPrecision"), [Literal(prec)]), "toUpperCase"
|
|
930
|
+
),
|
|
931
|
+
[],
|
|
932
|
+
)
|
|
933
|
+
elif type_char == "%":
|
|
934
|
+
# Percentage: multiply by 100, format as fixed, append %
|
|
935
|
+
prec = precision if precision is not None else 6
|
|
936
|
+
multiplied = Binary(expr, "*", Literal(100))
|
|
937
|
+
fixed = Call(Member(multiplied, "toFixed"), [Literal(prec)])
|
|
938
|
+
expr = Binary(fixed, "+", Literal("%"))
|
|
939
|
+
elif type_char == "c":
|
|
940
|
+
# Character: convert code point to character
|
|
941
|
+
expr = Call(Member(Identifier("String"), "fromCharCode"), [expr])
|
|
942
|
+
elif type_char == "n":
|
|
943
|
+
# Locale-aware number format
|
|
944
|
+
expr = Call(Member(expr, "toLocaleString"), [])
|
|
803
945
|
elif type_char == "s" or type_char == "":
|
|
804
|
-
# String - convert to string if not already
|
|
805
946
|
if type_char == "s" or (width is not None and align):
|
|
806
|
-
expr =
|
|
947
|
+
expr = Call(Identifier("String"), [expr])
|
|
948
|
+
|
|
949
|
+
# Apply thousand separator grouping
|
|
950
|
+
if grouping == ",":
|
|
951
|
+
# Use toLocaleString with en-US to get comma separators
|
|
952
|
+
expr = Call(Member(expr, "toLocaleString"), [Literal("en-US")])
|
|
953
|
+
elif grouping == "_":
|
|
954
|
+
# Use toLocaleString then replace commas with underscores
|
|
955
|
+
locale_expr = Call(Member(expr, "toLocaleString"), [Literal("en-US")])
|
|
956
|
+
expr = Call(
|
|
957
|
+
Member(locale_expr, "replace"), [Identifier(r"/,/g"), Literal("_")]
|
|
958
|
+
)
|
|
807
959
|
|
|
808
960
|
# Apply width/padding
|
|
809
961
|
if width is not None:
|
|
810
|
-
fill_str =
|
|
811
|
-
width_num =
|
|
962
|
+
fill_str = Literal(fill)
|
|
963
|
+
width_num = Literal(width)
|
|
812
964
|
|
|
813
965
|
if zero_pad and not align:
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
expr = JSCall(JSIdentifier("String"), [expr])
|
|
818
|
-
expr = JSMemberCall(
|
|
819
|
-
expr,
|
|
820
|
-
"padStart",
|
|
821
|
-
[width_num, JSString("0")],
|
|
966
|
+
expr = Call(
|
|
967
|
+
Member(Call(Identifier("String"), [expr]), "padStart"),
|
|
968
|
+
[width_num, Literal("0")],
|
|
822
969
|
)
|
|
823
970
|
elif align == "<":
|
|
824
|
-
|
|
825
|
-
expr = JSMemberCall(expr, "padEnd", [width_num, fill_str])
|
|
971
|
+
expr = Call(Member(expr, "padEnd"), [width_num, fill_str])
|
|
826
972
|
elif align == ">":
|
|
827
|
-
|
|
828
|
-
expr = JSMemberCall(expr, "padStart", [width_num, fill_str])
|
|
973
|
+
expr = Call(Member(expr, "padStart"), [width_num, fill_str])
|
|
829
974
|
elif align == "^":
|
|
830
|
-
# Center align
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
975
|
+
# Center align
|
|
976
|
+
expr = Call(
|
|
977
|
+
Member(
|
|
978
|
+
Call(
|
|
979
|
+
Member(expr, "padStart"),
|
|
980
|
+
[
|
|
981
|
+
Binary(
|
|
982
|
+
Binary(
|
|
983
|
+
Binary(width_num, "+", Member(expr, "length")),
|
|
984
|
+
"/",
|
|
985
|
+
Literal(2),
|
|
986
|
+
),
|
|
987
|
+
"|",
|
|
988
|
+
Literal(0),
|
|
842
989
|
),
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
],
|
|
990
|
+
fill_str,
|
|
991
|
+
],
|
|
992
|
+
),
|
|
993
|
+
"padEnd",
|
|
848
994
|
),
|
|
849
|
-
"padEnd",
|
|
850
995
|
[width_num, fill_str],
|
|
851
996
|
)
|
|
852
997
|
elif align == "=":
|
|
853
|
-
|
|
854
|
-
expr = JSMemberCall(expr, "padStart", [width_num, fill_str])
|
|
998
|
+
expr = Call(Member(expr, "padStart"), [width_num, fill_str])
|
|
855
999
|
elif zero_pad:
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
"padStart",
|
|
860
|
-
[width_num, JSString("0")],
|
|
1000
|
+
expr = Call(
|
|
1001
|
+
Member(Call(Identifier("String"), [expr]), "padStart"),
|
|
1002
|
+
[width_num, Literal("0")],
|
|
861
1003
|
)
|
|
862
1004
|
|
|
863
1005
|
return expr
|
|
864
1006
|
|
|
865
|
-
def _emit_lambda(self, node: ast.Lambda) ->
|
|
1007
|
+
def _emit_lambda(self, node: ast.Lambda) -> Expr:
|
|
866
1008
|
"""Emit a lambda expression as an arrow function."""
|
|
867
|
-
# Get parameter names
|
|
868
1009
|
params = [arg.arg for arg in node.args.args]
|
|
1010
|
+
|
|
869
1011
|
# Add params to locals temporarily
|
|
870
1012
|
saved_locals = set(self.locals)
|
|
871
1013
|
self.locals.update(params)
|
|
@@ -874,43 +1016,41 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
874
1016
|
|
|
875
1017
|
self.locals = saved_locals
|
|
876
1018
|
|
|
877
|
-
|
|
878
|
-
return JSArrowFunction("()", body)
|
|
879
|
-
elif len(params) == 1:
|
|
880
|
-
return JSArrowFunction(params[0], body)
|
|
881
|
-
else:
|
|
882
|
-
return JSArrowFunction(f"({', '.join(params)})", body)
|
|
1019
|
+
return Arrow(params, body)
|
|
883
1020
|
|
|
884
1021
|
def _emit_comprehension_chain(
|
|
885
1022
|
self,
|
|
886
1023
|
generators: list[ast.comprehension],
|
|
887
|
-
build_last: Callable[[],
|
|
888
|
-
) ->
|
|
1024
|
+
build_last: Callable[[], Expr],
|
|
1025
|
+
) -> Expr:
|
|
889
1026
|
"""Build a flatMap/map chain for comprehensions."""
|
|
890
1027
|
if len(generators) == 0:
|
|
891
|
-
raise
|
|
1028
|
+
raise TranspileError("Empty comprehension")
|
|
892
1029
|
|
|
893
1030
|
saved_locals = set(self.locals)
|
|
894
1031
|
|
|
895
|
-
def build_chain(gen_index: int) ->
|
|
1032
|
+
def build_chain(gen_index: int) -> Expr:
|
|
896
1033
|
gen = generators[gen_index]
|
|
897
1034
|
if gen.is_async:
|
|
898
|
-
raise
|
|
1035
|
+
raise TranspileError("Async comprehensions are not supported")
|
|
899
1036
|
|
|
900
1037
|
iter_expr = self.emit_expr(gen.iter)
|
|
901
|
-
|
|
1038
|
+
|
|
1039
|
+
# Get parameter and variable names from target
|
|
902
1040
|
if isinstance(gen.target, ast.Name):
|
|
903
|
-
|
|
1041
|
+
params = [gen.target.id]
|
|
904
1042
|
names = [gen.target.id]
|
|
905
1043
|
elif isinstance(gen.target, ast.Tuple) and all(
|
|
906
1044
|
isinstance(e, ast.Name) for e in gen.target.elts
|
|
907
1045
|
):
|
|
908
1046
|
names = [e.id for e in gen.target.elts if isinstance(e, ast.Name)]
|
|
909
|
-
|
|
1047
|
+
# For destructuring, use array pattern as single param: [a, b]
|
|
1048
|
+
params = [f"([{', '.join(names)}])"]
|
|
910
1049
|
else:
|
|
911
|
-
raise
|
|
1050
|
+
raise TranspileError(
|
|
912
1051
|
"Only name or tuple targets supported in comprehensions"
|
|
913
1052
|
)
|
|
1053
|
+
|
|
914
1054
|
for nm in names:
|
|
915
1055
|
self.locals.add(nm)
|
|
916
1056
|
|
|
@@ -919,20 +1059,36 @@ class JsTranspiler(ast.NodeVisitor):
|
|
|
919
1059
|
# Apply filters
|
|
920
1060
|
if gen.ifs:
|
|
921
1061
|
conds = [self.emit_expr(test) for test in gen.ifs]
|
|
922
|
-
cond =
|
|
923
|
-
|
|
1062
|
+
cond = conds[0]
|
|
1063
|
+
for c in conds[1:]:
|
|
1064
|
+
cond = Binary(cond, "&&", c)
|
|
1065
|
+
base = Call(Member(base, "filter"), [Arrow(params, cond)])
|
|
924
1066
|
|
|
925
1067
|
is_last = gen_index == len(generators) - 1
|
|
926
1068
|
if is_last:
|
|
927
1069
|
elt_expr = build_last()
|
|
928
|
-
return
|
|
929
|
-
base, "map", [JSArrowFunction(param_code, elt_expr)]
|
|
930
|
-
)
|
|
1070
|
+
return Call(Member(base, "map"), [Arrow(params, elt_expr)])
|
|
931
1071
|
|
|
932
1072
|
inner = build_chain(gen_index + 1)
|
|
933
|
-
return
|
|
1073
|
+
return Call(Member(base, "flatMap"), [Arrow(params, inner)])
|
|
934
1074
|
|
|
935
1075
|
try:
|
|
936
1076
|
return build_chain(0)
|
|
937
1077
|
finally:
|
|
938
1078
|
self.locals = saved_locals
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def transpile(
|
|
1082
|
+
fndef: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
1083
|
+
deps: Mapping[str, Expr] | None = None,
|
|
1084
|
+
) -> Function | Arrow:
|
|
1085
|
+
"""Transpile a Python function to a v2 Function or Arrow node.
|
|
1086
|
+
|
|
1087
|
+
Args:
|
|
1088
|
+
fndef: The function definition AST node
|
|
1089
|
+
deps: Dictionary mapping global names to Expr instances
|
|
1090
|
+
|
|
1091
|
+
Returns:
|
|
1092
|
+
Arrow for single-expression functions, Function for multi-statement
|
|
1093
|
+
"""
|
|
1094
|
+
return Transpiler(fndef, deps or {}).transpile()
|