pulse-framework 0.1.73__tar.gz → 0.1.74__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/PKG-INFO +1 -1
  2. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/pyproject.toml +1 -1
  3. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/__init__.py +5 -0
  4. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/function.py +56 -32
  5. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/nodes.py +17 -2
  6. pulse_framework-0.1.74/src/pulse/transpiler/parse.py +70 -0
  7. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/transpiler.py +327 -76
  8. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/README.md +0 -0
  9. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/__init__.py +0 -0
  10. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/_examples.py +0 -0
  11. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/app.py +0 -0
  12. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/channel.py +0 -0
  13. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/__init__.py +0 -0
  14. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/cmd.py +0 -0
  15. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/dependencies.py +0 -0
  16. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/folder_lock.py +0 -0
  17. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/helpers.py +0 -0
  18. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/logging.py +0 -0
  19. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/models.py +0 -0
  20. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/packages.py +0 -0
  21. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/processes.py +0 -0
  22. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/secrets.py +0 -0
  23. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cli/uvicorn_log_config.py +0 -0
  24. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/code_analysis.py +0 -0
  25. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/codegen/__init__.py +0 -0
  26. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/codegen/codegen.py +0 -0
  27. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/codegen/templates/__init__.py +0 -0
  28. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/codegen/templates/layout.py +0 -0
  29. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/codegen/templates/route.py +0 -0
  30. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/codegen/templates/routes_ts.py +0 -0
  31. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/codegen/utils.py +0 -0
  32. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/component.py +0 -0
  33. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/components/__init__.py +0 -0
  34. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/components/for_.py +0 -0
  35. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/components/if_.py +0 -0
  36. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/components/react_router.py +0 -0
  37. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/context.py +0 -0
  38. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/cookies.py +0 -0
  39. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/debounce.py +0 -0
  40. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/decorators.py +0 -0
  41. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/dom/__init__.py +0 -0
  42. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/dom/elements.py +0 -0
  43. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/dom/events.py +0 -0
  44. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/dom/props.py +0 -0
  45. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/dom/svg.py +0 -0
  46. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/dom/tags.py +0 -0
  47. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/dom/tags.pyi +0 -0
  48. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/env.py +0 -0
  49. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/forms.py +0 -0
  50. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/helpers.py +0 -0
  51. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/hooks/__init__.py +0 -0
  52. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/hooks/core.py +0 -0
  53. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/hooks/effects.py +0 -0
  54. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/hooks/init.py +0 -0
  55. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/hooks/runtime.py +0 -0
  56. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/hooks/setup.py +0 -0
  57. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/hooks/stable.py +0 -0
  58. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/hooks/state.py +0 -0
  59. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/__init__.py +0 -0
  60. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/__init__.pyi +0 -0
  61. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/_types.py +0 -0
  62. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/array.py +0 -0
  63. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/console.py +0 -0
  64. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/date.py +0 -0
  65. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/document.py +0 -0
  66. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/error.py +0 -0
  67. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/json.py +0 -0
  68. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/map.py +0 -0
  69. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/math.py +0 -0
  70. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/navigator.py +0 -0
  71. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/number.py +0 -0
  72. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/obj.py +0 -0
  73. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/object.py +0 -0
  74. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/promise.py +0 -0
  75. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/pulse.py +0 -0
  76. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/react.py +0 -0
  77. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/regexp.py +0 -0
  78. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/set.py +0 -0
  79. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/string.py +0 -0
  80. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/weakmap.py +0 -0
  81. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/weakset.py +0 -0
  82. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/js/window.py +0 -0
  83. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/messages.py +0 -0
  84. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/middleware.py +0 -0
  85. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/plugin.py +0 -0
  86. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/proxy.py +0 -0
  87. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/py.typed +0 -0
  88. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/queries/__init__.py +0 -0
  89. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/queries/client.py +0 -0
  90. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/queries/common.py +0 -0
  91. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/queries/effect.py +0 -0
  92. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/queries/infinite_query.py +0 -0
  93. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/queries/mutation.py +0 -0
  94. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/queries/protocol.py +0 -0
  95. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/queries/query.py +0 -0
  96. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/queries/store.py +0 -0
  97. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/react_component.py +0 -0
  98. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/reactive.py +0 -0
  99. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/reactive_extensions.py +0 -0
  100. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/render_session.py +0 -0
  101. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/renderer.py +0 -0
  102. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/request.py +0 -0
  103. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/requirements.py +0 -0
  104. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/routing.py +0 -0
  105. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/scheduling.py +0 -0
  106. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/serializer.py +0 -0
  107. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/state/__init__.py +0 -0
  108. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/state/property.py +0 -0
  109. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/state/query_param.py +0 -0
  110. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/state/state.py +0 -0
  111. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/test_helpers.py +0 -0
  112. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/assets.py +0 -0
  113. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/builtins.py +0 -0
  114. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/dynamic_import.py +0 -0
  115. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/emit_context.py +0 -0
  116. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/errors.py +0 -0
  117. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/id.py +0 -0
  118. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/imports.py +0 -0
  119. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/js_module.py +0 -0
  120. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/__init__.py +0 -0
  121. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/asyncio.py +0 -0
  122. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/json.py +0 -0
  123. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/math.py +0 -0
  124. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
  125. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
  126. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/typing.py +0 -0
  127. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/py_module.py +0 -0
  128. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/transpiler/vdom.py +0 -0
  129. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/types/__init__.py +0 -0
  130. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/types/event_handler.py +0 -0
  131. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/user_session.py +0 -0
  132. {pulse_framework-0.1.73 → pulse_framework-0.1.74}/src/pulse/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.73
