opencode-pyneruntime 6.6.4__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.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. pynecore/utils/stdlib_checker.py +17 -0
@@ -0,0 +1,428 @@
1
+ """
2
+ Closure Arguments Transformer
3
+
4
+ This transformer runs before function_isolation and converts closure variables in
5
+ inner functions to function arguments. It only processes functions in the main
6
+ function that is decorated with @lib.script.indicator or @lib.script.strategy.
7
+
8
+ Example:
9
+ # Before transformation:
10
+ @lib.script.indicator("Test", overlay=True)
11
+ def main():
12
+ length = lib.input.int(14)
13
+ multiplier = 2.0
14
+
15
+ def calculate(offset=0):
16
+ return lib.ta.sma(lib.close, length) * multiplier + offset
17
+
18
+ return calculate() + calculate(10)
19
+
20
+ # After transformation:
21
+ @lib.script.indicator("Test", overlay=True)
22
+ def main():
23
+ length = lib.input.int(14)
24
+ multiplier = 2.0
25
+
26
+ def calculate(length, multiplier, offset=0): # closure vars added at beginning
27
+ return lib.ta.sma(lib.close, length) * multiplier + offset
28
+
29
+ return calculate(length, multiplier) + calculate(length, multiplier, 10)
30
+ """
31
+
32
+ import ast
33
+ from typing import Set, Dict, List, Optional, cast, Any
34
+
35
+
36
+ def _is_persistent_annotation(annotation: ast.AST) -> bool:
37
+ """Check if annotation is Persistent[T] or just Persistent."""
38
+ if isinstance(annotation, ast.Name):
39
+ return annotation.id == 'Persistent'
40
+ elif isinstance(annotation, ast.Subscript):
41
+ if isinstance(annotation.value, ast.Name):
42
+ return annotation.value.id == 'Persistent'
43
+ return False
44
+
45
+
46
+ class ClosureArgumentsTransformer(ast.NodeTransformer):
47
+ """Transform closure variables in inner functions to function arguments."""
48
+
49
+ def __init__(self):
50
+ # Track if we're in a decorated main function
51
+ self.in_main_function = False
52
+ # Track current function scope
53
+ self.current_function: Optional[str] = None
54
+ # Stack of function scopes for nested functions
55
+ self.function_stack: List[str] = []
56
+ # Variables defined in each scope
57
+ self.scope_variables: Dict[str, Set[str]] = {}
58
+ # Track function definitions to update calls
59
+ self.inner_functions: Dict[str, ast.FunctionDef] = {}
60
+ # Track closure variables for each inner function
61
+ self.closure_vars: Dict[str, Set[str]] = {}
62
+ # Track type annotations for closure variables
63
+ self.closure_var_types: Dict[str, ast.AST] = {}
64
+ # ``scope.var`` keys declared Persistent (a PersistentSeries is both)
65
+ self.persistent_vars: Set[str] = set()
66
+
67
+ def visit_Module(self, node: ast.Module) -> ast.Module:
68
+ # First pass: collect all function definitions and their closure variables
69
+ collector = ClosureVariableCollector()
70
+ collector.visit(node)
71
+ self.scope_variables = collector.scope_variables
72
+ self.closure_vars = collector.closure_vars
73
+ self.closure_var_types = collector.closure_var_types
74
+ self.persistent_vars = collector.persistent_vars
75
+
76
+ # A plain-Series free variable (a parent-scope ``s: Series`` or a
77
+ # builtin price series anchored in main by LibrarySeriesTransformer) is
78
+ # resolved through the scope chain — the SeriesTransformer rewrites its
79
+ # reads to the parent's state slot, reached via the parent state param
80
+ # captured as a Python closure. Value-passing it as an argument instead
81
+ # would give the inner function its own per-call history buffer
82
+ # (advancing once per call, not once per bar), so a function invoked a
83
+ # bar-varying number of times reads the wrong history (issue #67). Drop
84
+ # them here so neither the parameter list nor the call sites thread them.
85
+ self._drop_series_closures()
86
+
87
+ # Second pass: transform the functions
88
+ return cast(ast.Module, self.generic_visit(node))
89
+
90
+ def _drop_series_closures(self) -> None:
91
+ """Remove plain-Series closure variables — they resolve through the
92
+ scope chain, not through argument passing.
93
+
94
+ A PersistentSeries is left threaded: its Series slot in the parent is
95
+ pruned by the UnusedSeriesDetector (the only history read sits in the
96
+ inner function), so the scope-chain path has no slot to reach and the
97
+ value argument is what carries the per-bar history into the call."""
98
+ for scope_key, closure_vars in self.closure_vars.items():
99
+ parent_scope = scope_key.rsplit('.', 1)[0]
100
+ kept = {var for var in closure_vars
101
+ if f"{parent_scope}.{var}" in self.persistent_vars
102
+ or not self._is_series_annotation(
103
+ self.closure_var_types.get(f"{parent_scope}.{var}"))}
104
+ closure_vars.intersection_update(kept)
105
+
106
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
107
+ # Check if this is the main function with required decorators
108
+ is_main_decorated = False
109
+ if node.name == 'main':
110
+ for decorator in node.decorator_list:
111
+ decorator_name = self._get_decorator_name(decorator)
112
+ if decorator_name in ('lib.script.indicator', 'lib.script.strategy',
113
+ 'script.indicator', 'script.strategy'):
114
+ is_main_decorated = True
115
+ break
116
+
117
+ # Store old state
118
+ old_in_main = self.in_main_function
119
+ old_current = self.current_function
120
+
121
+ # Update state
122
+ if is_main_decorated:
123
+ self.in_main_function = True
124
+ self.current_function = node.name
125
+ if self.current_function:
126
+ self.function_stack.append(self.current_function)
127
+
128
+ # Process inner functions if we're in main
129
+ if self.in_main_function and old_current == 'main' and node.name != 'main':
130
+ # This is an inner function in main
131
+ func_key = self._get_function_key(node.name)
132
+
133
+ # Check if function has closure variables
134
+ if func_key in self.closure_vars and self.closure_vars[func_key]:
135
+ # Add closure variables as parameters at the beginning
136
+ closure_vars = sorted(self.closure_vars[func_key])
137
+ new_args = []
138
+ for var in closure_vars:
139
+ # Get type annotation for this closure variable
140
+ parent_scope = 'main' # closure vars come from main scope
141
+ var_key = f"{parent_scope}.{var}"
142
+ annotation = self.closure_var_types.get(var_key, None)
143
+
144
+ # If annotation is Persistent[T], extract the inner type T
145
+ if annotation and _is_persistent_annotation(annotation):
146
+ annotation = self._extract_inner_type(annotation)
147
+
148
+ # Add closure vars at the beginning with processed annotation
149
+ new_args.append(ast.arg(arg=var, annotation=cast(ast.expr, annotation)))
150
+ # Add original args after closure vars
151
+ new_args.extend(node.args.args)
152
+ node.args.args = new_args
153
+
154
+ # Store the function definition
155
+ self.inner_functions[node.name] = node
156
+
157
+ # Visit the function body
158
+ node = cast(ast.FunctionDef, self.generic_visit(node))
159
+
160
+ # Restore state
161
+ self.in_main_function = old_in_main
162
+ self.current_function = old_current
163
+ if self.function_stack:
164
+ self.function_stack.pop()
165
+
166
+ return node
167
+
168
+ def visit_Call(self, node: ast.Call) -> ast.Call:
169
+ # First visit children
170
+ node = cast(ast.Call, self.generic_visit(node))
171
+
172
+ # Check if we're calling an inner function that needs closure arguments
173
+ if self.in_main_function and isinstance(node.func, ast.Name):
174
+ func_name = node.func.id
175
+
176
+ # Handle regular function calls
177
+ if func_name in self.inner_functions:
178
+ # Get the closure variables for this function
179
+ func_key = self._get_function_key(func_name)
180
+ if func_key in self.closure_vars and self.closure_vars[func_key]:
181
+ # Add closure variables as arguments at the beginning
182
+ closure_vars = sorted(self.closure_vars[func_key])
183
+ new_args = []
184
+ for var in closure_vars:
185
+ new_args.append(ast.Name(id=var, ctx=ast.Load()))
186
+ # Add original args after closure vars
187
+ new_args.extend(node.args)
188
+ node.args = new_args
189
+
190
+ # Handle method_call() calls - these need special handling
191
+ elif func_name == 'method_call' and len(node.args) >= 2:
192
+ method_name = None
193
+
194
+ # First argument can be either a string literal or a function reference
195
+ first_arg = node.args[0]
196
+ if (isinstance(first_arg, ast.Constant) and
197
+ isinstance(first_arg.value, str)):
198
+ # method_call('method_name', this_object, ...) format
199
+ method_name = first_arg.value
200
+ elif isinstance(first_arg, ast.Name):
201
+ # method_call(method_function, this_object, ...) format
202
+ method_name = first_arg.id
203
+
204
+ if method_name:
205
+ # Check if this method name corresponds to an inner function
206
+ if method_name in self.inner_functions:
207
+ # Get the closure variables for this function
208
+ func_key = self._get_function_key(method_name)
209
+ if func_key in self.closure_vars and self.closure_vars[func_key]:
210
+ # Add closure variables as arguments
211
+ # For method_call: method_call(method_ref, closure_vars..., this_obj, original_args...)
212
+ # — the converted method signature has the closure
213
+ # parameters prepended, so this order lines up as-is
214
+ closure_vars = sorted(self.closure_vars[func_key])
215
+ new_args: List[ast.expr] = [node.args[0]]
216
+
217
+ # Add closure variables after method name
218
+ for var in closure_vars:
219
+ new_args.append(ast.Name(id=var, ctx=ast.Load()))
220
+
221
+ # Add this object after closure vars
222
+ new_args.append(node.args[1])
223
+
224
+ # Add original args after this object (skip first 2 which are method name and this)
225
+ new_args.extend(node.args[2:])
226
+
227
+ node.args = new_args
228
+
229
+ return node
230
+
231
+ def _get_decorator_name(self, decorator: Any) -> Optional[str]:
232
+ """Get the full name of a decorator."""
233
+ if isinstance(decorator, ast.Name):
234
+ return decorator.id
235
+ elif isinstance(decorator, ast.Attribute):
236
+ parts = []
237
+ current = decorator
238
+ while isinstance(current, ast.Attribute):
239
+ parts.append(current.attr)
240
+ current = current.value
241
+ if isinstance(current, ast.Name):
242
+ parts.append(current.id)
243
+ return '.'.join(reversed(parts))
244
+ elif isinstance(decorator, ast.Call):
245
+ return self._get_decorator_name(decorator.func)
246
+ return None
247
+
248
+ @staticmethod
249
+ def _get_function_key(func_name: str) -> str:
250
+ """Get unique key for a function based on its scope."""
251
+ return 'main.' + func_name
252
+
253
+ @staticmethod
254
+ def _is_series_annotation(annotation: ast.AST | None) -> bool:
255
+ """Check if annotation is Series or Series[T]."""
256
+ if isinstance(annotation, ast.Subscript):
257
+ return (isinstance(annotation.value, ast.Name)
258
+ and annotation.value.id == 'Series')
259
+ return isinstance(annotation, ast.Name) and annotation.id == 'Series'
260
+
261
+ @staticmethod
262
+ def _extract_inner_type(annotation: ast.AST) -> Optional[ast.AST]:
263
+ """Extract inner type T from Persistent[T] annotation."""
264
+ if isinstance(annotation, ast.Subscript):
265
+ # Persistent[T] -> return T
266
+ return annotation.slice
267
+ elif isinstance(annotation, ast.Name) and annotation.id == 'Persistent':
268
+ # Just Persistent -> return None (no specific type)
269
+ return None
270
+ return annotation
271
+
272
+
273
+ class ClosureVariableCollector(ast.NodeVisitor):
274
+ """Collect closure variables for inner functions."""
275
+
276
+ def __init__(self):
277
+ # Current function being analyzed
278
+ self.current_function: Optional[str] = None
279
+ # Stack of function scopes
280
+ self.function_stack: List[str] = []
281
+ # Variables defined in each scope
282
+ self.scope_variables: Dict[str, Set[str]] = {}
283
+ # Variables used in each scope
284
+ self.scope_uses: Dict[str, Set[str]] = {}
285
+ # Closure variables for each inner function
286
+ self.closure_vars: Dict[str, Set[str]] = {}
287
+ # Type annotations for closure variables
288
+ self.closure_var_types: Dict[str, ast.AST] = {}
289
+ # ``scope.var`` keys declared with a Persistent annotation. A
290
+ # PersistentSeries splits into a Persistent + a Series declaration (see
291
+ # PersistentSeriesTransformer), so such a var carries BOTH annotations;
292
+ # this set distinguishes it from a plain Series.
293
+ self.persistent_vars: Set[str] = set()
294
+ # Track if we're in the main function
295
+ self.in_main_function = False
296
+
297
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
298
+ # Check if this is the main function
299
+ is_main = node.name == 'main' and self._has_required_decorator(node)
300
+
301
+ # Store old state
302
+ old_function = self.current_function
303
+ old_in_main = self.in_main_function
304
+
305
+ # Update state
306
+ if is_main:
307
+ self.in_main_function = True
308
+ self.current_function = node.name
309
+ if self.current_function:
310
+ self.function_stack.append(self.current_function)
311
+ scope_key = self._get_scope_key()
312
+ self.scope_variables[scope_key] = set()
313
+ self.scope_uses[scope_key] = set()
314
+
315
+ # Add function parameters to scope variables
316
+ for arg in node.args.args:
317
+ self.scope_variables[scope_key].add(arg.arg)
318
+
319
+ # Visit function body
320
+ self.generic_visit(node)
321
+
322
+ # Calculate closure variables if this is an inner function in main
323
+ if self.in_main_function and len(self.function_stack) > 1 and self.function_stack[0] == 'main':
324
+ scope_key = self._get_scope_key()
325
+ parent_scope = '.'.join(self.function_stack[:-1])
326
+
327
+ # Find variables used in this scope but defined in parent scope
328
+ found_vars: Set[str] = set()
329
+ for var in self.scope_uses.get(scope_key, set()):
330
+ if var not in self.scope_variables.get(scope_key, set()):
331
+ # Check if variable is defined in parent scope
332
+ if var in self.scope_variables.get(parent_scope, set()):
333
+ found_vars.add(var)
334
+
335
+ if found_vars:
336
+ self.closure_vars[scope_key] = found_vars
337
+
338
+ # Restore state
339
+ self.current_function = old_function
340
+ self.in_main_function = old_in_main
341
+ if self.function_stack:
342
+ self.function_stack.pop()
343
+
344
+ def visit_Name(self, node: ast.Name) -> None:
345
+ if self.current_function:
346
+ scope_key = self._get_scope_key()
347
+ if isinstance(node.ctx, ast.Store):
348
+ # Variable assignment
349
+ self.scope_variables[scope_key].add(node.id)
350
+ elif isinstance(node.ctx, ast.Load):
351
+ # Variable use
352
+ self.scope_uses[scope_key].add(node.id)
353
+ self.generic_visit(node)
354
+
355
+ def visit_Assign(self, node: ast.Assign) -> None:
356
+ # Handle assignments
357
+ if self.current_function:
358
+ scope_key = self._get_scope_key()
359
+ for target in node.targets:
360
+ if isinstance(target, ast.Name):
361
+ self.scope_variables[scope_key].add(target.id)
362
+ elif isinstance(target, ast.Tuple):
363
+ for elt in target.elts:
364
+ if isinstance(elt, ast.Name):
365
+ self.scope_variables[scope_key].add(elt.id)
366
+ self.generic_visit(node)
367
+
368
+ def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
369
+ # Handle annotated assignments
370
+ if self.current_function and isinstance(node.target, ast.Name):
371
+ scope_key = self._get_scope_key()
372
+ var_name = node.target.id
373
+ self.scope_variables[scope_key].add(var_name)
374
+
375
+ # Store type annotation for potential closure variable
376
+ var_key = f"{scope_key}.{var_name}"
377
+ self.closure_var_types[var_key] = node.annotation
378
+ if _is_persistent_annotation(node.annotation):
379
+ self.persistent_vars.add(var_key)
380
+ self.generic_visit(node)
381
+
382
+ def visit_For(self, node: ast.For) -> None:
383
+ # Handle for loop variables
384
+ if self.current_function:
385
+ scope_key = self._get_scope_key()
386
+ if isinstance(node.target, ast.Name):
387
+ self.scope_variables[scope_key].add(node.target.id)
388
+ elif isinstance(node.target, ast.Tuple):
389
+ for elt in node.target.elts:
390
+ if isinstance(elt, ast.Name):
391
+ self.scope_variables[scope_key].add(elt.id)
392
+ self.generic_visit(node)
393
+
394
+ def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
395
+ # Handle exception variables
396
+ if self.current_function and node.name:
397
+ scope_key = self._get_scope_key()
398
+ self.scope_variables[scope_key].add(node.name)
399
+ self.generic_visit(node)
400
+
401
+ def _has_required_decorator(self, node: ast.FunctionDef) -> bool:
402
+ """Check if function has required decorator."""
403
+ for decorator in node.decorator_list:
404
+ decorator_name = self._get_decorator_name(decorator)
405
+ if decorator_name in ('lib.script.indicator', 'lib.script.strategy', 'script.indicator', 'script.strategy'):
406
+ return True
407
+ return False
408
+
409
+ def _get_decorator_name(self, decorator: Any) -> Optional[str]:
410
+ """Get the full name of a decorator."""
411
+ if isinstance(decorator, ast.Name):
412
+ return decorator.id
413
+ elif isinstance(decorator, ast.Attribute):
414
+ parts = []
415
+ current = decorator
416
+ while isinstance(current, ast.Attribute):
417
+ parts.append(current.attr)
418
+ current = current.value
419
+ if isinstance(current, ast.Name):
420
+ parts.append(current.id)
421
+ return '.'.join(reversed(parts))
422
+ elif isinstance(decorator, ast.Call):
423
+ return self._get_decorator_name(decorator.func)
424
+ return None
425
+
426
+ def _get_scope_key(self) -> str:
427
+ """Get unique key for current scope."""
428
+ return '.'.join(self.function_stack)
@@ -0,0 +1,140 @@
1
+ """
2
+ Display rewrite for transformed-module dumps (debug paths only).
3
+
4
+ The slot scheme emits literal state indexes (``__state__[3]``), which make a
5
+ dump hard to read. This module rebuilds the dump text with named index
6
+ constants instead:
7
+
8
+ - ``__state__[3]`` -> ``__state__[__slot·main·p__]``,
9
+ - ``__resolve_slot__(__state__, 5, f)`` / ``__bind_any__(__state__, 7, f)``
10
+ index arguments are renamed the same way,
11
+ - ``__state__.__setitem__(2, v)`` (the walrus-write form) likewise,
12
+ - the constant definitions are inserted right after the module docstring.
13
+
14
+ The rewrite works on a CLEAN COPY of the tree (rebuilt from source, so no
15
+ stray node attributes travel along — never deepcopy an AST in this
16
+ pipeline). The compiled bytecode always stays the literal-index variant;
17
+ only what ``PYNE_AST_DEBUG`` / ``PYNE_AST_SAVE`` show is affected, while
18
+ ``PYNE_AST_DEBUG_RAW`` keeps printing the exact emission (the AST golden
19
+ tests compare against that).
20
+ """
21
+ from typing import cast
22
+ import ast
23
+
24
+ from .slot_layout import ModuleLayout, DEFAULT_STATE_PARAM, collect_scope_segments
25
+
26
+ __all__ = ['display_dump']
27
+
28
+ _INDEXED_HELPERS = ('__resolve_slot__', '__bind_any__')
29
+
30
+
31
+ class _IndexNamer(ast.NodeTransformer):
32
+ """Replace literal state-vector indexes with named constants."""
33
+
34
+ def __init__(self, layout: ModuleLayout, segments: dict[int, str]):
35
+ self.layout = layout
36
+ # The display copy is re-parsed, so the layout's node-identity map
37
+ # does not apply — segments are recomputed on the copy (the mapping
38
+ # is a pure function of the tree structure, so they agree).
39
+ self.segments = segments
40
+ self.stack: list[str] = []
41
+ self.used: dict[str, int] = {} # constant name -> slot index
42
+
43
+ # --- helpers ---------------------------------------------------------
44
+
45
+ def _scope_of_param(self, param: str) -> str | None:
46
+ """Map a state-parameter name to its scope id."""
47
+ if param == DEFAULT_STATE_PARAM:
48
+ return '·'.join(self.stack) if self.stack else None
49
+ if param.startswith('__state·') and param.endswith('__'):
50
+ return param[len('__state·'):-2]
51
+ return None
52
+
53
+ def _name_for(self, param: str, index: int) -> str | None:
54
+ """Named constant for a (state parameter, literal index) pair."""
55
+ scope_id = self._scope_of_param(param)
56
+ if scope_id is None:
57
+ return None
58
+ scope = self.layout.scopes.get(scope_id)
59
+ if scope is None or not 0 <= index < len(scope.slots):
60
+ return None
61
+ label = scope.slots[index].name.replace('.', '·').replace('<', '').replace('>', '')
62
+ if not label.startswith(f'{scope_id}·'):
63
+ label = f'{scope_id}·{label}'
64
+ name = f'__slot·{label}__'
65
+ existing = self.used.get(name)
66
+ if existing is not None and existing != index:
67
+ # duplicate display names in a scope (e.g. PersistentSeries pairs)
68
+ name = f'{name[:-2]}·{index}__'
69
+ self.used[name] = index
70
+ return name
71
+
72
+ @staticmethod
73
+ def _literal_index(node: ast.expr) -> int | None:
74
+ if (isinstance(node, ast.Constant) and isinstance(node.value, int)
75
+ and not isinstance(node.value, bool)):
76
+ return node.value
77
+ return None
78
+
79
+ # --- visitors --------------------------------------------------------
80
+
81
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
82
+ self.stack.append(self.segments.get(id(node), node.name))
83
+ self.generic_visit(node)
84
+ self.stack.pop()
85
+ return node
86
+
87
+ def visit_Subscript(self, node: ast.Subscript) -> ast.Subscript:
88
+ self.generic_visit(node)
89
+ if isinstance(node.value, ast.Name):
90
+ index = self._literal_index(node.slice)
91
+ if index is not None:
92
+ name = self._name_for(node.value.id, index)
93
+ if name:
94
+ node.slice = ast.Name(id=name, ctx=ast.Load())
95
+ return node
96
+
97
+ def visit_Call(self, node: ast.Call) -> ast.Call:
98
+ self.generic_visit(node)
99
+ # __resolve_slot__(P, N, f) / __bind_any__(P, N, f)
100
+ if (isinstance(node.func, ast.Name) and node.func.id in _INDEXED_HELPERS
101
+ and len(node.args) >= 2 and isinstance(node.args[0], ast.Name)):
102
+ index = self._literal_index(node.args[1])
103
+ if index is not None:
104
+ name = self._name_for(node.args[0].id, index)
105
+ if name:
106
+ node.args[1] = ast.Name(id=name, ctx=ast.Load())
107
+ # P.__setitem__(N, value) — the walrus-write form
108
+ elif (isinstance(node.func, ast.Attribute) and node.func.attr == '__setitem__'
109
+ and isinstance(node.func.value, ast.Name) and node.args):
110
+ index = self._literal_index(node.args[0])
111
+ if index is not None:
112
+ name = self._name_for(node.func.value.id, index)
113
+ if name:
114
+ node.args[0] = ast.Name(id=name, ctx=ast.Load())
115
+ return node
116
+
117
+
118
+ def display_dump(tree: ast.Module, layout: ModuleLayout) -> str:
119
+ """Readable unparse of a transformed module.
120
+
121
+ :param tree: The fully transformed module AST.
122
+ :param layout: The module's shared slot allocator.
123
+ :return: Source text with named index constants.
124
+ """
125
+ clean = ast.parse(ast.unparse(tree))
126
+ namer = _IndexNamer(layout, collect_scope_segments(clean))
127
+ clean = cast(ast.Module, namer.visit(clean))
128
+ if namer.used:
129
+ defs: list[ast.stmt] = [
130
+ ast.Assign(targets=[ast.Name(id=name, ctx=ast.Store())],
131
+ value=ast.Constant(value=index))
132
+ for name, index in sorted(namer.used.items())]
133
+ pos = 0
134
+ first = clean.body[0] if clean.body else None
135
+ if (isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant)
136
+ and isinstance(first.value.value, str)):
137
+ pos = 1
138
+ clean.body[pos:pos] = defs
139
+ ast.fix_missing_locations(clean)
140
+ return ast.unparse(clean)