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.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. 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)