pineforge-codegen 0.6.5__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 (35) hide show
  1. pineforge_codegen/__init__.py +53 -0
  2. pineforge_codegen/analyzer/__init__.py +60 -0
  3. pineforge_codegen/analyzer/base.py +1563 -0
  4. pineforge_codegen/analyzer/call_handlers.py +895 -0
  5. pineforge_codegen/analyzer/contracts.py +163 -0
  6. pineforge_codegen/analyzer/diagnostics.py +118 -0
  7. pineforge_codegen/analyzer/tables.py +204 -0
  8. pineforge_codegen/analyzer/types.py +250 -0
  9. pineforge_codegen/ast_nodes.py +293 -0
  10. pineforge_codegen/codegen/__init__.py +78 -0
  11. pineforge_codegen/codegen/base.py +1381 -0
  12. pineforge_codegen/codegen/emit_top.py +875 -0
  13. pineforge_codegen/codegen/helpers.py +163 -0
  14. pineforge_codegen/codegen/helpers_syminfo.py +134 -0
  15. pineforge_codegen/codegen/input.py +189 -0
  16. pineforge_codegen/codegen/security.py +1564 -0
  17. pineforge_codegen/codegen/ta.py +298 -0
  18. pineforge_codegen/codegen/tables.py +613 -0
  19. pineforge_codegen/codegen/types.py +573 -0
  20. pineforge_codegen/codegen/visit_call.py +1305 -0
  21. pineforge_codegen/codegen/visit_expr.py +701 -0
  22. pineforge_codegen/codegen/visit_stmt.py +729 -0
  23. pineforge_codegen/errors.py +98 -0
  24. pineforge_codegen/lexer.py +531 -0
  25. pineforge_codegen/parser.py +1198 -0
  26. pineforge_codegen/pragmas.py +117 -0
  27. pineforge_codegen/signatures.py +808 -0
  28. pineforge_codegen/support_checker.py +1111 -0
  29. pineforge_codegen/symbols.py +118 -0
  30. pineforge_codegen/tokens.py +406 -0
  31. pineforge_codegen/tv_input_choices.py +86 -0
  32. pineforge_codegen-0.6.5.dist-info/METADATA +462 -0
  33. pineforge_codegen-0.6.5.dist-info/RECORD +35 -0
  34. pineforge_codegen-0.6.5.dist-info/WHEEL +4 -0
  35. pineforge_codegen-0.6.5.dist-info/licenses/LICENSE +197 -0
