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,381 @@
1
+ """
2
+ Shared slot-layout allocator for the slot-based instance state scheme.
3
+
4
+ Every transformer that contributes per-instance state (persistent variables,
5
+ series, isolated call sites) allocates slots from a single
6
+ :class:`ModuleLayout` while walking the module. :func:`apply_layout` then
7
+ materializes the result into the module AST:
8
+
9
+ - the module-level ``__pyne_slot_layout__`` dict (one entry per
10
+ state-carrying scope),
11
+ - a ``func.__pyne_layout__ = __pyne_slot_layout__['<scope>']`` attach
12
+ statement after every state-carrying function definition (inside the parent
13
+ body for nested definitions); decorated definitions get an innermost
14
+ ``@__attach_layout__(...)`` decorator instead, so the layout lands on the
15
+ raw function and not on the decorator's return value,
16
+ - the hidden state parameter injected as the FIRST parameter of every
17
+ state-carrying function.
18
+
19
+ Scope identifiers join the function-name path with the middle-dot separator
20
+ (``main``, ``main·helper``) — the same convention the legacy global-name
21
+ mangling used. Repeated definitions of one name within a scope (``overload``
22
+ implementations) are disambiguated with an ordinal suffix (``highest·2``);
23
+ the mapping comes from :func:`collect_scope_segments` and every pipeline
24
+ stage reads it through :meth:`ModuleLayout.scope_segment`.
25
+
26
+ The hidden parameter is named ``__state__`` by default. Functions that
27
+ contain nested function definitions get a scope-qualified name
28
+ (``__state·main__``) instead, so a nested function can reach the parent's
29
+ state vector through a plain closure reference without its own hidden
30
+ parameter shadowing it.
31
+
32
+ The runtime side of the contract (layout dict format, ``_make_state``,
33
+ ``__resolve_slot__``, ...) lives in :mod:`pynecore.core.instance_state`.
34
+ """
35
+ import ast
36
+ from dataclasses import dataclass, field
37
+
38
+ __all__ = ['ModuleLayout', 'ScopeLayout', 'apply_layout', 'scope_for_function',
39
+ 'collect_scope_segments']
40
+
41
+ DEFAULT_STATE_PARAM = '__state__'
42
+
43
+
44
+ def _scope_defs(scope_node: ast.AST):
45
+ """Yield the function definitions belonging to one scope in source order
46
+ (descends through statements and class bodies, but not into nested
47
+ function definitions — those belong to the inner scope)."""
48
+ for child in ast.iter_child_nodes(scope_node):
49
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
50
+ yield child
51
+ elif not isinstance(child, ast.Lambda):
52
+ yield from _scope_defs(child)
53
+
54
+
55
+ def collect_scope_segments(tree: ast.Module) -> dict[int, str]:
56
+ """Compute the scope segment of every function definition in a module.
57
+
58
+ Repeated definitions of the same name within one scope (typically
59
+ ``overload`` implementations) get an ordinal suffix (``highest``,
60
+ ``highest·2``) so each keeps its own slot layout. The result is a pure
61
+ function of the tree structure, so every pipeline stage that recomputes
62
+ it gets identical segments.
63
+
64
+ :param tree: The module AST.
65
+ :return: ``id(def node) -> segment`` mapping.
66
+ """
67
+ segments: dict[int, str] = {}
68
+
69
+ def assign(scope_node: ast.AST) -> None:
70
+ counts: dict[str, int] = {}
71
+ for func in _scope_defs(scope_node):
72
+ n = counts.get(func.name, 0) + 1
73
+ counts[func.name] = n
74
+ segments[id(func)] = func.name if n == 1 else f'{func.name}·{n}'
75
+ assign(func)
76
+
77
+ assign(tree)
78
+ return segments
79
+
80
+
81
+ @dataclass
82
+ class _Slot:
83
+ """One slot of a scope's state vector."""
84
+ index: int
85
+ kind: str # 'var' | 'flag' | 'kahan' | 'series' | 'child' | 'anchor'
86
+ name: str # debug name for the layout 'names' tuple
87
+ init: ast.expr # template expression for the layout 'init' tuple
88
+ max_bars_back: ast.expr | None = None # series slots only
89
+ series_elem: str | None = None # series slots only: element type name ('float') or None
90
+ call_id: str | None = None # child/anchor slots only
91
+ in_loop: bool = False # child slots only
92
+ varip: bool = False # var slots and their kahan companions only
93
+
94
+
95
+ @dataclass
96
+ class ScopeLayout:
97
+ """Slot table of one scope (one function definition)."""
98
+ scope: str
99
+ slots: list[_Slot] = field(default_factory=list)
100
+ state_param: str = DEFAULT_STATE_PARAM
101
+
102
+ def _add(self, slot: _Slot) -> int:
103
+ self.slots.append(slot)
104
+ return slot.index
105
+
106
+ def add_var(self, name: str, init: ast.expr, *, varip: bool = False) -> int:
107
+ """Allocate a slot for a persistent variable.
108
+
109
+ :param name: Source-level variable name (debug only).
110
+ :param init: Template expression for the init tuple (literal or ``na``;
111
+ lazy-initialized variables pass ``Constant(None)`` and pair this
112
+ slot with :meth:`add_flag`).
113
+ :param varip: Whether the variable is ``varip`` (excluded from var rollback).
114
+ :return: The allocated slot index.
115
+ """
116
+ return self._add(_Slot(len(self.slots), 'var', name, init, varip=varip))
117
+
118
+ def add_flag(self, name: str) -> int:
119
+ """Allocate a lazy-init flag slot (init ``False``).
120
+
121
+ :param name: Name of the variable the flag belongs to.
122
+ :return: The allocated slot index.
123
+ """
124
+ return self._add(_Slot(len(self.slots), 'flag', f'{name}·flag', ast.Constant(value=False)))
125
+
126
+ def add_kahan(self, name: str, *, varip: bool = False) -> int:
127
+ """Allocate a Kahan compensation slot (init ``0.0``).
128
+
129
+ :param name: Name of the variable the compensation belongs to.
130
+ :param varip: Whether the compensated variable is ``varip`` — the
131
+ compensation must follow it out of the var rollback, otherwise a
132
+ rollback would desynchronize the pair.
133
+ :return: The allocated slot index.
134
+ """
135
+ return self._add(_Slot(len(self.slots), 'kahan', f'{name}·kahan', ast.Constant(value=0.0),
136
+ varip=varip))
137
+
138
+ def add_series(self, name: str, max_bars_back: ast.expr, elem: str | None = None) -> int:
139
+ """Allocate a series slot (``_make_state`` puts a fresh ``SeriesImpl`` here).
140
+
141
+ :param name: Source-level variable name (debug only).
142
+ :param max_bars_back: Expression for the series' ``max_bars_back`` argument.
143
+ :param elem: Element type name from the ``Series[T]`` annotation when it
144
+ is statically known (currently only ``'float'`` matters: those
145
+ series get the native nan as their out-of-range na value).
146
+ :return: The allocated slot index.
147
+ """
148
+ return self._add(_Slot(len(self.slots), 'series', name, ast.Constant(value=None),
149
+ max_bars_back=max_bars_back, series_elem=elem))
150
+
151
+ def add_child(self, call_id: str, *, in_loop: bool) -> int:
152
+ """Allocate a child slot for an isolated call site.
153
+
154
+ :param call_id: Call-site identifier (``main·ema·0`` style).
155
+ :param in_loop: Whether the call site sits in a loop (slot holds a
156
+ child list instead of a single child state).
157
+ :return: The allocated slot index.
158
+ """
159
+ return self._add(_Slot(len(self.slots), 'child', call_id, ast.Constant(value=None),
160
+ call_id=call_id, in_loop=in_loop))
161
+
162
+ def add_anchor(self, call_id: str, *, in_loop: bool = False) -> int:
163
+ """Allocate an anchor slot for a uniform-path call site.
164
+
165
+ Anchors are emitted into the layout's ``children`` tuple, so
166
+ ``reset()`` clears them and the next call rebinds; the bind helper
167
+ creates whatever it caches. Loop-shaped anchors hold a list of
168
+ ``(callee, bound)`` pairs indexed by the per-invocation counter.
169
+
170
+ :param call_id: Call-site identifier.
171
+ :param in_loop: Whether the call site sits in a loop.
172
+ :return: The allocated slot index.
173
+ """
174
+ return self._add(_Slot(len(self.slots), 'anchor', call_id, ast.Constant(value=None),
175
+ call_id=call_id, in_loop=in_loop))
176
+
177
+
178
+ class ModuleLayout:
179
+ """Slot layouts of every scope in one module, shared by the transformers."""
180
+
181
+ def __init__(self):
182
+ self.scopes: dict[str, ScopeLayout] = {}
183
+ self._segments: dict[int, str] = {}
184
+
185
+ def assign_scope_ids(self, tree: ast.Module) -> None:
186
+ """(Re)build the definition -> scope-segment mapping for a module.
187
+
188
+ Every transformer entry point (and :func:`apply_layout`) calls this;
189
+ the rebuild is deterministic, so repeated calls on the same tree
190
+ agree even after body mutations.
191
+
192
+ :param tree: The module AST.
193
+ """
194
+ self._segments = collect_scope_segments(tree)
195
+
196
+ def scope_segment(self, node: ast.FunctionDef) -> str:
197
+ """Disambiguated scope segment of a function definition.
198
+
199
+ :param node: The function definition.
200
+ :return: ``name`` or ``name·N`` for repeated names in one scope.
201
+ """
202
+ return self._segments.get(id(node), node.name)
203
+
204
+ def scope(self, scope_id: str) -> ScopeLayout:
205
+ """Return (creating on demand) the layout of a scope.
206
+
207
+ :param scope_id: Middle-dot joined function-name path.
208
+ :return: The scope's layout.
209
+ """
210
+ try:
211
+ return self.scopes[scope_id]
212
+ except KeyError:
213
+ scope = self.scopes[scope_id] = ScopeLayout(scope_id)
214
+ return scope
215
+
216
+ def state_carrying(self, scope_id: str) -> bool:
217
+ """Whether a scope has any state slot (and thus a hidden state parameter).
218
+
219
+ :param scope_id: Middle-dot joined function-name path.
220
+ :return: True if the scope carries state.
221
+ """
222
+ scope = self.scopes.get(scope_id)
223
+ return scope is not None and bool(scope.slots)
224
+
225
+ def state_param(self, scope_id: str) -> str:
226
+ """Name of a scope's hidden state parameter.
227
+
228
+ :param scope_id: Middle-dot joined function-name path.
229
+ :return: The parameter name (``__state__`` or scope-qualified).
230
+ """
231
+ scope = self.scopes.get(scope_id)
232
+ return scope.state_param if scope is not None else DEFAULT_STATE_PARAM
233
+
234
+
235
+ def scope_for_function(layout: ModuleLayout, scope_id: str, node: ast.FunctionDef) -> ScopeLayout:
236
+ """Return the scope layout of a function definition, qualifying the state
237
+ parameter name when the function contains nested definitions.
238
+
239
+ Every state-contributing transformer must enter scopes through this
240
+ helper so they agree on the parameter name (``__state__`` vs the
241
+ scope-qualified ``__state·{scope}__`` that nested definitions reach
242
+ through a closure).
243
+
244
+ :param layout: The module's shared allocator.
245
+ :param scope_id: Middle-dot joined function-name path.
246
+ :param node: The function definition being entered.
247
+ :return: The scope's layout.
248
+ """
249
+ scope = layout.scope(scope_id)
250
+ if any(isinstance(child, ast.FunctionDef)
251
+ for child in ast.walk(node) if child is not node):
252
+ scope.state_param = f'__state·{scope_id}__'
253
+ return scope
254
+
255
+
256
+ def _scope_entry_ast(scope: ScopeLayout) -> ast.Dict:
257
+ """Build the layout dict literal of one scope."""
258
+ init = ast.Tuple(elts=[slot.init for slot in scope.slots], ctx=ast.Load())
259
+ series = ast.Tuple(
260
+ elts=[ast.Tuple(elts=[ast.Constant(value=slot.index),
261
+ slot.max_bars_back if slot.max_bars_back is not None
262
+ else ast.Constant(value=None),
263
+ ast.Constant(value=slot.series_elem)],
264
+ ctx=ast.Load())
265
+ for slot in scope.slots if slot.kind == 'series'],
266
+ ctx=ast.Load())
267
+ varip = ast.Tuple(
268
+ elts=[ast.Constant(value=slot.index) for slot in scope.slots if slot.varip],
269
+ ctx=ast.Load())
270
+ children = ast.Tuple(
271
+ elts=[ast.Tuple(elts=[ast.Constant(value=slot.index),
272
+ ast.Constant(value=slot.call_id),
273
+ ast.Constant(value=slot.in_loop)],
274
+ ctx=ast.Load())
275
+ for slot in scope.slots if slot.kind in ('child', 'anchor')],
276
+ ctx=ast.Load())
277
+ names = ast.Tuple(
278
+ elts=[ast.Constant(value=slot.name) for slot in scope.slots],
279
+ ctx=ast.Load())
280
+ return ast.Dict(
281
+ keys=[ast.Constant(value=key) for key in ('init', 'series', 'varip', 'children', 'names')],
282
+ values=[init, series, varip, children, names],
283
+ )
284
+
285
+
286
+ def _layout_assign_ast(layout: ModuleLayout) -> ast.Assign:
287
+ """Build the module-level ``__pyne_slot_layout__`` assignment."""
288
+ carrying = [scope for scope in layout.scopes.values() if scope.slots]
289
+ return ast.Assign(
290
+ targets=[ast.Name(id='__pyne_slot_layout__', ctx=ast.Store())],
291
+ value=ast.Dict(
292
+ keys=[ast.Constant(value=scope.scope) for scope in carrying],
293
+ values=[_scope_entry_ast(scope) for scope in carrying],
294
+ ),
295
+ )
296
+
297
+
298
+ def _attach_ast(func_name: str, scope_id: str) -> ast.Assign:
299
+ """Build the ``func.__pyne_layout__ = __pyne_slot_layout__['scope']`` attach."""
300
+ return ast.Assign(
301
+ targets=[ast.Attribute(value=ast.Name(id=func_name, ctx=ast.Load()),
302
+ attr='__pyne_layout__', ctx=ast.Store())],
303
+ value=ast.Subscript(value=ast.Name(id='__pyne_slot_layout__', ctx=ast.Load()),
304
+ slice=ast.Constant(value=scope_id), ctx=ast.Load()),
305
+ )
306
+
307
+
308
+ def _attach_decorator_ast(scope_id: str) -> ast.Call:
309
+ """Build the ``__attach_layout__(__pyne_slot_layout__['scope'])`` decorator.
310
+
311
+ Used for decorated definitions: appended as the INNERMOST decorator it
312
+ tags the raw function, while a post-definition attribute assignment
313
+ would tag whatever the other decorators returned (e.g. an ``overload``
314
+ dispatcher).
315
+ """
316
+ return ast.Call(
317
+ func=ast.Name(id='__attach_layout__', ctx=ast.Load()),
318
+ args=[ast.Subscript(value=ast.Name(id='__pyne_slot_layout__', ctx=ast.Load()),
319
+ slice=ast.Constant(value=scope_id), ctx=ast.Load())],
320
+ keywords=[])
321
+
322
+
323
+ def _process_defs(body: list[ast.stmt], scope_prefix: str, layout: ModuleLayout,
324
+ used_imports: set[str]) -> list[ast.stmt]:
325
+ """Inject hidden state parameters and layout attaches into a statement list."""
326
+ new_body: list[ast.stmt] = []
327
+ for stmt in body:
328
+ new_body.append(stmt)
329
+ if not isinstance(stmt, ast.FunctionDef):
330
+ continue
331
+ segment = layout.scope_segment(stmt)
332
+ scope_id = f'{scope_prefix}·{segment}' if scope_prefix else segment
333
+ stmt.body = _process_defs(stmt.body, scope_id, layout, used_imports)
334
+ if layout.state_carrying(scope_id):
335
+ stmt.args.args.insert(0, ast.arg(arg=layout.state_param(scope_id)))
336
+ if stmt.decorator_list:
337
+ stmt.decorator_list.append(_attach_decorator_ast(scope_id))
338
+ used_imports.add('__attach_layout__')
339
+ else:
340
+ new_body.append(_attach_ast(stmt.name, scope_id))
341
+ return new_body
342
+
343
+
344
+ def _insert_index(body: list[ast.stmt]) -> int:
345
+ """Index right after the module docstring and the leading import block."""
346
+ index = 0
347
+ for i, stmt in enumerate(body):
348
+ if (isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant)
349
+ and isinstance(stmt.value.value, str)) or isinstance(stmt, (ast.Import,
350
+ ast.ImportFrom)):
351
+ index = i + 1
352
+ continue
353
+ break
354
+ return index
355
+
356
+
357
+ def apply_layout(tree: ast.Module, layout: ModuleLayout) -> ast.Module:
358
+ """Materialize the collected layout into the module AST.
359
+
360
+ Inserts the ``__pyne_slot_layout__`` dict after the import block, injects
361
+ the hidden state parameter into every state-carrying function definition
362
+ and appends the ``__pyne_layout__`` attach statement after each of them.
363
+
364
+ :param tree: The module AST (already processed by the slot transformers).
365
+ :param layout: The shared allocator the transformers filled.
366
+ :return: The same module object, updated in place.
367
+ """
368
+ # The dict is emitted even when empty: its presence marks the module as
369
+ # transformed, which the cross-module call-site classification relies on
370
+ # ("transformed module + no layout attribute -> provably stateless").
371
+ layout.assign_scope_ids(tree)
372
+ used_imports: set[str] = set()
373
+ tree.body = _process_defs(tree.body, '', layout, used_imports)
374
+ index = _insert_index(tree.body)
375
+ tree.body.insert(index, _layout_assign_ast(layout))
376
+ if used_imports:
377
+ tree.body.insert(index, ast.ImportFrom(
378
+ module='pynecore.core.instance_state',
379
+ names=[ast.alias(name=name, asname=None) for name in sorted(used_imports)],
380
+ level=0))
381
+ return tree
@@ -0,0 +1,25 @@
1
+ import ast
2
+
3
+
4
+ class TypeCheckingStripperTransformer(ast.NodeTransformer):
5
+ """
6
+ Remove `if TYPE_CHECKING:` blocks and the TYPE_CHECKING import from @pyne files.
7
+ These blocks contain IDE-only type hints (casts, re-annotations) that are unnecessary at runtime.
8
+ """
9
+
10
+ def visit_If(self, node: ast.If) -> ast.AST | None:
11
+ # Match: if TYPE_CHECKING:
12
+ if isinstance(node.test, ast.Name) and node.test.id == 'TYPE_CHECKING':
13
+ return None
14
+ # Match: if typing.TYPE_CHECKING:
15
+ if (isinstance(node.test, ast.Attribute) and node.test.attr == 'TYPE_CHECKING'
16
+ and isinstance(node.test.value, ast.Name) and node.test.value.id == 'typing'):
17
+ return None
18
+ return self.generic_visit(node)
19
+
20
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.ImportFrom | None:
21
+ if node.module == 'typing':
22
+ node.names = [alias for alias in node.names if alias.name != 'TYPE_CHECKING']
23
+ if not node.names:
24
+ return None
25
+ return node
@@ -0,0 +1,267 @@
1
+ """
2
+ Unused Series Detector Transformer
3
+
4
+ This transformer detects Series[T] annotated variables that are never actually indexed
5
+ and removes the Series annotation to optimize performance.
6
+
7
+ It runs before SeriesTransformer in the AST transformation pipeline.
8
+ """
9
+
10
+ import ast
11
+ from typing import Set, Dict
12
+
13
+
14
+ def _is_in_annotation_context(node: ast.Subscript) -> bool:
15
+ """Check if a subscript is part of a type annotation"""
16
+ # This is a simple heuristic: if the subscript value is "Series",
17
+ # it's likely a type annotation
18
+ if isinstance(node.value, ast.Name) and node.value.id == "Series":
19
+ return True
20
+ return False
21
+
22
+
23
+ class UnusedSeriesDetectorTransformer(ast.NodeTransformer):
24
+ """
25
+ AST transformer that removes unnecessary Series annotations.
26
+
27
+ For variables annotated as Series[T] but never indexed with subscript operator [],
28
+ this transformer changes their type to just T, avoiding unnecessary Series overhead.
29
+ """
30
+
31
+ def __init__(self):
32
+ # Track variables with their scope
33
+ self.series_vars: Dict[str, Set[str]] = {} # scope -> set of variable names
34
+ self.indexed_vars: Dict[str, Set[str]] = {} # scope -> set of variable names
35
+ self.current_scope: str = "__module__"
36
+ self.scope_stack: list[str] = []
37
+ # Track local variables in each scope to handle shadowing
38
+ self.local_vars: Dict[str, Set[str]] = {} # scope -> set of local variable names
39
+
40
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
41
+ """Track function scope and process function definitions"""
42
+ # Save current scope
43
+ self.scope_stack.append(self.current_scope)
44
+ self.current_scope = f"{self.current_scope}.{node.name}" if self.current_scope != "__module__" else node.name
45
+
46
+ # Process the function
47
+ self.generic_visit(node)
48
+
49
+ # Restore scope
50
+ self.current_scope = self.scope_stack.pop()
51
+
52
+ return node
53
+
54
+ def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AnnAssign:
55
+ """Detect Series annotations in variable assignments"""
56
+ if isinstance(node.target, ast.Name):
57
+ var_name = node.target.id
58
+
59
+ # Check if this is a Series annotation
60
+ if self._is_series_annotation(node.annotation):
61
+ if self.current_scope not in self.series_vars:
62
+ self.series_vars[self.current_scope] = set()
63
+ self.series_vars[self.current_scope].add(var_name)
64
+
65
+ # Track local variables to handle shadowing
66
+ if self.current_scope not in self.local_vars:
67
+ self.local_vars[self.current_scope] = set()
68
+ self.local_vars[self.current_scope].add(var_name)
69
+
70
+ # Continue visiting child nodes
71
+ self.generic_visit(node)
72
+ return node
73
+
74
+ def visit_arg(self, node: ast.arg) -> ast.arg:
75
+ """Detect Series annotations in function arguments"""
76
+ if node.annotation and self._is_series_annotation(node.annotation):
77
+ if self.current_scope not in self.series_vars:
78
+ self.series_vars[self.current_scope] = set()
79
+ self.series_vars[self.current_scope].add(node.arg)
80
+
81
+ # Track local variables to handle shadowing
82
+ if self.current_scope not in self.local_vars:
83
+ self.local_vars[self.current_scope] = set()
84
+ self.local_vars[self.current_scope].add(node.arg)
85
+
86
+ return node
87
+
88
+ def visit_Subscript(self, node: ast.Subscript) -> ast.Subscript:
89
+ """Track which variables are actually indexed"""
90
+ # Only track subscripts that are NOT type annotations
91
+ # Type annotations like Series[float] should not be counted as indexing
92
+ if isinstance(node.value, ast.Name) and not _is_in_annotation_context(node):
93
+ var_name = node.value.id
94
+
95
+ # Mark this variable as indexed
96
+ self._mark_variable_as_indexed(var_name)
97
+
98
+ # Continue visiting child nodes
99
+ self.generic_visit(node)
100
+ return node
101
+
102
+ def visit_Call(self, node: ast.Call) -> ast.Call:
103
+ """Just visit child nodes - we don't need to track function calls"""
104
+ # The key insight: if a variable is passed to a function, that doesn't mean
105
+ # it needs to be Series in the calling scope. Only direct indexing matters.
106
+ # The ClosureArgumentsTransformer preserves Series annotations on parameters
107
+ # that actually get indexed, so we can rely on that.
108
+
109
+ # Continue visiting child nodes
110
+ self.generic_visit(node)
111
+ return node
112
+
113
+ def _mark_variable_as_indexed(self, var_name: str):
114
+ """Mark a variable as indexed in the current scope"""
115
+ if self.current_scope not in self.indexed_vars:
116
+ self.indexed_vars[self.current_scope] = set()
117
+ self.indexed_vars[self.current_scope].add(var_name)
118
+
119
+ @staticmethod
120
+ def _is_series_annotation(annotation: ast.AST) -> bool:
121
+ """Check if an annotation is Series[T]"""
122
+ if isinstance(annotation, ast.Subscript):
123
+ if isinstance(annotation.value, ast.Name) and annotation.value.id == "Series":
124
+ return True
125
+ elif isinstance(annotation.value, ast.Attribute):
126
+ # Handle cases like types.Series
127
+ if annotation.value.attr == "Series":
128
+ return True
129
+ return False
130
+
131
+ @staticmethod
132
+ def _get_inner_type(annotation: ast.AST) -> ast.expr:
133
+ """Extract T from Series[T]"""
134
+ if isinstance(annotation, ast.Subscript) and isinstance(annotation.slice, ast.Name):
135
+ return annotation.slice
136
+ elif isinstance(annotation, ast.Subscript) and hasattr(annotation.slice, 'value'):
137
+ # Handle ast.Index in older Python versions
138
+ return getattr(annotation.slice, 'value')
139
+ # Default to Any if we can't determine the inner type
140
+ return ast.Name(id='Any', ctx=ast.Load())
141
+
142
+ def _resolve_series_owner(self, scope: str, var_name: str) -> str | None:
143
+ """Nearest enclosing scope (including ``scope`` itself) that declares
144
+ ``var_name`` as a Series.
145
+
146
+ Mirrors :meth:`SeriesTransformer._lookup`: a subscript resolves to the
147
+ closest series declaration walking outwards through the scope chain.
148
+ """
149
+ parts = scope.split('.')
150
+ for i in range(len(parts), 0, -1):
151
+ candidate = '.'.join(parts[:i])
152
+ if var_name in self.series_vars.get(candidate, set()):
153
+ return candidate
154
+ if var_name in self.series_vars.get("__module__", set()):
155
+ return "__module__"
156
+ return None
157
+
158
+ def optimize(self, tree: ast.AST) -> ast.AST:
159
+ """
160
+ Main optimization pass - removes unused Series annotations
161
+ """
162
+ # First pass: collect all Series variables and indexed variables
163
+ self.visit(tree)
164
+
165
+ # A parent-scope series indexed only from within a nested function is
166
+ # resolved through the scope chain by SeriesTransformer, so it must not
167
+ # be pruned. Credit the nearest enclosing scope that declares each
168
+ # indexed name as a Series, mirroring that resolution.
169
+ for scope, names in list(self.indexed_vars.items()):
170
+ for var_name in names:
171
+ owner = self._resolve_series_owner(scope, var_name)
172
+ if owner is not None and owner != scope:
173
+ self.indexed_vars.setdefault(owner, set()).add(var_name)
174
+
175
+ # Second pass: remove Series annotations from non-indexed variables
176
+ optimizer = SeriesOptimizer(self.series_vars, self.indexed_vars)
177
+ return optimizer.visit(tree)
178
+
179
+
180
+ class SeriesOptimizer(ast.NodeTransformer):
181
+ """Second pass transformer that actually removes the unused Series annotations"""
182
+
183
+ def __init__(self, series_vars: Dict[str, Set[str]], indexed_vars: Dict[str, Set[str]]):
184
+ self.series_vars = series_vars
185
+ self.indexed_vars = indexed_vars
186
+ self.current_scope = "__module__"
187
+ self.scope_stack: list[str] = []
188
+
189
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
190
+ """Track function scope"""
191
+ self.scope_stack.append(self.current_scope)
192
+ self.current_scope = f"{self.current_scope}.{node.name}" if self.current_scope != "__module__" else node.name
193
+
194
+ # Process the function
195
+ self.generic_visit(node)
196
+
197
+ # Restore scope
198
+ self.current_scope = self.scope_stack.pop()
199
+
200
+ return node
201
+
202
+ def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AnnAssign:
203
+ """Remove Series annotation if variable is never indexed"""
204
+ if isinstance(node.target, ast.Name):
205
+ var_name = node.target.id
206
+
207
+ # Check if this is an unused Series variable
208
+ if self._is_unused_series(var_name) and self._is_series_annotation(node.annotation):
209
+ # Replace Series[T] with just T
210
+ node.annotation = self._get_inner_type(node.annotation)
211
+
212
+ self.generic_visit(node)
213
+ return node
214
+
215
+ def visit_arg(self, node: ast.arg) -> ast.arg:
216
+ """Remove Series annotation from function arguments if never indexed"""
217
+ if (node.annotation and self._is_unused_series(node.arg) and
218
+ self._is_series_annotation(node.annotation)):
219
+ # Replace Series[T] with just T
220
+ node.annotation = self._get_inner_type(node.annotation)
221
+
222
+ return node
223
+
224
+ def _is_unused_series(self, var_name: str) -> bool:
225
+ """Check if a variable is Series-annotated but never indexed in the current scope"""
226
+ # Check if variable is Series in current scope
227
+ is_series = (self.current_scope in self.series_vars and
228
+ var_name in self.series_vars[self.current_scope])
229
+
230
+ # Check if variable is indexed in current scope
231
+ is_indexed = (self.current_scope in self.indexed_vars and
232
+ var_name in self.indexed_vars[self.current_scope])
233
+
234
+ # Return True if it's Series but not indexed in this scope
235
+ return is_series and not is_indexed
236
+
237
+ def _get_relevant_scopes(self) -> list[str]:
238
+ """Get all scopes that could contain the variable (current + parents)"""
239
+ scopes = ["__module__"]
240
+ if self.current_scope != "__module__":
241
+ parts = self.current_scope.split('.')
242
+ for i in range(len(parts)):
243
+ scopes.append('.'.join(parts[:i + 1]))
244
+ return scopes
245
+
246
+ @staticmethod
247
+ def _is_series_annotation(annotation: ast.AST) -> bool:
248
+ """Check if an annotation is Series[T]"""
249
+ if isinstance(annotation, ast.Subscript):
250
+ if isinstance(annotation.value, ast.Name) and annotation.value.id == "Series":
251
+ return True
252
+ elif isinstance(annotation.value, ast.Attribute):
253
+ if annotation.value.attr == "Series":
254
+ return True
255
+ return False
256
+
257
+ @staticmethod
258
+ def _get_inner_type(annotation: ast.AST) -> ast.expr:
259
+ """Extract T from Series[T]"""
260
+ if isinstance(annotation, ast.Subscript):
261
+ if isinstance(annotation.slice, ast.Name):
262
+ return annotation.slice
263
+ elif hasattr(annotation.slice, 'value'):
264
+ # Handle ast.Index in older Python versions
265
+ return getattr(annotation.slice, 'value')
266
+ # Default to float if we can't determine
267
+ return ast.Name(id='float', ctx=ast.Load())