pulse-framework 0.1.51__py3-none-any.whl → 0.1.52__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -1001
- pulse/render_session.py +74 -66
- pulse/renderer.py +311 -238
- pulse/routing.py +1 -10
- 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.51.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 -599
- pulse_framework-0.1.51.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.51.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
pulse/transpiler/function.py
CHANGED
|
@@ -1,182 +1,361 @@
|
|
|
1
|
+
"""Function transpilation system for transpiler.
|
|
2
|
+
|
|
3
|
+
Provides the @javascript decorator for marking Python functions for JS transpilation,
|
|
4
|
+
and JsFunction which wraps transpiled functions with their dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
from __future__ import annotations
|
|
2
8
|
|
|
3
9
|
import ast
|
|
4
10
|
import inspect
|
|
5
11
|
import textwrap
|
|
6
12
|
import types as pytypes
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from dataclasses import dataclass, field
|
|
7
15
|
from typing import (
|
|
8
16
|
Any,
|
|
9
|
-
Callable,
|
|
10
|
-
ClassVar,
|
|
11
17
|
Generic,
|
|
18
|
+
Literal,
|
|
19
|
+
ParamSpec,
|
|
12
20
|
TypeAlias,
|
|
13
21
|
TypeVar,
|
|
14
22
|
TypeVarTuple,
|
|
23
|
+
overload,
|
|
15
24
|
override,
|
|
16
25
|
)
|
|
17
26
|
|
|
18
|
-
# Import module registrations to ensure they're available for dependency analysis
|
|
19
|
-
import pulse.transpiler.modules # noqa: F401
|
|
20
27
|
from pulse.helpers import getsourcecode
|
|
21
|
-
from pulse.transpiler.
|
|
22
|
-
from pulse.transpiler.
|
|
23
|
-
from pulse.transpiler.context import is_interpreted_mode
|
|
24
|
-
from pulse.transpiler.errors import JSCompilationError
|
|
25
|
-
from pulse.transpiler.ids import generate_id
|
|
28
|
+
from pulse.transpiler.errors import TranspileError
|
|
29
|
+
from pulse.transpiler.id import next_id, reset_id_counter
|
|
26
30
|
from pulse.transpiler.imports import Import
|
|
27
|
-
from pulse.transpiler.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
from pulse.transpiler.nodes import (
|
|
32
|
+
EXPR_REGISTRY,
|
|
33
|
+
Arrow,
|
|
34
|
+
Expr,
|
|
35
|
+
Function,
|
|
36
|
+
Jsx,
|
|
37
|
+
Return,
|
|
38
|
+
to_js_identifier,
|
|
32
39
|
)
|
|
33
|
-
from pulse.transpiler.transpiler import
|
|
40
|
+
from pulse.transpiler.transpiler import Transpiler
|
|
41
|
+
from pulse.transpiler.vdom import VDOMNode
|
|
34
42
|
|
|
35
43
|
Args = TypeVarTuple("Args")
|
|
44
|
+
P = ParamSpec("P")
|
|
36
45
|
R = TypeVar("R")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
AnyJsFunction: TypeAlias = "JsFunction[*tuple[Any, ...], Any]"
|
|
46
|
+
AnyJsFunction: TypeAlias = "JsFunction[*tuple[Any, ...], Any] | JsxFunction[..., Any]"
|
|
40
47
|
|
|
41
48
|
# Global cache for deduplication across all transpiled functions
|
|
42
49
|
# Registered BEFORE analyzing deps to handle mutual recursion
|
|
43
|
-
|
|
50
|
+
# Stores JsFunction for regular @javascript, JsxFunction for @javascript(jsx=True)
|
|
51
|
+
FUNCTION_CACHE: dict[Callable[..., Any], AnyJsFunction] = {}
|
|
44
52
|
|
|
53
|
+
# Global registry for hoisted constants: id(value) -> Constant
|
|
54
|
+
# Used for deduplication of non-primitive values in transpiled functions
|
|
55
|
+
CONSTANT_REGISTRY: dict[int, "Constant"] = {}
|
|
45
56
|
|
|
46
|
-
class JsFunction(JSExpr, Generic[*Args, R]):
|
|
47
|
-
is_primary: ClassVar[bool] = True
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
def clear_function_cache() -> None:
|
|
59
|
+
"""Clear function/constant/ref caches and reset the shared ID counters."""
|
|
60
|
+
from pulse.transpiler.imports import clear_import_registry
|
|
61
|
+
|
|
62
|
+
FUNCTION_CACHE.clear()
|
|
63
|
+
CONSTANT_REGISTRY.clear()
|
|
64
|
+
clear_import_registry()
|
|
65
|
+
reset_id_counter()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(slots=True, init=False)
|
|
69
|
+
class Constant(Expr):
|
|
70
|
+
"""A hoisted constant value with a unique identifier.
|
|
71
|
+
|
|
72
|
+
Used for non-primitive values (lists, dicts, sets) referenced in transpiled
|
|
73
|
+
functions. The value is emitted once at module scope, and the function
|
|
74
|
+
references it by name.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
ITEMS = [1, 2, 3]
|
|
78
|
+
|
|
79
|
+
@javascript
|
|
80
|
+
def foo():
|
|
81
|
+
return ITEMS[0]
|
|
82
|
+
|
|
83
|
+
# Emits:
|
|
84
|
+
# const ITEMS_1 = [1, 2, 3];
|
|
85
|
+
# function foo_2() { return ITEMS_1[0]; }
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
value: Any
|
|
89
|
+
expr: Expr
|
|
50
90
|
id: str
|
|
51
|
-
|
|
91
|
+
name: str
|
|
52
92
|
|
|
53
|
-
def __init__(self,
|
|
54
|
-
self.
|
|
55
|
-
self.
|
|
93
|
+
def __init__(self, value: Any, expr: Expr, name: str = "") -> None:
|
|
94
|
+
self.value = value
|
|
95
|
+
self.expr = expr
|
|
96
|
+
self.id = next_id()
|
|
97
|
+
self.name = name
|
|
98
|
+
# Register in global cache
|
|
99
|
+
CONSTANT_REGISTRY[id(value)] = self
|
|
56
100
|
|
|
57
|
-
|
|
58
|
-
|
|
101
|
+
@property
|
|
102
|
+
def js_name(self) -> str:
|
|
103
|
+
"""Unique JS identifier for this constant."""
|
|
104
|
+
if self.name:
|
|
105
|
+
return f"{to_js_identifier(self.name)}_{self.id}"
|
|
106
|
+
return f"_const_{self.id}"
|
|
59
107
|
|
|
60
|
-
|
|
61
|
-
|
|
108
|
+
@override
|
|
109
|
+
def emit(self, out: list[str]) -> None:
|
|
110
|
+
"""Emit the unique JS identifier."""
|
|
111
|
+
out.append(self.js_name)
|
|
62
112
|
|
|
63
|
-
|
|
64
|
-
|
|
113
|
+
@override
|
|
114
|
+
def render(self) -> VDOMNode:
|
|
115
|
+
"""Render as a registry reference."""
|
|
116
|
+
return {"t": "ref", "key": self.id}
|
|
65
117
|
|
|
66
|
-
|
|
67
|
-
|
|
118
|
+
@staticmethod
|
|
119
|
+
def wrap(value: Any, name: str = "") -> "Constant":
|
|
120
|
+
"""Get or create a Constant for a value (cached by identity)."""
|
|
121
|
+
if (existing := CONSTANT_REGISTRY.get(id(value))) is not None:
|
|
122
|
+
return existing
|
|
123
|
+
expr = Expr.of(value)
|
|
124
|
+
return Constant(value, expr, name)
|
|
68
125
|
|
|
69
|
-
if value is None:
|
|
70
|
-
# Not in globals - check builtins (allows user to shadow builtins)
|
|
71
|
-
# Note: co_names includes both global names AND attribute names (e.g., 'input'
|
|
72
|
-
# from 'tags.input'). We only add supported builtins; unsupported ones are
|
|
73
|
-
# skipped since they might be attribute accesses handled during transpilation.
|
|
74
|
-
if name in BUILTINS:
|
|
75
|
-
deps[name] = BUILTINS[name]
|
|
76
|
-
continue
|
|
77
126
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
def registered_constants() -> list[Constant]:
|
|
128
|
+
"""Get all registered constants."""
|
|
129
|
+
return list(CONSTANT_REGISTRY.values())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _transpile_function_body(
|
|
133
|
+
fn: Callable[..., Any],
|
|
134
|
+
deps: dict[str, Expr],
|
|
135
|
+
*,
|
|
136
|
+
jsx: bool = False,
|
|
137
|
+
) -> tuple[Function | Arrow, str]:
|
|
138
|
+
"""Shared transpilation logic for JsFunction and JsxFunction.
|
|
139
|
+
|
|
140
|
+
Returns the transpiled Function/Arrow node and the source code.
|
|
141
|
+
"""
|
|
142
|
+
# Get and parse source
|
|
143
|
+
src = getsourcecode(fn)
|
|
144
|
+
src = textwrap.dedent(src)
|
|
145
|
+
module = ast.parse(src)
|
|
146
|
+
|
|
147
|
+
# Find the function definition
|
|
148
|
+
fndefs = [
|
|
149
|
+
n for n in module.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
150
|
+
]
|
|
151
|
+
if not fndefs:
|
|
152
|
+
raise TranspileError("No function definition found in source")
|
|
153
|
+
fndef = fndefs[-1]
|
|
154
|
+
|
|
155
|
+
# Get filename for error messages
|
|
156
|
+
try:
|
|
157
|
+
filename = inspect.getfile(fn)
|
|
158
|
+
except (TypeError, OSError):
|
|
159
|
+
filename = None
|
|
160
|
+
|
|
161
|
+
# Transpile with source context for errors
|
|
162
|
+
try:
|
|
163
|
+
transpiler = Transpiler(fndef, deps, jsx=jsx)
|
|
164
|
+
result = transpiler.transpile()
|
|
165
|
+
except TranspileError as e:
|
|
166
|
+
# Re-raise with source context if not already present
|
|
167
|
+
if e.source is None:
|
|
168
|
+
raise e.with_context(
|
|
169
|
+
source=src,
|
|
170
|
+
filename=filename,
|
|
171
|
+
func_name=fn.__name__,
|
|
172
|
+
) from None
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
return result, src
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(slots=True, init=False)
|
|
179
|
+
class JsFunction(Expr, Generic[*Args, R]):
|
|
180
|
+
"""A transpiled JavaScript function.
|
|
181
|
+
|
|
182
|
+
Wraps a Python function with:
|
|
183
|
+
- A unique identifier for deduplication
|
|
184
|
+
- Resolved dependencies (other functions, imports, constants, etc.)
|
|
185
|
+
- The ability to transpile to JavaScript code
|
|
186
|
+
|
|
187
|
+
When emitted, produces the unique JS function name (e.g., "myFunc_1").
|
|
188
|
+
"""
|
|
106
189
|
|
|
107
|
-
|
|
190
|
+
fn: Callable[[*Args], R]
|
|
191
|
+
id: str
|
|
192
|
+
deps: dict[str, Expr]
|
|
193
|
+
_transpiled: Function | None = field(default=None)
|
|
194
|
+
|
|
195
|
+
def __init__(self, fn: Callable[..., Any], *, _register: bool = True) -> None:
|
|
196
|
+
self.fn = fn
|
|
197
|
+
self.id = next_id()
|
|
198
|
+
self._transpiled = None
|
|
199
|
+
if _register:
|
|
200
|
+
# Register self in cache BEFORE analyzing deps (handles cycles)
|
|
201
|
+
FUNCTION_CACHE[fn] = self
|
|
202
|
+
# Now analyze and build deps (may recursively call JsFunction() which will find us in cache)
|
|
203
|
+
self.deps = analyze_deps(fn)
|
|
204
|
+
|
|
205
|
+
@override
|
|
206
|
+
def __call__(self, *args: *Args) -> R: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
207
|
+
return Expr.__call__(self, *args) # pyright: ignore[reportReturnType]
|
|
108
208
|
|
|
109
209
|
@property
|
|
110
210
|
def js_name(self) -> str:
|
|
111
211
|
"""Unique JS identifier for this function."""
|
|
112
|
-
return f"{self.fn.__name__}_{self.id}"
|
|
212
|
+
return f"{to_js_identifier(self.fn.__name__)}_{self.id}"
|
|
113
213
|
|
|
114
214
|
@override
|
|
115
|
-
def emit(self) ->
|
|
116
|
-
"""Emit
|
|
215
|
+
def emit(self, out: list[str]) -> None:
|
|
216
|
+
"""Emit this function as its unique JS identifier."""
|
|
217
|
+
out.append(self.js_name)
|
|
117
218
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
|
-
if is_interpreted_mode():
|
|
123
|
-
return f"get_object('{base}')"
|
|
124
|
-
return base
|
|
219
|
+
@override
|
|
220
|
+
def render(self) -> VDOMNode:
|
|
221
|
+
"""Render as a registry reference."""
|
|
222
|
+
return {"t": "ref", "key": self.id}
|
|
125
223
|
|
|
126
|
-
def
|
|
224
|
+
def transpile(self) -> Function:
|
|
225
|
+
"""Transpile this function to a v2 Function node.
|
|
226
|
+
|
|
227
|
+
Returns the Function node (cached after first call).
|
|
228
|
+
"""
|
|
229
|
+
if self._transpiled is not None:
|
|
230
|
+
return self._transpiled
|
|
231
|
+
|
|
232
|
+
result, _ = _transpile_function_body(self.fn, self.deps)
|
|
233
|
+
|
|
234
|
+
# Convert Arrow to Function if needed, and set the name
|
|
235
|
+
if isinstance(result, Function):
|
|
236
|
+
result = Function(
|
|
237
|
+
params=result.params,
|
|
238
|
+
body=result.body,
|
|
239
|
+
name=self.js_name,
|
|
240
|
+
is_async=result.is_async,
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
# Arrow - wrap in Function with name
|
|
244
|
+
result = Function(
|
|
245
|
+
params=list(result.params),
|
|
246
|
+
body=[Return(result.body)]
|
|
247
|
+
if isinstance(result.body, Expr)
|
|
248
|
+
else result.body,
|
|
249
|
+
name=self.js_name,
|
|
250
|
+
is_async=False,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
self._transpiled = result
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
def imports(self) -> dict[str, Expr]:
|
|
127
257
|
"""Get all Import dependencies."""
|
|
128
258
|
return {k: v for k, v in self.deps.items() if isinstance(v, Import)}
|
|
129
259
|
|
|
130
260
|
def functions(self) -> dict[str, AnyJsFunction]:
|
|
131
261
|
"""Get all JsFunction dependencies."""
|
|
132
|
-
return {k: v for k, v in self.deps.items() if isinstance(v, JsFunction)}
|
|
133
|
-
|
|
134
|
-
def constants(self) -> dict[str, JsConstant]:
|
|
135
|
-
"""Get all JsConstant dependencies."""
|
|
136
|
-
return {k: v for k, v in self.deps.items() if isinstance(v, JsConstant)}
|
|
137
|
-
|
|
138
|
-
def modules(self) -> dict[str, PyModuleExpr]:
|
|
139
|
-
"""Get all PyModuleExpr dependencies."""
|
|
140
|
-
return {k: v for k, v in self.deps.items() if isinstance(v, PyModuleExpr)}
|
|
141
|
-
|
|
142
|
-
def module_functions(self) -> dict[str, JSTransformer]:
|
|
143
|
-
"""Get all module function JSTransformer dependencies (named imports from modules)."""
|
|
144
|
-
from pulse.transpiler.builtins import BUILTINS
|
|
145
|
-
|
|
146
262
|
return {
|
|
147
263
|
k: v
|
|
148
264
|
for k, v in self.deps.items()
|
|
149
|
-
if isinstance(v,
|
|
265
|
+
if isinstance(v, (JsFunction, JsxFunction))
|
|
150
266
|
}
|
|
151
267
|
|
|
152
|
-
def transpile(self) -> str:
|
|
153
|
-
"""Transpile this JsFunction to JavaScript code.
|
|
154
268
|
|
|
155
|
-
|
|
156
|
-
|
|
269
|
+
@dataclass(slots=True, init=False)
|
|
270
|
+
class JsxFunction(Expr, Generic[P, R]):
|
|
271
|
+
"""A transpiled JSX/React component function.
|
|
272
|
+
|
|
273
|
+
Like JsFunction, but transpiles to a React component that receives
|
|
274
|
+
a single props object with destructuring.
|
|
275
|
+
|
|
276
|
+
For a Python function like:
|
|
277
|
+
def Component(*children, visible=True): ...
|
|
278
|
+
|
|
279
|
+
Generates:
|
|
280
|
+
function Component_1({children, visible = true}) { ... }
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
fn: Callable[P, R]
|
|
284
|
+
id: str
|
|
285
|
+
deps: dict[str, Expr]
|
|
286
|
+
_transpiled: Function | None = field(default=None)
|
|
287
|
+
|
|
288
|
+
def __init__(self, fn: Callable[..., Any]) -> None:
|
|
289
|
+
self.fn = fn
|
|
290
|
+
self.id = next_id()
|
|
291
|
+
self._transpiled = None
|
|
292
|
+
# Register self in cache BEFORE analyzing deps (handles cycles)
|
|
293
|
+
FUNCTION_CACHE[fn] = self
|
|
294
|
+
# Now analyze and build deps
|
|
295
|
+
self.deps = analyze_deps(fn)
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def js_name(self) -> str:
|
|
299
|
+
"""Unique JS identifier for this function."""
|
|
300
|
+
return f"{to_js_identifier(self.fn.__name__)}_{self.id}"
|
|
301
|
+
|
|
302
|
+
@override
|
|
303
|
+
def emit(self, out: list[str]) -> None:
|
|
304
|
+
"""Emit this function as its unique JS identifier."""
|
|
305
|
+
out.append(self.js_name)
|
|
157
306
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
307
|
+
@override
|
|
308
|
+
def render(self) -> VDOMNode:
|
|
309
|
+
"""Render as a registry reference."""
|
|
310
|
+
return {"t": "ref", "key": self.id}
|
|
311
|
+
|
|
312
|
+
def transpile(self) -> Function:
|
|
313
|
+
"""Transpile this JSX function to a React component.
|
|
314
|
+
|
|
315
|
+
The Transpiler handles converting parameters to a destructured props object.
|
|
316
|
+
"""
|
|
317
|
+
if self._transpiled is not None:
|
|
318
|
+
return self._transpiled
|
|
319
|
+
|
|
320
|
+
result, _ = _transpile_function_body(self.fn, self.deps, jsx=True)
|
|
321
|
+
|
|
322
|
+
# JSX transpilation always returns Function (never Arrow)
|
|
323
|
+
assert isinstance(result, Function), (
|
|
324
|
+
"JSX transpilation should always return Function"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Set the unique name
|
|
328
|
+
self._transpiled = Function(
|
|
329
|
+
params=result.params,
|
|
330
|
+
body=result.body,
|
|
331
|
+
name=self.js_name,
|
|
332
|
+
is_async=result.is_async,
|
|
333
|
+
)
|
|
334
|
+
return self._transpiled
|
|
335
|
+
|
|
336
|
+
def imports(self) -> dict[str, Expr]:
|
|
337
|
+
"""Get all Import dependencies."""
|
|
338
|
+
return {k: v for k, v in self.deps.items() if isinstance(v, Import)}
|
|
161
339
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for
|
|
167
|
-
if isinstance(
|
|
168
|
-
|
|
169
|
-
if not fndefs:
|
|
170
|
-
raise JSCompilationError("No function definition found in source")
|
|
171
|
-
fndef = fndefs[-1]
|
|
340
|
+
def functions(self) -> dict[str, AnyJsFunction]:
|
|
341
|
+
"""Get all function dependencies."""
|
|
342
|
+
return {
|
|
343
|
+
k: v
|
|
344
|
+
for k, v in self.deps.items()
|
|
345
|
+
if isinstance(v, (JsFunction, JsxFunction))
|
|
346
|
+
}
|
|
172
347
|
|
|
173
|
-
|
|
174
|
-
|
|
348
|
+
@override
|
|
349
|
+
def transpile_call(
|
|
350
|
+
self, args: list[ast.expr], kwargs: dict[str, ast.expr], ctx: Transpiler
|
|
351
|
+
) -> Expr:
|
|
352
|
+
# delegate JSX element building to the generic Jsx wrapper
|
|
353
|
+
return Jsx(self).transpile_call(args, kwargs, ctx)
|
|
175
354
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return
|
|
355
|
+
@override
|
|
356
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
357
|
+
# runtime/type-checking: produce Element via Jsx wrapper
|
|
358
|
+
return Jsx(self)(*args, **kwargs) # pyright: ignore[reportReturnType]
|
|
180
359
|
|
|
181
360
|
|
|
182
361
|
def analyze_code_object(
|
|
@@ -225,21 +404,121 @@ def analyze_code_object(
|
|
|
225
404
|
return effective_globals, all_names
|
|
226
405
|
|
|
227
406
|
|
|
228
|
-
def
|
|
229
|
-
"""
|
|
407
|
+
def analyze_deps(fn: Callable[..., Any]) -> dict[str, Expr]:
|
|
408
|
+
"""Analyze a function and return its dependencies as Expr instances.
|
|
409
|
+
|
|
410
|
+
Walks the function's code object to find all referenced names,
|
|
411
|
+
then resolves them from globals/closure and converts to Expr.
|
|
412
|
+
"""
|
|
413
|
+
# Analyze code object and resolve globals + closure vars
|
|
414
|
+
effective_globals, all_names = analyze_code_object(fn)
|
|
415
|
+
|
|
416
|
+
# Build dependencies dictionary - all values are Expr
|
|
417
|
+
deps: dict[str, Expr] = {}
|
|
418
|
+
|
|
419
|
+
for name in all_names:
|
|
420
|
+
value = effective_globals.get(name)
|
|
421
|
+
|
|
422
|
+
if value is None:
|
|
423
|
+
# Not in globals - could be a builtin or unresolved
|
|
424
|
+
# For now, skip - builtins will be handled by the transpiler
|
|
425
|
+
# TODO: Add builtin support
|
|
426
|
+
continue
|
|
427
|
+
|
|
428
|
+
# Already an Expr
|
|
429
|
+
if isinstance(value, Expr):
|
|
430
|
+
deps[name] = value
|
|
431
|
+
continue
|
|
432
|
+
|
|
433
|
+
# Check global registry (for registered values like math.floor)
|
|
434
|
+
if id(value) in EXPR_REGISTRY:
|
|
435
|
+
deps[name] = EXPR_REGISTRY[id(value)]
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
# Module imports must be registered (module object itself is in EXPR_REGISTRY)
|
|
439
|
+
if inspect.ismodule(value):
|
|
440
|
+
raise TranspileError(
|
|
441
|
+
f"Could not resolve module '{name}' (value: {value!r}). "
|
|
442
|
+
+ "Register the module (or its values) in EXPR_REGISTRY."
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Functions - check cache, then create JsFunction
|
|
446
|
+
if inspect.isfunction(value):
|
|
447
|
+
if value in FUNCTION_CACHE:
|
|
448
|
+
deps[name] = FUNCTION_CACHE[value]
|
|
449
|
+
else:
|
|
450
|
+
deps[name] = JsFunction(value)
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
# Skip Expr subclasses (the classes themselves) as they are often
|
|
454
|
+
# used for type hinting or within function scope and handled
|
|
455
|
+
# by the transpiler via other means (e.g. BUILTINS or special cases)
|
|
456
|
+
if isinstance(value, type) and issubclass(value, Expr):
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
# Other callables (classes, methods, etc.) - not supported
|
|
460
|
+
if callable(value): # pyright: ignore[reportUnknownArgumentType]
|
|
461
|
+
raise TranspileError(
|
|
462
|
+
f"Callable '{name}' (type: {type(value).__name__}) is not supported. " # pyright: ignore[reportUnknownArgumentType]
|
|
463
|
+
+ "Only functions can be transpiled."
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# Constants - primitives inline, non-primitives hoisted
|
|
467
|
+
if isinstance(value, (bool, int, float, str)) or value is None:
|
|
468
|
+
deps[name] = Expr.of(value)
|
|
469
|
+
else:
|
|
470
|
+
# Non-primitive: wrap in Constant for hoisting
|
|
471
|
+
try:
|
|
472
|
+
deps[name] = Constant.wrap(value, name)
|
|
473
|
+
except TypeError:
|
|
474
|
+
raise TranspileError(
|
|
475
|
+
f"Cannot convert '{name}' (type: {type(value).__name__}) to Expr"
|
|
476
|
+
) from None
|
|
230
477
|
|
|
231
|
-
|
|
232
|
-
@javascript
|
|
233
|
-
def my_func(x: int) -> int:
|
|
234
|
-
return x + 1
|
|
478
|
+
return deps
|
|
235
479
|
|
|
236
|
-
|
|
480
|
+
|
|
481
|
+
@overload
|
|
482
|
+
def javascript(fn: Callable[[*Args], R]) -> JsFunction[*Args, R]: ...
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@overload
|
|
486
|
+
def javascript(
|
|
487
|
+
*, jsx: Literal[False] = ...
|
|
488
|
+
) -> Callable[[Callable[[*Args], R]], JsFunction[*Args, R]]: ...
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@overload
|
|
492
|
+
def javascript(*, jsx: Literal[True]) -> Callable[[Callable[P, R]], Jsx]: ...
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def javascript(fn: Callable[[*Args], R] | None = None, *, jsx: bool = False) -> Any:
|
|
496
|
+
"""Decorator to convert a Python function into a JsFunction or JsxFunction.
|
|
497
|
+
|
|
498
|
+
When jsx=False (default), returns a JsFunction instance.
|
|
499
|
+
When jsx=True, returns a JsxFunction instance.
|
|
500
|
+
|
|
501
|
+
Both are cached in FUNCTION_CACHE for deduplication and code generation.
|
|
237
502
|
"""
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
503
|
+
|
|
504
|
+
def decorator(f: Callable[[*Args], R]) -> Any:
|
|
505
|
+
cached = FUNCTION_CACHE.get(f)
|
|
506
|
+
if cached is not None:
|
|
507
|
+
# Already cached - return as-is (respects original jsx setting)
|
|
508
|
+
return cached
|
|
509
|
+
|
|
510
|
+
if jsx:
|
|
511
|
+
# Create JsxFunction for React component semantics
|
|
512
|
+
jsx_fn = JsxFunction(f)
|
|
513
|
+
# Preserve the original function's type signature for type checkers
|
|
514
|
+
return jsx_fn.as_(type(f))
|
|
515
|
+
|
|
516
|
+
# Create regular JsFunction
|
|
517
|
+
return JsFunction(f)
|
|
518
|
+
|
|
519
|
+
if fn is not None:
|
|
520
|
+
return decorator(fn)
|
|
521
|
+
return decorator
|
|
243
522
|
|
|
244
523
|
|
|
245
524
|
def registered_functions() -> list[AnyJsFunction]:
|
|
@@ -247,4 +526,55 @@ def registered_functions() -> list[AnyJsFunction]:
|
|
|
247
526
|
return list(FUNCTION_CACHE.values())
|
|
248
527
|
|
|
249
528
|
|
|
250
|
-
|
|
529
|
+
def _unwrap_jsfunction(expr: Expr) -> AnyJsFunction | None:
|
|
530
|
+
"""Unwrap common wrappers to get the underlying JsFunction or JsxFunction."""
|
|
531
|
+
if isinstance(expr, (JsFunction, JsxFunction)):
|
|
532
|
+
return expr
|
|
533
|
+
if isinstance(expr, Jsx):
|
|
534
|
+
inner = expr.expr
|
|
535
|
+
if isinstance(inner, Expr):
|
|
536
|
+
return _unwrap_jsfunction(inner)
|
|
537
|
+
return None
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def collect_function_graph(
|
|
541
|
+
functions: list[AnyJsFunction] | None = None,
|
|
542
|
+
) -> tuple[list[Constant], list[AnyJsFunction]]:
|
|
543
|
+
"""Collect all constants and functions in dependency order (depth-first).
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
functions: Functions to walk. If None, uses all registered functions.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Tuple of (constants, functions) in dependency order.
|
|
550
|
+
"""
|
|
551
|
+
if functions is None:
|
|
552
|
+
functions = registered_functions()
|
|
553
|
+
|
|
554
|
+
seen_funcs: set[str] = set()
|
|
555
|
+
seen_consts: set[str] = set()
|
|
556
|
+
all_funcs: list[AnyJsFunction] = []
|
|
557
|
+
all_consts: list[Constant] = []
|
|
558
|
+
|
|
559
|
+
def walk(fn: AnyJsFunction) -> None:
|
|
560
|
+
if fn.id in seen_funcs:
|
|
561
|
+
return
|
|
562
|
+
seen_funcs.add(fn.id)
|
|
563
|
+
|
|
564
|
+
for dep in fn.deps.values():
|
|
565
|
+
if isinstance(dep, Constant):
|
|
566
|
+
if dep.id not in seen_consts:
|
|
567
|
+
seen_consts.add(dep.id)
|
|
568
|
+
all_consts.append(dep)
|
|
569
|
+
continue
|
|
570
|
+
if isinstance(dep, Expr):
|
|
571
|
+
inner_fn = _unwrap_jsfunction(dep)
|
|
572
|
+
if inner_fn is not None:
|
|
573
|
+
walk(inner_fn)
|
|
574
|
+
|
|
575
|
+
all_funcs.append(fn)
|
|
576
|
+
|
|
577
|
+
for fn in functions:
|
|
578
|
+
walk(fn)
|
|
579
|
+
|
|
580
|
+
return all_consts, all_funcs
|
pulse/transpiler/id.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Shared ID generator for unique identifiers across imports, functions, constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
_id_counter: int = 0
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def next_id() -> str:
|
|
9
|
+
"""Generate a unique ID for imports, functions, or constants."""
|
|
10
|
+
global _id_counter
|
|
11
|
+
_id_counter += 1
|
|
12
|
+
return str(_id_counter)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def reset_id_counter() -> None:
|
|
16
|
+
"""Reset the shared ID counter. Called by clear_* functions."""
|
|
17
|
+
global _id_counter
|
|
18
|
+
_id_counter = 0
|