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,757 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transform function call sites to the slot-based instance-state scheme.
|
|
3
|
+
|
|
4
|
+
Every isolated call site is classified at TRANSFORM time and emitted on one
|
|
5
|
+
of three routes (see ``work/benchmark`` plan, section 3.4):
|
|
6
|
+
|
|
7
|
+
- **fast** (provably state-carrying callee): the child instance's state
|
|
8
|
+
lives in a compile-time-assigned slot of the CALLER's state vector::
|
|
9
|
+
|
|
10
|
+
f((__st__ if (__st__ := __state__[5]) is not None
|
|
11
|
+
else __resolve_slot__(__state__, 5, f)), x, 12)
|
|
12
|
+
|
|
13
|
+
Loop-shaped sites hold a child list indexed by a per-invocation counter
|
|
14
|
+
(hoisted to the function prologue together with the list)::
|
|
15
|
+
|
|
16
|
+
f((__chl_0__[__i__] if (__i__ := (__cnt_0__ := __cnt_0__ + 1) - 1)
|
|
17
|
+
< len(__chl_0__) else __grow__(__chl_0__, f)), x)
|
|
18
|
+
|
|
19
|
+
- **direct** (provably stateless callee): plain call, zero overhead.
|
|
20
|
+
|
|
21
|
+
- **uniform** (anything not provable): the caller anchors a
|
|
22
|
+
``(callee, bound)`` pair in its own slot; the hot path is one identity
|
|
23
|
+
check, ``__bind_any__`` / ``__bind_any_loop__`` (re)binds on a miss::
|
|
24
|
+
|
|
25
|
+
(__b__[1] if (__b__ := __state__[7]) is not None and __b__[0] is f
|
|
26
|
+
else __bind_any__(__state__, 7, f))(x)
|
|
27
|
+
|
|
28
|
+
Classification sources:
|
|
29
|
+
|
|
30
|
+
- same-module functions: the shared :class:`ModuleLayout` (this transformer
|
|
31
|
+
must run AFTER the Persistent and Series transformers) plus a carrier
|
|
32
|
+
fixpoint over the module's call graph — a function carries state if it has
|
|
33
|
+
own slots or any non-direct call site; a name whose LAST definition is
|
|
34
|
+
decorated routes uniform (the runtime value is the decorator's return
|
|
35
|
+
value — an ``overload`` dispatcher, an ``lru_cache`` wrapper, ...);
|
|
36
|
+
- cross-module callees (``lib.*``, user Pyne libraries): the callee module
|
|
37
|
+
is imported at transform time and the object inspected —
|
|
38
|
+
``__pyne_bind__`` marks an overload dispatcher (uniform),
|
|
39
|
+
``__pyne_layout__`` proves state-carrying, a ``__pyne_slot_layout__``
|
|
40
|
+
marker in the function's globals with no layout attribute proves
|
|
41
|
+
stateless, everything else falls to uniform.
|
|
42
|
+
|
|
43
|
+
Unprovable always degrades to uniform (correct, only slower) — an error can
|
|
44
|
+
only come from a false proof, never from missing knowledge.
|
|
45
|
+
|
|
46
|
+
Deliberately left untouched (raw calls): module-level call sites (a stateful
|
|
47
|
+
callee there raises a transform error), decorator and default-argument
|
|
48
|
+
expressions, class bodies, ``__test_*__`` functions (the test framework
|
|
49
|
+
calls them with fixtures, they must not grow a hidden parameter), and calls
|
|
50
|
+
whose callee is not a plain name/attribute. Calls inside lambdas are
|
|
51
|
+
anchored on the straight-line uniform route (a loop counter would bind
|
|
52
|
+
lambda-local and break).
|
|
53
|
+
"""
|
|
54
|
+
from typing import cast, Any
|
|
55
|
+
import ast
|
|
56
|
+
import builtins
|
|
57
|
+
import importlib
|
|
58
|
+
import types
|
|
59
|
+
|
|
60
|
+
from ..core.pine_export import Exported
|
|
61
|
+
from ..utils.stdlib_checker import is_stdlib
|
|
62
|
+
# noinspection PyProtectedMember
|
|
63
|
+
from .slot_layout import DEFAULT_STATE_PARAM, ModuleLayout, scope_for_function
|
|
64
|
+
|
|
65
|
+
__all__ = ['FunctionIsolationTransformer', 'NON_TRANSFORMABLE_FUNCTIONS']
|
|
66
|
+
|
|
67
|
+
# Functions that should not be transformed because they:
|
|
68
|
+
# - don't return anything (plotting, display)
|
|
69
|
+
# - can't have Series values
|
|
70
|
+
# - are purely for output/display purposes
|
|
71
|
+
# This makes code run little bit faster
|
|
72
|
+
NON_TRANSFORMABLE_FUNCTIONS = {
|
|
73
|
+
# Plot and display related (function-and-namespace modules appear as their
|
|
74
|
+
# self-named function after the module property rewrite, e.g. lib.plot.plot)
|
|
75
|
+
'lib.plot.plot', 'lib.plotchar', 'lib.plotshape', 'lib.plotarrow',
|
|
76
|
+
'lib.label', 'lib.table', 'lib.box', 'lib.line', 'lib.hline.hline',
|
|
77
|
+
'lib.fill', 'lib.bgcolor', 'lib.barcolor', 'lib.plotcandle',
|
|
78
|
+
'lib.alert.alert', 'lib.alertcondition', 'lib.na',
|
|
79
|
+
|
|
80
|
+
# Other builtin functions
|
|
81
|
+
'lib.timestamp', 'lib.dayofmonth', 'lib.dayofweek.dayofweek', 'lib.hour', 'lib.minute', 'lib.month',
|
|
82
|
+
'lib.second', 'lib.weekofyear', 'lib.year', 'lib.time', 'lib.time_close', 'lib.time_tradingday',
|
|
83
|
+
'lib.timenow', 'lib.is_na', 'lib.nz', 'lib.timestamp',
|
|
84
|
+
|
|
85
|
+
# Strategy
|
|
86
|
+
'lib.strategy.entry', 'lib.strategy.order', 'lib.strategy.exit', 'lib.strategy.close',
|
|
87
|
+
'lib.strategy.cancel', 'lib.strategy.cancel_all',
|
|
88
|
+
'lib.strategy.equity', 'lib.strategy.eventrades', 'lib.strategy.initial_capital',
|
|
89
|
+
'lib.strategy.grossloss', 'lib.strategy.grossprofit', 'lib.strategy.losstrades',
|
|
90
|
+
'lib.strategy.max_drawdown', 'lib.strategy.max_runup', 'lib.strategy.netprofit',
|
|
91
|
+
'lib.strategy.openprofit', 'lib.strategy.position_size', 'lib.strategy.position_avg_price',
|
|
92
|
+
'lib.strategy.wintrades',
|
|
93
|
+
'lib.strategy.closedtrades.commission', 'lib.strategy.closedtrades.entry_bar_index',
|
|
94
|
+
'lib.strategy.closedtrades.entry_comment', 'lib.strategy.closedtrades.entry_id',
|
|
95
|
+
'lib.strategy.closedtrades.entry_price', 'lib.strategy.closedtrades.entry_time',
|
|
96
|
+
'lib.strategy.closedtrades.exit_bar_index', 'lib.strategy.closedtrades.exit_comment',
|
|
97
|
+
'lib.strategy.closedtrades.exit_id', 'lib.strategy.closedtrades.exit_price',
|
|
98
|
+
'lib.strategy.closedtrades.exit_time', 'lib.strategy.closedtrades.max_drawdown',
|
|
99
|
+
'lib.strategy.closedtrades.max_drawdown_percent', 'lib.strategy.closedtrades.max_runup',
|
|
100
|
+
'lib.strategy.closedtrades.max_runup_percent', 'lib.strategy.closedtrades.profit',
|
|
101
|
+
'lib.strategy.closedtrades.profit_percent', 'lib.strategy.closedtrades.size',
|
|
102
|
+
'lib.strategy.opentrades.commission', 'lib.strategy.opentrades.entry_bar_index',
|
|
103
|
+
'lib.strategy.opentrades.entry_comment', 'lib.strategy.opentrades.entry_id',
|
|
104
|
+
'lib.strategy.opentrades.entry_price', 'lib.strategy.opentrades.entry_time',
|
|
105
|
+
'lib.strategy.opentrades.max_drawdown', 'lib.strategy.opentrades.max_drawdown_percent',
|
|
106
|
+
'lib.strategy.opentrades.max_runup', 'lib.strategy.opentrades.max_runup_percent',
|
|
107
|
+
'lib.strategy.opentrades.profit', 'lib.strategy.opentrades.profit_percent',
|
|
108
|
+
'lib.strategy.opentrades.size',
|
|
109
|
+
'lib.strategy.opentrades.opentrades', 'lib.strategy.closedtrades.closedtrades',
|
|
110
|
+
|
|
111
|
+
# Input functions
|
|
112
|
+
'lib.input', 'lib.input.int', 'lib.input.float', 'lib.input.bool', 'lib.input.string',
|
|
113
|
+
'lib.input.source', 'lib.input.color',
|
|
114
|
+
|
|
115
|
+
# Timeframe functions
|
|
116
|
+
'lib.timeframe.in_seconds', 'lib.timeframe.from_seconds',
|
|
117
|
+
|
|
118
|
+
# Logging
|
|
119
|
+
'lib.log.info', 'lib.log.error', 'lib.log.warning',
|
|
120
|
+
|
|
121
|
+
# Math functions
|
|
122
|
+
'lib.math.abs', 'lib.math.acos', 'lib.math.asin', 'lib.math.atan', 'lib.math.avg', 'lib.math.ceil', 'lib.math.cos',
|
|
123
|
+
'lib.math.exp', 'lib.math.floor', 'lib.math.log', 'lib.math.log10', 'lib.math.max', 'lib.math.min', 'lib.math.pow',
|
|
124
|
+
'lib.math.round', 'lib.math.round_to_mintick', 'lib.math.sign', 'lib.math.sin', 'lib.math.sqrt',
|
|
125
|
+
'lib.math.tan', 'lib.math.todegrees', 'lib.math.toradians',
|
|
126
|
+
|
|
127
|
+
# String functions
|
|
128
|
+
'lib.string.contains', 'lib.string.endswith', 'lib.string.format', 'lib.string.format_time', 'lib.string.length',
|
|
129
|
+
'lib.string.lower', 'lib.string.match', 'lib.string.pos', 'lib.string.repeat', 'lib.string.replace',
|
|
130
|
+
'lib.string.replace_all', 'lib.string.split', 'lib.string.startswith', 'lib.string.substring',
|
|
131
|
+
'lib.string.tonumber', 'lib.string.tostring', 'lib.string.trim', 'lib.string.upper',
|
|
132
|
+
|
|
133
|
+
# Array functions
|
|
134
|
+
'lib.array.abs', 'lib.array.avg', 'lib.array.binary_search', 'lib.array.binary_search_leftmost',
|
|
135
|
+
'lib.array.binary_search_rightmost', 'lib.array.clear', 'lib.array.concat', 'lib.array.copy',
|
|
136
|
+
'lib.array.covariance', 'lib.array.every', 'lib.array.fill', 'lib.array.first', 'lib.array.from_items',
|
|
137
|
+
'lib.array.get', 'lib.array.includes', 'lib.array.indexof', 'lib.array.insert', 'lib.array.join',
|
|
138
|
+
'lib.array.last', 'lib.array.lastindexof', 'lib.array.max', 'lib.array.median', 'lib.array.min',
|
|
139
|
+
'lib.array.mode', 'lib.array.percentrank', 'lib.array.percentile_linear_interpolation',
|
|
140
|
+
'percentile_nearest_rank', 'percentile_nearest_rank', 'lib.array.pop', 'lib.array.push', 'lib.array.range',
|
|
141
|
+
'lib.array.remove', 'lib.array.reverse', 'lib.array.set', 'lib.array.shift', 'lib.array.size', 'lib.array.slice',
|
|
142
|
+
'lib.array.some', 'lib.array.sort', 'lib.array.sort_indices', 'lib.array.standardize', 'lib.array.stdev',
|
|
143
|
+
'lib.array.sum', 'lib.array.unshift', 'lib.array.variance', 'lib.array.new',
|
|
144
|
+
'lib.array.new_bool', 'lib.array.new_color', 'lib.array.new_float', 'lib.array.new_int', 'lib.array.new_string',
|
|
145
|
+
|
|
146
|
+
# Map functions
|
|
147
|
+
'lib.map.clear', 'lib.map.contains', 'lib.map.copy', 'lib.map.get', 'lib.map.keys', 'lib.map.new',
|
|
148
|
+
'lib.map.put', 'lib.map.put_all', 'lib.map.remove', 'lib.map.size', 'lib.map.values',
|
|
149
|
+
|
|
150
|
+
# Color functions
|
|
151
|
+
'lib.color.new', 'lib.color.r', 'lib.color.g', 'lib.color.b', 'lib.color.a',
|
|
152
|
+
'lib.color.rgb', 'lib.color.from_gradient',
|
|
153
|
+
|
|
154
|
+
# Strategy functions
|
|
155
|
+
"lib.strategy.fixed", "lib.strategy.cash", "lib.strategy.percent_of_equity", "lib.strategy.long",
|
|
156
|
+
"lib.strategy.short", 'lib.strategy.direction', "lib.strategy.cancel", "lib.strategy.cancel_all",
|
|
157
|
+
"lib.strategy.close", "lib.strategy.close_all", "lib.strategy.entry", "lib.strategy.exit",
|
|
158
|
+
"lib.strategy.closedtrades", "lib.strategy.opentrades",
|
|
159
|
+
|
|
160
|
+
# Other
|
|
161
|
+
'lib.max_bars_back',
|
|
162
|
+
|
|
163
|
+
'copy', 'dataclass', 'dccopy',
|
|
164
|
+
'pytest.raises',
|
|
165
|
+
|
|
166
|
+
'method_call', 'pine_range'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Call-site routes decided at transform time. Same-module defs resolve to a
|
|
170
|
+
# ('same', scope_id) tuple first and collapse to fast/direct through the
|
|
171
|
+
# carrier fixpoint.
|
|
172
|
+
_SKIP = 'skip'
|
|
173
|
+
_DIRECT = 'direct'
|
|
174
|
+
_FAST = 'fast'
|
|
175
|
+
_UNIFORM = 'uniform'
|
|
176
|
+
|
|
177
|
+
_Route = str | tuple[str, str]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _is_test_function(name: str) -> bool:
|
|
181
|
+
"""Whether a function follows the ``__test_*__`` convention (called by
|
|
182
|
+
the test framework with fixtures — must stay untouched)."""
|
|
183
|
+
return name.startswith('__test_') and name.endswith('__')
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _get_func_path(func: ast.expr) -> str | None:
|
|
187
|
+
"""Get the full dotted path of a callee expression."""
|
|
188
|
+
if isinstance(func, ast.Name):
|
|
189
|
+
return func.id
|
|
190
|
+
if isinstance(func, ast.Attribute):
|
|
191
|
+
parts = []
|
|
192
|
+
current: ast.expr = func
|
|
193
|
+
while isinstance(current, ast.Attribute):
|
|
194
|
+
parts.append(current.attr)
|
|
195
|
+
current = current.value
|
|
196
|
+
if isinstance(current, ast.Name):
|
|
197
|
+
parts.append(current.id)
|
|
198
|
+
return '.'.join(reversed(parts))
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class _ScopeIndex(ast.NodeVisitor):
|
|
203
|
+
"""Pass 1a: per-scope name bindings (defs, classes, everything else
|
|
204
|
+
assigned) and the module-level import map."""
|
|
205
|
+
|
|
206
|
+
def __init__(self, layout: ModuleLayout):
|
|
207
|
+
self.layout = layout
|
|
208
|
+
# scope -> name -> (target scope id of the LAST def, has decorators);
|
|
209
|
+
# the last definition wins, like the runtime name binding does
|
|
210
|
+
self.defs: dict[str, dict[str, tuple[str, bool]]] = {'': {}}
|
|
211
|
+
self.classes: dict[str, set[str]] = {'': set()}
|
|
212
|
+
self.assigned: dict[str, set[str]] = {'': set()}
|
|
213
|
+
# name -> (module path, attribute or None)
|
|
214
|
+
self.import_map: dict[str, tuple[str, str | None]] = {}
|
|
215
|
+
self._stack: list[str] = []
|
|
216
|
+
|
|
217
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
218
|
+
outer = '·'.join(self._stack)
|
|
219
|
+
segment = self.layout.scope_segment(node)
|
|
220
|
+
target = f'{outer}·{segment}' if outer else segment
|
|
221
|
+
self.defs[outer][node.name] = (target, bool(node.decorator_list))
|
|
222
|
+
self._stack.append(segment)
|
|
223
|
+
scope = '·'.join(self._stack)
|
|
224
|
+
self.defs.setdefault(scope, {})
|
|
225
|
+
self.classes.setdefault(scope, set())
|
|
226
|
+
assigned = self.assigned.setdefault(scope, set())
|
|
227
|
+
args = node.args
|
|
228
|
+
for arg in args.args + args.posonlyargs + args.kwonlyargs:
|
|
229
|
+
assigned.add(arg.arg)
|
|
230
|
+
if args.vararg:
|
|
231
|
+
assigned.add(args.vararg.arg)
|
|
232
|
+
if args.kwarg:
|
|
233
|
+
assigned.add(args.kwarg.arg)
|
|
234
|
+
self.generic_visit(node)
|
|
235
|
+
self._stack.pop()
|
|
236
|
+
|
|
237
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
238
|
+
self.classes['·'.join(self._stack)].add(node.name)
|
|
239
|
+
# Class bodies are not isolation scopes — don't index their content
|
|
240
|
+
|
|
241
|
+
def visit_Name(self, node: ast.Name) -> None:
|
|
242
|
+
if isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
243
|
+
self.assigned['·'.join(self._stack)].add(node.id)
|
|
244
|
+
|
|
245
|
+
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
|
|
246
|
+
if node.name:
|
|
247
|
+
self.assigned['·'.join(self._stack)].add(node.name)
|
|
248
|
+
self.generic_visit(node)
|
|
249
|
+
|
|
250
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
251
|
+
scope = '·'.join(self._stack)
|
|
252
|
+
for alias in node.names:
|
|
253
|
+
bound = alias.asname or alias.name.split('.')[0]
|
|
254
|
+
if scope:
|
|
255
|
+
self.assigned[scope].add(bound)
|
|
256
|
+
else:
|
|
257
|
+
module = alias.name if alias.asname else alias.name.split('.')[0]
|
|
258
|
+
self.import_map[bound] = (module, None)
|
|
259
|
+
|
|
260
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
261
|
+
scope = '·'.join(self._stack)
|
|
262
|
+
for alias in node.names:
|
|
263
|
+
bound = alias.asname or alias.name
|
|
264
|
+
if scope:
|
|
265
|
+
self.assigned[scope].add(bound)
|
|
266
|
+
elif node.module and not node.level:
|
|
267
|
+
self.import_map[bound] = (node.module, alias.name)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class _RouteCollector(ast.NodeVisitor):
|
|
271
|
+
"""Pass 1b: prelim route of every call site per scope, input of the
|
|
272
|
+
carrier fixpoint. Mirrors the transformer's skip rules (decorators,
|
|
273
|
+
defaults, class bodies, test functions are not isolation territory)."""
|
|
274
|
+
|
|
275
|
+
def __init__(self, transformer: 'FunctionIsolationTransformer'):
|
|
276
|
+
self.transformer = transformer
|
|
277
|
+
self.scope_routes: dict[str, list[_Route]] = {}
|
|
278
|
+
self._stack: list[str] = []
|
|
279
|
+
|
|
280
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
281
|
+
if _is_test_function(node.name):
|
|
282
|
+
return
|
|
283
|
+
self._stack.append(self.transformer.layout.scope_segment(node))
|
|
284
|
+
self.scope_routes.setdefault('·'.join(self._stack), [])
|
|
285
|
+
for stmt in node.body:
|
|
286
|
+
self.visit(stmt)
|
|
287
|
+
self._stack.pop()
|
|
288
|
+
|
|
289
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
293
|
+
self.generic_visit(node)
|
|
294
|
+
if self._stack and isinstance(node.func, (ast.Name, ast.Attribute)):
|
|
295
|
+
route = self.transformer.route_for_callee(node.func, self._stack)
|
|
296
|
+
self.scope_routes['·'.join(self._stack)].append(route)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class FunctionIsolationTransformer(ast.NodeTransformer):
|
|
300
|
+
"""Rewrite call sites to the parent-slot / anchored emission (pass 2)."""
|
|
301
|
+
|
|
302
|
+
def __init__(self, layout: ModuleLayout):
|
|
303
|
+
self.layout = layout
|
|
304
|
+
self.index = _ScopeIndex(layout)
|
|
305
|
+
self.carrier: dict[str, bool] = {}
|
|
306
|
+
self._scope_stack: list[str] = []
|
|
307
|
+
self._loop_depth = 0
|
|
308
|
+
self._lambda_depth = 0
|
|
309
|
+
self._ordinals: dict[str, int] = {}
|
|
310
|
+
# per-function pending loop hoists: (counter name, list name, slot)
|
|
311
|
+
self._loop_hoists: list[list[tuple[str, str, int]]] = []
|
|
312
|
+
self._used_helpers: set[str] = set()
|
|
313
|
+
self._resolve_cache: dict[str, Any] = {}
|
|
314
|
+
|
|
315
|
+
# --- classification ----------------------------------------------------
|
|
316
|
+
|
|
317
|
+
def route_for_callee(self, func: ast.expr, scope_stack: list[str]) -> _Route:
|
|
318
|
+
"""Classify a callee expression in a scope context.
|
|
319
|
+
|
|
320
|
+
:param func: The callee (Name or Attribute).
|
|
321
|
+
:param scope_stack: Function-name path of the call site's scope.
|
|
322
|
+
:return: One of the route constants or ``('same', scope_id)``.
|
|
323
|
+
"""
|
|
324
|
+
if self._is_series_slot_method(func, scope_stack):
|
|
325
|
+
# Synthetic SeriesImpl method call emitted by the Series
|
|
326
|
+
# transformer (__state__[N].add / .set) — stateless by
|
|
327
|
+
# construction, and an anchor could never hit anyway (a bound
|
|
328
|
+
# method is a fresh object on every attribute access)
|
|
329
|
+
return _SKIP
|
|
330
|
+
path = _get_func_path(func)
|
|
331
|
+
if path is None:
|
|
332
|
+
return _UNIFORM
|
|
333
|
+
if path in NON_TRANSFORMABLE_FUNCTIONS:
|
|
334
|
+
return _SKIP
|
|
335
|
+
parts = path.split('.')
|
|
336
|
+
base = parts[0]
|
|
337
|
+
|
|
338
|
+
# Innermost-first scope-chain resolution of the base name
|
|
339
|
+
for i in range(len(scope_stack), -1, -1):
|
|
340
|
+
scope = '·'.join(scope_stack[:i])
|
|
341
|
+
is_assigned = base in self.index.assigned.get(scope, ())
|
|
342
|
+
entry = self.index.defs.get(scope, {}).get(base)
|
|
343
|
+
if entry is not None:
|
|
344
|
+
target, decorated = entry
|
|
345
|
+
if is_assigned or len(parts) > 1 or decorated:
|
|
346
|
+
# Rebound name, attribute on a def, or a decorated def
|
|
347
|
+
# (the runtime value is the decorator's return value —
|
|
348
|
+
# an overload dispatcher, an lru_cache wrapper, ...)
|
|
349
|
+
return _UNIFORM
|
|
350
|
+
return 'same', target
|
|
351
|
+
if base in self.index.classes.get(scope, ()):
|
|
352
|
+
# Constructor or class attribute — the legacy runtime guard
|
|
353
|
+
# returned types untouched, skipping is the same net effect
|
|
354
|
+
return _SKIP if not is_assigned else _UNIFORM
|
|
355
|
+
if is_assigned:
|
|
356
|
+
return _UNIFORM # local value (function value, object, ...)
|
|
357
|
+
|
|
358
|
+
entry = self.index.import_map.get(base)
|
|
359
|
+
if entry is not None:
|
|
360
|
+
if is_stdlib(entry[0]):
|
|
361
|
+
return _SKIP
|
|
362
|
+
obj = self._resolve_imported(path, parts)
|
|
363
|
+
return self._classify_object(obj) if obj is not None else _UNIFORM
|
|
364
|
+
if len(parts) == 1 and base in vars(builtins):
|
|
365
|
+
return _SKIP
|
|
366
|
+
if base.startswith('_'):
|
|
367
|
+
return _SKIP # unresolvable private name — legacy parity
|
|
368
|
+
return _UNIFORM
|
|
369
|
+
|
|
370
|
+
def _is_series_slot_method(self, func: ast.expr, scope_stack: list[str]) -> bool:
|
|
371
|
+
"""Whether a callee is a method of a series slot
|
|
372
|
+
(``__state__[N].add`` / ``__state·scope__[N].set``).
|
|
373
|
+
|
|
374
|
+
:param func: The callee expression.
|
|
375
|
+
:param scope_stack: Function-name path of the call site's scope.
|
|
376
|
+
:return: True if the slot under the attribute is a series slot.
|
|
377
|
+
"""
|
|
378
|
+
if not (isinstance(func, ast.Attribute) and isinstance(func.value, ast.Subscript)):
|
|
379
|
+
return False
|
|
380
|
+
sub = func.value
|
|
381
|
+
if not (isinstance(sub.value, ast.Name) and isinstance(sub.slice, ast.Constant)
|
|
382
|
+
and isinstance(sub.slice.value, int)):
|
|
383
|
+
return False
|
|
384
|
+
param = sub.value.id
|
|
385
|
+
if param == DEFAULT_STATE_PARAM:
|
|
386
|
+
scope_id = '·'.join(scope_stack)
|
|
387
|
+
elif param.startswith('__state·') and param.endswith('__'):
|
|
388
|
+
scope_id = param[len('__state·'):-2]
|
|
389
|
+
else:
|
|
390
|
+
return False
|
|
391
|
+
scope = self.layout.scopes.get(scope_id)
|
|
392
|
+
if scope is None:
|
|
393
|
+
return False
|
|
394
|
+
index = sub.slice.value
|
|
395
|
+
return 0 <= index < len(scope.slots) and scope.slots[index].kind == 'series'
|
|
396
|
+
|
|
397
|
+
def _resolve_imported(self, path: str, parts: list[str]) -> Any | None:
|
|
398
|
+
"""Resolve a dotted callee path through the module-level import map
|
|
399
|
+
at transform time (imports are cached in sys.modules)."""
|
|
400
|
+
try:
|
|
401
|
+
return self._resolve_cache[path]
|
|
402
|
+
except KeyError:
|
|
403
|
+
pass
|
|
404
|
+
module_name, attr = self.index.import_map[parts[0]]
|
|
405
|
+
obj: Any | None
|
|
406
|
+
try:
|
|
407
|
+
obj = importlib.import_module(module_name)
|
|
408
|
+
for name in ([attr] if attr else []) + parts[1:]:
|
|
409
|
+
try:
|
|
410
|
+
obj = getattr(obj, name)
|
|
411
|
+
except AttributeError:
|
|
412
|
+
# Submodule not yet loaded: the script's own import only
|
|
413
|
+
# runs after compilation, so import it here — otherwise
|
|
414
|
+
# the route would depend on what happens to be in
|
|
415
|
+
# sys.modules and the emission would not be deterministic
|
|
416
|
+
if not isinstance(obj, types.ModuleType):
|
|
417
|
+
raise
|
|
418
|
+
obj = importlib.import_module(f'{obj.__name__}.{name}')
|
|
419
|
+
except Exception: # noqa: any resolution failure means "unprovable"
|
|
420
|
+
obj = None
|
|
421
|
+
self._resolve_cache[path] = obj
|
|
422
|
+
return obj
|
|
423
|
+
|
|
424
|
+
@staticmethod
|
|
425
|
+
def _classify_object(obj: Any) -> str:
|
|
426
|
+
"""Classify a transform-time resolved callee object."""
|
|
427
|
+
if isinstance(obj, Exported):
|
|
428
|
+
return _UNIFORM # the anchor's bind unwraps it
|
|
429
|
+
if isinstance(obj, type):
|
|
430
|
+
return _SKIP
|
|
431
|
+
bound_self = getattr(obj, '__self__', None)
|
|
432
|
+
if bound_self is not None and isinstance(bound_self, type):
|
|
433
|
+
return _SKIP # classmethod
|
|
434
|
+
if isinstance(obj, (types.BuiltinFunctionType, types.BuiltinMethodType)):
|
|
435
|
+
return _SKIP
|
|
436
|
+
if getattr(obj, '__pyne_bind__', None) is not None:
|
|
437
|
+
# Overload dispatcher — the implementation is chosen at runtime.
|
|
438
|
+
# Must be checked BEFORE the layout: functools.wraps copies the
|
|
439
|
+
# first implementation's __dict__ (its __pyne_layout__ included)
|
|
440
|
+
# onto the dispatcher.
|
|
441
|
+
return _UNIFORM
|
|
442
|
+
if getattr(obj, '__pyne_layout__', None) is not None:
|
|
443
|
+
return _FAST
|
|
444
|
+
if getattr(obj, '__module_property__', False):
|
|
445
|
+
return _SKIP # Pine-style module property getter — stateless by design
|
|
446
|
+
if isinstance(obj, types.FunctionType) and '__pyne_slot_layout__' in obj.__globals__:
|
|
447
|
+
return _DIRECT # transformed module, no layout -> provably stateless
|
|
448
|
+
return _UNIFORM
|
|
449
|
+
|
|
450
|
+
def _is_carrier(self, scope_id: str) -> bool:
|
|
451
|
+
"""Whether a same-module scope carries state (fixpoint result)."""
|
|
452
|
+
try:
|
|
453
|
+
return self.carrier[scope_id]
|
|
454
|
+
except KeyError:
|
|
455
|
+
return self.layout.state_carrying(scope_id)
|
|
456
|
+
|
|
457
|
+
def _run_fixpoint(self, scope_routes: dict[str, list[_Route]]) -> dict[str, bool]:
|
|
458
|
+
"""Carrier fixpoint: a scope carries state if it has own slots or any
|
|
459
|
+
non-direct call site (fast/uniform, or same-module to a carrier)."""
|
|
460
|
+
carrier = {scope: self.layout.state_carrying(scope) for scope in scope_routes}
|
|
461
|
+
for routes in scope_routes.values():
|
|
462
|
+
for route in routes:
|
|
463
|
+
if isinstance(route, tuple):
|
|
464
|
+
carrier.setdefault(route[1], self.layout.state_carrying(route[1]))
|
|
465
|
+
changed = True
|
|
466
|
+
while changed:
|
|
467
|
+
changed = False
|
|
468
|
+
for scope, routes in scope_routes.items():
|
|
469
|
+
if carrier[scope]:
|
|
470
|
+
continue
|
|
471
|
+
for route in routes:
|
|
472
|
+
if (route in (_FAST, _UNIFORM)
|
|
473
|
+
or (isinstance(route, tuple) and carrier.get(route[1], False))):
|
|
474
|
+
carrier[scope] = True
|
|
475
|
+
changed = True
|
|
476
|
+
break
|
|
477
|
+
return carrier
|
|
478
|
+
|
|
479
|
+
# --- emission helpers ----------------------------------------------------
|
|
480
|
+
|
|
481
|
+
def _state_param(self) -> str:
|
|
482
|
+
return self.layout.state_param('·'.join(self._scope_stack))
|
|
483
|
+
|
|
484
|
+
@staticmethod
|
|
485
|
+
def _copy_callee(func: ast.expr) -> ast.expr:
|
|
486
|
+
"""Fresh, attribute-free copy of a callee expression. Other
|
|
487
|
+
transformers hang ``parent`` backlinks on nodes, which would make a
|
|
488
|
+
``deepcopy`` drag the entire module tree along — rebuilding from
|
|
489
|
+
source sidesteps that. The reparse stamps ``lineno=1`` on every node;
|
|
490
|
+
those must be overwritten with the original callee's location, or the
|
|
491
|
+
lazy-resolve branch emits line-1 line events mid-statement (double
|
|
492
|
+
breakpoint hits and derailed step-over on the first bar)."""
|
|
493
|
+
copy = cast(ast.expr, ast.parse(ast.unparse(func), mode='eval').body)
|
|
494
|
+
for node in ast.walk(copy):
|
|
495
|
+
ast.copy_location(node, func)
|
|
496
|
+
return copy
|
|
497
|
+
|
|
498
|
+
@staticmethod
|
|
499
|
+
def _slot_ref(param: str, slot: int) -> ast.Subscript:
|
|
500
|
+
return ast.Subscript(value=ast.Name(id=param, ctx=ast.Load()),
|
|
501
|
+
slice=ast.Constant(value=slot), ctx=ast.Load())
|
|
502
|
+
|
|
503
|
+
@staticmethod
|
|
504
|
+
def _counter_walrus(counter: str) -> ast.NamedExpr:
|
|
505
|
+
"""``(__i__ := (<counter> := <counter> + 1) - 1)``"""
|
|
506
|
+
increment = ast.NamedExpr(
|
|
507
|
+
target=ast.Name(id=counter, ctx=ast.Store()),
|
|
508
|
+
value=ast.BinOp(left=ast.Name(id=counter, ctx=ast.Load()),
|
|
509
|
+
op=ast.Add(), right=ast.Constant(value=1)))
|
|
510
|
+
return ast.NamedExpr(
|
|
511
|
+
target=ast.Name(id='__i__', ctx=ast.Store()),
|
|
512
|
+
value=ast.BinOp(left=increment, op=ast.Sub(), right=ast.Constant(value=1)))
|
|
513
|
+
|
|
514
|
+
@staticmethod
|
|
515
|
+
def _list_len(children: str) -> ast.Call:
|
|
516
|
+
"""``<children>.__len__()`` — the loop-site counter guard's list length.
|
|
517
|
+
|
|
518
|
+
A bare ``len(...)`` is unsafe here: a script variable named ``len``
|
|
519
|
+
(one of the most common Pine input names) shadows the builtin in the
|
|
520
|
+
function scope, so the emitted ``len`` would resolve to that value
|
|
521
|
+
(e.g. an ``int``) and the guard would raise ``'int' object is not
|
|
522
|
+
callable``. ``<children>`` is always our own hoisted list, so calling
|
|
523
|
+
its ``__len__`` slot directly sidesteps name resolution entirely.
|
|
524
|
+
"""
|
|
525
|
+
return ast.Call(
|
|
526
|
+
func=ast.Attribute(value=ast.Name(id=children, ctx=ast.Load()),
|
|
527
|
+
attr='__len__', ctx=ast.Load()),
|
|
528
|
+
args=[], keywords=[])
|
|
529
|
+
|
|
530
|
+
def _add_loop_hoist(self, slot: int) -> tuple[str, str]:
|
|
531
|
+
"""Register a loop site's counter + hoisted list for the prologue."""
|
|
532
|
+
k = len(self._loop_hoists[-1])
|
|
533
|
+
counter, children = f'__cnt_{k}__', f'__chl_{k}__'
|
|
534
|
+
self._loop_hoists[-1].append((counter, children, slot))
|
|
535
|
+
return counter, children
|
|
536
|
+
|
|
537
|
+
def _emit_fast(self, node: ast.Call, slot: int, in_loop: bool) -> ast.Call:
|
|
538
|
+
"""Prepend the child-state expression as the hidden first argument."""
|
|
539
|
+
param = self._state_param()
|
|
540
|
+
callee_copy = self._copy_callee(node.func)
|
|
541
|
+
if not in_loop:
|
|
542
|
+
self._used_helpers.add('__resolve_slot__')
|
|
543
|
+
state_expr = ast.IfExp(
|
|
544
|
+
test=ast.Compare(
|
|
545
|
+
left=ast.NamedExpr(target=ast.Name(id='__st__', ctx=ast.Store()),
|
|
546
|
+
value=self._slot_ref(param, slot)),
|
|
547
|
+
ops=[ast.IsNot()], comparators=[ast.Constant(value=None)]),
|
|
548
|
+
body=ast.Name(id='__st__', ctx=ast.Load()),
|
|
549
|
+
orelse=ast.Call(func=ast.Name(id='__resolve_slot__', ctx=ast.Load()),
|
|
550
|
+
args=[ast.Name(id=param, ctx=ast.Load()),
|
|
551
|
+
ast.Constant(value=slot), callee_copy],
|
|
552
|
+
keywords=[]))
|
|
553
|
+
else:
|
|
554
|
+
self._used_helpers.add('__grow__')
|
|
555
|
+
counter, children = self._add_loop_hoist(slot)
|
|
556
|
+
state_expr = ast.IfExp(
|
|
557
|
+
test=ast.Compare(
|
|
558
|
+
left=self._counter_walrus(counter), ops=[ast.Lt()],
|
|
559
|
+
comparators=[self._list_len(children)]),
|
|
560
|
+
body=ast.Subscript(value=ast.Name(id=children, ctx=ast.Load()),
|
|
561
|
+
slice=ast.Name(id='__i__', ctx=ast.Load()), ctx=ast.Load()),
|
|
562
|
+
orelse=ast.Call(func=ast.Name(id='__grow__', ctx=ast.Load()),
|
|
563
|
+
args=[ast.Name(id=children, ctx=ast.Load()), callee_copy],
|
|
564
|
+
keywords=[]))
|
|
565
|
+
node.args.insert(0, state_expr)
|
|
566
|
+
return node
|
|
567
|
+
|
|
568
|
+
def _emit_uniform(self, node: ast.Call, slot: int, in_loop: bool) -> ast.Call:
|
|
569
|
+
"""Wrap the call in the anchored bind form."""
|
|
570
|
+
param = self._state_param()
|
|
571
|
+
callee, callee_copy = node.func, self._copy_callee(node.func)
|
|
572
|
+
pair = ast.Name(id='__b__', ctx=ast.Load())
|
|
573
|
+
if not in_loop:
|
|
574
|
+
self._used_helpers.add('__bind_any__')
|
|
575
|
+
test: ast.expr = ast.BoolOp(op=ast.And(), values=[
|
|
576
|
+
ast.Compare(
|
|
577
|
+
left=ast.NamedExpr(target=ast.Name(id='__b__', ctx=ast.Store()),
|
|
578
|
+
value=self._slot_ref(param, slot)),
|
|
579
|
+
ops=[ast.IsNot()], comparators=[ast.Constant(value=None)]),
|
|
580
|
+
ast.Compare(
|
|
581
|
+
left=ast.Subscript(value=pair, slice=ast.Constant(value=0), ctx=ast.Load()),
|
|
582
|
+
ops=[ast.Is()], comparators=[callee]),
|
|
583
|
+
])
|
|
584
|
+
rebind: ast.expr = ast.Call(
|
|
585
|
+
func=ast.Name(id='__bind_any__', ctx=ast.Load()),
|
|
586
|
+
args=[ast.Name(id=param, ctx=ast.Load()), ast.Constant(value=slot), callee_copy],
|
|
587
|
+
keywords=[])
|
|
588
|
+
else:
|
|
589
|
+
self._used_helpers.add('__bind_any_loop__')
|
|
590
|
+
counter, children = self._add_loop_hoist(slot)
|
|
591
|
+
test = ast.BoolOp(op=ast.And(), values=[
|
|
592
|
+
ast.Compare(
|
|
593
|
+
left=self._counter_walrus(counter), ops=[ast.Lt()],
|
|
594
|
+
comparators=[self._list_len(children)]),
|
|
595
|
+
ast.Compare(
|
|
596
|
+
left=ast.Subscript(
|
|
597
|
+
value=ast.NamedExpr(
|
|
598
|
+
target=ast.Name(id='__b__', ctx=ast.Store()),
|
|
599
|
+
value=ast.Subscript(value=ast.Name(id=children, ctx=ast.Load()),
|
|
600
|
+
slice=ast.Name(id='__i__', ctx=ast.Load()),
|
|
601
|
+
ctx=ast.Load())),
|
|
602
|
+
slice=ast.Constant(value=0), ctx=ast.Load()),
|
|
603
|
+
ops=[ast.Is()], comparators=[callee]),
|
|
604
|
+
])
|
|
605
|
+
rebind = ast.Call(
|
|
606
|
+
func=ast.Name(id='__bind_any_loop__', ctx=ast.Load()),
|
|
607
|
+
args=[ast.Name(id=children, ctx=ast.Load()),
|
|
608
|
+
ast.Name(id='__i__', ctx=ast.Load()), callee_copy],
|
|
609
|
+
keywords=[])
|
|
610
|
+
bound = ast.IfExp(
|
|
611
|
+
test=test,
|
|
612
|
+
body=ast.Subscript(value=ast.Name(id='__b__', ctx=ast.Load()),
|
|
613
|
+
slice=ast.Constant(value=1), ctx=ast.Load()),
|
|
614
|
+
orelse=rebind)
|
|
615
|
+
return ast.Call(func=bound, args=node.args, keywords=node.keywords)
|
|
616
|
+
|
|
617
|
+
# --- visitors ------------------------------------------------------------
|
|
618
|
+
|
|
619
|
+
def visit_Module(self, node: ast.Module) -> ast.Module:
|
|
620
|
+
self.layout.assign_scope_ids(node)
|
|
621
|
+
self.index = _ScopeIndex(self.layout)
|
|
622
|
+
self.index.visit(node)
|
|
623
|
+
collector = _RouteCollector(self)
|
|
624
|
+
collector.visit(node)
|
|
625
|
+
self.carrier = self._run_fixpoint(collector.scope_routes)
|
|
626
|
+
|
|
627
|
+
node = cast(ast.Module, self.generic_visit(node))
|
|
628
|
+
|
|
629
|
+
if self._used_helpers:
|
|
630
|
+
import_stmt = ast.ImportFrom(
|
|
631
|
+
module='pynecore.core.instance_state',
|
|
632
|
+
names=[ast.alias(name=name, asname=None)
|
|
633
|
+
for name in sorted(self._used_helpers)],
|
|
634
|
+
level=0)
|
|
635
|
+
insert_pos = 0
|
|
636
|
+
first = node.body[0] if node.body else None
|
|
637
|
+
if (isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant)
|
|
638
|
+
and isinstance(first.value.value, str)):
|
|
639
|
+
insert_pos = 1
|
|
640
|
+
for i in range(insert_pos, len(node.body)):
|
|
641
|
+
if isinstance(node.body[i], (ast.Import, ast.ImportFrom)):
|
|
642
|
+
insert_pos = i + 1
|
|
643
|
+
elif not isinstance(node.body[i], ast.Expr):
|
|
644
|
+
break
|
|
645
|
+
node.body.insert(insert_pos, import_stmt)
|
|
646
|
+
return node
|
|
647
|
+
|
|
648
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef:
|
|
649
|
+
return node # class bodies stay raw (no hidden-parameter injection path)
|
|
650
|
+
|
|
651
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
652
|
+
if _is_test_function(node.name):
|
|
653
|
+
return node
|
|
654
|
+
self._scope_stack.append(self.layout.scope_segment(node))
|
|
655
|
+
scope = '·'.join(self._scope_stack)
|
|
656
|
+
scope_for_function(self.layout, scope, node)
|
|
657
|
+
old_loop, self._loop_depth = self._loop_depth, 0
|
|
658
|
+
old_lambda, self._lambda_depth = self._lambda_depth, 0
|
|
659
|
+
self._loop_hoists.append([])
|
|
660
|
+
|
|
661
|
+
# Only the body is isolation territory (decorators and argument
|
|
662
|
+
# defaults are evaluated outside the instance, legacy parity)
|
|
663
|
+
node.body = [cast(ast.stmt, self.visit(stmt)) for stmt in node.body]
|
|
664
|
+
|
|
665
|
+
hoists = self._loop_hoists.pop()
|
|
666
|
+
if hoists:
|
|
667
|
+
param = self.layout.state_param(scope)
|
|
668
|
+
prologue: list[ast.stmt] = []
|
|
669
|
+
for counter, children, slot in hoists:
|
|
670
|
+
prologue.append(ast.Assign(
|
|
671
|
+
targets=[ast.Name(id=counter, ctx=ast.Store())],
|
|
672
|
+
value=ast.Constant(value=0)))
|
|
673
|
+
prologue.append(ast.Assign(
|
|
674
|
+
targets=[ast.Name(id=children, ctx=ast.Store())],
|
|
675
|
+
value=self._slot_ref(param, slot)))
|
|
676
|
+
insert_pos = 0
|
|
677
|
+
first = node.body[0] if node.body else None
|
|
678
|
+
if (isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant)
|
|
679
|
+
and isinstance(first.value.value, str)):
|
|
680
|
+
insert_pos = 1
|
|
681
|
+
node.body[insert_pos:insert_pos] = prologue
|
|
682
|
+
|
|
683
|
+
self._loop_depth, self._lambda_depth = old_loop, old_lambda
|
|
684
|
+
self._scope_stack.pop()
|
|
685
|
+
return node
|
|
686
|
+
|
|
687
|
+
def visit_For(self, node: ast.For) -> ast.For:
|
|
688
|
+
node.iter = cast(ast.expr, self.visit(node.iter))
|
|
689
|
+
self._loop_depth += 1
|
|
690
|
+
node.body = [cast(ast.stmt, self.visit(stmt)) for stmt in node.body]
|
|
691
|
+
node.orelse = [cast(ast.stmt, self.visit(stmt)) for stmt in node.orelse]
|
|
692
|
+
self._loop_depth -= 1
|
|
693
|
+
return node
|
|
694
|
+
|
|
695
|
+
def visit_While(self, node: ast.While) -> ast.While:
|
|
696
|
+
self._loop_depth += 1
|
|
697
|
+
node.test = cast(ast.expr, self.visit(node.test))
|
|
698
|
+
node.body = [cast(ast.stmt, self.visit(stmt)) for stmt in node.body]
|
|
699
|
+
node.orelse = [cast(ast.stmt, self.visit(stmt)) for stmt in node.orelse]
|
|
700
|
+
self._loop_depth -= 1
|
|
701
|
+
return node
|
|
702
|
+
|
|
703
|
+
def _visit_comprehension(self, node: ast.AST) -> ast.AST:
|
|
704
|
+
"""Comprehension parts run per element — loop context (walruses bind
|
|
705
|
+
in the enclosing function scope per PEP 572, so counters work)."""
|
|
706
|
+
self._loop_depth += 1
|
|
707
|
+
node = self.generic_visit(node)
|
|
708
|
+
self._loop_depth -= 1
|
|
709
|
+
return node
|
|
710
|
+
|
|
711
|
+
visit_ListComp = _visit_comprehension
|
|
712
|
+
visit_SetComp = _visit_comprehension
|
|
713
|
+
visit_DictComp = _visit_comprehension
|
|
714
|
+
visit_GeneratorExp = _visit_comprehension
|
|
715
|
+
|
|
716
|
+
def visit_Lambda(self, node: ast.Lambda) -> ast.Lambda:
|
|
717
|
+
self._lambda_depth += 1
|
|
718
|
+
node.body = cast(ast.expr, self.visit(node.body))
|
|
719
|
+
self._lambda_depth -= 1
|
|
720
|
+
return node
|
|
721
|
+
|
|
722
|
+
def visit_Call(self, node: ast.Call) -> ast.expr:
|
|
723
|
+
node.args = [cast(ast.expr, self.visit(arg)) for arg in node.args]
|
|
724
|
+
node.keywords = [cast(ast.keyword, self.visit(kw)) for kw in node.keywords]
|
|
725
|
+
if not isinstance(node.func, (ast.Name, ast.Attribute)):
|
|
726
|
+
# Immediately-called expressions stay raw (legacy parity), but
|
|
727
|
+
# calls inside the callee expression still get their own sites
|
|
728
|
+
node.func = cast(ast.expr, self.visit(node.func))
|
|
729
|
+
return node
|
|
730
|
+
|
|
731
|
+
route = self.route_for_callee(node.func, self._scope_stack)
|
|
732
|
+
if not self._scope_stack:
|
|
733
|
+
if route == _FAST or (isinstance(route, tuple) and self._is_carrier(route[1])):
|
|
734
|
+
raise SyntaxError("Stateful function calls are not supported at module level")
|
|
735
|
+
return node
|
|
736
|
+
if isinstance(route, tuple):
|
|
737
|
+
route = _FAST if self._is_carrier(route[1]) else _DIRECT
|
|
738
|
+
if route in (_SKIP, _DIRECT):
|
|
739
|
+
return node
|
|
740
|
+
|
|
741
|
+
scope = '·'.join(self._scope_stack)
|
|
742
|
+
if self._lambda_depth:
|
|
743
|
+
# Loop counters would bind lambda-local; the straight-line
|
|
744
|
+
# anchor is the only emission that stays correct inside a lambda
|
|
745
|
+
route, in_loop = _UNIFORM, False
|
|
746
|
+
else:
|
|
747
|
+
in_loop = self._loop_depth > 0
|
|
748
|
+
|
|
749
|
+
ordinal = self._ordinals.get(scope, 0)
|
|
750
|
+
self._ordinals[scope] = ordinal + 1
|
|
751
|
+
call_id = f'{scope}·{_get_func_path(node.func) or "<callee>"}·{ordinal}'
|
|
752
|
+
scope_layout = self.layout.scope(scope)
|
|
753
|
+
if route == _FAST:
|
|
754
|
+
slot = scope_layout.add_child(call_id, in_loop=in_loop)
|
|
755
|
+
return self._emit_fast(node, slot, in_loop)
|
|
756
|
+
slot = scope_layout.add_anchor(call_id, in_loop=in_loop)
|
|
757
|
+
return self._emit_uniform(node, slot, in_loop)
|