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,381 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared slot-layout allocator for the slot-based instance state scheme.
|
|
3
|
+
|
|
4
|
+
Every transformer that contributes per-instance state (persistent variables,
|
|
5
|
+
series, isolated call sites) allocates slots from a single
|
|
6
|
+
:class:`ModuleLayout` while walking the module. :func:`apply_layout` then
|
|
7
|
+
materializes the result into the module AST:
|
|
8
|
+
|
|
9
|
+
- the module-level ``__pyne_slot_layout__`` dict (one entry per
|
|
10
|
+
state-carrying scope),
|
|
11
|
+
- a ``func.__pyne_layout__ = __pyne_slot_layout__['<scope>']`` attach
|
|
12
|
+
statement after every state-carrying function definition (inside the parent
|
|
13
|
+
body for nested definitions); decorated definitions get an innermost
|
|
14
|
+
``@__attach_layout__(...)`` decorator instead, so the layout lands on the
|
|
15
|
+
raw function and not on the decorator's return value,
|
|
16
|
+
- the hidden state parameter injected as the FIRST parameter of every
|
|
17
|
+
state-carrying function.
|
|
18
|
+
|
|
19
|
+
Scope identifiers join the function-name path with the middle-dot separator
|
|
20
|
+
(``main``, ``main·helper``) — the same convention the legacy global-name
|
|
21
|
+
mangling used. Repeated definitions of one name within a scope (``overload``
|
|
22
|
+
implementations) are disambiguated with an ordinal suffix (``highest·2``);
|
|
23
|
+
the mapping comes from :func:`collect_scope_segments` and every pipeline
|
|
24
|
+
stage reads it through :meth:`ModuleLayout.scope_segment`.
|
|
25
|
+
|
|
26
|
+
The hidden parameter is named ``__state__`` by default. Functions that
|
|
27
|
+
contain nested function definitions get a scope-qualified name
|
|
28
|
+
(``__state·main__``) instead, so a nested function can reach the parent's
|
|
29
|
+
state vector through a plain closure reference without its own hidden
|
|
30
|
+
parameter shadowing it.
|
|
31
|
+
|
|
32
|
+
The runtime side of the contract (layout dict format, ``_make_state``,
|
|
33
|
+
``__resolve_slot__``, ...) lives in :mod:`pynecore.core.instance_state`.
|
|
34
|
+
"""
|
|
35
|
+
import ast
|
|
36
|
+
from dataclasses import dataclass, field
|
|
37
|
+
|
|
38
|
+
__all__ = ['ModuleLayout', 'ScopeLayout', 'apply_layout', 'scope_for_function',
|
|
39
|
+
'collect_scope_segments']
|
|
40
|
+
|
|
41
|
+
DEFAULT_STATE_PARAM = '__state__'
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _scope_defs(scope_node: ast.AST):
|
|
45
|
+
"""Yield the function definitions belonging to one scope in source order
|
|
46
|
+
(descends through statements and class bodies, but not into nested
|
|
47
|
+
function definitions — those belong to the inner scope)."""
|
|
48
|
+
for child in ast.iter_child_nodes(scope_node):
|
|
49
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
50
|
+
yield child
|
|
51
|
+
elif not isinstance(child, ast.Lambda):
|
|
52
|
+
yield from _scope_defs(child)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def collect_scope_segments(tree: ast.Module) -> dict[int, str]:
|
|
56
|
+
"""Compute the scope segment of every function definition in a module.
|
|
57
|
+
|
|
58
|
+
Repeated definitions of the same name within one scope (typically
|
|
59
|
+
``overload`` implementations) get an ordinal suffix (``highest``,
|
|
60
|
+
``highest·2``) so each keeps its own slot layout. The result is a pure
|
|
61
|
+
function of the tree structure, so every pipeline stage that recomputes
|
|
62
|
+
it gets identical segments.
|
|
63
|
+
|
|
64
|
+
:param tree: The module AST.
|
|
65
|
+
:return: ``id(def node) -> segment`` mapping.
|
|
66
|
+
"""
|
|
67
|
+
segments: dict[int, str] = {}
|
|
68
|
+
|
|
69
|
+
def assign(scope_node: ast.AST) -> None:
|
|
70
|
+
counts: dict[str, int] = {}
|
|
71
|
+
for func in _scope_defs(scope_node):
|
|
72
|
+
n = counts.get(func.name, 0) + 1
|
|
73
|
+
counts[func.name] = n
|
|
74
|
+
segments[id(func)] = func.name if n == 1 else f'{func.name}·{n}'
|
|
75
|
+
assign(func)
|
|
76
|
+
|
|
77
|
+
assign(tree)
|
|
78
|
+
return segments
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class _Slot:
|
|
83
|
+
"""One slot of a scope's state vector."""
|
|
84
|
+
index: int
|
|
85
|
+
kind: str # 'var' | 'flag' | 'kahan' | 'series' | 'child' | 'anchor'
|
|
86
|
+
name: str # debug name for the layout 'names' tuple
|
|
87
|
+
init: ast.expr # template expression for the layout 'init' tuple
|
|
88
|
+
max_bars_back: ast.expr | None = None # series slots only
|
|
89
|
+
series_elem: str | None = None # series slots only: element type name ('float') or None
|
|
90
|
+
call_id: str | None = None # child/anchor slots only
|
|
91
|
+
in_loop: bool = False # child slots only
|
|
92
|
+
varip: bool = False # var slots and their kahan companions only
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class ScopeLayout:
|
|
97
|
+
"""Slot table of one scope (one function definition)."""
|
|
98
|
+
scope: str
|
|
99
|
+
slots: list[_Slot] = field(default_factory=list)
|
|
100
|
+
state_param: str = DEFAULT_STATE_PARAM
|
|
101
|
+
|
|
102
|
+
def _add(self, slot: _Slot) -> int:
|
|
103
|
+
self.slots.append(slot)
|
|
104
|
+
return slot.index
|
|
105
|
+
|
|
106
|
+
def add_var(self, name: str, init: ast.expr, *, varip: bool = False) -> int:
|
|
107
|
+
"""Allocate a slot for a persistent variable.
|
|
108
|
+
|
|
109
|
+
:param name: Source-level variable name (debug only).
|
|
110
|
+
:param init: Template expression for the init tuple (literal or ``na``;
|
|
111
|
+
lazy-initialized variables pass ``Constant(None)`` and pair this
|
|
112
|
+
slot with :meth:`add_flag`).
|
|
113
|
+
:param varip: Whether the variable is ``varip`` (excluded from var rollback).
|
|
114
|
+
:return: The allocated slot index.
|
|
115
|
+
"""
|
|
116
|
+
return self._add(_Slot(len(self.slots), 'var', name, init, varip=varip))
|
|
117
|
+
|
|
118
|
+
def add_flag(self, name: str) -> int:
|
|
119
|
+
"""Allocate a lazy-init flag slot (init ``False``).
|
|
120
|
+
|
|
121
|
+
:param name: Name of the variable the flag belongs to.
|
|
122
|
+
:return: The allocated slot index.
|
|
123
|
+
"""
|
|
124
|
+
return self._add(_Slot(len(self.slots), 'flag', f'{name}·flag', ast.Constant(value=False)))
|
|
125
|
+
|
|
126
|
+
def add_kahan(self, name: str, *, varip: bool = False) -> int:
|
|
127
|
+
"""Allocate a Kahan compensation slot (init ``0.0``).
|
|
128
|
+
|
|
129
|
+
:param name: Name of the variable the compensation belongs to.
|
|
130
|
+
:param varip: Whether the compensated variable is ``varip`` — the
|
|
131
|
+
compensation must follow it out of the var rollback, otherwise a
|
|
132
|
+
rollback would desynchronize the pair.
|
|
133
|
+
:return: The allocated slot index.
|
|
134
|
+
"""
|
|
135
|
+
return self._add(_Slot(len(self.slots), 'kahan', f'{name}·kahan', ast.Constant(value=0.0),
|
|
136
|
+
varip=varip))
|
|
137
|
+
|
|
138
|
+
def add_series(self, name: str, max_bars_back: ast.expr, elem: str | None = None) -> int:
|
|
139
|
+
"""Allocate a series slot (``_make_state`` puts a fresh ``SeriesImpl`` here).
|
|
140
|
+
|
|
141
|
+
:param name: Source-level variable name (debug only).
|
|
142
|
+
:param max_bars_back: Expression for the series' ``max_bars_back`` argument.
|
|
143
|
+
:param elem: Element type name from the ``Series[T]`` annotation when it
|
|
144
|
+
is statically known (currently only ``'float'`` matters: those
|
|
145
|
+
series get the native nan as their out-of-range na value).
|
|
146
|
+
:return: The allocated slot index.
|
|
147
|
+
"""
|
|
148
|
+
return self._add(_Slot(len(self.slots), 'series', name, ast.Constant(value=None),
|
|
149
|
+
max_bars_back=max_bars_back, series_elem=elem))
|
|
150
|
+
|
|
151
|
+
def add_child(self, call_id: str, *, in_loop: bool) -> int:
|
|
152
|
+
"""Allocate a child slot for an isolated call site.
|
|
153
|
+
|
|
154
|
+
:param call_id: Call-site identifier (``main·ema·0`` style).
|
|
155
|
+
:param in_loop: Whether the call site sits in a loop (slot holds a
|
|
156
|
+
child list instead of a single child state).
|
|
157
|
+
:return: The allocated slot index.
|
|
158
|
+
"""
|
|
159
|
+
return self._add(_Slot(len(self.slots), 'child', call_id, ast.Constant(value=None),
|
|
160
|
+
call_id=call_id, in_loop=in_loop))
|
|
161
|
+
|
|
162
|
+
def add_anchor(self, call_id: str, *, in_loop: bool = False) -> int:
|
|
163
|
+
"""Allocate an anchor slot for a uniform-path call site.
|
|
164
|
+
|
|
165
|
+
Anchors are emitted into the layout's ``children`` tuple, so
|
|
166
|
+
``reset()`` clears them and the next call rebinds; the bind helper
|
|
167
|
+
creates whatever it caches. Loop-shaped anchors hold a list of
|
|
168
|
+
``(callee, bound)`` pairs indexed by the per-invocation counter.
|
|
169
|
+
|
|
170
|
+
:param call_id: Call-site identifier.
|
|
171
|
+
:param in_loop: Whether the call site sits in a loop.
|
|
172
|
+
:return: The allocated slot index.
|
|
173
|
+
"""
|
|
174
|
+
return self._add(_Slot(len(self.slots), 'anchor', call_id, ast.Constant(value=None),
|
|
175
|
+
call_id=call_id, in_loop=in_loop))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ModuleLayout:
|
|
179
|
+
"""Slot layouts of every scope in one module, shared by the transformers."""
|
|
180
|
+
|
|
181
|
+
def __init__(self):
|
|
182
|
+
self.scopes: dict[str, ScopeLayout] = {}
|
|
183
|
+
self._segments: dict[int, str] = {}
|
|
184
|
+
|
|
185
|
+
def assign_scope_ids(self, tree: ast.Module) -> None:
|
|
186
|
+
"""(Re)build the definition -> scope-segment mapping for a module.
|
|
187
|
+
|
|
188
|
+
Every transformer entry point (and :func:`apply_layout`) calls this;
|
|
189
|
+
the rebuild is deterministic, so repeated calls on the same tree
|
|
190
|
+
agree even after body mutations.
|
|
191
|
+
|
|
192
|
+
:param tree: The module AST.
|
|
193
|
+
"""
|
|
194
|
+
self._segments = collect_scope_segments(tree)
|
|
195
|
+
|
|
196
|
+
def scope_segment(self, node: ast.FunctionDef) -> str:
|
|
197
|
+
"""Disambiguated scope segment of a function definition.
|
|
198
|
+
|
|
199
|
+
:param node: The function definition.
|
|
200
|
+
:return: ``name`` or ``name·N`` for repeated names in one scope.
|
|
201
|
+
"""
|
|
202
|
+
return self._segments.get(id(node), node.name)
|
|
203
|
+
|
|
204
|
+
def scope(self, scope_id: str) -> ScopeLayout:
|
|
205
|
+
"""Return (creating on demand) the layout of a scope.
|
|
206
|
+
|
|
207
|
+
:param scope_id: Middle-dot joined function-name path.
|
|
208
|
+
:return: The scope's layout.
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
return self.scopes[scope_id]
|
|
212
|
+
except KeyError:
|
|
213
|
+
scope = self.scopes[scope_id] = ScopeLayout(scope_id)
|
|
214
|
+
return scope
|
|
215
|
+
|
|
216
|
+
def state_carrying(self, scope_id: str) -> bool:
|
|
217
|
+
"""Whether a scope has any state slot (and thus a hidden state parameter).
|
|
218
|
+
|
|
219
|
+
:param scope_id: Middle-dot joined function-name path.
|
|
220
|
+
:return: True if the scope carries state.
|
|
221
|
+
"""
|
|
222
|
+
scope = self.scopes.get(scope_id)
|
|
223
|
+
return scope is not None and bool(scope.slots)
|
|
224
|
+
|
|
225
|
+
def state_param(self, scope_id: str) -> str:
|
|
226
|
+
"""Name of a scope's hidden state parameter.
|
|
227
|
+
|
|
228
|
+
:param scope_id: Middle-dot joined function-name path.
|
|
229
|
+
:return: The parameter name (``__state__`` or scope-qualified).
|
|
230
|
+
"""
|
|
231
|
+
scope = self.scopes.get(scope_id)
|
|
232
|
+
return scope.state_param if scope is not None else DEFAULT_STATE_PARAM
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def scope_for_function(layout: ModuleLayout, scope_id: str, node: ast.FunctionDef) -> ScopeLayout:
|
|
236
|
+
"""Return the scope layout of a function definition, qualifying the state
|
|
237
|
+
parameter name when the function contains nested definitions.
|
|
238
|
+
|
|
239
|
+
Every state-contributing transformer must enter scopes through this
|
|
240
|
+
helper so they agree on the parameter name (``__state__`` vs the
|
|
241
|
+
scope-qualified ``__state·{scope}__`` that nested definitions reach
|
|
242
|
+
through a closure).
|
|
243
|
+
|
|
244
|
+
:param layout: The module's shared allocator.
|
|
245
|
+
:param scope_id: Middle-dot joined function-name path.
|
|
246
|
+
:param node: The function definition being entered.
|
|
247
|
+
:return: The scope's layout.
|
|
248
|
+
"""
|
|
249
|
+
scope = layout.scope(scope_id)
|
|
250
|
+
if any(isinstance(child, ast.FunctionDef)
|
|
251
|
+
for child in ast.walk(node) if child is not node):
|
|
252
|
+
scope.state_param = f'__state·{scope_id}__'
|
|
253
|
+
return scope
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _scope_entry_ast(scope: ScopeLayout) -> ast.Dict:
|
|
257
|
+
"""Build the layout dict literal of one scope."""
|
|
258
|
+
init = ast.Tuple(elts=[slot.init for slot in scope.slots], ctx=ast.Load())
|
|
259
|
+
series = ast.Tuple(
|
|
260
|
+
elts=[ast.Tuple(elts=[ast.Constant(value=slot.index),
|
|
261
|
+
slot.max_bars_back if slot.max_bars_back is not None
|
|
262
|
+
else ast.Constant(value=None),
|
|
263
|
+
ast.Constant(value=slot.series_elem)],
|
|
264
|
+
ctx=ast.Load())
|
|
265
|
+
for slot in scope.slots if slot.kind == 'series'],
|
|
266
|
+
ctx=ast.Load())
|
|
267
|
+
varip = ast.Tuple(
|
|
268
|
+
elts=[ast.Constant(value=slot.index) for slot in scope.slots if slot.varip],
|
|
269
|
+
ctx=ast.Load())
|
|
270
|
+
children = ast.Tuple(
|
|
271
|
+
elts=[ast.Tuple(elts=[ast.Constant(value=slot.index),
|
|
272
|
+
ast.Constant(value=slot.call_id),
|
|
273
|
+
ast.Constant(value=slot.in_loop)],
|
|
274
|
+
ctx=ast.Load())
|
|
275
|
+
for slot in scope.slots if slot.kind in ('child', 'anchor')],
|
|
276
|
+
ctx=ast.Load())
|
|
277
|
+
names = ast.Tuple(
|
|
278
|
+
elts=[ast.Constant(value=slot.name) for slot in scope.slots],
|
|
279
|
+
ctx=ast.Load())
|
|
280
|
+
return ast.Dict(
|
|
281
|
+
keys=[ast.Constant(value=key) for key in ('init', 'series', 'varip', 'children', 'names')],
|
|
282
|
+
values=[init, series, varip, children, names],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _layout_assign_ast(layout: ModuleLayout) -> ast.Assign:
|
|
287
|
+
"""Build the module-level ``__pyne_slot_layout__`` assignment."""
|
|
288
|
+
carrying = [scope for scope in layout.scopes.values() if scope.slots]
|
|
289
|
+
return ast.Assign(
|
|
290
|
+
targets=[ast.Name(id='__pyne_slot_layout__', ctx=ast.Store())],
|
|
291
|
+
value=ast.Dict(
|
|
292
|
+
keys=[ast.Constant(value=scope.scope) for scope in carrying],
|
|
293
|
+
values=[_scope_entry_ast(scope) for scope in carrying],
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _attach_ast(func_name: str, scope_id: str) -> ast.Assign:
|
|
299
|
+
"""Build the ``func.__pyne_layout__ = __pyne_slot_layout__['scope']`` attach."""
|
|
300
|
+
return ast.Assign(
|
|
301
|
+
targets=[ast.Attribute(value=ast.Name(id=func_name, ctx=ast.Load()),
|
|
302
|
+
attr='__pyne_layout__', ctx=ast.Store())],
|
|
303
|
+
value=ast.Subscript(value=ast.Name(id='__pyne_slot_layout__', ctx=ast.Load()),
|
|
304
|
+
slice=ast.Constant(value=scope_id), ctx=ast.Load()),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _attach_decorator_ast(scope_id: str) -> ast.Call:
|
|
309
|
+
"""Build the ``__attach_layout__(__pyne_slot_layout__['scope'])`` decorator.
|
|
310
|
+
|
|
311
|
+
Used for decorated definitions: appended as the INNERMOST decorator it
|
|
312
|
+
tags the raw function, while a post-definition attribute assignment
|
|
313
|
+
would tag whatever the other decorators returned (e.g. an ``overload``
|
|
314
|
+
dispatcher).
|
|
315
|
+
"""
|
|
316
|
+
return ast.Call(
|
|
317
|
+
func=ast.Name(id='__attach_layout__', ctx=ast.Load()),
|
|
318
|
+
args=[ast.Subscript(value=ast.Name(id='__pyne_slot_layout__', ctx=ast.Load()),
|
|
319
|
+
slice=ast.Constant(value=scope_id), ctx=ast.Load())],
|
|
320
|
+
keywords=[])
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _process_defs(body: list[ast.stmt], scope_prefix: str, layout: ModuleLayout,
|
|
324
|
+
used_imports: set[str]) -> list[ast.stmt]:
|
|
325
|
+
"""Inject hidden state parameters and layout attaches into a statement list."""
|
|
326
|
+
new_body: list[ast.stmt] = []
|
|
327
|
+
for stmt in body:
|
|
328
|
+
new_body.append(stmt)
|
|
329
|
+
if not isinstance(stmt, ast.FunctionDef):
|
|
330
|
+
continue
|
|
331
|
+
segment = layout.scope_segment(stmt)
|
|
332
|
+
scope_id = f'{scope_prefix}·{segment}' if scope_prefix else segment
|
|
333
|
+
stmt.body = _process_defs(stmt.body, scope_id, layout, used_imports)
|
|
334
|
+
if layout.state_carrying(scope_id):
|
|
335
|
+
stmt.args.args.insert(0, ast.arg(arg=layout.state_param(scope_id)))
|
|
336
|
+
if stmt.decorator_list:
|
|
337
|
+
stmt.decorator_list.append(_attach_decorator_ast(scope_id))
|
|
338
|
+
used_imports.add('__attach_layout__')
|
|
339
|
+
else:
|
|
340
|
+
new_body.append(_attach_ast(stmt.name, scope_id))
|
|
341
|
+
return new_body
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _insert_index(body: list[ast.stmt]) -> int:
|
|
345
|
+
"""Index right after the module docstring and the leading import block."""
|
|
346
|
+
index = 0
|
|
347
|
+
for i, stmt in enumerate(body):
|
|
348
|
+
if (isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant)
|
|
349
|
+
and isinstance(stmt.value.value, str)) or isinstance(stmt, (ast.Import,
|
|
350
|
+
ast.ImportFrom)):
|
|
351
|
+
index = i + 1
|
|
352
|
+
continue
|
|
353
|
+
break
|
|
354
|
+
return index
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def apply_layout(tree: ast.Module, layout: ModuleLayout) -> ast.Module:
|
|
358
|
+
"""Materialize the collected layout into the module AST.
|
|
359
|
+
|
|
360
|
+
Inserts the ``__pyne_slot_layout__`` dict after the import block, injects
|
|
361
|
+
the hidden state parameter into every state-carrying function definition
|
|
362
|
+
and appends the ``__pyne_layout__`` attach statement after each of them.
|
|
363
|
+
|
|
364
|
+
:param tree: The module AST (already processed by the slot transformers).
|
|
365
|
+
:param layout: The shared allocator the transformers filled.
|
|
366
|
+
:return: The same module object, updated in place.
|
|
367
|
+
"""
|
|
368
|
+
# The dict is emitted even when empty: its presence marks the module as
|
|
369
|
+
# transformed, which the cross-module call-site classification relies on
|
|
370
|
+
# ("transformed module + no layout attribute -> provably stateless").
|
|
371
|
+
layout.assign_scope_ids(tree)
|
|
372
|
+
used_imports: set[str] = set()
|
|
373
|
+
tree.body = _process_defs(tree.body, '', layout, used_imports)
|
|
374
|
+
index = _insert_index(tree.body)
|
|
375
|
+
tree.body.insert(index, _layout_assign_ast(layout))
|
|
376
|
+
if used_imports:
|
|
377
|
+
tree.body.insert(index, ast.ImportFrom(
|
|
378
|
+
module='pynecore.core.instance_state',
|
|
379
|
+
names=[ast.alias(name=name, asname=None) for name in sorted(used_imports)],
|
|
380
|
+
level=0))
|
|
381
|
+
return tree
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TypeCheckingStripperTransformer(ast.NodeTransformer):
|
|
5
|
+
"""
|
|
6
|
+
Remove `if TYPE_CHECKING:` blocks and the TYPE_CHECKING import from @pyne files.
|
|
7
|
+
These blocks contain IDE-only type hints (casts, re-annotations) that are unnecessary at runtime.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def visit_If(self, node: ast.If) -> ast.AST | None:
|
|
11
|
+
# Match: if TYPE_CHECKING:
|
|
12
|
+
if isinstance(node.test, ast.Name) and node.test.id == 'TYPE_CHECKING':
|
|
13
|
+
return None
|
|
14
|
+
# Match: if typing.TYPE_CHECKING:
|
|
15
|
+
if (isinstance(node.test, ast.Attribute) and node.test.attr == 'TYPE_CHECKING'
|
|
16
|
+
and isinstance(node.test.value, ast.Name) and node.test.value.id == 'typing'):
|
|
17
|
+
return None
|
|
18
|
+
return self.generic_visit(node)
|
|
19
|
+
|
|
20
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.ImportFrom | None:
|
|
21
|
+
if node.module == 'typing':
|
|
22
|
+
node.names = [alias for alias in node.names if alias.name != 'TYPE_CHECKING']
|
|
23
|
+
if not node.names:
|
|
24
|
+
return None
|
|
25
|
+
return node
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unused Series Detector Transformer
|
|
3
|
+
|
|
4
|
+
This transformer detects Series[T] annotated variables that are never actually indexed
|
|
5
|
+
and removes the Series annotation to optimize performance.
|
|
6
|
+
|
|
7
|
+
It runs before SeriesTransformer in the AST transformation pipeline.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
from typing import Set, Dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_in_annotation_context(node: ast.Subscript) -> bool:
|
|
15
|
+
"""Check if a subscript is part of a type annotation"""
|
|
16
|
+
# This is a simple heuristic: if the subscript value is "Series",
|
|
17
|
+
# it's likely a type annotation
|
|
18
|
+
if isinstance(node.value, ast.Name) and node.value.id == "Series":
|
|
19
|
+
return True
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UnusedSeriesDetectorTransformer(ast.NodeTransformer):
|
|
24
|
+
"""
|
|
25
|
+
AST transformer that removes unnecessary Series annotations.
|
|
26
|
+
|
|
27
|
+
For variables annotated as Series[T] but never indexed with subscript operator [],
|
|
28
|
+
this transformer changes their type to just T, avoiding unnecessary Series overhead.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
# Track variables with their scope
|
|
33
|
+
self.series_vars: Dict[str, Set[str]] = {} # scope -> set of variable names
|
|
34
|
+
self.indexed_vars: Dict[str, Set[str]] = {} # scope -> set of variable names
|
|
35
|
+
self.current_scope: str = "__module__"
|
|
36
|
+
self.scope_stack: list[str] = []
|
|
37
|
+
# Track local variables in each scope to handle shadowing
|
|
38
|
+
self.local_vars: Dict[str, Set[str]] = {} # scope -> set of local variable names
|
|
39
|
+
|
|
40
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
41
|
+
"""Track function scope and process function definitions"""
|
|
42
|
+
# Save current scope
|
|
43
|
+
self.scope_stack.append(self.current_scope)
|
|
44
|
+
self.current_scope = f"{self.current_scope}.{node.name}" if self.current_scope != "__module__" else node.name
|
|
45
|
+
|
|
46
|
+
# Process the function
|
|
47
|
+
self.generic_visit(node)
|
|
48
|
+
|
|
49
|
+
# Restore scope
|
|
50
|
+
self.current_scope = self.scope_stack.pop()
|
|
51
|
+
|
|
52
|
+
return node
|
|
53
|
+
|
|
54
|
+
def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AnnAssign:
|
|
55
|
+
"""Detect Series annotations in variable assignments"""
|
|
56
|
+
if isinstance(node.target, ast.Name):
|
|
57
|
+
var_name = node.target.id
|
|
58
|
+
|
|
59
|
+
# Check if this is a Series annotation
|
|
60
|
+
if self._is_series_annotation(node.annotation):
|
|
61
|
+
if self.current_scope not in self.series_vars:
|
|
62
|
+
self.series_vars[self.current_scope] = set()
|
|
63
|
+
self.series_vars[self.current_scope].add(var_name)
|
|
64
|
+
|
|
65
|
+
# Track local variables to handle shadowing
|
|
66
|
+
if self.current_scope not in self.local_vars:
|
|
67
|
+
self.local_vars[self.current_scope] = set()
|
|
68
|
+
self.local_vars[self.current_scope].add(var_name)
|
|
69
|
+
|
|
70
|
+
# Continue visiting child nodes
|
|
71
|
+
self.generic_visit(node)
|
|
72
|
+
return node
|
|
73
|
+
|
|
74
|
+
def visit_arg(self, node: ast.arg) -> ast.arg:
|
|
75
|
+
"""Detect Series annotations in function arguments"""
|
|
76
|
+
if node.annotation and self._is_series_annotation(node.annotation):
|
|
77
|
+
if self.current_scope not in self.series_vars:
|
|
78
|
+
self.series_vars[self.current_scope] = set()
|
|
79
|
+
self.series_vars[self.current_scope].add(node.arg)
|
|
80
|
+
|
|
81
|
+
# Track local variables to handle shadowing
|
|
82
|
+
if self.current_scope not in self.local_vars:
|
|
83
|
+
self.local_vars[self.current_scope] = set()
|
|
84
|
+
self.local_vars[self.current_scope].add(node.arg)
|
|
85
|
+
|
|
86
|
+
return node
|
|
87
|
+
|
|
88
|
+
def visit_Subscript(self, node: ast.Subscript) -> ast.Subscript:
|
|
89
|
+
"""Track which variables are actually indexed"""
|
|
90
|
+
# Only track subscripts that are NOT type annotations
|
|
91
|
+
# Type annotations like Series[float] should not be counted as indexing
|
|
92
|
+
if isinstance(node.value, ast.Name) and not _is_in_annotation_context(node):
|
|
93
|
+
var_name = node.value.id
|
|
94
|
+
|
|
95
|
+
# Mark this variable as indexed
|
|
96
|
+
self._mark_variable_as_indexed(var_name)
|
|
97
|
+
|
|
98
|
+
# Continue visiting child nodes
|
|
99
|
+
self.generic_visit(node)
|
|
100
|
+
return node
|
|
101
|
+
|
|
102
|
+
def visit_Call(self, node: ast.Call) -> ast.Call:
|
|
103
|
+
"""Just visit child nodes - we don't need to track function calls"""
|
|
104
|
+
# The key insight: if a variable is passed to a function, that doesn't mean
|
|
105
|
+
# it needs to be Series in the calling scope. Only direct indexing matters.
|
|
106
|
+
# The ClosureArgumentsTransformer preserves Series annotations on parameters
|
|
107
|
+
# that actually get indexed, so we can rely on that.
|
|
108
|
+
|
|
109
|
+
# Continue visiting child nodes
|
|
110
|
+
self.generic_visit(node)
|
|
111
|
+
return node
|
|
112
|
+
|
|
113
|
+
def _mark_variable_as_indexed(self, var_name: str):
|
|
114
|
+
"""Mark a variable as indexed in the current scope"""
|
|
115
|
+
if self.current_scope not in self.indexed_vars:
|
|
116
|
+
self.indexed_vars[self.current_scope] = set()
|
|
117
|
+
self.indexed_vars[self.current_scope].add(var_name)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _is_series_annotation(annotation: ast.AST) -> bool:
|
|
121
|
+
"""Check if an annotation is Series[T]"""
|
|
122
|
+
if isinstance(annotation, ast.Subscript):
|
|
123
|
+
if isinstance(annotation.value, ast.Name) and annotation.value.id == "Series":
|
|
124
|
+
return True
|
|
125
|
+
elif isinstance(annotation.value, ast.Attribute):
|
|
126
|
+
# Handle cases like types.Series
|
|
127
|
+
if annotation.value.attr == "Series":
|
|
128
|
+
return True
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def _get_inner_type(annotation: ast.AST) -> ast.expr:
|
|
133
|
+
"""Extract T from Series[T]"""
|
|
134
|
+
if isinstance(annotation, ast.Subscript) and isinstance(annotation.slice, ast.Name):
|
|
135
|
+
return annotation.slice
|
|
136
|
+
elif isinstance(annotation, ast.Subscript) and hasattr(annotation.slice, 'value'):
|
|
137
|
+
# Handle ast.Index in older Python versions
|
|
138
|
+
return getattr(annotation.slice, 'value')
|
|
139
|
+
# Default to Any if we can't determine the inner type
|
|
140
|
+
return ast.Name(id='Any', ctx=ast.Load())
|
|
141
|
+
|
|
142
|
+
def _resolve_series_owner(self, scope: str, var_name: str) -> str | None:
|
|
143
|
+
"""Nearest enclosing scope (including ``scope`` itself) that declares
|
|
144
|
+
``var_name`` as a Series.
|
|
145
|
+
|
|
146
|
+
Mirrors :meth:`SeriesTransformer._lookup`: a subscript resolves to the
|
|
147
|
+
closest series declaration walking outwards through the scope chain.
|
|
148
|
+
"""
|
|
149
|
+
parts = scope.split('.')
|
|
150
|
+
for i in range(len(parts), 0, -1):
|
|
151
|
+
candidate = '.'.join(parts[:i])
|
|
152
|
+
if var_name in self.series_vars.get(candidate, set()):
|
|
153
|
+
return candidate
|
|
154
|
+
if var_name in self.series_vars.get("__module__", set()):
|
|
155
|
+
return "__module__"
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def optimize(self, tree: ast.AST) -> ast.AST:
|
|
159
|
+
"""
|
|
160
|
+
Main optimization pass - removes unused Series annotations
|
|
161
|
+
"""
|
|
162
|
+
# First pass: collect all Series variables and indexed variables
|
|
163
|
+
self.visit(tree)
|
|
164
|
+
|
|
165
|
+
# A parent-scope series indexed only from within a nested function is
|
|
166
|
+
# resolved through the scope chain by SeriesTransformer, so it must not
|
|
167
|
+
# be pruned. Credit the nearest enclosing scope that declares each
|
|
168
|
+
# indexed name as a Series, mirroring that resolution.
|
|
169
|
+
for scope, names in list(self.indexed_vars.items()):
|
|
170
|
+
for var_name in names:
|
|
171
|
+
owner = self._resolve_series_owner(scope, var_name)
|
|
172
|
+
if owner is not None and owner != scope:
|
|
173
|
+
self.indexed_vars.setdefault(owner, set()).add(var_name)
|
|
174
|
+
|
|
175
|
+
# Second pass: remove Series annotations from non-indexed variables
|
|
176
|
+
optimizer = SeriesOptimizer(self.series_vars, self.indexed_vars)
|
|
177
|
+
return optimizer.visit(tree)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SeriesOptimizer(ast.NodeTransformer):
|
|
181
|
+
"""Second pass transformer that actually removes the unused Series annotations"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, series_vars: Dict[str, Set[str]], indexed_vars: Dict[str, Set[str]]):
|
|
184
|
+
self.series_vars = series_vars
|
|
185
|
+
self.indexed_vars = indexed_vars
|
|
186
|
+
self.current_scope = "__module__"
|
|
187
|
+
self.scope_stack: list[str] = []
|
|
188
|
+
|
|
189
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
190
|
+
"""Track function scope"""
|
|
191
|
+
self.scope_stack.append(self.current_scope)
|
|
192
|
+
self.current_scope = f"{self.current_scope}.{node.name}" if self.current_scope != "__module__" else node.name
|
|
193
|
+
|
|
194
|
+
# Process the function
|
|
195
|
+
self.generic_visit(node)
|
|
196
|
+
|
|
197
|
+
# Restore scope
|
|
198
|
+
self.current_scope = self.scope_stack.pop()
|
|
199
|
+
|
|
200
|
+
return node
|
|
201
|
+
|
|
202
|
+
def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AnnAssign:
|
|
203
|
+
"""Remove Series annotation if variable is never indexed"""
|
|
204
|
+
if isinstance(node.target, ast.Name):
|
|
205
|
+
var_name = node.target.id
|
|
206
|
+
|
|
207
|
+
# Check if this is an unused Series variable
|
|
208
|
+
if self._is_unused_series(var_name) and self._is_series_annotation(node.annotation):
|
|
209
|
+
# Replace Series[T] with just T
|
|
210
|
+
node.annotation = self._get_inner_type(node.annotation)
|
|
211
|
+
|
|
212
|
+
self.generic_visit(node)
|
|
213
|
+
return node
|
|
214
|
+
|
|
215
|
+
def visit_arg(self, node: ast.arg) -> ast.arg:
|
|
216
|
+
"""Remove Series annotation from function arguments if never indexed"""
|
|
217
|
+
if (node.annotation and self._is_unused_series(node.arg) and
|
|
218
|
+
self._is_series_annotation(node.annotation)):
|
|
219
|
+
# Replace Series[T] with just T
|
|
220
|
+
node.annotation = self._get_inner_type(node.annotation)
|
|
221
|
+
|
|
222
|
+
return node
|
|
223
|
+
|
|
224
|
+
def _is_unused_series(self, var_name: str) -> bool:
|
|
225
|
+
"""Check if a variable is Series-annotated but never indexed in the current scope"""
|
|
226
|
+
# Check if variable is Series in current scope
|
|
227
|
+
is_series = (self.current_scope in self.series_vars and
|
|
228
|
+
var_name in self.series_vars[self.current_scope])
|
|
229
|
+
|
|
230
|
+
# Check if variable is indexed in current scope
|
|
231
|
+
is_indexed = (self.current_scope in self.indexed_vars and
|
|
232
|
+
var_name in self.indexed_vars[self.current_scope])
|
|
233
|
+
|
|
234
|
+
# Return True if it's Series but not indexed in this scope
|
|
235
|
+
return is_series and not is_indexed
|
|
236
|
+
|
|
237
|
+
def _get_relevant_scopes(self) -> list[str]:
|
|
238
|
+
"""Get all scopes that could contain the variable (current + parents)"""
|
|
239
|
+
scopes = ["__module__"]
|
|
240
|
+
if self.current_scope != "__module__":
|
|
241
|
+
parts = self.current_scope.split('.')
|
|
242
|
+
for i in range(len(parts)):
|
|
243
|
+
scopes.append('.'.join(parts[:i + 1]))
|
|
244
|
+
return scopes
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def _is_series_annotation(annotation: ast.AST) -> bool:
|
|
248
|
+
"""Check if an annotation is Series[T]"""
|
|
249
|
+
if isinstance(annotation, ast.Subscript):
|
|
250
|
+
if isinstance(annotation.value, ast.Name) and annotation.value.id == "Series":
|
|
251
|
+
return True
|
|
252
|
+
elif isinstance(annotation.value, ast.Attribute):
|
|
253
|
+
if annotation.value.attr == "Series":
|
|
254
|
+
return True
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _get_inner_type(annotation: ast.AST) -> ast.expr:
|
|
259
|
+
"""Extract T from Series[T]"""
|
|
260
|
+
if isinstance(annotation, ast.Subscript):
|
|
261
|
+
if isinstance(annotation.slice, ast.Name):
|
|
262
|
+
return annotation.slice
|
|
263
|
+
elif hasattr(annotation.slice, 'value'):
|
|
264
|
+
# Handle ast.Index in older Python versions
|
|
265
|
+
return getattr(annotation.slice, 'value')
|
|
266
|
+
# Default to float if we can't determine
|
|
267
|
+
return ast.Name(id='float', ctx=ast.Load())
|