3
+ Version: 0.1.74
4
4
  Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
5
  Requires-Dist: fastapi>=0.128.0
6
6
  Requires-Dist: uvicorn>=0.24.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pulse-framework"
3
- version = "0.1.73"
3
+ version = "0.1.74"
4
4
  description = "Pulse - Full-stack framework for building real-time React applications in Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -106,6 +106,11 @@ from pulse.transpiler.nodes import While as While
106
106
  # Emit
107
107
  from pulse.transpiler.nodes import emit as emit
108
108
 
109
+ # Parse helpers
110
+ from pulse.transpiler.parse import ParsedSource as ParsedSource
111
+ from pulse.transpiler.parse import get_ast as get_ast
112
+ from pulse.transpiler.parse import get_source as get_source
113
+
109
114
  # Transpiler
110
115
  from pulse.transpiler.transpiler import Transpiler as Transpiler
111
116
  from pulse.transpiler.transpiler import transpile as transpile
@@ -7,8 +7,8 @@ and JsFunction which wraps transpiled functions with their dependencies.
7
7
  from __future__ import annotations
8
8
 
9
9
  import ast
10
+ import dis
10
11
  import inspect
11
- import textwrap
12
12
  import types as pytypes
13
13
  from collections.abc import Callable
14
14
  from dataclasses import dataclass, field
@@ -25,7 +25,6 @@ from typing import (
25
25
  override,
26
26
  )
27
27
 
28
- from pulse.helpers import getsourcecode
29
28
  from pulse.transpiler.errors import TranspileError
30
29
  from pulse.transpiler.id import next_id, reset_id_counter
31
30
  from pulse.transpiler.imports import Import
@@ -38,6 +37,7 @@ from pulse.transpiler.nodes import (
38
37
  Return,
39
38
  to_js_identifier,
40
39
  )
40
+ from pulse.transpiler.parse import clear_parse_cache, get_ast, get_source
41
41
  from pulse.transpiler.transpiler import Transpiler
42
42
  from pulse.transpiler.vdom import VDOMExpr
43
43
 
@@ -63,6 +63,7 @@ def clear_function_cache() -> None:
63
63
 
64
64
  FUNCTION_CACHE.clear()
65
65
  CONSTANT_REGISTRY.clear()
66
+ clear_parse_cache()
66
67
  clear_import_registry()
67
68
  clear_asset_registry()
68
69
  reset_id_counter()
@@ -137,33 +138,17 @@ def _transpile_function_body(
137
138
  deps: dict[str, Expr],
138
139
  *,
139
140
  jsx: bool = False,
140
- ) -> tuple[Function | Arrow, str]:
141
+ ) -> Function | Arrow:
141
142
  """Shared transpilation logic for JsFunction and JsxFunction.
142
143
 
143
- Returns the transpiled Function/Arrow node and the source code.
144
+ Returns the transpiled Function/Arrow node.
144
145
  """
145
146
  # Get and parse source