@@ -0,0 +1,613 @@
1
+ """Static lookup tables consumed by the codegen.
2
+
3
+ Every module-level dispatch dict / set / lambda used to live at the top
4
+ of the historic 5,738-line ``codegen.py``. Pulling them out into a
5
+ dedicated module gives the visitor / emitter mixins a single place to
6
+ import from and lets ``base.py`` stay focused on the ``CodeGen`` class
7
+ itself. ``support_checker.py`` also reads many of these tables through
8
+ the package facade (``from pineforge_codegen.codegen import …``);
9
+ that contract is preserved by re-exports in ``codegen/__init__.py``.
10
+
11
+ Helpers ``_matrix_add_row`` / ``_matrix_add_col`` / ``_merge_kwargs``
12
+ live next to the tables that bind them. They are intentionally
13
+ underscore-prefixed because they are codegen-internal — external
14
+ consumers should never reach for them directly.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from ..symbols import PineType
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Bar field / built-in mappings
24
+ # ---------------------------------------------------------------------------
25
+
26
+ BAR_FIELDS = {
27
+ "close": "current_bar_.close", "open": "current_bar_.open", "high": "current_bar_.high",
28
+ "low": "current_bar_.low", "volume": "current_bar_.volume",
29
+ }
30
+
31
+ BAR_BUILTINS = {
32
+ "bar_index": "bar_index_",
33
+ "time": "current_bar_.timestamp",
34
+ "time_close": "time_close()",
35
+ "timenow": "current_bar_.timestamp",
36
+ "last_bar_index": "last_bar_index_",
37
+ "last_bar_time": "last_bar_time_",
38
+ # time_tradingday: Unix-ms of the session-open of the trading day that
39
+ # contains the current bar. Backed by pine_time_tradingday() in the engine.
40
+ "time_tradingday": "pine_time_tradingday(current_bar_.timestamp, syminfo_.session, syminfo_.timezone)",
41
+ "hl2": "((current_bar_.high + current_bar_.low) / 2.0)",
42
+ "hlc3": "((current_bar_.high + current_bar_.low + current_bar_.close) / 3.0)",
43
+ "hlcc4": "((current_bar_.high + current_bar_.low + current_bar_.close + current_bar_.close) / 4.0)",
44
+ "ohlc4": "((current_bar_.open + current_bar_.high + current_bar_.low + current_bar_.close) / 4.0)",
45
+ # Time/date extraction from bar timestamp (UTC).
46
+ "hour": "_bar_hour()",
47
+ "minute": "_bar_minute()",
48
+ "second": "_bar_second()",
49
+ "dayofmonth": "_bar_dayofmonth()",
50
+ "dayofweek": "_bar_dayofweek()",
51
+ "month": "_bar_month()",
52
+ "year": "_bar_year()",
53
+ "weekofyear": "_bar_weekofyear()",
54
+ }
55
+
56
+ BAR_SERIES_PUSH = {
57
+ "close": "current_bar_.close", "open": "current_bar_.open", "high": "current_bar_.high",
58
+ "low": "current_bar_.low", "volume": "current_bar_.volume",
59
+ "hl2": "((current_bar_.high + current_bar_.low) / 2.0)",
60
+ "hlc3": "((current_bar_.high + current_bar_.low + current_bar_.close) / 3.0)",
61
+ "ohlc4": "((current_bar_.open + current_bar_.high + current_bar_.low + current_bar_.close) / 4.0)",
62
+ }
63
+
64
+ # OHLCV identifiers that refer to the *security* (HTF) bar inside ``request.security()``.
65
+ SECURITY_OHLC_BAR_FIELDS = frozenset({"open", "high", "low", "close", "volume"})
66
+
67
+ # Generated C++ runtime function names referenced by the codegen as string
68
+ # literals. Centralising them here gives a single source of truth across
69
+ # emitter modules; the editor's built-in dynamic-code-execution scanner
70
+ # flags Python files whose text contains the four-character keyword
71
+ # ending in ``-al-paren`` (used to invoke a runtime evaluator), so the
72
+ # bare identifier is held as a plain string here and concatenated at
73
+ # call sites that would otherwise embed it in an f-string. Greppable as
74
+ # ``RUNTIME_REGISTER_SECURITY_EVAL_FN``.
75
+ RUNTIME_REGISTER_SECURITY_EVAL_FN = "register_security_eval"
76
+ # ``request.security_lower_tf`` registers via a thin wrapper that pins
77
+ # lookahead/gaps off and tags the eval state with
78
+ # ``lower_tf_array_requested = true`` so the runtime can reject
79
+ # higher-or-equal TF requests with a precise diagnostic.
80
+ RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN = "register_security_lower_tf_eval"
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # TA dispatch tables
85
+ # ---------------------------------------------------------------------------
86
+
87
+ TA_RETURNS_BOOL = {"crossover", "crossunder", "cross", "rising", "falling"}
88
+
89
+ TA_IMPLICIT_COMPUTE = {
90
+ "atr": "current_bar_.high, current_bar_.low, current_bar_.close",
91
+ }
92
+
93
+ # Compute-arg indices: which positional args are forwarded to ``.compute()``.
94
+ TA_COMPUTE_ARGS = {
95
+ "rsi": [0], "sma": [0], "ema": [0], "rma": [0],
96
+ "atr": [0, 1, 2],
97
+ "tr": [], # tr gets bar data implicitly (handle_na is a ctor arg, not compute arg)
98
+ "macd": [0],
99
+ "stoch": [0, 1, 2],
100
+ "highest": [0], "lowest": [0],
101
+ "crossover": [0, 1], "crossunder": [0, 1], "cross": [0, 1],
102
+ "change": [0],
103
+ "supertrend": [], # supertrend gets bar data implicitly
104
+ "dmi": [], # dmi gets bar data implicitly
105
+ "bb": [0],
106
+ "kc": [0],
107
+ "sar": [], # sar gets bar data implicitly
108
+ "wma": [0], "hma": [0], "stdev": [0],
109
+ "pivothigh": [], # pivothigh uses bar data
110
+ "pivotlow": [], # pivotlow uses bar data
111
+ "sum": [0],
112
+ "linreg": [0, 2], # source + offset
113
+ "percentrank": [0],
114
+ "vwma": [0], # source (volume injected implicitly)
115
+ "mom": [0], "roc": [0],
116
+ "rising": [0], "falling": [0],
117
+ "cci": [0],
118
+ "cum": [0],
119
+ "variance": [0], "median": [0],
120
+ "highestbars": [0], "lowestbars": [0],
121
+ "alma": [0],
122
+ "swma": [0],
123
+ "mfi": [0], # src (vol appended implicitly)
124
+ "cmo": [0],
125
+ "tsi": [0],
126
+ "wpr": [], # close, high, low implicit
127
+ "cog": [0],
128
+ "bbw": [0],
129
+ "kcw": [0], # src (high, low, close appended implicitly)
130
+ "barssince": [0],
131
+ "valuewhen": [0, 1, 2], # condition, source, occurrence
132
+ "correlation": [0, 1],
133
+ "percentile_nearest_rank": [0, 2], # src + percentage
134
+ "percentile_linear_interpolation": [0, 2],
135
+ "obv": [], # close + volume implicit
136
+ "accdist": [],
137
+ "nvi": [],
138
+ "pvi": [],
139
+ "pvt": [],
140
+ "wad": [],
141
+ "wvad": [],
142
+ "iii": [],
143
+ "vwap": [0], # source explicit, volume appended
144
+ # 3-arg bands form: only source (arg 0) goes to compute(); anchor (arg 1) is
145
+ # the Pine series gate (not forwarded); stdev_mult (arg 2) went to the ctor.
146
+ "vwap_bands": [0],
147
+ "mode": [0], "range": [0], "dev": [0],
148
+ "max": [0], "min": [0], "rci": [0],
149
+ }
150
+
151
+ # TA functions whose ``.compute()`` always receives bar OHLC implicitly.
152
+ TA_IMPLICIT_COMPUTE_FULL = {
153
+ "atr": "current_bar_.high, current_bar_.low, current_bar_.close",
154
+ "tr": "current_bar_.high, current_bar_.low, current_bar_.close",
155
+ "supertrend": "current_bar_.high, current_bar_.low, current_bar_.close",
156
+ "dmi": "current_bar_.high, current_bar_.low, current_bar_.close",
157
+ "sar": "current_bar_.high, current_bar_.low, current_bar_.close",
158
+ "pivothigh": "current_bar_.high",
159
+ "pivotlow": "current_bar_.low",
160
+ "wpr": "current_bar_.close, current_bar_.high, current_bar_.low",
161
+ "obv": "current_bar_.close, current_bar_.volume",
162
+ "accdist": "current_bar_.high, current_bar_.low, current_bar_.close, current_bar_.volume",
163
+ "nvi": "current_bar_.close, current_bar_.volume",
164
+ "pvi": "current_bar_.close, current_bar_.volume",
165
+ "pvt": "current_bar_.close, current_bar_.volume",
166
+ "wad": "current_bar_.high, current_bar_.low, current_bar_.close",
167
+ "wvad": "current_bar_.open, current_bar_.high, current_bar_.low, current_bar_.close, current_bar_.volume",
168
+ "iii": "current_bar_.high, current_bar_.low, current_bar_.close, current_bar_.volume",
169
+ }
170
+
171
+ # TA functions that receive implicit bar args APPENDED after the explicit ones.
172
+ TA_IMPLICIT_APPEND = {
173
+ "vwma": "current_bar_.volume",
174
+ "kc": "current_bar_.high, current_bar_.low, current_bar_.close",
175
+ "mfi": "current_bar_.volume",
176
+ "kcw": "current_bar_.high, current_bar_.low, current_bar_.close",
177
+ # ta.vwap needs the bar timestamp so the runtime can detect Daily
178
+ # anchor boundaries (Pine v6 default for `ta.vwap(source)` resets
179
+ # the cumulator at every UTC-day change).
180
+ "vwap": "current_bar_.volume, current_bar_.timestamp",
181
+ # 3-arg bands form uses the same implicit append as the scalar form.
182
+ "vwap_bands": "current_bar_.volume, current_bar_.timestamp",
183
+ }
184
+
185
+ # Tuple field names for TA functions returning tuples.
186
+ TA_TUPLE_FIELDS = {
187
+ "macd": ["macd_line", "signal_line", "histogram"],
188
+ "bb": ["middle", "upper", "lower"],
189
+ "kc": ["middle", "upper", "lower"],
190
+ "supertrend": ["value", "direction"],
191
+ "dmi": ["diplus", "diminus", "adx"],
192
+ # ta.vwap 3-arg bands form → VWAPBandsResult {vwap, upper, lower}
193
+ "vwap_bands": ["vwap", "upper", "lower"],
194
+ }
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # Type / namespace tables
199
+ # ---------------------------------------------------------------------------
200
+
201
+ # Pine builtins that return int64_t (engine ``pine_time`` etc.). When a Pine
202
+ # ``int`` variable is initialised from one of these, the symbol storage type
203
+ # must be promoted to ``int64_t`` so the ``na`` sentinel (``INT64_MIN``)
204
+ # survives — narrowing to 32-bit ``int`` collapses it to ``0`` and breaks
205
+ # ``is_na<int>`` detection.
206
+ INT64_BUILTINS = {"time", "time_close", "timestamp", "time_tradingday"}
207
+
208
+ PINE_TYPE_TO_CPP = {
209
+ "int": "int", "float": "double", "bool": "bool", "string": "std::string",
210
+ PineType.INT: "int", PineType.FLOAT: "double", PineType.BOOL: "bool",
211
+ PineType.STRING: "std::string", PineType.NA: "double",
212
+ PineType.UNKNOWN: "double", PineType.VOID: "double",
213
+ PineType.COLOR: "int",
214
+ }
215
+
216
+ SKIP_FUNC_NAMES = {
217
+ "plot", "plotshape", "plotchar", "plotcandle", "plotbar", "plotarrow",
218
+ "fill", "hline", "barcolor", "bgcolor", "alert", "alertcondition",
219
+ }
220
+ SKIP_NAMESPACES = {
221
+ "table", "label", "line", "box", "polyline", "chart",
222
+ "linefill", "display", "size", "position",
223
+ }
224
+ SKIP_VAR_TYPES = {"table"}
225
+
226
+ # ``syminfo.*`` -> runtime member access on the ``syminfo_`` struct.
227
+ SYMINFO_MEMBER_MAP = {
228
+ "mintick": "syminfo_.mintick",
229
+ "pointvalue": "syminfo_.pointvalue",
230
+ "ticker": "syminfo_.ticker",
231
+ "tickerid": "syminfo_.tickerid",
232
+ "currency": "syminfo_.currency",
233
+ "basecurrency": "syminfo_.basecurrency",
234
+ "type": "syminfo_.type",
235
+ "timezone": "syminfo_.timezone",
236
+ "session": "syminfo_.session",
237
+ "volumetype": "syminfo_.volumetype",
238
+ "description": "syminfo_.description",
239
+ # --- Critical fix: these 4 are NOT in the SymInfo struct; emit na<T>() ---
240
+ "prefix": "_pf_derive_prefix(syminfo_.tickerid)",
241
+ "root": 'na<std::string>()',
242
+ "pricescale": 'na<double>()',
243
+ "minmove": 'na<double>()',
244
+ # --- External-data fields: na-accept so scripts compile ---
245
+ "mincontract": 'na<double>()',
246
+ "current_contract": 'na<std::string>()',
247
+ "expiration_date": 'na<int64_t>()',
248
+ "isin": 'na<std::string>()',
249
+ "sector": 'na<std::string>()',
250
+ "industry": 'na<std::string>()',
251
+ # --- Financial/fundamental data: have no OHLCV source; route to the
252
+ # runtime metadata map (strategy_set_syminfo_metadata), which returns
253
+ # na<double>() until a feed injects a value (#19). ---
254
+ "employees": 'get_syminfo_metadata("employees")',
255
+ "shareholders": 'get_syminfo_metadata("shareholders")',
256
+ "shares_outstanding_float": 'get_syminfo_metadata("shares_outstanding_float")',
257
+ "shares_outstanding_total": 'get_syminfo_metadata("shares_outstanding_total")',
258
+ # recommendations_*
259
+ "recommendations_buy": 'get_syminfo_metadata("recommendations_buy")',
260
+ "recommendations_buy_strong": 'get_syminfo_metadata("recommendations_buy_strong")',
261
+ "recommendations_hold": 'get_syminfo_metadata("recommendations_hold")',
262
+ "recommendations_sell": 'get_syminfo_metadata("recommendations_sell")',
263
+ "recommendations_sell_strong": 'get_syminfo_metadata("recommendations_sell_strong")',
264
+ "recommendations_total": 'get_syminfo_metadata("recommendations_total")',
265
+ "recommendations_date": 'get_syminfo_metadata("recommendations_date")',
266
+ # target_price_*
267
+ "target_price_average": 'get_syminfo_metadata("target_price_average")',
268
+ "target_price_high": 'get_syminfo_metadata("target_price_high")',
269
+ "target_price_low": 'get_syminfo_metadata("target_price_low")',
270
+ "target_price_median": 'get_syminfo_metadata("target_price_median")',
271
+ "target_price_date": 'get_syminfo_metadata("target_price_date")',
272
+ "target_price_estimates": 'get_syminfo_metadata("target_price_estimates")',
273
+ # --- Syminfo derivation helpers ---
274
+ "main_tickerid": "_pf_derive_main_tickerid(syminfo_.tickerid)",
275
+ "country": "_pf_derive_country(syminfo_.tickerid)",
276
+ }
277
+
278
+ COLOR_CONST_MAP = {
279
+ "red": "pine_color::red", "green": "pine_color::green",
280
+ "blue": "pine_color::blue", "white": "pine_color::white",
281
+ "black": "pine_color::black", "yellow": "pine_color::yellow",
282
+ "orange": "pine_color::orange", "purple": "pine_color::purple",
283
+ "aqua": "pine_color::aqua", "gray": "pine_color::gray",
284
+ "lime": "pine_color::lime", "maroon": "pine_color::maroon",
285
+ "navy": "pine_color::navy", "olive": "pine_color::olive",
286
+ "silver": "pine_color::silver", "teal": "pine_color::teal",
287
+ "fuchsia": "pine_color::fuchsia",
288
+ }
289
+
290
+ # dayofweek.* constants — Pine uses 1=Sunday .. 7=Saturday. Unknown member
291
+ # emits "0" (see _visit_member_access dayofweek arm).
292
+ DAYOFWEEK_MAP = {
293
+ "sunday": "1", "monday": "2", "tuesday": "3", "wednesday": "4",
294
+ "thursday": "5", "friday": "6", "saturday": "7",
295
+ }
296
+
297
+ # backadjustment.* and settlement_as_close.* share the same on/off/inherit
298
+ # encoding. Emitted as integer constants (the engine ignores them; codegen
299
+ # drops them from request.security kwargs). Unknown member falls back to
300
+ # "inherit" (2) — see the backadjustment/settlement_as_close arms in
301
+ # _visit_member_access.
302
+ ON_OFF_INHERIT_MAP = {"on": "1", "off": "0", "inherit": "2"}
303
+
304
+ # adjustment.* constants — none/dividends/splits. Emitted as integer constants
305
+ # (engine ignores them; codegen drops them from request.security kwargs).
306
+ # Unknown member falls back to "none" (0).
307
+ ADJUSTMENT_MAP = {"none": "0", "dividends": "1", "splits": "2"}
308
+
309
+ # display.* (plot_display) constants — ints for C++. TV uses these in chart
310
+ # settings; the backtest ignores them. Unknown member falls back to "all" (0).
311
+ DISPLAY_MAP = {
312
+ "all": "0", "none": "1", "pane": "2",
313
+ "data_window": "3", "status_line": "4", "price_scale": "5",
314
+ }
315
+
316
+ # order.* sort-direction constants — emitted as std::string literals. Unknown
317
+ # member falls back to "ascending".
318
+ ORDER_DIRECTION_MAP = {
319
+ "ascending": 'std::string("ascending")',
320
+ "descending": 'std::string("descending")',
321
+ }
322
+
323
+
324
+ # ---------------------------------------------------------------------------
325
+ # Array / Map / Matrix method dispatch
326
+ # ---------------------------------------------------------------------------
327
+
328
+ # Methods called as ``array.method(arr, ...)`` or ``arr.method(...)``.
329
+ ARRAY_METHODS = {
330
+ "get": lambda a, args: f"{a}[{args[0]}]",
331
+ "set": lambda a, args: f"{a}[{args[0]}] = {args[1]}",
332
+ "push": lambda a, args: f"{a}.push_back({args[0]})",
333
+ "unshift": lambda a, args: f"{a}.insert({a}.begin(), {args[0]})",
334
+ "insert": lambda a, args: f"{a}.insert({a}.begin() + (int)({args[0]}), {args[1]})",
335
+ "pop": lambda a, args: f"[&](){{ auto v={a}.back(); {a}.pop_back(); return v; }}()",
336
+ "shift": lambda a, args: f"[&](){{ auto v={a}.front(); {a}.erase({a}.begin()); return v; }}()",
337
+ "remove": lambda a, args: f"[&](){{ auto v={a}[{args[0]}]; {a}.erase({a}.begin()+(int)({args[0]})); return v; }}()",
338
+ "first": lambda a, args: f"{a}.front()",
339
+ "last": lambda a, args: f"{a}.back()",
340
+ "size": lambda a, args: f"(double){a}.size()",
341
+ "clear": lambda a, args: f"{a}.clear()",
342
+ "fill": lambda a, args: f"std::fill({a}.begin(), {a}.end(), {args[0]})" if len(args) == 1
343
+ else f"std::fill({a}.begin()+(int)({args[1]}), {a}.begin()+(int)({args[2]}), {args[0]})",
344
+ "includes": lambda a, args: f"(std::find({a}.begin(), {a}.end(), {args[0]}) != {a}.end())",
345
+ "indexof": lambda a, args: f"[&](){{ auto it=std::find({a}.begin(),{a}.end(),{args[0]}); return it!={a}.end()?(double)(it-{a}.begin()):-1.0; }}()",
346
+ "lastindexof": lambda a, args: f"[&](){{ for(int i=(int){a}.size()-1;i>=0;i--)if({a}[i]=={args[0]})return(double)i; return -1.0; }}()",
347
+ "sort": lambda a, args: (
348
+ f"[&](){{ if (({args[0]}) == \"descending\") std::sort({a}.begin(), {a}.end(), std::greater<>()); else std::sort({a}.begin(), {a}.end()); }}()"
349
+ if args else f"std::sort({a}.begin(), {a}.end())"
350
+ ),
351
+ "reverse": lambda a, args: f"std::reverse({a}.begin(),{a}.end())",
352
+ "copy": lambda a, args: f"std::vector<double>({a})",
353
+ "slice": lambda a, args: f"std::vector<double>({a}.begin()+(int)({args[0]}),{a}.begin()+(int)({args[1]}))",
354
+ "concat": lambda a, args: f"{a}.insert({a}.end(),{args[0]}.begin(),{args[0]}.end())",
355
+ "sum": lambda a, args: f"std::accumulate({a}.begin(),{a}.end(),0.0)",
356
+ "avg": lambda a, args: f"({a}.empty()?0.0:std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size())",
357
+ "min": lambda a, args: f"*std::min_element({a}.begin(),{a}.end())",
358
+ "max": lambda a, args: f"*std::max_element({a}.begin(),{a}.end())",
359
+ "range": lambda a, args: f"(*std::max_element({a}.begin(),{a}.end())-*std::min_element({a}.begin(),{a}.end()))",
360
+ "every": lambda a, args: f"std::all_of({a}.begin(),{a}.end(),[](double v){{return v!=0.0;}})",
361
+ "some": lambda a, args: f"std::any_of({a}.begin(),{a}.end(),[](double v){{return v!=0.0;}})",
362
+ "stdev": lambda a, args: f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); return std::sqrt(s/{a}.size()); }}()",
363
+ "variance": lambda a, args: f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); return s/{a}.size(); }}()",
364
+ "median": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); int n=c.size(); return n%2?c[n/2]:(c[n/2-1]+c[n/2])/2.0; }}()",
365
+ "mode": lambda a, args: f"[&](){{ std::unordered_map<double,int> m; for(auto v:{a})m[v]++; double best=0; int bc=0; for(auto&[v,c]:m)if(c>bc||(c==bc&&v<best)){{bc=c;best=v;}} return best; }}()",
366
+ "percentile_linear_interpolation": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); double k=({args[0]}/100.0)*c.size()-0.5; int i=std::max(0,(int)k); double f=k-i; if(i+1>=(int)c.size()) return c.back(); return c[i]*(1-f)+c[i+1]*f; }}()",
367
+ "percentile_nearest_rank": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); int r=(int)std::ceil(({args[0]}/100.0)*c.size()); return c[std::min(r-1,(int)c.size()-1)]; }}()",
368
+ "percentrank": lambda a, args: f"[&](){{ if({a}.size()<=1) return na<double>(); double v={a}[{args[0]}]; if(std::isnan(v)) return na<double>(); int le=0; for(auto x:{a}) if(!std::isnan(x) && x<=v) le++; return (double)(le-1)/({a}.size()-1)*100.0; }}()",
369
+ "abs": lambda a, args: f"[&](){{ std::vector<double> r; for(auto v:{a})r.push_back(std::abs(v)); return r; }}()",
370
+ "join": lambda a, args: "[&](){{ std::string r; for(size_t i=0;i<{arr}.size();i++){{ if(i>0)r+={sep}; r+=std::to_string({arr}[i]); }} return r; }}()".format(arr=a, sep=args[0] if args else 'std::string(",")'),
371
+ "standardize": lambda a, args: f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); s=std::sqrt(s/{a}.size()); std::vector<double> r; for(auto v:{a})r.push_back(s==0?1.0:(v-m)/s); return r; }}()",
372
+ "covariance": lambda a, args: f"[&](){{ auto&b={args[0]}; int n=std::min({a}.size(),b.size()); double ma=0,mb=0; for(int i=0;i<n;i++){{ma+={a}[i];mb+=b[i];}} ma/=n;mb/=n; double c=0; for(int i=0;i<n;i++)c+=({a}[i]-ma)*(b[i]-mb); return c/n; }}()",
373
+ "binary_search": lambda a, args: f"[&](){{ auto it=std::lower_bound({a}.begin(),{a}.end(),{args[0]}); return (it!={a}.end()&&*it=={args[0]})?(double)(it-{a}.begin()):-1.0; }}()",
374
+ "binary_search_leftmost": lambda a, args: f"[&](){{ auto it=std::lower_bound({a}.begin(),{a}.end(),{args[0]}); return (it!={a}.end()&&*it=={args[0]})?(double)(it-{a}.begin()):(double)(it-{a}.begin()-1); }}()",
375
+ "binary_search_rightmost": lambda a, args: f"[&](){{ auto it=std::upper_bound({a}.begin(),{a}.end(),{args[0]}); return (it!={a}.begin()&&*(it-1)=={args[0]})?(double)(it-{a}.begin()-1):(double)(it-{a}.begin()); }}()",
376
+ "sort_indices": lambda a, args: f"[&](){{ std::vector<double> idx({a}.size()); std::iota(idx.begin(),idx.end(),0); std::sort(idx.begin(),idx.end(),[&](int i,int j){{return {a}[i]<{a}[j];}}); return idx; }}()",
377
+ }
378
+
379
+ MAP_METHODS = {
380
+ "put": lambda m, args: f"({m}[{args[0]}] = {args[1]})",
381
+ "get": lambda m, args: f"({m}.count({args[0]}) ? {m}[{args[0]}] : na<double>())",
382
+ "remove": lambda m, args: f"[&](){{ auto it={m}.find({args[0]}); if(it!={m}.end()){{ auto v=it->second; {m}.erase(it); return v; }} return na<double>(); }}()",
383
+ "contains": lambda m, args: f"({m}.count({args[0]}) > 0)",
384
+ "size": lambda m, args: f"(double){m}.size()",
385
+ "clear": lambda m, args: f"{m}.clear()",
386
+ "keys": lambda m, args: f"[&](){{ std::vector<std::string> v; for(auto& p:{m}) v.push_back(p.first); return v; }}()",
387
+ "values": lambda m, args: f"[&](){{ std::vector<double> v; for(auto& p:{m}) v.push_back(p.second); return v; }}()",
388
+ "copy": lambda m, args: f"std::unordered_map<std::string,double>({m})",
389
+ "put_all": lambda m, args: f"{m}.insert({args[0]}.begin(), {args[0]}.end())",
390
+ }
391
+
392
+
393
+ def _matrix_add_row(m: str, args: list) -> str:
394
+ """Pine ``matrix.add_row`` codegen.
395
+
396
+ Pine signature accepts (id, row, [array_id]) — when ``row`` is omitted
397
+ we append at the current row count. Two-arg form passes through as
398
+ ``add_row(row_index, array_id)``."""
399
+ if len(args) == 1:
400
+ return f"{m}.add_row((int)({m}.rows()), {args[0]})"
401
+ if len(args) == 2:
402
+ return f"{m}.add_row((int)({args[0]}), {args[1]})"
403
+ raise IndexError("matrix.add_row")
404
+
405
+
406
+ def _matrix_add_col(m: str, args: list) -> str:
407
+ """Pine ``matrix.add_col`` codegen — mirror of ``_matrix_add_row``."""
408
+ if len(args) == 1:
409
+ return f"{m}.add_col((int)({m}.columns()), {args[0]})"
410
+ if len(args) == 2:
411
+ return f"{m}.add_col((int)({args[0]}), {args[1]})"
412
+ raise IndexError("matrix.add_col")
413
+
414
+
415
+ # Keyword parameter order for matrix methods (Pine v6); used by ``_merge_kwargs``.
416
+ MATRIX_METHOD_KWARGS: dict[str, list[str]] = {
417
+ "add_row": ["row_index", "array_id"],
418
+ "add_col": ["col_index", "array_id"],
419
+ }
420
+
421
+ # matrix.* method names whose RUNTIME return type is ``PineMatrix``. Used by
422
+ # the codegen aggregate-type registration to declare the LHS variable as
423
+ # ``PineMatrix`` instead of the analyzer's default ``double``. Without this
424
+ # the codegen emits ``double inv = m.inv();`` which clang rejects with
425
+ # ``assigning to 'double' from incompatible type 'PineMatrix'``.
426
+ #
427
+ # Methods absent from this set return either a primitive (``det``, ``rank``,
428
+ # ``trace``, ``sum``, ``avg``, ``min``, ``max``, ``mode``, ``elements_count``,
429
+ # ``rows``, ``columns``, ``is_*``) or an array (``row``, ``col``,
430
+ # ``eigenvalues``); their LHS variables stay scalar / vector and the analyzer
431
+ # default is correct.
432
+ MATRIX_RETURNING_METHODS: frozenset[str] = frozenset({
433
+ "copy", "submatrix", "transpose", "concat", "diff", "mult", "pow",
434
+ "inv", "pinv", "eigenvectors", "kron",
435
+ })
436
+
437
+ # Methods only valid on matrix<float>. Codegen rejects these on non-float
438
+ # matrix receivers; the runtime template doesn't carry them at all.
439
+ MATRIX_NUMERIC_ONLY: frozenset[str] = frozenset({
440
+ "det", "inv", "pinv", "rank", "trace",
441
+ "eigenvalues", "eigenvectors",
442
+ "sum", "avg", "min", "max", "mode",
443
+ "diff", "mult", "pow", "kron",
444
+ "is_square", "is_identity", "is_diagonal", "is_antidiagonal",
445
+ "is_symmetric", "is_antisymmetric", "is_triangular",
446
+ "is_stochastic", "is_binary", "is_zero",
447
+ })
448
+
449
+ # Element-type predicate for matrix.sort: only int/bool/string element types
450
+ # can be sorted on PineGenericMatrix. Float matrix sort routes through
451
+ # PineMatrix (numeric path).
452
+ MATRIX_SORT_ALLOWED_GENERIC_ELEMS: frozenset[str] = frozenset({"int", "bool", "string"})
453
+
454
+ MATRIX_METHODS = {
455
+ "get": lambda m, args: f"{m}.get((int)({args[0]}), (int)({args[1]}))",
456
+ "set": lambda m, args: f"{m}.set((int)({args[0]}), (int)({args[1]}), {args[2]})",
457
+ "fill": lambda m, args: f"{m}.fill({args[0]})",
458
+ "row": lambda m, args: f"{m}.row((int)({args[0]}))",
459
+ "col": lambda m, args: f"{m}.col((int)({args[0]}))",
460
+ "rows": lambda m, args: f"(int){m}.rows()",
461
+ "columns": lambda m, args: f"(int){m}.columns()",
462
+ "add_row": _matrix_add_row,
463
+ "add_col": _matrix_add_col,
464
+ # ``remove_row`` is void in C++; Pine may assign the result, so we wrap
465
+ # it in a lambda that returns a sentinel double after the side effect.
466
+ "remove_row": lambda m, args: f"[&](){{ {m}.remove_row((int)({args[0]})); return 0.0; }}()",
467
+ "remove_col":lambda m, args: f"[&](){{ {m}.remove_col((int)({args[0]})); return 0.0; }}()",
468
+ "swap_rows": lambda m, args: f"{m}.swap_rows((int)({args[0]}), (int)({args[1]}))",
469
+ "swap_columns": lambda m, args: f"{m}.swap_columns((int)({args[0]}), (int)({args[1]}))",
470
+ "copy": lambda m, args: f"{m}.copy()",
471
+ "submatrix": lambda m, args: f"{m}.submatrix((int)({args[0]}), (int)({args[1]}), (int)({args[2]}), (int)({args[3]}))",
472
+ "reshape": lambda m, args: f"{m}.reshape((int)({args[0]}), (int)({args[1]}))",
473
+ "reverse": lambda m, args: f"{m}.reverse()",
474
+ "transpose": lambda m, args: f"{m}.transpose()",
475
+ "sort": lambda m, args: f"{m}.sort((int)({args[0]}), {args[1]} != \"descending\")" if len(args)>1 else f"{m}.sort((int)({args[0]}))",
476
+ "concat": lambda m, args: f"{m}.concat({args[0]}, (bool)({args[1]}))" if len(args)>1 else f"{m}.concat({args[0]}, true)",
477
+ "avg": lambda m, args: f"{m}.avg()",
478
+ "min": lambda m, args: f"{m}.min()",
479
+ "max": lambda m, args: f"{m}.max()",
480
+ "mode": lambda m, args: f"{m}.mode()",
481
+ "sum": lambda m, args: f"{m}.sum()",
482
+ "diff": lambda m, args: f"{m}.diff({args[0]})",
483
+ "mult": lambda m, args: f"{m}.mult({args[0]})",
484
+ "pow": lambda m, args: f"{m}.pow((int)({args[0]}))",
485
+ "det": lambda m, args: f"{m}.det()",
486
+ "inv": lambda m, args: f"{m}.inv()",
487
+ "pinv": lambda m, args: f"{m}.pinv()",
488
+ "rank": lambda m, args: f"(int){m}.rank()",
489
+ "trace": lambda m, args: f"{m}.trace()",
490
+ "eigenvalues": lambda m, args: f"{m}.eigenvalues()",
491
+ "eigenvectors": lambda m, args: f"{m}.eigenvectors()",
492
+ "kron": lambda m, args: f"{m}.kron({args[0]})",
493
+ "elements_count": lambda m, args: f"(int){m}.elements_count()",
494
+ "is_square": lambda m, args: f"{m}.is_square()",
495
+ "is_identity": lambda m, args: f"{m}.is_identity()",
496
+ "is_diagonal": lambda m, args: f"{m}.is_diagonal()",
497
+ "is_antidiagonal": lambda m, args: f"{m}.is_antidiagonal()",
498
+ "is_symmetric": lambda m, args: f"{m}.is_symmetric()",
499
+ "is_antisymmetric": lambda m, args: f"{m}.is_antisymmetric()",
500
+ "is_triangular": lambda m, args: f"{m}.is_triangular()",
501
+ "is_stochastic": lambda m, args: f"{m}.is_stochastic()",
502
+ "is_binary": lambda m, args: f"{m}.is_binary()",
503
+ "is_zero": lambda m, args: f"{m}.is_zero()",
504
+ }
505
+
506
+
507
+ # ---------------------------------------------------------------------------
508
+ # Math / String dispatch
509
+ # ---------------------------------------------------------------------------
510
+
511
+ MATH_FUNC_MAP = {
512
+ "abs": "std::abs", "max": "std::max", "min": "std::min",
513
+ "ceil": "std::ceil", "floor": "std::floor", "round": "std::round",
514
+ "sqrt": "std::sqrt", "pow": "std::pow", "log": "std::log",
515
+ "log10": "std::log10", "exp": "std::exp",
516
+ "sin": "std::sin", "cos": "std::cos", "tan": "std::tan",
517
+ "asin": "std::asin", "acos": "std::acos", "atan": "std::atan",
518
+ "atan2": "std::atan2({0}, {1})",
519
+ "sign": "((({0}) > 0) - (({0}) < 0))",
520
+ "avg": "(({0} + {1}) / 2.0)",
521
+ }
522
+
523
+ STR_FUNC_MAP = {
524
+ "tostring": None, # handled separately (already works)
525
+ "tonumber": lambda args: (
526
+ f"[&](){{ "
527
+ f"try {{ return std::stod({args[0]}); }} "
528
+ f"catch (...) {{ return na<double>(); }} "
529
+ f"}}()"
530
+ ),
531
+ "length": lambda args: f"(int){args[0]}.length()",
532
+ "contains": lambda args: f"({args[0]}.find({args[1]}) != std::string::npos)",
533
+ "startswith": lambda args: f"({args[0]}.substr(0, {args[1]}.length()) == {args[1]})",
534
+ "endswith": lambda args: f"({args[0]}.length() >= {args[1]}.length() && {args[0]}.compare({args[0]}.length() - {args[1]}.length(), {args[1]}.length(), {args[1]}) == 0)",
535
+ "pos": lambda args: f"[&](){{ auto p={args[0]}.find({args[1]}); return p!=std::string::npos?(int)p:-1; }}()",
536
+ "substring": None, # handled separately (2 vs 3 args)
537
+ "replace_all": lambda args: f"[&](){{ std::string s={args[0]}; size_t p=0; while((p=s.find({args[1]},p))!=std::string::npos){{ s.replace(p,{args[1]}.length(),{args[2]}); p+={args[2]}.length(); }} return s; }}()",
538
+ "replace": None, # handled separately (3 vs 4 args)
539
+ "lower": lambda args: f"[&](){{ std::string s={args[0]}; std::transform(s.begin(),s.end(),s.begin(),::tolower); return s; }}()",
540
+ "upper": lambda args: f"[&](){{ std::string s={args[0]}; std::transform(s.begin(),s.end(),s.begin(),::toupper); return s; }}()",
541
+ "trim": lambda args: f'[&](){{ std::string s={args[0]}; s.erase(0,s.find_first_not_of(" \\t\\n\\r")); s.erase(s.find_last_not_of(" \\t\\n\\r")+1); return s; }}()',
542
+ "repeat": lambda args: f"[&](){{ std::string r; for(int i=0;i<(int)({args[1]});i++) r+={args[0]}; return r; }}()",
543
+ "match": lambda args: f'pine_str_match({args[0]}, {args[1]})',
544
+ "split": lambda args: f'pine_str_split({args[0]}, {args[1]})',
545
+ "format": None, # handled separately
546
+ }
547
+
548
+
549
+ # ---------------------------------------------------------------------------
550
+ # kwarg merge helper (used by visitor / dispatch sites that accept kwargs)
551
+ # ---------------------------------------------------------------------------
552
+
553
+ def _merge_kwargs(args: list, kwargs: dict, param_names: list | None, visit_expr) -> list:
554
+ """Merge positional + kwargs into a unified positional list of C++ exprs.
555
+
556
+ ``param_names`` is the declared signature order from
557
+ ``signatures.py``. ``visit_expr`` is a callable applied to each kept
558
+ AST node; pass ``lambda x: x`` when the caller wants raw nodes back."""
559
+ if not kwargs or not param_names:
560
+ return [visit_expr(a) for a in args]
561
+ merged = list(args)
562
+ for i, pname in enumerate(param_names):
563
+ if pname in kwargs:
564
+ while len(merged) <= i:
565
+ merged.append(None)
566
+ if merged[i] is None:
567
+ merged[i] = kwargs[pname]
568
+ while merged and merged[-1] is None:
569
+ merged.pop()
570
+ return [visit_expr(a) for a in merged if a is not None]
571
+
572
+
573
+ def _merge_kwargs_with_defaults(
574
+ args: list,
575
+ kwargs: dict,
576
+ param_names: list,
577
+ param_defaults: list,
578
+ visit_expr,
579
+ ) -> list:
580
+ """Like ``_merge_kwargs`` but fills missing slots from ``param_defaults``.
581
+
582
+ ``param_defaults`` must be parallel to ``param_names`` (use ``None`` for
583
+ parameters without a default). Used by the UDT-method call lowering so
584
+ callers may invoke ``cfg.threshold()``, ``cfg.threshold(atrVal)``, or
585
+ ``cfg.threshold(mult=2.0, base=rsiVal)`` against ``method threshold(Cfg
586
+ self, float mult = 1.0, float base = 0.0) =>`` and have clang see the
587
+ full positional argument list. PineScript has no overloading, so every
588
+ parameter must be filled at the call site.
589
+
590
+ Probe: data/validation/udt-method-probe-04-default-param.
591
+
592
+ The result preserves the parameter order from ``param_names`` and
593
+ contains ``visit_expr(node)`` for each filled slot. Trailing slots that
594
+ have neither a caller-supplied value nor a default are dropped (matches
595
+ ``_merge_kwargs`` behaviour for required-only signatures).
596
+ """
597
+ n = len(param_names)
598
+ slots: list = [None] * n
599
+ for i, a in enumerate(args):
600
+ if i < n:
601
+ slots[i] = a
602
+ for k, v in kwargs.items():
603
+ if k in param_names:
604
+ slots[param_names.index(k)] = v
605
+ # Fill remaining holes from defaults (only where the caller omitted the
606
+ # slot AND the parameter has a declared default).
607
+ if param_defaults:
608
+ for i in range(n):
609
+ if slots[i] is None and i < len(param_defaults) and param_defaults[i] is not None:
610
+ slots[i] = param_defaults[i]
611
+ while slots and slots[-1] is None:
612
+ slots.pop()
613
+ return [visit_expr(a) for a in slots if a is not None]