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