146
- src = getsourcecode(fn)
147
- src = textwrap.dedent(src)
148
- try:
149
- source_start_line = inspect.getsourcelines(fn)[1]
150
- except (OSError, TypeError):
151
- source_start_line = None
152
- module = ast.parse(src)
153
-
154
- # Find the function definition
155
- fndefs = [
156
- n for n in module.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
157
- ]
158
- if not fndefs:
159
- raise TranspileError("No function definition found in source")
160
- fndef = fndefs[-1]
161
-
162
- # Get filename for error messages and source file resolution
163
- try:
164
- filename = inspect.getfile(fn)
165
- except (TypeError, OSError):
166
- filename = None
147
+ parsed = get_source(fn)
148
+ src = parsed.source
149
+ fndef = get_ast(fn)
150
+ filename = parsed.filename
151
+ source_start_line = parsed.source_start_line
167
152
 
168
153
  # Transpile with source context for errors
169
154
  try:
@@ -181,7 +166,7 @@ def _transpile_function_body(
181
166
  ) from None
182
167
  raise
183
168
 
184
- return result, src
169
+ return result
185
170
 
186
171
 
187
172
  @dataclass(slots=True, init=False)
@@ -238,7 +223,7 @@ class JsFunction(Expr, Generic[*Args, R]):
238
223
  if self._transpiled is not None:
239
224
  return self._transpiled
240
225
 
241
- result, _ = _transpile_function_body(self.fn, self.deps)
226
+ result = _transpile_function_body(self.fn, self.deps)
242
227
 
243
228
  # Convert Arrow to Function if needed, and set the name
244
229
  if isinstance(result, Function):
@@ -326,7 +311,7 @@ class JsxFunction(Expr, Generic[P, R]):
326
311
  if self._transpiled is not None:
327
312
  return self._transpiled
328
313
 
329
- result, _ = _transpile_function_body(self.fn, self.deps, jsx=True)
314
+ result = _transpile_function_body(self.fn, self.deps, jsx=True)
330
315
 
331
316
  # JSX transpilation always returns Function (never Arrow)
