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