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,221 @@
1
+ from typing import cast, Any
2
+ import ast
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ class ModulePropertyTransformer(ast.NodeTransformer):
8
+ """
9
+ Transform lib.xxx references based on the generated module_properties.json registry.
10
+
11
+ - ``property`` entries (Pine names that are values): bare reads become calls
12
+ (``ta.tr`` -> ``ta.tr()``); explicit calls are left untouched.
13
+ - ``variable`` entries: left as plain attribute reads.
14
+ - Function-and-namespace modules (``plot``, ``dayofweek``, ...): calls and promoted
15
+ bare reads are routed to the module's self-named function
16
+ (``plot(x)`` -> ``plot.plot(x)``, bare ``dayofweek`` -> ``dayofweek.dayofweek()``).
17
+ - Unknown names on known pynecore.lib modules raise at transform time — the
18
+ registry is exhaustive, so this catches typos and a stale registry early.
19
+ - Unknown module paths (user ``lib.*`` workdir libraries) and ``_``-prefixed
20
+ names are plain attribute reads.
21
+ """
22
+
23
+ def __init__(self):
24
+ # Structure: module -> name -> {"type": "property"|"variable"}
25
+ self.module_info: dict[str, dict[str, dict[str, Any]]] = {}
26
+
27
+ # Load config
28
+ try:
29
+ with open(Path(__file__).parent / "module_properties.json") as f:
30
+ self.module_info = json.load(f)
31
+ except (IOError, json.JSONDecodeError) as e:
32
+ raise RuntimeError(f"Failed to load module properties config: {e}")
33
+
34
+ def visit(self, node: ast.AST) -> ast.AST:
35
+ """
36
+ Override the generic visit method to set .parent on each child node
37
+ for chain detection.
38
+ """
39
+ # Set parent on children
40
+ for field, value in ast.iter_fields(node):
41
+ if isinstance(value, ast.AST):
42
+ setattr(value, "parent", node)
43
+ elif isinstance(value, list):
44
+ for item in value:
45
+ if isinstance(item, ast.AST):
46
+ setattr(item, "parent", node)
47
+
48
+ return super().visit(node)
49
+
50
+ def visit_Attribute(self, node: ast.Attribute) -> ast.AST:
51
+ """Process attribute access, but skip if inside type annotations."""
52
+ node = cast(ast.Attribute, self.generic_visit(node))
53
+
54
+ # Skip if inside type annotations
55
+ if self._is_in_type_annotation(node):
56
+ return node
57
+
58
+ # Retrieve the AST parent node
59
+ parent = getattr(node, 'parent', None)
60
+
61
+ # Intermediate module - if the parent is also an Attribute, this is not the topmost attribute
62
+ if isinstance(parent, ast.Attribute):
63
+ return node
64
+
65
+ # If this node has already been processed, or the chain does not start with lib..., skip
66
+ if hasattr(node, '_processed') or not self._is_lib_reference(node):
67
+ return node
68
+
69
+ # Now it's the topmost attribute (e.g., ...data_window)
70
+ # Check the full module path and the final attribute
71
+ module_path, name = self._get_module_info(node)
72
+ if not module_path or not name:
73
+ return node
74
+
75
+ full_path = f"{module_path}.{name}"
76
+
77
+ # Call site — explicit calls stay as they are, except when the callee is a
78
+ # function-and-namespace module (its registry entry contains a self-named
79
+ # function): ``lib.plot(...)`` routes to ``lib.plot.plot(...)``
80
+ if isinstance(parent, ast.Call) and parent.func == node:
81
+ inner_attrs = self.module_info.get(full_path)
82
+ if inner_attrs is not None and name in inner_attrs:
83
+ result: ast.expr = ast.Attribute(value=node, attr=name, ctx=ast.Load())
84
+ setattr(result, "_processed", True)
85
+ return result
86
+ return node
87
+
88
+ module_attrs = self.module_info.get(module_path)
89
+ if module_attrs is None:
90
+ # Unknown module path: a user workdir library or a class-attribute chain —
91
+ # plain Python attribute access
92
+ return node
93
+
94
+ attr_info = module_attrs.get(name)
95
+ if attr_info is not None:
96
+ if attr_info["type"] == "property":
97
+ if full_path == "lib.na":
98
+ # Bare ``na`` is a constant value (the interned typeless NA):
99
+ # load it directly instead of emitting a per-bar ``lib.na()``
100
+ # call. Explicit ``na(x)`` predicate calls are untouched above.
101
+ result = ast.Attribute(value=ast.Name(id='lib', ctx=ast.Load()),
102
+ attr='_na_none', ctx=ast.Load())
103
+ setattr(result, "_processed", True)
104
+ return result
105
+ inner_attrs = self.module_info.get(full_path)
106
+ if inner_attrs is not None and name in inner_attrs:
107
+ # Promoted self-named property of a function-and-namespace module:
108
+ # bare ``dayofweek`` -> ``lib.dayofweek.dayofweek()``
109
+ func: ast.expr = ast.Attribute(value=self._copy_node(node), attr=name,
110
+ ctx=ast.Load())
111
+ else:
112
+ func = self._copy_node(node)
113
+ result = ast.Call(func=func, args=[], keywords=[])
114
+ else:
115
+ result = node
116
+
117
+ # Submodule reference — leave as is
118
+ elif full_path in self.module_info:
119
+ result = node
120
+
121
+ # Internal names are never module properties — plain attribute access
122
+ elif name.startswith('_'):
123
+ result = node
124
+
125
+ else:
126
+ raise SyntaxError(
127
+ f"unknown attribute '{name}' on module '{module_path}' (line {node.lineno}); "
128
+ f"if this is a new pynecore.lib name, regenerate module_properties.json "
129
+ f"with scripts/module_property_collector.py"
130
+ )
131
+
132
+ setattr(result, "_processed", True)
133
+ return result
134
+
135
+ @staticmethod
136
+ def _is_lib_reference(node: ast.Attribute) -> bool:
137
+ """Check if the attribute chain starts with 'lib'."""
138
+ current = node
139
+ while isinstance(current, ast.Attribute):
140
+ current = current.value
141
+ return isinstance(current, ast.Name) and current.id == 'lib'
142
+
143
+ @staticmethod
144
+ def _get_module_info(node: ast.Attribute) -> tuple[str | None, str | None]:
145
+ """
146
+ Gather the full chain of attributes until we reach 'lib',
147
+ then split into (module_path, final_attribute).
148
+ Example: lib.display.data_window -> (lib.display, data_window)
149
+ """
150
+ attrs = []
151
+ current = node
152
+ while isinstance(current, ast.Attribute):
153
+ attrs.append(current.attr)
154
+ current = current.value
155
+
156
+ if isinstance(current, ast.Name) and current.id == 'lib':
157
+ attrs.append('lib')
158
+ attrs.reverse()
159
+ # Example: ['lib', 'display', 'data_window']
160
+ if len(attrs) < 2:
161
+ return None, None
162
+ module_path = '.'.join(attrs[:-1]) # 'lib.display'
163
+ final_attr = attrs[-1] # 'data_window'
164
+ return module_path, final_attr
165
+ return None, None
166
+
167
+ def _is_in_type_annotation(self, node: ast.Attribute) -> bool:
168
+ """Check if the node is inside a type annotation."""
169
+ current = node
170
+ while hasattr(current, 'parent'):
171
+ parent = getattr(current, 'parent', None)
172
+
173
+ # Check if we're in an annotated assignment's annotation
174
+ if (isinstance(parent, ast.AnnAssign) and parent.annotation and
175
+ self._is_node_in_subtree(node, cast(ast.AST, parent.annotation))):
176
+ return True
177
+
178
+ # Check if we're in a function argument's annotation
179
+ if (isinstance(parent, ast.arg) and parent.annotation and
180
+ self._is_node_in_subtree(node, cast(ast.AST, parent.annotation))):
181
+ return True
182
+
183
+ # Check if we're in a function return annotation
184
+ if (isinstance(parent, ast.FunctionDef) and parent.returns and
185
+ self._is_node_in_subtree(node, parent.returns)):
186
+ return True
187
+
188
+ # Check if we're in an async function return annotation
189
+ if (isinstance(parent, ast.AsyncFunctionDef) and parent.returns and
190
+ self._is_node_in_subtree(node, parent.returns)):
191
+ return True
192
+
193
+ current = parent
194
+
195
+ return False
196
+
197
+ @staticmethod
198
+ def _is_node_in_subtree(node: ast.AST, subtree: ast.AST | None) -> bool:
199
+ """Check if a node is contained within a subtree."""
200
+ if subtree is None:
201
+ return False
202
+
203
+ if node is subtree:
204
+ return True
205
+
206
+ # Recursively check all child nodes
207
+ for child in ast.walk(subtree):
208
+ if child is node:
209
+ return True
210
+
211
+ return False
212
+
213
+ @staticmethod
214
+ def _copy_node(node: ast.AST) -> ast.expr:
215
+ """Create a shallow copy of an AST node (Attribute or Name)."""
216
+ if isinstance(node, ast.Name):
217
+ return cast(ast.expr, ast.Name(id=node.id, ctx=node.ctx))
218
+ elif isinstance(node, ast.Attribute):
219
+ value = ModulePropertyTransformer._copy_node(cast(ast.AST, node.value))
220
+ return cast(ast.expr, ast.Attribute(value=value, attr=node.attr, ctx=node.ctx))
221
+ return cast(ast.expr, node)
@@ -0,0 +1,70 @@
1
+ import ast
2
+
3
+
4
+ def _is_skippable_const(node: ast.expr) -> bool:
5
+ """A ``str``/``bool`` constant operand: Pine only allows comparing it to the
6
+ same type, so the comparison is homogeneous non-float and a na operand is an
7
+ ``NA`` object whose ``__ne__`` is already False — no nan guard needed."""
8
+ return isinstance(node, ast.Constant) and isinstance(node.value, (str, bool))
9
+
10
+
11
+ class NeGuardTransformer(ast.NodeTransformer):
12
+ """
13
+ Give ``!=`` TradingView's na semantics on native nan operands.
14
+
15
+ Pine's float na is a native IEEE-754 nan. Raw IEEE keeps ``==``/``<``/``>``/
16
+ ``<=``/``>=`` falsy on nan — matching TradingView, where every comparison
17
+ with na is false — but ``nan != x`` would be True. Compiled scripts keep the
18
+ readable ``(l) != (r)`` form; this transformer rewrites it at load time to
19
+
20
+ l == l and r == r and l != r
21
+
22
+ which is False whenever either operand is nan. It is semantically neutral
23
+ for every other operand type: ``x == x`` is True for str/int/bool/objects,
24
+ and an ``NA`` object fails its own ``__eq__`` exactly like its ``__ne__``.
25
+
26
+ Operands that are not simple names/constants are bound once via a walrus so
27
+ side effects (function calls) don't run twice. ``str``/``bool`` constant
28
+ comparisons are homogeneous non-float in Pine and stay untouched.
29
+ """
30
+
31
+ def __init__(self):
32
+ self._temp_counter = 0
33
+
34
+ @staticmethod
35
+ def _copy_simple(node: ast.expr) -> ast.expr:
36
+ """Fresh node for re-reading a simple operand (AST nodes must not be shared)."""
37
+ if isinstance(node, ast.Name):
38
+ return ast.Name(id=node.id, ctx=ast.Load())
39
+ assert isinstance(node, ast.Constant)
40
+ return ast.Constant(value=node.value)
41
+
42
+ def _bind_once(self, node: ast.expr) -> tuple[ast.expr, ast.expr, ast.expr]:
43
+ """Return (first_use, second_use, third_use) for an operand.
44
+
45
+ Simple names/constants are re-read; anything else is bound once via a
46
+ walrus so side effects (function calls) don't run twice."""
47
+ if isinstance(node, (ast.Name, ast.Constant)):
48
+ return node, self._copy_simple(node), self._copy_simple(node)
49
+ self._temp_counter += 1
50
+ name = f"__ne{self._temp_counter}__"
51
+ return (ast.NamedExpr(target=ast.Name(id=name, ctx=ast.Store()), value=node),
52
+ ast.Name(id=name, ctx=ast.Load()),
53
+ ast.Name(id=name, ctx=ast.Load()))
54
+
55
+ def visit_Compare(self, node: ast.Compare) -> ast.expr:
56
+ self.generic_visit(node)
57
+
58
+ if len(node.ops) != 1 or not isinstance(node.ops[0], ast.NotEq):
59
+ return node
60
+ left, right = node.left, node.comparators[0]
61
+ if _is_skippable_const(left) or _is_skippable_const(right):
62
+ return node
63
+
64
+ left1, left2, left3 = self._bind_once(left)
65
+ right1, right2, right3 = self._bind_once(right)
66
+ return ast.BoolOp(op=ast.And(), values=[
67
+ ast.Compare(left=left1, ops=[ast.Eq()], comparators=[left2]),
68
+ ast.Compare(left=right1, ops=[ast.Eq()], comparators=[right2]),
69
+ ast.Compare(left=left3, ops=[ast.NotEq()], comparators=[right3]),
70
+ ])
@@ -0,0 +1,320 @@
1
+ """
2
+ Transform Persistent type annotations and accesses into state-vector slots.
3
+
4
+ A persistent variable lives in a compile-time-assigned slot of its scope's
5
+ state vector (a plain list the function receives as its hidden first
6
+ parameter, see :mod:`pynecore.transformers.slot_layout`); every access is a
7
+ literal-index subscript:
8
+
9
+ - ``p: Persistent[float] = 0.0`` -> slot allocated, declaration removed (the
10
+ initial value moves into the layout's init template),
11
+ - non-literal initializers keep the lazy pattern: a value slot plus a flag
12
+ slot and an ``if not __state__[flag]: ...`` guard at the declaration site,
13
+ - reads/writes become ``__state__[N]``,
14
+ - ``+=`` with a non-literal value keeps Kahan summation, emitted as a
15
+ four-statement sequence (a slot cannot be the target of a walrus, so the
16
+ legacy single-expression form does not carry over — statement position is
17
+ guaranteed because ``+=`` is always a statement); variables declared with
18
+ a ``str``/``bool`` element type skip Kahan (numeric error compensation
19
+ would crash string concatenation),
20
+ - a walrus write to a persistent (expression position) is emitted through
21
+ ``__state__.__setitem__`` inside a tuple expression, preserving the value.
22
+
23
+ Scope rules mirror the legacy transformer: scopes are the middle-dot joined
24
+ function-name path, nested definitions see parent persistents (resolved to
25
+ the parent's state vector through a closure reference on the parent's
26
+ scope-qualified state parameter), and a plain local assignment shadows a
27
+ parent persistent of the same name.
28
+ """
29
+ from typing import cast
30
+ import ast
31
+
32
+ from .slot_layout import ModuleLayout, scope_for_function
33
+
34
+ __all__ = ['PersistentTransformer']
35
+
36
+ PERSISTENT_TYPES = ('Persistent', 'IBPersistent', 'IBPersistentSeries')
37
+ VARIP_TYPES = ('IBPersistent', 'IBPersistentSeries')
38
+
39
+
40
+ class PersistentTransformer(ast.NodeTransformer):
41
+ """Rewrite Persistent declarations and accesses to state-vector slots."""
42
+
43
+ def __init__(self, layout: ModuleLayout):
44
+ self.layout = layout
45
+ self.scope_stack: list[str] = []
46
+ self.current_scope: str = ''
47
+ # scope -> var name -> (value slot, flag slot or None, kahan slot or None)
48
+ self.var_slots: dict[str, dict[str, int]] = {}
49
+ self.kahan_slots: dict[str, dict[str, int]] = {}
50
+ # scope -> var name -> declared element type name (None when unknown)
51
+ self.var_types: dict[str, dict[str, str | None]] = {}
52
+ self.persistent_declarations: dict[str, set[str]] = {}
53
+ self.local_vars: dict[str, set[str]] = {}
54
+
55
+ # --- helpers ---------------------------------------------------------
56
+
57
+ def _lookup(self, var_name: str) -> tuple[str, int] | None:
58
+ """Resolve a name to its declaring scope and value slot.
59
+
60
+ A name locally declared in the current scope (but not as Persistent)
61
+ shadows any parent persistent of the same name.
62
+
63
+ :param var_name: Source-level variable name.
64
+ :return: (declaring scope, slot index) or None.
65
+ """
66
+ if (var_name in self.local_vars.get(self.current_scope, ())
67
+ and var_name not in self.persistent_declarations.get(self.current_scope, ())):
68
+ return None
69
+ slots = self.var_slots.get(self.current_scope)
70
+ if slots is not None and var_name in slots:
71
+ return self.current_scope, slots[var_name]
72
+ for i in range(len(self.scope_stack) - 1, 0, -1):
73
+ scope = '·'.join(self.scope_stack[:i])
74
+ slots = self.var_slots.get(scope)
75
+ if slots is not None and var_name in slots:
76
+ return scope, slots[var_name]
77
+ return None
78
+
79
+ def _state_ref(self, scope: str, slot: int, ctx: ast.expr_context) -> ast.Subscript:
80
+ """Build a ``<state param>[slot]`` reference for a scope."""
81
+ return ast.Subscript(
82
+ value=ast.Name(id=self.layout.state_param(scope), ctx=ast.Load()),
83
+ slice=ast.Constant(value=slot), ctx=ctx)
84
+
85
+ @staticmethod
86
+ def _is_persistent_type(annotation: ast.expr) -> bool:
87
+ """Check if the annotation is any form of Persistent type."""
88
+ if isinstance(annotation, ast.Name):
89
+ return annotation.id in PERSISTENT_TYPES
90
+ if isinstance(annotation, ast.Subscript) and isinstance(annotation.value, ast.Name):
91
+ return annotation.value.id in PERSISTENT_TYPES
92
+ if isinstance(annotation, ast.Attribute):
93
+ return annotation.attr in PERSISTENT_TYPES
94
+ return False
95
+
96
+ @staticmethod
97
+ def _is_varip_type(annotation: ast.expr) -> bool:
98
+ """Check if the annotation is a varip (IBPersistent) type."""
99
+ if isinstance(annotation, ast.Name):
100
+ return annotation.id in VARIP_TYPES
101
+ if isinstance(annotation, ast.Subscript) and isinstance(annotation.value, ast.Name):
102
+ return annotation.value.id in VARIP_TYPES
103
+ if isinstance(annotation, ast.Attribute):
104
+ return annotation.attr in VARIP_TYPES
105
+ return False
106
+
107
+ @staticmethod
108
+ def _is_literal_or_na(node: ast.expr) -> bool:
109
+ """Check if a node is a literal value or ``na``."""
110
+ if isinstance(node, ast.Constant):
111
+ return True
112
+ return isinstance(node, ast.Name) and node.id == 'na'
113
+
114
+ @staticmethod
115
+ def _inner_type_name(annotation: ast.expr) -> str | None:
116
+ """Declared element type name of a ``Persistent[...]`` annotation."""
117
+ if isinstance(annotation, ast.Subscript) and isinstance(annotation.slice, ast.Name):
118
+ return annotation.slice.id
119
+ return None
120
+
121
+ # --- visitors --------------------------------------------------------
122
+
123
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.ImportFrom | None:
124
+ """Strip the Persistent name from pynecore imports."""
125
+ if node.module and node.module.startswith('pynecore'):
126
+ new_names = [name for name in node.names if name.name != 'Persistent']
127
+ if not new_names:
128
+ return None
129
+ node.names = new_names
130
+ return node
131
+
132
+ def visit_Module(self, node: ast.Module) -> ast.Module:
133
+ self.layout.assign_scope_ids(node)
134
+ return cast(ast.Module, self.generic_visit(node))
135
+
136
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
137
+ """Track scopes, qualify the state parameter when nested defs exist."""
138
+ self.scope_stack.append(self.layout.scope_segment(node))
139
+ self.current_scope = '·'.join(self.scope_stack)
140
+
141
+ scope_for_function(self.layout, self.current_scope, node)
142
+
143
+ self.local_vars.setdefault(self.current_scope, set())
144
+ self.persistent_declarations.setdefault(self.current_scope, set())
145
+ for arg in node.args.args:
146
+ self.local_vars[self.current_scope].add(arg.arg)
147
+
148
+ node = cast(ast.FunctionDef, self.generic_visit(node))
149
+
150
+ # Persistent names are not closure variables anymore — drop them from
151
+ # nonlocal statements (shadowed locals keep theirs).
152
+ ancestors = set()
153
+ for i in range(len(self.scope_stack) - 1, 0, -1):
154
+ scope = '·'.join(self.scope_stack[:i])
155
+ ancestors.update(self.persistent_declarations.get(scope, ()))
156
+ new_body: list[ast.stmt] = []
157
+ for stmt in node.body:
158
+ if isinstance(stmt, ast.Nonlocal):
159
+ stmt.names = [name for name in stmt.names if name not in ancestors]
160
+ if not stmt.names:
161
+ continue
162
+ new_body.append(stmt)
163
+ node.body = new_body
164
+
165
+ self.scope_stack.pop()
166
+ self.current_scope = '·'.join(self.scope_stack)
167
+ return node
168
+
169
+ def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AST | None:
170
+ """Convert Persistent declarations into slot allocations."""
171
+ if not (isinstance(node.target, ast.Name) and self._is_persistent_type(node.annotation)):
172
+ if isinstance(node.target, ast.Name) and self.current_scope:
173
+ # An annotated assignment declares a local — it shadows a
174
+ # same-named parent persistent, like a plain assignment does.
175
+ self.local_vars.setdefault(self.current_scope, set()).add(node.target.id)
176
+ if node.value:
177
+ node.value = cast(ast.expr, self.visit(node.value))
178
+ return node
179
+
180
+ if not self.current_scope:
181
+ raise SyntaxError("Persistent variables must be declared inside a function")
182
+
183
+ var_name = node.target.id
184
+ self.persistent_declarations[self.current_scope].add(var_name)
185
+ self.local_vars[self.current_scope].add(var_name)
186
+ self.var_types.setdefault(self.current_scope, {})[var_name] = \
187
+ self._inner_type_name(node.annotation)
188
+ varip = self._is_varip_type(node.annotation)
189
+ scope_layout = self.layout.scope(self.current_scope)
190
+
191
+ if node.value is not None and not self._is_literal_or_na(node.value):
192
+ # Lazy pattern: value slot + flag slot, initializer runs on first call
193
+ slot = scope_layout.add_var(var_name, ast.Constant(value=None), varip=varip)
194
+ self.var_slots.setdefault(self.current_scope, {})[var_name] = slot
195
+ flag = scope_layout.add_flag(var_name)
196
+ value = cast(ast.expr, self.visit(node.value))
197
+ return ast.If(
198
+ test=ast.UnaryOp(op=ast.Not(),
199
+ operand=self._state_ref(self.current_scope, flag, ast.Load())),
200
+ body=[
201
+ ast.Assign(targets=[self._state_ref(self.current_scope, slot, ast.Store())],
202
+ value=value),
203
+ ast.Assign(targets=[self._state_ref(self.current_scope, flag, ast.Store())],
204
+ value=ast.Constant(value=True)),
205
+ ],
206
+ orelse=[])
207
+
208
+ # A bare-``na`` default must be the na VALUE, not the ``na`` name — at module
209
+ # level ``na`` resolves to the ``is_na`` function object. Emit ``lib.na()``.
210
+ na_init = ast.Call(
211
+ func=ast.Attribute(value=ast.Name(id='lib', ctx=ast.Load()), attr='na', ctx=ast.Load()),
212
+ args=[], keywords=[])
213
+ init = node.value if node.value is not None else na_init
214
+ slot = scope_layout.add_var(var_name, init, varip=varip)
215
+ self.var_slots.setdefault(self.current_scope, {})[var_name] = slot
216
+ return None
217
+
218
+ def visit_Assign(self, node: ast.Assign) -> ast.Assign:
219
+ """Convert assignments to persistent variables into slot writes."""
220
+ if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
221
+ var_name = cast(ast.Name, node.targets[0]).id
222
+ # The first plain assignment to a not-yet-known name declares a
223
+ # local, shadowing a same-named parent persistent.
224
+ if var_name not in self.local_vars.get(self.current_scope, ()):
225
+ self.local_vars.setdefault(self.current_scope, set()).add(var_name)
226
+ found = self._lookup(var_name)
227
+ if found:
228
+ scope, slot = found
229
+ return ast.Assign(
230
+ targets=[self._state_ref(scope, slot, ast.Store())],
231
+ value=cast(ast.expr, self.visit(node.value)))
232
+ node.targets = [cast(ast.expr, self.visit(t)) for t in node.targets]
233
+ node.value = cast(ast.expr, self.visit(node.value))
234
+ return node
235
+
236
+ def visit_AugAssign(self, node: ast.AugAssign) -> ast.AST | list[ast.stmt]:
237
+ """Slot-target augmented assignment; Kahan summation for non-literal ``+=``."""
238
+ if isinstance(node.target, ast.Name):
239
+ found = self._lookup(node.target.id)
240
+ if found:
241
+ scope, slot = found
242
+ if (isinstance(node.op, ast.Add) and not self._is_literal_or_na(node.value)
243
+ # Kahan is numeric error compensation — a declared
244
+ # str/bool element type concatenates/accumulates plain
245
+ and self.var_types.get(scope, {}).get(node.target.id)
246
+ not in ('str', 'bool')):
247
+ return self._emit_kahan(scope, slot, node.target.id,
248
+ cast(ast.expr, self.visit(node.value)))
249
+ node.target = self._state_ref(scope, slot, ast.Store())
250
+ node.value = cast(ast.expr, self.visit(node.value))
251
+ return node
252
+ return cast(ast.AST, self.generic_visit(node))
253
+
254
+ def _emit_kahan(self, scope: str, slot: int, var_name: str,
255
+ value: ast.expr) -> list[ast.stmt]:
256
+ """Emit the Kahan-summation statement sequence for ``var += value``."""
257
+ scope_kahans = self.kahan_slots.setdefault(scope, {})
258
+ comp = scope_kahans.get(var_name)
259
+ if comp is None:
260
+ scope_layout = self.layout.scope(scope)
261
+ comp = scope_kahans[var_name] = scope_layout.add_kahan(
262
+ var_name, varip=scope_layout.slots[slot].varip)
263
+
264
+ def var_ref(ctx: ast.expr_context) -> ast.Subscript:
265
+ return self._state_ref(scope, slot, ctx)
266
+
267
+ def comp_ref(ctx: ast.expr_context) -> ast.Subscript:
268
+ return self._state_ref(scope, comp, ctx)
269
+
270
+ corrected = ast.Name(id='__kahan_corrected__', ctx=ast.Load())
271
+ new_sum = ast.Name(id='__kahan_new_sum__', ctx=ast.Load())
272
+ return [
273
+ # __kahan_corrected__ = <value> - <comp>
274
+ ast.Assign(targets=[ast.Name(id='__kahan_corrected__', ctx=ast.Store())],
275
+ value=ast.BinOp(left=value, op=ast.Sub(), right=comp_ref(ast.Load()))),
276
+ # __kahan_new_sum__ = <var> + __kahan_corrected__
277
+ ast.Assign(targets=[ast.Name(id='__kahan_new_sum__', ctx=ast.Store())],
278
+ value=ast.BinOp(left=var_ref(ast.Load()), op=ast.Add(), right=corrected)),
279
+ # <comp> = (__kahan_new_sum__ - <var>) - __kahan_corrected__
280
+ ast.Assign(targets=[comp_ref(ast.Store())],
281
+ value=ast.BinOp(
282
+ left=ast.BinOp(left=new_sum, op=ast.Sub(),
283
+ right=var_ref(ast.Load())),
284
+ op=ast.Sub(), right=corrected)),
285
+ # <var> = __kahan_new_sum__
286
+ ast.Assign(targets=[var_ref(ast.Store())], value=new_sum),
287
+ ]
288
+
289
+ def visit_NamedExpr(self, node: ast.NamedExpr) -> ast.AST:
290
+ """Walrus write to a persistent: route through ``__setitem__`` so the
291
+ construct stays a valid expression and still yields the value."""
292
+ if isinstance(node.target, ast.Name):
293
+ found = self._lookup(node.target.id)
294
+ if found:
295
+ scope, slot = found
296
+ value = cast(ast.expr, self.visit(node.value))
297
+ param = self.layout.state_param(scope)
298
+ return ast.Subscript(
299
+ value=ast.Tuple(
300
+ elts=[
301
+ ast.NamedExpr(target=ast.Name(id='__pyne_w__', ctx=ast.Store()),
302
+ value=value),
303
+ ast.Call(
304
+ func=ast.Attribute(value=ast.Name(id=param, ctx=ast.Load()),
305
+ attr='__setitem__', ctx=ast.Load()),
306
+ args=[ast.Constant(value=slot),
307
+ ast.Name(id='__pyne_w__', ctx=ast.Load())],
308
+ keywords=[]),
309
+ ],
310
+ ctx=ast.Load()),
311
+ slice=ast.Constant(value=0), ctx=ast.Load())
312
+ return cast(ast.AST, self.generic_visit(node))
313
+
314
+ def visit_Name(self, node: ast.Name) -> ast.AST:
315
+ """Convert persistent references using scope-aware lookup."""
316
+ found = self._lookup(node.id)
317
+ if found:
318
+ scope, slot = found
319
+ return self._state_ref(scope, slot, node.ctx)
320
+ return node