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,274 @@
1
+ """Per-call-site instantiation of security-bearing functions (Pine semantics).
2
+
3
+ In Pine Script every call of a user function creates a separate INSTANCE: a
4
+ ``request.security()`` inside a function called from N sites is N distinct
5
+ data requests, each bound to its own symbol/timeframe arguments. PyneCore's
6
+ SecurityTransformer allocates one sec_id per *syntactic*
7
+ ``request.security()`` call, and the runtime binds a sec_id to ONE resolved
8
+ (symbol, timeframe) on its first ``__sec_signal__`` — so multiple call sites
9
+ of the same function would silently share the FIRST call's binding (a
10
+ 6-timeframe ``f_htf_trend(tf)`` helper would read the first timeframe's
11
+ series six times).
12
+
13
+ This pass restores Pine's instantiation semantics statically, BEFORE
14
+ SecurityTransformer runs: any function whose subtree contains a
15
+ ``request.security[_lower_tf]`` call — or a direct call to another
16
+ security-bearing function — is cloned per direct-Name call site. Each clone
17
+ is a full deep copy inserted right after the original def, and exactly one
18
+ call site is rewritten to each clone, so SecurityTransformer then allocates
19
+ fresh sec_ids per call site and the whole downstream machinery (context
20
+ registry, ``--security`` discovery, subprocesses) works unchanged.
21
+
22
+ Bail-outs — the affected function keeps the legacy shared-context behavior:
23
+
24
+ - recursive functions (any name-level call-graph cycle),
25
+ - decorated functions (the runtime value is the decorator's return value),
26
+ - functions whose name is referenced outside a direct-call position
27
+ (aliases, callbacks, stores),
28
+ - duplicate top-of-scope definitions of the same name (shadowing),
29
+ - attribute-style call sites (methods, cross-module library calls) are not
30
+ rewritten — a library function with security calls instantiated from
31
+ several script call sites remains a single shared context (documented
32
+ limitation).
33
+
34
+ Must run after ImportNormalizerTransformer (security calls are in their
35
+ ``lib.request.security`` form) and before SecurityTransformer.
36
+ """
37
+ import ast
38
+ import copy
39
+
40
+ from .security import SecurityTransformer
41
+
42
+ __all__ = ['SecurityInstantiationTransformer']
43
+
44
+ # Hard ceiling on clones per module — far above any real script (TradingView
45
+ # itself caps unique request.* calls at 40) but low enough to stop a
46
+ # pathological call-graph blow-up with a clear error instead of a hang.
47
+ _MAX_CLONES = 64
48
+
49
+
50
+ class _FuncInfo:
51
+ """One function definition eligible for instantiation analysis."""
52
+
53
+ __slots__ = ('node', 'owner_body', 'index', 'region')
54
+
55
+ def __init__(self, node: ast.FunctionDef | ast.AsyncFunctionDef,
56
+ owner_body: list[ast.stmt], index: int, region: ast.AST):
57
+ self.node = node
58
+ self.owner_body = owner_body
59
+ self.index = index
60
+ # The subtree in which this def's name is in scope (the whole module
61
+ # for module-level defs, the enclosing function for nested defs).
62
+ self.region = region
63
+
64
+
65
+ def _ordered_walk(node: ast.AST):
66
+ """DFS in source order (``ast.walk`` is BFS; clone/call-site numbering
67
+ must be stable and follow the source)."""
68
+ yield node
69
+ for child in ast.iter_child_nodes(node):
70
+ yield from _ordered_walk(child)
71
+
72
+
73
+ class SecurityInstantiationTransformer:
74
+ """Not an ``ast.NodeTransformer`` — a whole-module fixpoint pass with the
75
+ same ``visit(tree) -> tree`` pipeline interface."""
76
+
77
+ def __init__(self):
78
+ self._clones_made = 0
79
+ self._module: ast.Module | None = None
80
+
81
+ # --- collection ---
82
+
83
+ @staticmethod
84
+ def _is_security_call(node: ast.AST) -> bool:
85
+ return isinstance(node, ast.Call) and (
86
+ SecurityTransformer._is_security_call(node) # noqa
87
+ or SecurityTransformer._is_security_lower_tf_call(node) # noqa
88
+ )
89
+
90
+ def _collect_functions(self, module: ast.Module) -> list[_FuncInfo]:
91
+ """Every FunctionDef with its owner body and scope region. Class
92
+ bodies are skipped entirely (methods are called by attribute, which
93
+ this pass never rewrites)."""
94
+ result: list[_FuncInfo] = []
95
+
96
+ def scan_body(body: list[ast.stmt], region: ast.AST) -> None:
97
+ for idx, stmt in enumerate(body):
98
+ if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
99
+ result.append(_FuncInfo(stmt, body, idx, region))
100
+ scan_body(stmt.body, stmt)
101
+ elif isinstance(stmt, ast.ClassDef):
102
+ continue
103
+ else:
104
+ # Compound statements can nest defs (if/try/with/for).
105
+ scan_sub(stmt, region)
106
+
107
+ def scan_sub(stmt: ast.stmt, region: ast.AST) -> None:
108
+ for field_body in ('body', 'orelse', 'finalbody'):
109
+ sub = getattr(stmt, field_body, None)
110
+ if isinstance(sub, list):
111
+ scan_body(sub, region)
112
+ for handler in getattr(stmt, 'handlers', []) or []:
113
+ scan_body(handler.body, region)
114
+ for case in getattr(stmt, 'cases', []) or []:
115
+ scan_body(case.body, region)
116
+
117
+ scan_body(module.body, module)
118
+ return result
119
+
120
+ @staticmethod
121
+ def _call_sites(region: ast.AST, name: str) -> list[ast.Call]:
122
+ """Direct-Name call sites of ``name`` within ``region`` in source
123
+ order, excluding sites inside a nested def that redefines the name
124
+ (approximated: shadowing defs disqualify the whole function via
125
+ duplicate-name bail-out in ``_analyze``)."""
126
+ return [n for n in _ordered_walk(region)
127
+ if isinstance(n, ast.Call)
128
+ and isinstance(n.func, ast.Name) and n.func.id == name]
129
+
130
+ @staticmethod
131
+ def _non_call_refs(region: ast.AST, name: str,
132
+ own_def: ast.AST) -> bool:
133
+ """Whether ``name`` is referenced outside its own def and outside a
134
+ direct-call func position (alias, callback argument, store, ...)."""
135
+ call_funcs = {
136
+ id(n.func) for n in _ordered_walk(region)
137
+ if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
138
+ }
139
+ for n in _ordered_walk(region):
140
+ if n is own_def:
141
+ continue
142
+ if isinstance(n, ast.Name) and n.id == name and id(n) not in call_funcs:
143
+ return True
144
+ if isinstance(n, (ast.Global, ast.Nonlocal)) and name in n.names:
145
+ return True
146
+ return False
147
+
148
+ # --- analysis ---
149
+
150
+ def _analyze(self, module: ast.Module) -> tuple[list[_FuncInfo], set[str]]:
151
+ """Return (eligible security-bearing functions with >1 call site,
152
+ bearing name set). Eligibility applies every bail-out."""
153
+ infos = self._collect_functions(module)
154
+
155
+ by_name: dict[str, list[_FuncInfo]] = {}
156
+ for info in infos:
157
+ by_name.setdefault(info.node.name, []).append(info)
158
+
159
+ # Direct bearers: subtree contains a security call.
160
+ bearing: set[str] = set()
161
+ for info in infos:
162
+ if any(self._is_security_call(n) for n in _ordered_walk(info.node)):
163
+ bearing.add(info.node.name)
164
+
165
+ # Name-level call edges among module functions (for transitivity and
166
+ # cycle detection).
167
+ edges: dict[str, set[str]] = {}
168
+ for info in infos:
169
+ callees = {
170
+ n.func.id for n in _ordered_walk(info.node)
171
+ if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
172
+ and n.func.id in by_name
173
+ }
174
+ edges.setdefault(info.node.name, set()).update(callees)
175
+
176
+ # Transitive closure: calling a bearer makes the caller a bearer.
177
+ changed = True
178
+ while changed:
179
+ changed = False
180
+ for name, callees in edges.items():
181
+ if name not in bearing and callees & bearing:
182
+ bearing.add(name)
183
+ changed = True
184
+
185
+ # Cycle detection over the bearing subgraph — any bearer on a cycle
186
+ # (recursion, mutual recursion) is excluded, or cloning would never
187
+ # converge.
188
+ on_cycle: set[str] = set()
189
+
190
+ def reaches(start: str, target: str, seen: set[str]) -> bool:
191
+ for callee in edges.get(start, ()):
192
+ if callee == target:
193
+ return True
194
+ if callee not in seen:
195
+ seen.add(callee)
196
+ if reaches(callee, target, seen):
197
+ return True
198
+ return False
199
+
200
+ for name in bearing:
201
+ if reaches(name, name, set()):
202
+ on_cycle.add(name)
203
+
204
+ eligible: list[_FuncInfo] = []
205
+ for info in infos:
206
+ name = info.node.name
207
+ if name not in bearing or name in on_cycle:
208
+ continue
209
+ if len(by_name[name]) > 1: # shadowing / duplicate defs
210
+ continue
211
+ if info.node.decorator_list:
212
+ continue
213
+ if self._non_call_refs(info.region, name, info.node):
214
+ continue
215
+ if len(self._call_sites(info.region, name)) > 1:
216
+ eligible.append(info)
217
+ return eligible, bearing
218
+
219
+ # --- cloning ---
220
+
221
+ def _unique_name(self, base: str) -> str:
222
+ """Collision-free clone name (module-wide check; the global clone
223
+ counter keeps names unique even across nested re-instantiation)."""
224
+ assert self._module is not None
225
+ existing = {
226
+ n.name for n in _ordered_walk(self._module)
227
+ if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
228
+ }
229
+ candidate = f"{base}__pyne_inst{self._clones_made}"
230
+ while candidate in existing:
231
+ candidate += "_"
232
+ return candidate
233
+
234
+ def _clone_one(self, module: ast.Module) -> bool:
235
+ """Clone the first eligible multi-site bearer; True if work was done."""
236
+ eligible, _ = self._analyze(module)
237
+ if not eligible:
238
+ return False
239
+ info = eligible[0]
240
+ name = info.node.name
241
+ sites = self._call_sites(info.region, name)
242
+ # Re-locate the def index (earlier clones may have shifted the body).
243
+ index = info.owner_body.index(info.node)
244
+ # Call site 1 keeps the original function; each further site gets a
245
+ # fresh clone inserted after the original (source order preserved).
246
+ for k, site in enumerate(sites[1:], start=2):
247
+ self._clones_made += 1
248
+ if self._clones_made > _MAX_CLONES:
249
+ raise SyntaxError(
250
+ f"security instantiation exceeded {_MAX_CLONES} function "
251
+ f"clones — the script's request.security call graph is "
252
+ f"too large to instantiate per call site"
253
+ )
254
+ clone = copy.deepcopy(info.node)
255
+ clone.name = self._unique_name(name)
256
+ info.owner_body.insert(index + k - 1, clone)
257
+ site_func = site.func
258
+ assert isinstance(site_func, ast.Name)
259
+ site_func.id = clone.name
260
+ return True
261
+
262
+ # --- pipeline API ---
263
+
264
+ def visit(self, module: ast.Module) -> ast.Module:
265
+ if not any(self._is_security_call(n) for n in ast.walk(module)):
266
+ return module
267
+ self._module = module
268
+ # Fixpoint: cloning a caller duplicates its callees' call sites, so
269
+ # re-analyze until no eligible multi-site bearer remains. Bounded by
270
+ # the clone cap (each iteration makes at least one clone).
271
+ for _ in range(_MAX_CLONES + 1):
272
+ if not self._clone_one(module):
273
+ break
274
+ return module
@@ -0,0 +1,275 @@
1
+ """
2
+ Transform Series type annotations and accesses into state-vector slots.
3
+
4
+ A series variable's circular buffer (a
5
+ :class:`~pynecore.core.series.SeriesImpl`) lives in a compile-time-assigned
6
+ slot of its scope's state vector; ``_make_state`` creates a fresh buffer for
7
+ every instance from the layout's ``series`` entries. The emitted code
8
+ addresses the buffer with a literal index:
9
+
10
+ - ``s: Series[float] = value`` -> ``s = __state__[N].add(value)`` (the local
11
+ name keeps tracking the current scalar value, plain reads stay untouched),
12
+ - ``s = value`` -> ``s = __state__[N].set(value)``,
13
+ - ``s += value`` -> ``s = __state__[N].set(s + value)``,
14
+ - ``s[idx]`` -> ``__state__[N][idx]``,
15
+ - ``lib.max_bars_back(s, num)`` in statement position
16
+ -> ``__state__[N].max_bars_back = num``
17
+ (other positions are left to the ``lib.max_bars_back`` runtime no-op),
18
+ - a ``Series``-annotated parameter loses the Series wrapper from its
19
+ annotation and gets ``s = __state__[N].add(s)`` prepended to the body.
20
+
21
+ Subscript READS resolve through the scope chain: a nested definition reaches
22
+ a parent's series buffer through a closure reference on the parent's
23
+ scope-qualified state parameter. The library-series declarations that
24
+ :class:`~pynecore.transformers.lib_series.LibrarySeriesTransformer` anchors
25
+ in ``main`` rely on this. Writes stay same-scope only, like the legacy
26
+ transformer: a plain assignment in a nested scope declares a local that
27
+ shadows the parent's series.
28
+ """
29
+ from typing import cast
30
+ import ast
31
+
32
+ from .slot_layout import ModuleLayout, scope_for_function
33
+
34
+ __all__ = ['SeriesTransformer']
35
+
36
+
37
+ class SeriesTransformer(ast.NodeTransformer):
38
+ """Rewrite Series declarations and accesses to state-vector slots."""
39
+
40
+ def __init__(self, layout: ModuleLayout):
41
+ self.layout = layout
42
+ self.scope_stack: list[str] = []
43
+ self.current_scope: str = ''
44
+ # scope -> var name -> series slot
45
+ self.series_slots: dict[str, dict[str, int]] = {}
46
+ self.series_declarations: dict[str, set[str]] = {}
47
+ self.local_vars: dict[str, set[str]] = {}
48
+
49
+ # --- helpers ---------------------------------------------------------
50
+
51
+ def _lookup(self, var_name: str) -> tuple[str, int] | None:
52
+ """Resolve a name to its declaring scope and series slot.
53
+
54
+ A name locally assigned in the current scope (but not declared as a
55
+ Series there) shadows any parent series of the same name.
56
+
57
+ :param var_name: Source-level variable name.
58
+ :return: (declaring scope, slot index) or None.
59
+ """
60
+ if (var_name in self.local_vars.get(self.current_scope, ())
61
+ and var_name not in self.series_declarations.get(self.current_scope, ())):
62
+ return None
63
+ slots = self.series_slots.get(self.current_scope)
64
+ if slots is not None and var_name in slots:
65
+ return self.current_scope, slots[var_name]
66
+ for i in range(len(self.scope_stack) - 1, 0, -1):
67
+ scope = '·'.join(self.scope_stack[:i])
68
+ slots = self.series_slots.get(scope)
69
+ if slots is not None and var_name in slots:
70
+ return scope, slots[var_name]
71
+ return None
72
+
73
+ def _state_ref(self, scope: str, slot: int) -> ast.Subscript:
74
+ """Build a ``<state param>[slot]`` reference for a scope."""
75
+ return ast.Subscript(
76
+ value=ast.Name(id=self.layout.state_param(scope), ctx=ast.Load()),
77
+ slice=ast.Constant(value=slot), ctx=ast.Load())
78
+
79
+ def _buffer_call(self, scope: str, slot: int, method: str, args: list[ast.expr]) -> ast.Call:
80
+ """Build a ``<state param>[slot].<method>(...)`` call."""
81
+ return ast.Call(
82
+ func=ast.Attribute(value=self._state_ref(scope, slot),
83
+ attr=method, ctx=ast.Load()),
84
+ args=args, keywords=[])
85
+
86
+ def _register(self, var_name: str, elem: str | None = None) -> int:
87
+ """Allocate a series slot for a declaration in the current scope.
88
+
89
+ :param var_name: Source-level variable name.
90
+ :param elem: Statically known element type name from the ``Series[T]``
91
+ annotation (``'float'`` selects the native-nan na value), or None.
92
+ :return: The allocated slot index.
93
+ """
94
+ slot = self.layout.scope(self.current_scope).add_series(
95
+ var_name, ast.Constant(value=None), elem)
96
+ self.series_slots.setdefault(self.current_scope, {})[var_name] = slot
97
+ self.series_declarations[self.current_scope].add(var_name)
98
+ self.local_vars[self.current_scope].add(var_name)
99
+ return slot
100
+
101
+ @staticmethod
102
+ def _is_series_type(annotation: ast.expr) -> bool:
103
+ """Check if a type annotation is Series."""
104
+ if isinstance(annotation, ast.Subscript):
105
+ return (isinstance(annotation.value, ast.Name)
106
+ and annotation.value.id == 'Series')
107
+ return isinstance(annotation, ast.Name) and annotation.id == 'Series'
108
+
109
+ @staticmethod
110
+ def _series_elem(annotation: ast.expr) -> str | None:
111
+ """Element type name of a ``Series[T]`` annotation, if statically known.
112
+
113
+ Only ``'float'`` is meaningful downstream (it selects the native nan
114
+ as the series' na value); everything else — bare ``Series``, other
115
+ element types, complex annotations — yields None.
116
+
117
+ :param annotation: The (Series) type annotation.
118
+ :return: ``'float'`` for ``Series[float]``, otherwise None.
119
+ """
120
+ if (isinstance(annotation, ast.Subscript)
121
+ and isinstance(annotation.slice, ast.Name)
122
+ and annotation.slice.id == 'float'):
123
+ return 'float'
124
+ return None
125
+
126
+ # --- visitors --------------------------------------------------------
127
+
128
+ def visit_Module(self, node: ast.Module) -> ast.Module:
129
+ self.layout.assign_scope_ids(node)
130
+ return cast(ast.Module, self.generic_visit(node))
131
+
132
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.ImportFrom | None:
133
+ """Strip the Series name from pynecore imports."""
134
+ if node.module and node.module.startswith('pynecore'):
135
+ new_names = [name for name in node.names if name.name != 'Series']
136
+ if not new_names:
137
+ return None
138
+ node.names = new_names
139
+ return node
140
+
141
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
142
+ """Track scopes and convert Series-annotated parameters."""
143
+ self.scope_stack.append(self.layout.scope_segment(node))
144
+ self.current_scope = '·'.join(self.scope_stack)
145
+ scope_for_function(self.layout, self.current_scope, node)
146
+ self.local_vars.setdefault(self.current_scope, set())
147
+ self.series_declarations.setdefault(self.current_scope, set())
148
+
149
+ param_inits: list[ast.stmt] = []
150
+ for arg in node.args.args:
151
+ self.local_vars[self.current_scope].add(arg.arg)
152
+ if arg.annotation is not None and self._is_series_type(arg.annotation):
153
+ slot = self._register(arg.arg, self._series_elem(arg.annotation))
154
+ arg.annotation = (arg.annotation.slice
155
+ if isinstance(arg.annotation, ast.Subscript) else None)
156
+ param_inits.append(ast.Assign(
157
+ targets=[ast.Name(id=arg.arg, ctx=ast.Store())],
158
+ value=self._buffer_call(self.current_scope, slot, 'add',
159
+ [ast.Name(id=arg.arg, ctx=ast.Load())])))
160
+
161
+ node = cast(ast.FunctionDef, self.generic_visit(node))
162
+
163
+ if param_inits:
164
+ insert_pos = 0
165
+ first = node.body[0] if node.body else None
166
+ if (isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant)
167
+ and isinstance(first.value.value, str)):
168
+ insert_pos = 1
169
+ node.body[insert_pos:insert_pos] = param_inits
170
+
171
+ self.scope_stack.pop()
172
+ self.current_scope = '·'.join(self.scope_stack)
173
+ return node
174
+
175
+ def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AST | None:
176
+ """Convert Series declarations into slot allocations with ``add()``."""
177
+ if not (isinstance(node.target, ast.Name) and self._is_series_type(node.annotation)):
178
+ if isinstance(node.target, ast.Name) and self.current_scope:
179
+ # An annotated assignment declares a local — it shadows a
180
+ # same-named parent series, like a plain assignment does.
181
+ self.local_vars.setdefault(self.current_scope, set()).add(node.target.id)
182
+ if node.value:
183
+ node.value = cast(ast.expr, self.visit(node.value))
184
+ return node
185
+
186
+ if not self.current_scope:
187
+ raise SyntaxError("Series variables must be declared inside a function")
188
+
189
+ slot = self._register(node.target.id, self._series_elem(node.annotation))
190
+ if node.value is None:
191
+ return None
192
+ return ast.Assign(
193
+ targets=[ast.Name(id=node.target.id, ctx=ast.Store())],
194
+ value=self._buffer_call(self.current_scope, slot, 'add',
195
+ [cast(ast.expr, self.visit(node.value))]))
196
+
197
+ def visit_Assign(self, node: ast.Assign) -> ast.AST | list[ast.stmt]:
198
+ """Convert assignments to same-scope series variables into buffer writes."""
199
+ if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
200
+ var_name = cast(ast.Name, node.targets[0]).id
201
+ slots = self.series_slots.get(self.current_scope)
202
+ if slots is not None and var_name in slots:
203
+ return ast.Assign(
204
+ targets=[ast.Name(id=var_name, ctx=ast.Store())],
205
+ value=self._buffer_call(self.current_scope, slots[var_name], 'set',
206
+ [cast(ast.expr, self.visit(node.value))]))
207
+ if self.current_scope:
208
+ self.local_vars.setdefault(self.current_scope, set()).add(var_name)
209
+
210
+ # Tuple/list unpacking (``a, b = f()``): a series element is declared bare
211
+ # (``a: Series`` with no initializer, so no per-bar ``add()``) and the
212
+ # unpack only binds the local name — its buffer never advances. Emit an
213
+ # ``add()`` per series element to record history so ``a[1]`` works.
214
+ target = node.targets[0] if len(node.targets) == 1 else None
215
+ if (isinstance(target, (ast.Tuple, ast.List))
216
+ and self.current_scope
217
+ and all(isinstance(e, ast.Name) for e in target.elts)):
218
+ slots = self.series_slots.get(self.current_scope, {})
219
+ node.value = cast(ast.expr, self.visit(node.value))
220
+ stmts: list[ast.stmt] = [node]
221
+ for elt in target.elts:
222
+ name = cast(ast.Name, elt).id
223
+ if name in slots:
224
+ stmts.append(ast.Assign(
225
+ targets=[ast.Name(id=name, ctx=ast.Store())],
226
+ value=self._buffer_call(self.current_scope, slots[name], 'add',
227
+ [ast.Name(id=name, ctx=ast.Load())])))
228
+ else:
229
+ self.local_vars.setdefault(self.current_scope, set()).add(name)
230
+ return stmts if len(stmts) > 1 else node
231
+
232
+ return cast(ast.AST, self.generic_visit(node))
233
+
234
+ def visit_AugAssign(self, node: ast.AugAssign) -> ast.AST:
235
+ """Convert augmented assignments to same-scope series into ``set()``."""
236
+ if isinstance(node.target, ast.Name):
237
+ slots = self.series_slots.get(self.current_scope)
238
+ if slots is not None and node.target.id in slots:
239
+ value = ast.BinOp(left=ast.Name(id=node.target.id, ctx=ast.Load()),
240
+ op=node.op,
241
+ right=cast(ast.expr, self.visit(node.value)))
242
+ return ast.Assign(
243
+ targets=[ast.Name(id=node.target.id, ctx=ast.Store())],
244
+ value=self._buffer_call(self.current_scope, slots[node.target.id],
245
+ 'set', [value]))
246
+ return cast(ast.AST, self.generic_visit(node))
247
+
248
+ def visit_Subscript(self, node: ast.Subscript) -> ast.AST:
249
+ """Rewrite series indexing to address the buffer in its slot."""
250
+ if isinstance(node.value, ast.Name):
251
+ found = self._lookup(node.value.id)
252
+ if found:
253
+ scope, slot = found
254
+ node.value = self._state_ref(scope, slot)
255
+ node.slice = cast(ast.expr, self.visit(node.slice))
256
+ return node
257
+ return cast(ast.AST, self.generic_visit(node))
258
+
259
+ def visit_Expr(self, node: ast.Expr) -> ast.AST:
260
+ """Convert statement-position ``lib.max_bars_back(s, n)`` calls into
261
+ a ``max_bars_back`` attribute assignment on the buffer."""
262
+ call = node.value
263
+ if (isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute)
264
+ and call.func.attr == 'max_bars_back'
265
+ and isinstance(call.func.value, ast.Name)
266
+ and call.func.value.id == 'lib'
267
+ and len(call.args) >= 2 and isinstance(call.args[0], ast.Name)):
268
+ found = self._lookup(call.args[0].id)
269
+ if found:
270
+ scope, slot = found
271
+ return ast.Assign(
272
+ targets=[ast.Attribute(value=self._state_ref(scope, slot),
273
+ attr='max_bars_back', ctx=ast.Store())],
274
+ value=cast(ast.expr, self.visit(call.args[1])))
275
+ return cast(ast.AST, self.generic_visit(node))