pulse-framework 0.1.72__py3-none-any.whl → 0.1.74__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 +16 -4
- pulse/cli/processes.py +2 -0
- pulse/debounce.py +79 -0
- pulse/decorators.py +4 -3
- pulse/hooks/effects.py +5 -5
- pulse/hooks/runtime.py +25 -8
- pulse/hooks/setup.py +6 -10
- pulse/hooks/stable.py +5 -9
- pulse/hooks/state.py +4 -8
- pulse/queries/common.py +1 -1
- pulse/queries/infinite_query.py +2 -1
- pulse/queries/mutation.py +2 -1
- pulse/queries/query.py +2 -1
- pulse/render_session.py +2 -2
- pulse/renderer.py +30 -2
- pulse/routing.py +19 -5
- pulse/serializer.py +38 -19
- pulse/state/__init__.py +1 -0
- pulse/state/property.py +218 -0
- pulse/state/query_param.py +538 -0
- pulse/{state.py → state/state.py} +66 -220
- pulse/transpiler/__init__.py +5 -0
- pulse/transpiler/function.py +56 -32
- pulse/transpiler/nodes.py +43 -4
- pulse/transpiler/parse.py +70 -0
- pulse/transpiler/transpiler.py +413 -81
- pulse/transpiler/vdom.py +1 -1
- {pulse_framework-0.1.72.dist-info → pulse_framework-0.1.74.dist-info}/METADATA +2 -2
- {pulse_framework-0.1.72.dist-info → pulse_framework-0.1.74.dist-info}/RECORD +31 -26
- {pulse_framework-0.1.72.dist-info → pulse_framework-0.1.74.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.72.dist-info → pulse_framework-0.1.74.dist-info}/entry_points.txt +0 -0
pulse/transpiler/transpiler.py
CHANGED
|
@@ -10,8 +10,9 @@ from __future__ import annotations
|
|
|
10
10
|
import ast
|
|
11
11
|
import re
|
|
12
12
|
from collections.abc import Callable, Mapping
|
|
13
|
+
from dataclasses import dataclass
|
|
13
14
|
from pathlib import Path
|
|
14
|
-
from typing import Any, cast
|
|
15
|
+
from typing import Any, cast, override
|
|
15
16
|
|
|
16
17
|
from pulse.transpiler.builtins import BUILTINS, emit_method
|
|
17
18
|
from pulse.transpiler.errors import TranspileError
|
|
@@ -30,6 +31,7 @@ from pulse.transpiler.nodes import (
|
|
|
30
31
|
Function,
|
|
31
32
|
Identifier,
|
|
32
33
|
If,
|
|
34
|
+
LetDecl,
|
|
33
35
|
Literal,
|
|
34
36
|
Member,
|
|
35
37
|
New,
|
|
@@ -78,6 +80,228 @@ ALLOWED_CMPOPS: dict[type[ast.cmpop], str] = {
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
|
|
83
|
+
def _collect_param_names(args: ast.arguments) -> list[str]:
|
|
84
|
+
"""Collect argument names (regular, vararg, kwonly, kwarg)."""
|
|
85
|
+
names: list[str] = [arg.arg for arg in args.args]
|
|
86
|
+
if args.vararg:
|
|
87
|
+
names.append(args.vararg.arg)
|
|
88
|
+
names.extend(arg.arg for arg in args.kwonlyargs)
|
|
89
|
+
if args.kwarg:
|
|
90
|
+
names.append(args.kwarg.arg)
|
|
91
|
+
return names
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(slots=True)
|
|
95
|
+
class Scope:
|
|
96
|
+
locals: set[str]
|
|
97
|
+
params: set[str]
|
|
98
|
+
parent: "Scope | None" = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ScopeAnalyzer(ast.NodeVisitor):
|
|
102
|
+
"""Collect locals per scope (function/lambda/comprehension)."""
|
|
103
|
+
|
|
104
|
+
_scopes: dict[ast.AST, Scope]
|
|
105
|
+
_stack: list[Scope]
|
|
106
|
+
_global_scope: Scope
|
|
107
|
+
|
|
108
|
+
def __init__(self) -> None:
|
|
109
|
+
self._scopes = {}
|
|
110
|
+
self._stack = []
|
|
111
|
+
self._global_scope = Scope(locals=set(), params=set(), parent=None)
|
|
112
|
+
|
|
113
|
+
def analyze(
|
|
114
|
+
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
115
|
+
) -> dict[ast.AST, Scope]:
|
|
116
|
+
self._stack.append(self._global_scope)
|
|
117
|
+
self._visit_defaults(node.args)
|
|
118
|
+
scope = self._new_scope(
|
|
119
|
+
node, _collect_param_names(node.args), parent=self._global_scope
|
|
120
|
+
)
|
|
121
|
+
self._stack.append(scope)
|
|
122
|
+
for stmt in node.body:
|
|
123
|
+
self.visit(stmt)
|
|
124
|
+
self._stack.pop()
|
|
125
|
+
self._stack.pop()
|
|
126
|
+
return self._scopes
|
|
127
|
+
|
|
128
|
+
def _new_scope(
|
|
129
|
+
self,
|
|
130
|
+
node: ast.AST,
|
|
131
|
+
params: list[str],
|
|
132
|
+
*,
|
|
133
|
+
parent: Scope | None,
|
|
134
|
+
) -> Scope:
|
|
135
|
+
param_set = set(params)
|
|
136
|
+
scope = Scope(locals=set(param_set), params=param_set, parent=parent)
|
|
137
|
+
self._scopes[node] = scope
|
|
138
|
+
return scope
|
|
139
|
+
|
|
140
|
+
def _current_scope(self) -> Scope:
|
|
141
|
+
return self._stack[-1]
|
|
142
|
+
|
|
143
|
+
def _add_local(self, name: str) -> None:
|
|
144
|
+
self._current_scope().locals.add(name)
|
|
145
|
+
|
|
146
|
+
def _add_targets(self, target: ast.expr) -> None:
|
|
147
|
+
if isinstance(target, ast.Name):
|
|
148
|
+
self._add_local(target.id)
|
|
149
|
+
return
|
|
150
|
+
if isinstance(target, (ast.Tuple, ast.List)):
|
|
151
|
+
for elt in target.elts:
|
|
152
|
+
if isinstance(elt, ast.Name):
|
|
153
|
+
self._add_local(elt.id)
|
|
154
|
+
|
|
155
|
+
def _visit_target_expr(self, target: ast.expr) -> None:
|
|
156
|
+
if isinstance(target, (ast.Tuple, ast.List)):
|
|
157
|
+
for elt in target.elts:
|
|
158
|
+
self._visit_target_expr(elt)
|
|
159
|
+
return
|
|
160
|
+
self.visit(target)
|
|
161
|
+
|
|
162
|
+
def _analyze_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
163
|
+
self._visit_defaults(node.args)
|
|
164
|
+
scope = self._new_scope(
|
|
165
|
+
node,
|
|
166
|
+
_collect_param_names(node.args),
|
|
167
|
+
parent=self._current_scope(),
|
|
168
|
+
)
|
|
169
|
+
self._stack.append(scope)
|
|
170
|
+
for stmt in node.body:
|
|
171
|
+
self.visit(stmt)
|
|
172
|
+
self._stack.pop()
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def global_scope(self) -> Scope:
|
|
176
|
+
return self._global_scope
|
|
177
|
+
|
|
178
|
+
def _visit_defaults(self, args: ast.arguments) -> None:
|
|
179
|
+
for default in args.defaults:
|
|
180
|
+
self.visit(default)
|
|
181
|
+
for default in args.kw_defaults:
|
|
182
|
+
if default is not None:
|
|
183
|
+
self.visit(default)
|
|
184
|
+
|
|
185
|
+
@override
|
|
186
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
187
|
+
self._add_local(node.name)
|
|
188
|
+
self._analyze_function(node)
|
|
189
|
+
|
|
190
|
+
@override
|
|
191
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
192
|
+
self._add_local(node.name)
|
|
193
|
+
self._analyze_function(node)
|
|
194
|
+
|
|
195
|
+
@override
|
|
196
|
+
def visit_Lambda(self, node: ast.Lambda) -> None:
|
|
197
|
+
scope = self._new_scope(
|
|
198
|
+
node,
|
|
199
|
+
_collect_param_names(node.args),
|
|
200
|
+
parent=self._current_scope(),
|
|
201
|
+
)
|
|
202
|
+
self._stack.append(scope)
|
|
203
|
+
self.visit(node.body)
|
|
204
|
+
self._stack.pop()
|
|
205
|
+
|
|
206
|
+
def _visit_comprehension(
|
|
207
|
+
self,
|
|
208
|
+
node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp,
|
|
209
|
+
*,
|
|
210
|
+
elt: ast.expr,
|
|
211
|
+
key: ast.expr | None = None,
|
|
212
|
+
) -> None:
|
|
213
|
+
scope = self._new_scope(node, [], parent=self._current_scope())
|
|
214
|
+
self._stack.append(scope)
|
|
215
|
+
if key is not None:
|
|
216
|
+
self.visit(key)
|
|
217
|
+
self.visit(elt)
|
|
218
|
+
for gen in node.generators:
|
|
219
|
+
self.visit(gen.iter)
|
|
220
|
+
for if_node in gen.ifs:
|
|
221
|
+
self.visit(if_node)
|
|
222
|
+
self._stack.pop()
|
|
223
|
+
|
|
224
|
+
@override
|
|
225
|
+
def visit_ListComp(self, node: ast.ListComp) -> None:
|
|
226
|
+
self._visit_comprehension(node, elt=node.elt)
|
|
227
|
+
|
|
228
|
+
@override
|
|
229
|
+
def visit_SetComp(self, node: ast.SetComp) -> None:
|
|
230
|
+
self._visit_comprehension(node, elt=node.elt)
|
|
231
|
+
|
|
232
|
+
@override
|
|
233
|
+
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
|
|
234
|
+
self._visit_comprehension(node, elt=node.elt)
|
|
235
|
+
|
|
236
|
+
@override
|
|
237
|
+
def visit_DictComp(self, node: ast.DictComp) -> None:
|
|
238
|
+
self._visit_comprehension(node, elt=node.value, key=node.key)
|
|
239
|
+
|
|
240
|
+
@override
|
|
241
|
+
def visit_Assign(self, node: ast.Assign) -> None:
|
|
242
|
+
for target in node.targets:
|
|
243
|
+
if isinstance(target, (ast.Name, ast.Tuple, ast.List)):
|
|
244
|
+
self._add_targets(target)
|
|
245
|
+
self._visit_target_expr(target)
|
|
246
|
+
self.visit(node.value)
|
|
247
|
+
|
|
248
|
+
@override
|
|
249
|
+
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
|
|
250
|
+
if isinstance(node.target, (ast.Name, ast.Tuple, ast.List)):
|
|
251
|
+
self._add_targets(node.target)
|
|
252
|
+
self._visit_target_expr(node.target)
|
|
253
|
+
if node.value is not None:
|
|
254
|
+
self.visit(node.value)
|
|
255
|
+
|
|
256
|
+
@override
|
|
257
|
+
def visit_AugAssign(self, node: ast.AugAssign) -> None:
|
|
258
|
+
if isinstance(node.target, (ast.Name, ast.Tuple, ast.List)):
|
|
259
|
+
self._add_targets(node.target)
|
|
260
|
+
self._visit_target_expr(node.target)
|
|
261
|
+
self.visit(node.value)
|
|
262
|
+
|
|
263
|
+
@override
|
|
264
|
+
def visit_For(self, node: ast.For) -> None:
|
|
265
|
+
if isinstance(node.target, (ast.Name, ast.Tuple, ast.List)):
|
|
266
|
+
self._add_targets(node.target)
|
|
267
|
+
self._visit_target_expr(node.target)
|
|
268
|
+
self.visit(node.iter)
|
|
269
|
+
for stmt in node.body:
|
|
270
|
+
self.visit(stmt)
|
|
271
|
+
for stmt in node.orelse:
|
|
272
|
+
self.visit(stmt)
|
|
273
|
+
|
|
274
|
+
@override
|
|
275
|
+
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
|
|
276
|
+
if node.name:
|
|
277
|
+
self._add_local(node.name)
|
|
278
|
+
if node.type is not None:
|
|
279
|
+
self.visit(node.type)
|
|
280
|
+
for stmt in node.body:
|
|
281
|
+
self.visit(stmt)
|
|
282
|
+
|
|
283
|
+
@override
|
|
284
|
+
def visit_With(self, node: ast.With) -> None:
|
|
285
|
+
for item in node.items:
|
|
286
|
+
if item.optional_vars and isinstance(
|
|
287
|
+
item.optional_vars, (ast.Name, ast.Tuple, ast.List)
|
|
288
|
+
):
|
|
289
|
+
self._add_targets(item.optional_vars)
|
|
290
|
+
if item.optional_vars is not None:
|
|
291
|
+
self._visit_target_expr(item.optional_vars)
|
|
292
|
+
self.visit(item.context_expr)
|
|
293
|
+
for stmt in node.body:
|
|
294
|
+
self.visit(stmt)
|
|
295
|
+
|
|
296
|
+
@override
|
|
297
|
+
def visit_Global(self, node: ast.Global) -> None:
|
|
298
|
+
raise TranspileError("global is not supported", node=node)
|
|
299
|
+
|
|
300
|
+
@override
|
|
301
|
+
def visit_Nonlocal(self, node: ast.Nonlocal) -> None:
|
|
302
|
+
raise TranspileError("nonlocal is not supported", node=node)
|
|
303
|
+
|
|
304
|
+
|
|
81
305
|
class Transpiler:
|
|
82
306
|
"""Transpile Python AST to v2 Expr/Stmt AST nodes.
|
|
83
307
|
|
|
@@ -93,10 +317,12 @@ class Transpiler:
|
|
|
93
317
|
fndef: ast.FunctionDef | ast.AsyncFunctionDef
|
|
94
318
|
args: list[str]
|
|
95
319
|
deps: Mapping[str, Expr]
|
|
96
|
-
locals: set[str]
|
|
97
320
|
jsx: bool
|
|
98
321
|
source_file: Path | None
|
|
99
322
|
_temp_counter: int
|
|
323
|
+
_scope_map: dict[ast.AST, Scope]
|
|
324
|
+
_scope_stack: list[Scope]
|
|
325
|
+
_global_scope: Scope
|
|
100
326
|
|
|
101
327
|
def __init__(
|
|
102
328
|
self,
|
|
@@ -109,22 +335,21 @@ class Transpiler:
|
|
|
109
335
|
self.fndef = fndef
|
|
110
336
|
self.source_file = source_file
|
|
111
337
|
# Collect all argument names (regular, vararg, kwonly, kwarg)
|
|
112
|
-
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
|
|
338
|
+
self.args = _collect_param_names(fndef.args)
|
|
119
339
|
self.deps = deps
|
|
120
340
|
self.jsx = jsx
|
|
121
|
-
self.locals = set(self.args)
|
|
122
341
|
self._temp_counter = 0
|
|
342
|
+
analyzer = ScopeAnalyzer()
|
|
343
|
+
self._scope_map = analyzer.analyze(fndef)
|
|
344
|
+
self._global_scope = analyzer.global_scope
|
|
345
|
+
self._scope_stack = [self._scope_map[fndef]]
|
|
123
346
|
self.init_temp_counter()
|
|
124
347
|
|
|
125
348
|
def init_temp_counter(self) -> None:
|
|
126
349
|
"""Initialize temp counter to avoid collisions with args or globals."""
|
|
127
|
-
all_names = set(self.
|
|
350
|
+
all_names = set(self.deps.keys())
|
|
351
|
+
for scope in self._scope_map.values():
|
|
352
|
+
all_names.update(scope.locals)
|
|
128
353
|
counter = 0
|
|
129
354
|
while f"$tmp{counter}" in all_names:
|
|
130
355
|
counter += 1
|
|
@@ -136,6 +361,38 @@ class Transpiler:
|
|
|
136
361
|
self._temp_counter += 1
|
|
137
362
|
return name
|
|
138
363
|
|
|
364
|
+
def _current_scope(self) -> Scope:
|
|
365
|
+
return self._scope_stack[-1]
|
|
366
|
+
|
|
367
|
+
def _push_scope(self, node: ast.AST) -> None:
|
|
368
|
+
self._scope_stack.append(self._scope_map[node])
|
|
369
|
+
|
|
370
|
+
def _pop_scope(self) -> None:
|
|
371
|
+
self._scope_stack.pop()
|
|
372
|
+
|
|
373
|
+
def _is_local_here(self, name: str) -> bool:
|
|
374
|
+
return name in self._current_scope().locals
|
|
375
|
+
|
|
376
|
+
def _is_local(self, name: str) -> bool:
|
|
377
|
+
scope: Scope | None = self._current_scope()
|
|
378
|
+
while scope is not None:
|
|
379
|
+
if name in scope.locals:
|
|
380
|
+
return True
|
|
381
|
+
scope = scope.parent
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
def _require_local(
|
|
385
|
+
self, name: str, *, node: ast.expr | ast.stmt | ast.excepthandler
|
|
386
|
+
) -> None:
|
|
387
|
+
if not self._is_local_here(name):
|
|
388
|
+
raise TranspileError(f"Assignment to unknown local: {name}", node=node)
|
|
389
|
+
|
|
390
|
+
def _function_prelude(self, scope: Scope) -> list[Stmt]:
|
|
391
|
+
names = sorted(scope.locals - scope.params)
|
|
392
|
+
if not names:
|
|
393
|
+
return []
|
|
394
|
+
return [LetDecl(names)]
|
|
395
|
+
|
|
139
396
|
# --- Entrypoint ---------------------------------------------------------
|
|
140
397
|
|
|
141
398
|
def transpile(self) -> Function | Arrow:
|
|
@@ -176,7 +433,8 @@ class Transpiler:
|
|
|
176
433
|
return Arrow(self.args, expr)
|
|
177
434
|
|
|
178
435
|
# General case: Function (for JSX or multi-statement)
|
|
179
|
-
|
|
436
|
+
prelude = self._function_prelude(self._current_scope())
|
|
437
|
+
stmts = prelude + [self.emit_stmt(s) for s in body]
|
|
180
438
|
is_async = isinstance(self.fndef, ast.AsyncFunctionDef)
|
|
181
439
|
args = [self._jsx_args()] if self.jsx else self.args
|
|
182
440
|
return Function(args, stmts, is_async=is_async)
|
|
@@ -202,7 +460,7 @@ class Transpiler:
|
|
|
202
460
|
if default_idx >= 0:
|
|
203
461
|
# Has a default value
|
|
204
462
|
default_node = args.defaults[default_idx]
|
|
205
|
-
default_expr = self.
|
|
463
|
+
default_expr = self._emit_default_expr(default_node)
|
|
206
464
|
default_out.clear()
|
|
207
465
|
default_expr.emit(default_out)
|
|
208
466
|
destructure_parts.append(f"{param_name} = {''.join(default_out)}")
|
|
@@ -220,7 +478,7 @@ class Transpiler:
|
|
|
220
478
|
default_node = args.kw_defaults[i]
|
|
221
479
|
if default_node is not None:
|
|
222
480
|
# Has a default value
|
|
223
|
-
default_expr = self.
|
|
481
|
+
default_expr = self._emit_default_expr(default_node)
|
|
224
482
|
default_out.clear()
|
|
225
483
|
default_expr.emit(default_out)
|
|
226
484
|
destructure_parts.append(f"{param_name} = {''.join(default_out)}")
|
|
@@ -234,6 +492,14 @@ class Transpiler:
|
|
|
234
492
|
|
|
235
493
|
return "{" + ", ".join(destructure_parts) + "}"
|
|
236
494
|
|
|
495
|
+
def _emit_default_expr(self, node: ast.expr) -> Expr:
|
|
496
|
+
"""Emit defaults in defining-scope context."""
|
|
497
|
+
self._scope_stack.append(self._global_scope)
|
|
498
|
+
try:
|
|
499
|
+
return self.emit_expr(node)
|
|
500
|
+
finally:
|
|
501
|
+
self._scope_stack.pop()
|
|
502
|
+
|
|
237
503
|
# --- Statements ----------------------------------------------------------
|
|
238
504
|
|
|
239
505
|
def emit_stmt(self, node: ast.stmt) -> Stmt:
|
|
@@ -253,11 +519,16 @@ class Transpiler:
|
|
|
253
519
|
return Block([])
|
|
254
520
|
|
|
255
521
|
if isinstance(node, ast.AugAssign):
|
|
522
|
+
if isinstance(node.target, ast.Subscript):
|
|
523
|
+
return self._emit_augmented_subscript_assign(node)
|
|
524
|
+
if isinstance(node.target, ast.Attribute):
|
|
525
|
+
return self._emit_augmented_attribute_assign(node)
|
|
256
526
|
if not isinstance(node.target, ast.Name):
|
|
257
527
|
raise TranspileError(
|
|
258
528
|
"Only simple augmented assignments supported", node=node
|
|
259
529
|
)
|
|
260
530
|
target = node.target.id
|
|
531
|
+
self._require_local(target, node=node)
|
|
261
532
|
op_type = type(node.op)
|
|
262
533
|
if op_type not in ALLOWED_BINOPS:
|
|
263
534
|
raise TranspileError(
|
|
@@ -278,6 +549,12 @@ class Transpiler:
|
|
|
278
549
|
if isinstance(target_node, (ast.Tuple, ast.List)):
|
|
279
550
|
return self._emit_unpacking_assign(target_node, node.value)
|
|
280
551
|
|
|
552
|
+
if isinstance(target_node, ast.Subscript):
|
|
553
|
+
return self._emit_subscript_assign(target_node, node.value)
|
|
554
|
+
|
|
555
|
+
if isinstance(target_node, ast.Attribute):
|
|
556
|
+
return self._emit_attribute_assign(target_node, node.value)
|
|
557
|
+
|
|
281
558
|
if not isinstance(target_node, ast.Name):
|
|
282
559
|
raise TranspileError(
|
|
283
560
|
"Only simple assignments to local names supported", node=node
|
|
@@ -286,22 +563,16 @@ class Transpiler:
|
|
|
286
563
|
target = target_node.id
|
|
287
564
|
value_expr = self.emit_expr(node.value)
|
|
288
565
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
else:
|
|
292
|
-
self.locals.add(target)
|
|
293
|
-
return Assign(target, value_expr, declare="let")
|
|
566
|
+
self._require_local(target, node=node)
|
|
567
|
+
return Assign(target, value_expr)
|
|
294
568
|
|
|
295
569
|
if isinstance(node, ast.AnnAssign):
|
|
296
570
|
if not isinstance(node.target, ast.Name):
|
|
297
571
|
raise TranspileError("Only simple annotated assignments supported")
|
|
298
572
|
target = node.target.id
|
|
299
573
|
value = Literal(None) if node.value is None else self.emit_expr(node.value)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
else:
|
|
303
|
-
self.locals.add(target)
|
|
304
|
-
return Assign(target, value, declare="let")
|
|
574
|
+
self._require_local(target, node=node)
|
|
575
|
+
return Assign(target, value)
|
|
305
576
|
|
|
306
577
|
if isinstance(node, ast.If):
|
|
307
578
|
cond = self.emit_expr(node.test)
|
|
@@ -348,14 +619,87 @@ class Transpiler:
|
|
|
348
619
|
assert isinstance(e, ast.Name)
|
|
349
620
|
name = e.id
|
|
350
621
|
sub = Subscript(Identifier(tmp_name), Literal(idx))
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
else:
|
|
354
|
-
self.locals.add(name)
|
|
355
|
-
stmts.append(Assign(name, sub, declare="let"))
|
|
622
|
+
self._require_local(name, node=target)
|
|
623
|
+
stmts.append(Assign(name, sub))
|
|
356
624
|
|
|
357
625
|
return StmtSequence(stmts)
|
|
358
626
|
|
|
627
|
+
def _emit_subscript_assign(self, target: ast.Subscript, value: ast.expr) -> Stmt:
|
|
628
|
+
"""Emit subscript assignment: obj[key] = value"""
|
|
629
|
+
if isinstance(target.slice, ast.Tuple):
|
|
630
|
+
raise TranspileError(
|
|
631
|
+
"Multiple indices not supported in subscript", node=target.slice
|
|
632
|
+
)
|
|
633
|
+
if isinstance(target.slice, ast.Slice):
|
|
634
|
+
raise TranspileError("Slice assignment not supported", node=target.slice)
|
|
635
|
+
obj_expr = self.emit_expr(target.value)
|
|
636
|
+
target_expr = obj_expr.transpile_subscript(target.slice, self)
|
|
637
|
+
if not isinstance(target_expr, (Identifier, Member, Subscript)):
|
|
638
|
+
raise TranspileError(
|
|
639
|
+
"Only simple subscript assignments supported", node=target
|
|
640
|
+
)
|
|
641
|
+
value_expr = self.emit_expr(value)
|
|
642
|
+
return Assign(target_expr, value_expr)
|
|
643
|
+
|
|
644
|
+
def _emit_attribute_assign(self, target: ast.Attribute, value: ast.expr) -> Stmt:
|
|
645
|
+
"""Emit attribute assignment: obj.attr = value"""
|
|
646
|
+
obj_expr = self.emit_expr(target.value)
|
|
647
|
+
value_expr = self.emit_expr(value)
|
|
648
|
+
target_expr = obj_expr.transpile_getattr(target.attr, self)
|
|
649
|
+
if not isinstance(target_expr, (Identifier, Member, Subscript)):
|
|
650
|
+
raise TranspileError(
|
|
651
|
+
"Only simple attribute assignments supported", node=target
|
|
652
|
+
)
|
|
653
|
+
return Assign(target_expr, value_expr)
|
|
654
|
+
|
|
655
|
+
def _emit_augmented_subscript_assign(self, node: ast.AugAssign) -> Stmt:
|
|
656
|
+
"""Emit augmented subscript assignment: arr[i] += x"""
|
|
657
|
+
target = node.target
|
|
658
|
+
assert isinstance(target, ast.Subscript)
|
|
659
|
+
|
|
660
|
+
if isinstance(target.slice, ast.Tuple):
|
|
661
|
+
raise TranspileError(
|
|
662
|
+
"Multiple indices not supported in subscript", node=target.slice
|
|
663
|
+
)
|
|
664
|
+
if isinstance(target.slice, ast.Slice):
|
|
665
|
+
raise TranspileError("Slice assignment not supported", node=target.slice)
|
|
666
|
+
|
|
667
|
+
obj_expr = self.emit_expr(target.value)
|
|
668
|
+
op_type = type(node.op)
|
|
669
|
+
if op_type not in ALLOWED_BINOPS:
|
|
670
|
+
raise TranspileError(
|
|
671
|
+
f"Unsupported augmented assignment operator: {op_type.__name__}",
|
|
672
|
+
node=node,
|
|
673
|
+
)
|
|
674
|
+
target_expr = obj_expr.transpile_subscript(target.slice, self)
|
|
675
|
+
if not isinstance(target_expr, (Identifier, Member, Subscript)):
|
|
676
|
+
raise TranspileError(
|
|
677
|
+
"Only simple subscript assignments supported", node=target
|
|
678
|
+
)
|
|
679
|
+
value_expr = self.emit_expr(node.value)
|
|
680
|
+
return Assign(target_expr, value_expr, op=ALLOWED_BINOPS[op_type])
|
|
681
|
+
|
|
682
|
+
def _emit_augmented_attribute_assign(self, node: ast.AugAssign) -> Stmt:
|
|
683
|
+
"""Emit augmented attribute assignment: obj.attr += x"""
|
|
684
|
+
target = node.target
|
|
685
|
+
assert isinstance(target, ast.Attribute)
|
|
686
|
+
|
|
687
|
+
obj_expr = self.emit_expr(target.value)
|
|
688
|
+
op_type = type(node.op)
|
|
689
|
+
if op_type not in ALLOWED_BINOPS:
|
|
690
|
+
raise TranspileError(
|
|
691
|
+
f"Unsupported augmented assignment operator: {op_type.__name__}",
|
|
692
|
+
node=node,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
target_expr = obj_expr.transpile_getattr(target.attr, self)
|
|
696
|
+
if not isinstance(target_expr, (Identifier, Member, Subscript)):
|
|
697
|
+
raise TranspileError(
|
|
698
|
+
"Only simple attribute assignments supported", node=target
|
|
699
|
+
)
|
|
700
|
+
value_expr = self.emit_expr(node.value)
|
|
701
|
+
return Assign(target_expr, value_expr, op=ALLOWED_BINOPS[op_type])
|
|
702
|
+
|
|
359
703
|
def _emit_for_loop(self, node: ast.For) -> Stmt:
|
|
360
704
|
"""Emit a for loop."""
|
|
361
705
|
# Handle tuple unpacking in for target
|
|
@@ -367,7 +711,7 @@ class Transpiler:
|
|
|
367
711
|
"Only simple name targets supported in for-loop unpacking"
|
|
368
712
|
)
|
|
369
713
|
names.append(e.id)
|
|
370
|
-
self.
|
|
714
|
+
self._require_local(e.id, node=node)
|
|
371
715
|
iter_expr = self.emit_expr(node.iter)
|
|
372
716
|
body = [self.emit_stmt(s) for s in node.body]
|
|
373
717
|
# Use array pattern for destructuring
|
|
@@ -378,7 +722,7 @@ class Transpiler:
|
|
|
378
722
|
raise TranspileError("Only simple name targets supported in for-loops")
|
|
379
723
|
|
|
380
724
|
target = node.target.id
|
|
381
|
-
self.
|
|
725
|
+
self._require_local(target, node=node)
|
|
382
726
|
iter_expr = self.emit_expr(node.iter)
|
|
383
727
|
body = [self.emit_stmt(s) for s in node.body]
|
|
384
728
|
return ForOf(target, iter_expr, body)
|
|
@@ -388,31 +732,29 @@ class Transpiler:
|
|
|
388
732
|
) -> Stmt:
|
|
389
733
|
"""Emit a nested function definition."""
|
|
390
734
|
name = node.name
|
|
391
|
-
params =
|
|
735
|
+
params = _collect_param_names(node.args)
|
|
736
|
+
self._require_local(name, node=node)
|
|
392
737
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
):
|
|
405
|
-
body_stmts = body_stmts[1:]
|
|
406
|
-
|
|
407
|
-
stmts: list[Stmt] = [self.emit_stmt(s) for s in body_stmts]
|
|
738
|
+
self._push_scope(node)
|
|
739
|
+
try:
|
|
740
|
+
# Skip docstrings and emit body
|
|
741
|
+
body_stmts = node.body
|
|
742
|
+
if (
|
|
743
|
+
body_stmts
|
|
744
|
+
and isinstance(body_stmts[0], ast.Expr)
|
|
745
|
+
and isinstance(body_stmts[0].value, ast.Constant)
|
|
746
|
+
and isinstance(body_stmts[0].value.value, str)
|
|
747
|
+
):
|
|
748
|
+
body_stmts = body_stmts[1:]
|
|
408
749
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
750
|
+
prelude = self._function_prelude(self._current_scope())
|
|
751
|
+
stmts: list[Stmt] = prelude + [self.emit_stmt(s) for s in body_stmts]
|
|
752
|
+
finally:
|
|
753
|
+
self._pop_scope()
|
|
412
754
|
|
|
413
755
|
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
414
756
|
fn = Function(params, stmts, is_async=is_async)
|
|
415
|
-
return Assign(name, fn
|
|
757
|
+
return Assign(name, fn)
|
|
416
758
|
|
|
417
759
|
def _emit_try(self, node: ast.Try) -> Stmt:
|
|
418
760
|
"""Emit a try/except/finally statement."""
|
|
@@ -431,7 +773,6 @@ class Transpiler:
|
|
|
431
773
|
handler = node.handlers[0]
|
|
432
774
|
if handler.name:
|
|
433
775
|
catch_param = handler.name
|
|
434
|
-
self.locals.add(catch_param)
|
|
435
776
|
catch_body = [self.emit_stmt(s) for s in handler.body]
|
|
436
777
|
|
|
437
778
|
# Handle finally
|
|
@@ -508,23 +849,21 @@ class Transpiler:
|
|
|
508
849
|
|
|
509
850
|
if isinstance(node, ast.ListComp):
|
|
510
851
|
return self._emit_comprehension_chain(
|
|
511
|
-
node
|
|
852
|
+
node, lambda: self.emit_expr(node.elt)
|
|
512
853
|
)
|
|
513
854
|
|
|
514
855
|
if isinstance(node, ast.GeneratorExp):
|
|
515
856
|
return self._emit_comprehension_chain(
|
|
516
|
-
node
|
|
857
|
+
node, lambda: self.emit_expr(node.elt)
|
|
517
858
|
)
|
|
518
859
|
|
|
519
860
|
if isinstance(node, ast.SetComp):
|
|
520
|
-
arr = self._emit_comprehension_chain(
|
|
521
|
-
node.generators, lambda: self.emit_expr(node.elt)
|
|
522
|
-
)
|
|
861
|
+
arr = self._emit_comprehension_chain(node, lambda: self.emit_expr(node.elt))
|
|
523
862
|
return New(Identifier("Set"), [arr])
|
|
524
863
|
|
|
525
864
|
if isinstance(node, ast.DictComp):
|
|
526
865
|
pairs = self._emit_comprehension_chain(
|
|
527
|
-
node
|
|
866
|
+
node,
|
|
528
867
|
lambda: Array([self.emit_expr(node.key), self.emit_expr(node.value)]),
|
|
529
868
|
)
|
|
530
869
|
return New(Identifier("Map"), [pairs])
|
|
@@ -562,14 +901,14 @@ class Transpiler:
|
|
|
562
901
|
"""Emit a name reference."""
|
|
563
902
|
name = node.id
|
|
564
903
|
|
|
565
|
-
#
|
|
904
|
+
# Local variable (current or enclosing scope)
|
|
905
|
+
if self._is_local(name):
|
|
906
|
+
return Identifier(name)
|
|
907
|
+
|
|
908
|
+
# Check deps
|
|
566
909
|
if name in self.deps:
|
|
567
910
|
return self.deps[name]
|
|
568
911
|
|
|
569
|
-
# Local variable
|
|
570
|
-
if name in self.locals:
|
|
571
|
-
return Identifier(name)
|
|
572
|
-
|
|
573
912
|
# Check builtins
|
|
574
913
|
if name in BUILTINS:
|
|
575
914
|
return BUILTINS[name]
|
|
@@ -772,11 +1111,6 @@ class Transpiler:
|
|
|
772
1111
|
if isinstance(node.slice, ast.Slice):
|
|
773
1112
|
return self._emit_slice(value, node.slice)
|
|
774
1113
|
|
|
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
1114
|
# Delegate to Expr.transpile_subscript (default returns Subscript)
|
|
781
1115
|
return value.transpile_subscript(node.slice, self)
|
|
782
1116
|
|
|
@@ -1012,29 +1346,26 @@ class Transpiler:
|
|
|
1012
1346
|
|
|
1013
1347
|
def _emit_lambda(self, node: ast.Lambda) -> Expr:
|
|
1014
1348
|
"""Emit a lambda expression as an arrow function."""
|
|
1015
|
-
params =
|
|
1349
|
+
params = _collect_param_names(node.args)
|
|
1016
1350
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
self.locals = saved_locals
|
|
1351
|
+
self._push_scope(node)
|
|
1352
|
+
try:
|
|
1353
|
+
body = self.emit_expr(node.body)
|
|
1354
|
+
finally:
|
|
1355
|
+
self._pop_scope()
|
|
1024
1356
|
|
|
1025
1357
|
return Arrow(params, body)
|
|
1026
1358
|
|
|
1027
1359
|
def _emit_comprehension_chain(
|
|
1028
1360
|
self,
|
|
1029
|
-
|
|
1361
|
+
node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp,
|
|
1030
1362
|
build_last: Callable[[], Expr],
|
|
1031
1363
|
) -> Expr:
|
|
1032
1364
|
"""Build a flatMap/map chain for comprehensions."""
|
|
1365
|
+
generators = node.generators
|
|
1033
1366
|
if len(generators) == 0:
|
|
1034
1367
|
raise TranspileError("Empty comprehension")
|
|
1035
1368
|
|
|
1036
|
-
saved_locals = set(self.locals)
|
|
1037
|
-
|
|
1038
1369
|
def build_chain(gen_index: int) -> Expr:
|
|
1039
1370
|
gen = generators[gen_index]
|
|
1040
1371
|
if gen.is_async:
|
|
@@ -1058,7 +1389,7 @@ class Transpiler:
|
|
|
1058
1389
|
)
|
|
1059
1390
|
|
|
1060
1391
|
for nm in names:
|
|
1061
|
-
self.locals.add(nm)
|
|
1392
|
+
self._current_scope().locals.add(nm)
|
|
1062
1393
|
|
|
1063
1394
|
base = iter_expr
|
|
1064
1395
|
|
|
@@ -1078,10 +1409,11 @@ class Transpiler:
|
|
|
1078
1409
|
inner = build_chain(gen_index + 1)
|
|
1079
1410
|
return Call(Member(base, "flatMap"), [Arrow(params, inner)])
|
|
1080
1411
|
|
|
1412
|
+
self._push_scope(node)
|
|
1081
1413
|
try:
|
|
1082
1414
|
return build_chain(0)
|
|
1083
1415
|
finally:
|
|
1084
|
-
self.
|
|
1416
|
+
self._pop_scope()
|
|
1085
1417
|
|
|
1086
1418
|
|
|
1087
1419
|
def transpile(
|