332
317
  assert isinstance(result, Function), (
@@ -376,7 +361,6 @@ def analyze_code_object(
376
361
  - effective_globals: dict mapping names to their values (includes closure vars)
377
362
  - all_names: set of all names referenced in the code (including nested functions)
378
363
  """
379
- import dis
380
364
 
381
365
  code = fn.__code__
382
366
 
@@ -443,14 +427,54 @@ def analyze_deps(fn: Callable[..., Any]) -> dict[str, Expr]:
443
427
  """
444
428
  # Analyze code object and resolve globals + closure vars
445
429
  effective_globals, all_names = analyze_code_object(fn)
430
+ code_names = set(all_names)
431
+ default_names: set[str] = set()
432
+ default_name_values: dict[str, Any] = {}
433
+
434
+ # Include names referenced only in default expressions (not in bytecode)
435
+ try:
436
+ args = get_ast(fn).args
437
+ pos_defaults = list(args.defaults)
438
+ py_defaults = fn.__defaults__ or ()
439
+ num_args = len(args.args)
440
+ num_defaults = len(pos_defaults)
441
+ for i, _arg in enumerate(args.args):
442
+ default_idx = i - (num_args - num_defaults)
443
+ if default_idx < 0 or default_idx >= len(pos_defaults):
444
+ continue
445
+ default_node = pos_defaults[default_idx]
446
+ if isinstance(default_node, ast.Name) and default_idx < len(py_defaults):
447
+ default_name_values[default_node.id] = py_defaults[default_idx]
448
+ for node in ast.walk(default_node):
449
+ if isinstance(node, ast.Name):
450
+ default_names.add(node.id)
451
+
452
+ py_kwdefaults = fn.__kwdefaults__ or {}
453
+ for i, kwarg in enumerate(args.kwonlyargs):
454
+ default_node = args.kw_defaults[i]
455
+ if default_node is None:
456
+ continue
457
+ if isinstance(default_node, ast.Name) and kwarg.arg in py_kwdefaults:
458
+ default_name_values[default_node.id] = py_kwdefaults[kwarg.arg]
459
+ for node in ast.walk(default_node):
460
+ if isinstance(node, ast.Name):
461
+ default_names.add(node.id)
462
+ except (OSError, TypeError, SyntaxError, TranspileError):
463
+ pass
464
+
465
+ all_names.update(default_names)
466
+ default_only_names = default_names - code_names
446
467
 
447
468
  # Build dependencies dictionary - all values are Expr
448
469
  deps: dict[str, Expr] = {}
449
470
 
471
+ missing = object()
450
472
  for name in all_names:
451
- value = effective_globals.get(name)
452
-
453
- if value is None:
473
+ if name in default_only_names and name in default_name_values:
474
+ value = default_name_values[name]
475
+ else:
476
+ value = effective_globals.get(name, missing)
477
+ if value is missing:
454
478
  # Not in globals - could be a builtin or unresolved
455
479
  # For now, skip - builtins will be handled by the transpiler
456
480
  # TODO: Add builtin support
@@ -1517,7 +1517,7 @@ class If(Stmt):
1517
1517
 
1518
1518
  @dataclass(slots=True)
1519
1519
  class ForOf(Stmt):
1520
- """JS for-of loop: for (const x of iter) { ... }
1520
+ """JS for-of loop: for (x of iter) { ... }
1521
1521
 
1522
1522
  target can be a single name or array pattern for destructuring: [a, b]
1523
1523
  """
@@ -1528,7 +1528,7 @@ class ForOf(Stmt):
1528
1528
 
1529
1529
  @override
1530
1530
  def emit(self, out: list[str]) -> None:
1531
- out.append("for (const ")
1531
+ out.append("for (")
1532
1532
  out.append(self.target)
1533
1533
  out.append(" of ")
1534
1534
  self.iter.emit(out)
@@ -1619,6 +1619,21 @@ class Assign(Stmt):
1619
1619
  out.append(";")
1620
1620
 
1621
1621
 
1622
+ @dataclass(slots=True)
1623
+ class LetDecl(Stmt):
1624
+ """JS let declaration: let a, b;"""
1625
+
1626
+ names: Sequence[str]
1627
+
1628
+ @override
1629
+ def emit(self, out: list[str]) -> None:
1630
+ if not self.names:
1631
+ return
1632
+ out.append("let ")
1633
+ out.append(", ".join(self.names))
1634
+ out.append(";")
1635
+
1636
+
1622
1637
  @dataclass(slots=True)
1623
1638
  class ExprStmt(Stmt):
1624
1639
  """JS expression statement: expr;"""
@@ -0,0 +1,70 @@
1
+ """Cached parsing helpers for transpiler source inspection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import inspect
7
+ import textwrap
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ from pulse.helpers import getsourcecode
13
+ from pulse.transpiler.errors import TranspileError
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class ParsedSource:
18
+ source: str
19
+ filename: str | None
20
+ source_start_line: int | None
21
+
22
+
23
+ _SOURCE_CACHE: dict[Callable[..., Any], ParsedSource] = {}
24
+ _AST_CACHE: dict[Callable[..., Any], ast.FunctionDef | ast.AsyncFunctionDef] = {}
25
+
26
+
27
+ def clear_parse_cache() -> None:
28
+ _SOURCE_CACHE.clear()
29
+ _AST_CACHE.clear()
30
+
31
+
32
+ def get_source(fn: Callable[..., Any]) -> ParsedSource:
33
+ cached = _SOURCE_CACHE.get(fn)
34
+ if cached is not None:
35
+ return cached
36
+
37
+ src = getsourcecode(fn)
38
+ src = textwrap.dedent(src)
39
+ try:
40
+ source_start_line = inspect.getsourcelines(fn)[1]
41
+ except (OSError, TypeError):
42
+ source_start_line = None
43
+ try:
44
+ filename = inspect.getfile(fn)
45
+ except (TypeError, OSError):
46
+ filename = None
47
+
48
+ parsed = ParsedSource(
49
+ source=src,
50
+ filename=filename,
51
+ source_start_line=source_start_line,
52
+ )
53
+ _SOURCE_CACHE[fn] = parsed
54
+ return parsed
55
+
56
+
57
+ def get_ast(fn: Callable[..., Any]) -> ast.FunctionDef | ast.AsyncFunctionDef:
58
+ cached = _AST_CACHE.get(fn)
59
+ if cached is not None:
60
+ return cached
61
+
62
+ module = ast.parse(get_source(fn).source)
63
+ fndefs = [
64
+ n for n in module.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
65
+ ]
66
+ if not fndefs:
67
+ raise TranspileError("No function definition found in source")
68
+ fndef = fndefs[-1]
69
+ _AST_CACHE[fn] = fndef
70
+ return fndef