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