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.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- 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
|