@pineforge/codegen-pyodide 0.8.1 → 0.10.0
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.
- package/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +350 -36
- package/pineforge_codegen/analyzer/call_handlers.py +244 -133
- package/pineforge_codegen/analyzer/contracts.py +29 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/codegen/base.py +1348 -113
- package/pineforge_codegen/codegen/drawing.py +1 -1
- package/pineforge_codegen/codegen/emit_top.py +258 -21
- package/pineforge_codegen/codegen/input.py +17 -0
- package/pineforge_codegen/codegen/security.py +559 -36
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +56 -7
- package/pineforge_codegen/codegen/types.py +165 -30
- package/pineforge_codegen/codegen/visit_call.py +184 -53
- package/pineforge_codegen/codegen/visit_expr.py +149 -15
- package/pineforge_codegen/codegen/visit_stmt.py +85 -38
- package/pineforge_codegen/parser.py +73 -6
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +39 -2
- package/pineforge_codegen-0.10.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +6 -3
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
|
@@ -136,6 +136,7 @@ from ..ast_nodes import (
|
|
|
136
136
|
FuncCall,
|
|
137
137
|
Identifier,
|
|
138
138
|
MemberAccess,
|
|
139
|
+
NaLiteral,
|
|
139
140
|
TupleLiteral,
|
|
140
141
|
StringLiteral,
|
|
141
142
|
)
|
|
@@ -160,6 +161,7 @@ from .tables import (
|
|
|
160
161
|
SKIP_VAR_TYPES,
|
|
161
162
|
STR_FUNC_MAP,
|
|
162
163
|
TIME_FIELD_EXPRS,
|
|
164
|
+
_math_minmax_na_expr,
|
|
163
165
|
_merge_kwargs,
|
|
164
166
|
_merge_kwargs_with_defaults,
|
|
165
167
|
tz_time_field_lambda,
|
|
@@ -174,9 +176,21 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
|
174
176
|
"MMM DD YYYY ..." forms. A dateString without a time zone is GMT+0 per
|
|
175
177
|
the Pine reference. Returns None when the string cannot be parsed.
|
|
176
178
|
"""
|
|
177
|
-
|
|
179
|
+
import re
|
|
180
|
+
from datetime import datetime, timezone, timedelta
|
|
178
181
|
|
|
179
182
|
txt = text.strip()
|
|
183
|
+
# Pine dateStrings may carry a trailing timezone WORD ("2024-01-01 00:00 UTC",
|
|
184
|
+
# "1 Jan 2020 09:30 GMT+2") that neither fromisoformat nor the strptime forms
|
|
185
|
+
# below can read. Peel it off and fold it into an explicit offset (UTC/GMT
|
|
186
|
+
# with an optional ±H[:MM] suffix; the bare word is +00:00).
|
|
187
|
+
tzoff = None
|
|
188
|
+
m = re.search(r"\s+(?:UTC|GMT)([+-]\d{1,2})?(?::?(\d{2}))?$", txt, re.I)
|
|
189
|
+
if m:
|
|
190
|
+
txt = txt[: m.start()].strip()
|
|
191
|
+
h = int(m.group(1) or 0)
|
|
192
|
+
mm = int(m.group(2) or 0)
|
|
193
|
+
tzoff = timezone(timedelta(hours=h, minutes=(mm if h >= 0 else -mm)))
|
|
180
194
|
dt = None
|
|
181
195
|
try:
|
|
182
196
|
dt = datetime.fromisoformat(txt)
|
|
@@ -186,6 +200,7 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
|
186
200
|
"%d %b %Y %H:%M:%S", "%d %b %Y %H:%M", "%d %b %Y",
|
|
187
201
|
"%b %d %Y %H:%M:%S %z", "%b %d %Y %H:%M %z",
|
|
188
202
|
"%b %d %Y %H:%M:%S", "%b %d %Y %H:%M", "%b %d %Y",
|
|
203
|
+
"%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d",
|
|
189
204
|
):
|
|
190
205
|
try:
|
|
191
206
|
dt = datetime.strptime(txt, fmt)
|
|
@@ -195,7 +210,7 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
|
195
210
|
if dt is None:
|
|
196
211
|
return None
|
|
197
212
|
if dt.tzinfo is None:
|
|
198
|
-
dt = dt.replace(tzinfo=timezone.utc)
|
|
213
|
+
dt = dt.replace(tzinfo=(tzoff or timezone.utc))
|
|
199
214
|
return int(dt.timestamp() * 1000)
|
|
200
215
|
|
|
201
216
|
|
|
@@ -209,6 +224,63 @@ class CallVisitor:
|
|
|
209
224
|
# Function-call dispatch
|
|
210
225
|
# ------------------------------------------------------------------
|
|
211
226
|
|
|
227
|
+
def _udt_method_call_emit_name(self, fi, node: FuncCall) -> str:
|
|
228
|
+
"""Resolve a UDT method call through the ordinary UDF clone graph."""
|
|
229
|
+
base = self._emit_udt_method_cpp_name(fi)
|
|
230
|
+
dispatch = self._instance_dispatch.get(
|
|
231
|
+
(self._current_instance_name, id(node))
|
|
232
|
+
)
|
|
233
|
+
if dispatch is not None:
|
|
234
|
+
return dispatch
|
|
235
|
+
|
|
236
|
+
cs_info = self.ctx.func_call_cs_map.get(id(node))
|
|
237
|
+
if self._active_call_site_idx is not None and cs_info is not None:
|
|
238
|
+
return f"{base}_cs{self._active_call_site_idx}"
|
|
239
|
+
if cs_info is not None and cs_info[0] == fi.name:
|
|
240
|
+
return f"{base}_cs{cs_info[1]}"
|
|
241
|
+
if (self._active_call_site_idx is not None
|
|
242
|
+
and self.ctx.func_call_site_counts.get(fi.name, 0) > 1):
|
|
243
|
+
return f"{base}_cs{self._active_call_site_idx}"
|
|
244
|
+
return base
|
|
245
|
+
|
|
246
|
+
def _array_init_value_expr(self, elem_spec: TypeSpec | None, value_node) -> str:
|
|
247
|
+
if isinstance(value_node, NaLiteral):
|
|
248
|
+
if elem_spec is not None and elem_spec.kind == "udt":
|
|
249
|
+
return self._default_for_spec(elem_spec)
|
|
250
|
+
cpp_type = self._type_spec_to_cpp(elem_spec)
|
|
251
|
+
if cpp_type in ("double", "int", "int64_t", "bool", "std::string"):
|
|
252
|
+
return f"na<{cpp_type}>()"
|
|
253
|
+
return self._default_for_spec(elem_spec)
|
|
254
|
+
return self._visit_expr(value_node)
|
|
255
|
+
|
|
256
|
+
def _array_method_args(
|
|
257
|
+
self, method: str, arg_nodes: list, spec: TypeSpec | None,
|
|
258
|
+
) -> list[str]:
|
|
259
|
+
elem_spec = (
|
|
260
|
+
spec.element
|
|
261
|
+
if spec is not None and spec.kind == "array" and spec.element is not None
|
|
262
|
+
else TypeSpec.primitive("float")
|
|
263
|
+
)
|
|
264
|
+
value_arg_indexes = {
|
|
265
|
+
"set": {1},
|
|
266
|
+
"push": {0},
|
|
267
|
+
"unshift": {0},
|
|
268
|
+
"insert": {1},
|
|
269
|
+
"fill": {0},
|
|
270
|
+
"includes": {0},
|
|
271
|
+
"indexof": {0},
|
|
272
|
+
"lastindexof": {0},
|
|
273
|
+
"binary_search": {0},
|
|
274
|
+
"binary_search_leftmost": {0},
|
|
275
|
+
"binary_search_rightmost": {0},
|
|
276
|
+
}.get(method, set())
|
|
277
|
+
return [
|
|
278
|
+
self._array_init_value_expr(elem_spec, arg)
|
|
279
|
+
if idx in value_arg_indexes
|
|
280
|
+
else self._visit_expr(arg)
|
|
281
|
+
for idx, arg in enumerate(arg_nodes)
|
|
282
|
+
]
|
|
283
|
+
|
|
212
284
|
def _visit_func_call(self, node: FuncCall) -> str:
|
|
213
285
|
callee = node.callee
|
|
214
286
|
if isinstance(callee, MemberAccess):
|
|
@@ -217,7 +289,7 @@ class CallVisitor:
|
|
|
217
289
|
mk = f"{recv_spec.name}.{callee.member}"
|
|
218
290
|
fi_u = self._func_info_map.get(mk)
|
|
219
291
|
if fi_u is not None and getattr(fi_u, "is_udt_method", False):
|
|
220
|
-
fn_cpp = self.
|
|
292
|
+
fn_cpp = self._udt_method_call_emit_name(fi_u, node)
|
|
221
293
|
recv_e = self._visit_expr(callee.object)
|
|
222
294
|
param_names = list(fi_u.node.params[1:]) if fi_u.node else []
|
|
223
295
|
# Drop the leading ``self`` slot from param_defaults so the
|
|
@@ -270,7 +342,9 @@ class CallVisitor:
|
|
|
270
342
|
meth = callee.member
|
|
271
343
|
raw_args = [self._visit_expr(a) for a in node.args]
|
|
272
344
|
if recv_spec is not None and recv_spec.kind == "array" and meth in ARRAY_METHODS:
|
|
273
|
-
return self._array_method_expr(
|
|
345
|
+
return self._array_method_expr(
|
|
346
|
+
recv, meth, self._array_method_args(meth, node.args, recv_spec), recv_spec
|
|
347
|
+
)
|
|
274
348
|
if recv_spec is not None and recv_spec.kind == "map" and meth in MAP_METHODS:
|
|
275
349
|
return self._map_method_expr(recv, meth, raw_args, recv_spec)
|
|
276
350
|
args = ", ".join(raw_args)
|
|
@@ -294,7 +368,9 @@ class CallVisitor:
|
|
|
294
368
|
return self._map_method_expr(m, meth_raw, margs, self._map_spec_for_name(oname))
|
|
295
369
|
if oname in self._array_vars and meth_raw in ARRAY_METHODS:
|
|
296
370
|
arr = self._safe_name(oname)
|
|
297
|
-
margs =
|
|
371
|
+
margs = self._array_method_args(
|
|
372
|
+
meth_raw, node.args, self._array_spec_for_name(oname)
|
|
373
|
+
)
|
|
298
374
|
return self._array_method_expr(arr, meth_raw, margs, self._array_spec_for_name(oname))
|
|
299
375
|
if oname in self._matrix_specs and meth_raw in MATRIX_METHODS:
|
|
300
376
|
arr = self._safe_name(oname)
|
|
@@ -323,7 +399,7 @@ class CallVisitor:
|
|
|
323
399
|
mk = f"{udt_t}.{meth_raw}"
|
|
324
400
|
fi_u = self._func_info_map.get(mk)
|
|
325
401
|
if fi_u is not None and getattr(fi_u, "is_udt_method", False):
|
|
326
|
-
fn_cpp = self.
|
|
402
|
+
fn_cpp = self._udt_method_call_emit_name(fi_u, node)
|
|
327
403
|
recv_e = self._visit_expr(obj)
|
|
328
404
|
param_names = list(fi_u.node.params[1:]) if fi_u.node else []
|
|
329
405
|
# Drop the leading ``self`` slot so param_defaults
|
|
@@ -387,11 +463,19 @@ class CallVisitor:
|
|
|
387
463
|
if site is not None:
|
|
388
464
|
compute_args = self._ta_compute_args_for_site(site)
|
|
389
465
|
ta_mem = self._ta_member_name(site)
|
|
390
|
-
|
|
466
|
+
uses_precalc = self._ta_site_uses_precalc(site)
|
|
467
|
+
if getattr(self, "_precalc_loop_active", False) and uses_precalc:
|
|
391
468
|
return f"_precalc_{ta_mem}[i]"
|
|
392
|
-
if
|
|
393
|
-
return
|
|
394
|
-
|
|
469
|
+
if uses_precalc:
|
|
470
|
+
return (
|
|
471
|
+
f"(_use_precalc ? _precalc_{ta_mem}[bar_index_] : "
|
|
472
|
+
f"(history_advances_new_bar() ? {ta_mem}.compute({compute_args}) "
|
|
473
|
+
f": {ta_mem}.recompute({compute_args})))"
|
|
474
|
+
)
|
|
475
|
+
return (
|
|
476
|
+
f"(history_advances_new_bar() ? {ta_mem}.compute({compute_args}) "
|
|
477
|
+
f": {ta_mem}.recompute({compute_args}))"
|
|
478
|
+
)
|
|
395
479
|
|
|
396
480
|
# math.* calls
|
|
397
481
|
if namespace == "math":
|
|
@@ -451,18 +535,23 @@ class CallVisitor:
|
|
|
451
535
|
# Array method syntax: arr.push(val) where namespace is the array variable name
|
|
452
536
|
if namespace is not None and namespace in self._array_vars and func_name in ARRAY_METHODS:
|
|
453
537
|
arr = self._safe_name(namespace)
|
|
454
|
-
|
|
455
|
-
|
|
538
|
+
spec = self._array_spec_for_name(namespace)
|
|
539
|
+
args = self._array_method_args(func_name, node.args, spec)
|
|
540
|
+
return self._array_method_expr(arr, func_name, args, spec)
|
|
456
541
|
|
|
457
542
|
# Array operations — emit proper C++ vector operations
|
|
458
543
|
if namespace == "array":
|
|
459
544
|
if func_name in ("new", "new_float", "new_int", "new_bool", "new_string") or func_name in ARRAY_DRAWING_NEW_CTORS:
|
|
460
545
|
spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
|
|
461
546
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
462
|
-
|
|
547
|
+
elem_spec = spec.element if spec.element is not None else TypeSpec.primitive("float")
|
|
548
|
+
init_default = self._default_for_spec(elem_spec)
|
|
463
549
|
if node.args:
|
|
464
550
|
size_arg = self._visit_expr(node.args[0])
|
|
465
|
-
|
|
551
|
+
if len(node.args) > 1:
|
|
552
|
+
init_val = self._array_init_value_expr(elem_spec, node.args[1])
|
|
553
|
+
else:
|
|
554
|
+
init_val = init_default
|
|
466
555
|
return f"{cpp_type}((size_t)({size_arg}), {init_val})"
|
|
467
556
|
return f"{cpp_type}()"
|
|
468
557
|
if func_name == "from":
|
|
@@ -472,8 +561,8 @@ class CallVisitor:
|
|
|
472
561
|
# Method calls: array.method(arr, args...)
|
|
473
562
|
if func_name in ARRAY_METHODS and node.args:
|
|
474
563
|
arr = self._visit_expr(node.args[0])
|
|
475
|
-
rest = [self._visit_expr(a) for a in node.args[1:]]
|
|
476
564
|
spec = self._type_spec_from_expr(node.args[0])
|
|
565
|
+
rest = self._array_method_args(func_name, node.args[1:], spec)
|
|
477
566
|
return self._array_method_expr(arr, func_name, rest, spec)
|
|
478
567
|
return "0"
|
|
479
568
|
|
|
@@ -665,11 +754,12 @@ class CallVisitor:
|
|
|
665
754
|
# mutex-guarded setenv+localtime_r block as the 2-arg
|
|
666
755
|
# form.
|
|
667
756
|
tz_arg = "syminfo_.timezone"
|
|
668
|
-
#
|
|
669
|
-
#
|
|
670
|
-
#
|
|
671
|
-
#
|
|
672
|
-
|
|
757
|
+
# Route through the engine's cached pine_<field>() (session_time.hpp),
|
|
758
|
+
# same as the bare variable forms (BAR_BUILTINS) — value-identical but
|
|
759
|
+
# free of the per-call setenv+tzset churn (KI-35). field_expr is unused
|
|
760
|
+
# now (the engine applies the Pine offsets internally).
|
|
761
|
+
del field_expr
|
|
762
|
+
return f"pine_{func_name}((int64_t)({ts_arg}), {tz_arg})"
|
|
673
763
|
|
|
674
764
|
# time(timeframe) or time(timeframe, session[, tz])
|
|
675
765
|
if func_name == "time" and namespace is None and (node.args or node.kwargs):
|
|
@@ -699,6 +789,8 @@ class CallVisitor:
|
|
|
699
789
|
is_tz_first = True
|
|
700
790
|
elif isinstance(node.args[0], StringLiteral):
|
|
701
791
|
is_tz_first = True
|
|
792
|
+
elif self._infer_type(node.args[0]) == "std::string":
|
|
793
|
+
is_tz_first = True
|
|
702
794
|
|
|
703
795
|
if is_tz_first:
|
|
704
796
|
# A single string argument is the timestamp(dateString)
|
|
@@ -748,7 +840,7 @@ class CallVisitor:
|
|
|
748
840
|
sc = args[6] if len(args) > 6 else "0"
|
|
749
841
|
return (
|
|
750
842
|
f"[&]() -> int64_t {{ "
|
|
751
|
-
f"std::string _tz = ({tz}); "
|
|
843
|
+
f"std::string _tz = pineforge::normalize_timezone_for_posix(({tz})); "
|
|
752
844
|
f"int _yr = ({yr}); int _mo = ({mo}); int _dy = ({dy}); "
|
|
753
845
|
f"int _hr = ({hr}); int _min = ({mn}); int _sc = ({sc}); "
|
|
754
846
|
f"static thread_local std::string _last_tz; "
|
|
@@ -760,6 +852,7 @@ class CallVisitor:
|
|
|
760
852
|
f"struct tm t = {{}}; "
|
|
761
853
|
f"t.tm_year = _yr - 1900; t.tm_mon = _mo - 1; "
|
|
762
854
|
f"t.tm_mday = _dy; t.tm_hour = _hr; t.tm_min = _min; t.tm_sec = _sc; "
|
|
855
|
+
f"t.tm_isdst = -1; "
|
|
763
856
|
f"int64_t _res; "
|
|
764
857
|
f"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") {{ "
|
|
765
858
|
f"_res = (int64_t)timegm(&t) * 1000; "
|
|
@@ -840,7 +933,15 @@ class CallVisitor:
|
|
|
840
933
|
if func_name == "float" and namespace is None and node.args:
|
|
841
934
|
return f"(double)({self._visit_expr(node.args[0])})"
|
|
842
935
|
if func_name == "bool" and namespace is None and node.args:
|
|
843
|
-
|
|
936
|
+
# Pine v6 bools are two-state. Explicit bool(int/float) treats na
|
|
937
|
+
# like false, while a raw C++ cast would make NaN truthy.
|
|
938
|
+
x = self._visit_expr(node.args[0])
|
|
939
|
+
return (
|
|
940
|
+
f"[&](){{ auto _pf_v = ({x}); "
|
|
941
|
+
f"using _pf_t = std::decay_t<decltype(_pf_v)>; "
|
|
942
|
+
f"if constexpr (std::is_same_v<_pf_t, bool>) {{ return _pf_v; }} "
|
|
943
|
+
f"else {{ return is_na(_pf_v) ? false : (bool)_pf_v; }} }}()"
|
|
944
|
+
)
|
|
844
945
|
if func_name == "string" and namespace is None and node.args:
|
|
845
946
|
# Pine string(x) cast — same emission as str.tostring(x), with
|
|
846
947
|
# string passthrough and TV-style "true"/"false" for bools
|
|
@@ -1060,17 +1161,32 @@ class CallVisitor:
|
|
|
1060
1161
|
|
|
1061
1162
|
def _visit_arg_for_series(arg_node, arg_idx):
|
|
1062
1163
|
"""Visit a function argument, returning Series ref for series params."""
|
|
1063
|
-
if arg_idx in _func_series_param_indices
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
safe
|
|
1073
|
-
|
|
1164
|
+
if arg_idx in _func_series_param_indices:
|
|
1165
|
+
if isinstance(arg_node, Identifier):
|
|
1166
|
+
aname = arg_node.name
|
|
1167
|
+
# Bar field: pass _s_close instead of current_bar_.close
|
|
1168
|
+
if aname in BAR_FIELDS or aname in BAR_SERIES_PUSH:
|
|
1169
|
+
return f"_s_{aname}"
|
|
1170
|
+
# Series var: pass the Series object directly
|
|
1171
|
+
if aname in self.ctx.series_vars:
|
|
1172
|
+
safe = self._safe_name(aname)
|
|
1173
|
+
if self._active_var_remap and safe in self._active_var_remap:
|
|
1174
|
+
safe = self._active_var_remap[safe]
|
|
1175
|
+
return safe
|
|
1176
|
+
expr_cpp = self._visit_expr(arg_node)
|
|
1177
|
+
cpp_t = self._infer_type(arg_node)
|
|
1178
|
+
if cpp_t not in ("double", "int", "bool"):
|
|
1179
|
+
cpp_t = "double"
|
|
1180
|
+
member = self._inline_history_member(
|
|
1181
|
+
"series_arg", node, arg_idx=arg_idx
|
|
1182
|
+
)
|
|
1183
|
+
return (
|
|
1184
|
+
f"([&]() -> const Series<{cpp_t}>& {{ "
|
|
1185
|
+
f"{cpp_t} _sv = ({expr_cpp}); "
|
|
1186
|
+
f"if (history_advances_new_bar()) {member}.push(_sv); "
|
|
1187
|
+
f"else {member}.update(_sv); "
|
|
1188
|
+
f"return {member}; }}())"
|
|
1189
|
+
)
|
|
1074
1190
|
return self._visit_expr(arg_node)
|
|
1075
1191
|
|
|
1076
1192
|
if node.kwargs:
|
|
@@ -1167,8 +1283,34 @@ class CallVisitor:
|
|
|
1167
1283
|
|
|
1168
1284
|
def _visit_fixnan(self, node: FuncCall) -> str:
|
|
1169
1285
|
"""Emit fixnan with persistent state member."""
|
|
1170
|
-
|
|
1171
|
-
|
|
1286
|
+
# Variant-aware lookup keyed off the analyzer-tracked site:
|
|
1287
|
+
# * function-owned site -> dispatch through the active per-call-site
|
|
1288
|
+
# remap so each emitted variant (cs0/cs1/__ni{N}) references its
|
|
1289
|
+
# OWN previous-value member.
|
|
1290
|
+
# * top-level site (owner_func is None) -> use ``site.member_name``
|
|
1291
|
+
# directly. The declarations come from ``ctx.fixnan_sites`` keyed
|
|
1292
|
+
# by these member names, so referencing anything else (e.g. the
|
|
1293
|
+
# legacy monotonic counter) would either dangle a declaration or
|
|
1294
|
+
# silently alias another site's state. In particular, when a
|
|
1295
|
+
# function-owned fixnan is analyzed BEFORE a top-level one, the
|
|
1296
|
+
# counter would restart at 1 and collide with the function's
|
|
1297
|
+
# ``_prev_fixnan_1`` -- corrupting both. Using the site's own
|
|
1298
|
+
# member name keeps declaration and reference in lockstep.
|
|
1299
|
+
# * unmapped site (node not in the site map, e.g. a fixnan reached
|
|
1300
|
+
# only through a path the analyzer didn't register) -> fall back
|
|
1301
|
+
# to the legacy monotonic counter so emission still produces a
|
|
1302
|
+
# referenceable member.
|
|
1303
|
+
site = self._fixnan_site_map.get(id(node))
|
|
1304
|
+
if site is not None:
|
|
1305
|
+
if site.member_name in self._func_fixnan_members:
|
|
1306
|
+
member = self._active_fixnan_remap.get(
|
|
1307
|
+
site.member_name, site.member_name
|
|
1308
|
+
)
|
|
1309
|
+
else:
|
|
1310
|
+
member = site.member_name
|
|
1311
|
+
else:
|
|
1312
|
+
self._fixnan_counter += 1
|
|
1313
|
+
member = f"_prev_fixnan_{self._fixnan_counter}"
|
|
1172
1314
|
x = self._visit_expr(node.args[0])
|
|
1173
1315
|
return f"(is_na({x}) ? {member} : ({member} = {x}))"
|
|
1174
1316
|
|
|
@@ -1252,17 +1394,17 @@ class CallVisitor:
|
|
|
1252
1394
|
qty_val = self._visit_expr(qty_n) if qty_n else "na<double>()"
|
|
1253
1395
|
comment = self._visit_expr(comment_n) if comment_n is not None else '""'
|
|
1254
1396
|
oca_val = self._visit_expr(oca_name_n) if oca_name_n is not None else '""'
|
|
1397
|
+
profit_ticks = "na<double>()"
|
|
1398
|
+
loss_ticks = "na<double>()"
|
|
1255
1399
|
|
|
1256
1400
|
if profit_n and not limit_n:
|
|
1257
|
-
|
|
1258
|
-
limit_val = f"(position_entry_price_ + (signed_position_size() > 0 ? 1.0 : -1.0) * ({ticks}) * syminfo_mintick_)"
|
|
1401
|
+
profit_ticks = self._visit_expr(profit_n)
|
|
1259
1402
|
if loss_n and not stop_n:
|
|
1260
|
-
|
|
1261
|
-
stop_val = f"(position_entry_price_ - (signed_position_size() > 0 ? 1.0 : -1.0) * ({ticks}) * syminfo_mintick_)"
|
|
1403
|
+
loss_ticks = self._visit_expr(loss_n)
|
|
1262
1404
|
|
|
1263
1405
|
return (f"strategy_exit({exit_id}, {from_id}, {limit_val}, {stop_val}, "
|
|
1264
1406
|
f"{trail_pts}, {trail_off}, {trail_pr}, {qty_pct}, {comment}, "
|
|
1265
|
-
f"{qty_val}, {oca_val})")
|
|
1407
|
+
f"{qty_val}, {oca_val}, {profit_ticks}, {loss_ticks})")
|
|
1266
1408
|
close_comment = self._visit_expr(comment_n) if comment_n is not None else '""'
|
|
1267
1409
|
return f"strategy_close({exit_id}, {close_comment})"
|
|
1268
1410
|
|
|
@@ -1483,23 +1625,12 @@ class CallVisitor:
|
|
|
1483
1625
|
if func_name == "avg" and len(args) > 2:
|
|
1484
1626
|
sum_expr = " + ".join(f"(double)({a})" for a in args)
|
|
1485
1627
|
return f"(({sum_expr}) / {len(args)}.0)"
|
|
1486
|
-
if func_name
|
|
1487
|
-
|
|
1488
|
-
for a in args[2:]:
|
|
1489
|
-
result = f"std::max({result}, (double)({a}))"
|
|
1490
|
-
return result
|
|
1491
|
-
if func_name == "min" and len(args) > 2:
|
|
1492
|
-
result = f"std::min((double)({args[0]}), (double)({args[1]}))"
|
|
1493
|
-
for a in args[2:]:
|
|
1494
|
-
result = f"std::min({result}, (double)({a}))"
|
|
1495
|
-
return result
|
|
1628
|
+
if func_name in ("min", "max"):
|
|
1629
|
+
return _math_minmax_na_expr(func_name, args)
|
|
1496
1630
|
if func_name in MATH_FUNC_MAP:
|
|
1497
1631
|
mapped = MATH_FUNC_MAP[func_name]
|
|
1498
1632
|
if "{0}" in mapped:
|
|
1499
1633
|
return mapped.format(*args)
|
|
1500
|
-
# std::min/std::max require same types — cast to double
|
|
1501
|
-
if func_name in ("min", "max") and len(args) == 2:
|
|
1502
|
-
return f"{mapped}((double)({args[0]}), (double)({args[1]}))"
|
|
1503
1634
|
return f"{mapped}({', '.join(args)})"
|
|
1504
1635
|
# Unknown math.* — safe fallback
|
|
1505
1636
|
return f"0.0 /* unsupported: math.{func_name} */"
|
|
@@ -166,6 +166,20 @@ _BUILTIN_NAMESPACE_NAMES: frozenset[str] = frozenset({
|
|
|
166
166
|
})
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
# KI-71: Pine relational comparisons (``==`` ``!=`` ``<`` ``>`` ``<=`` ``>=``)
|
|
170
|
+
# with an ``na`` operand evaluate *falsy*. Naive C++ relationals do not honour
|
|
171
|
+
# this for the engine's na sentinels, so these ops route through na-aware
|
|
172
|
+
# lowering when an operand can be na (see ``_visit_binop`` / ``_operand_na_kind``).
|
|
173
|
+
_RELATIONAL_OPS: frozenset[str] = frozenset({"==", "!=", "<", ">", "<=", ">="})
|
|
174
|
+
|
|
175
|
+
# C++ scalar types carrying a detectable ``na`` sentinel via ``is_na``:
|
|
176
|
+
# ``double`` -> NaN (IEEE), ``int``/``int64_t`` -> ``numeric_limits<T>::min()``.
|
|
177
|
+
# Only these route through the na-aware relational lowering — ``is_na`` has no
|
|
178
|
+
# overload for ``bool``/``std::string``/vector/UDT-value operands, and Pine's
|
|
179
|
+
# na-bool is engine-indistinguishable from ``false`` (na<bool>() == false).
|
|
180
|
+
_NA_SCALAR_CPP: frozenset[str] = frozenset({"int", "int64_t", "double"})
|
|
181
|
+
|
|
182
|
+
|
|
169
183
|
class ExprVisitor:
|
|
170
184
|
"""Expression-level visitor methods shared across the codegen.
|
|
171
185
|
|
|
@@ -275,6 +289,16 @@ class ExprVisitor:
|
|
|
275
289
|
return str(val)
|
|
276
290
|
if isinstance(val, str):
|
|
277
291
|
return f'std::string("{val}")'
|
|
292
|
+
# TA runtime-reset lowering: an input-backed var renders as its
|
|
293
|
+
# override-aware getter (not the member name), because the reset may
|
|
294
|
+
# run before the input members are initialised (evaluate_security path).
|
|
295
|
+
if (self._reset_input_getter_mode
|
|
296
|
+
and name in self._input_backed_vars
|
|
297
|
+
and name in self._input_var_to_call):
|
|
298
|
+
call_node = self._input_var_to_call[name]
|
|
299
|
+
func_name_i, namespace_i = self._resolve_callee(call_node.callee)
|
|
300
|
+
title = self._get_input_title(call_node, var_name=name)
|
|
301
|
+
return self._render_input_value(call_node, func_name_i, namespace_i, title)
|
|
278
302
|
# Pine type name `color` used as a value (no variable) → int64 color constant.
|
|
279
303
|
# Params handled above; symbol table does not retain function locals after analysis.
|
|
280
304
|
if name == "color":
|
|
@@ -373,7 +397,12 @@ class ExprVisitor:
|
|
|
373
397
|
if node.member == "position_size":
|
|
374
398
|
return "signed_position_size()"
|
|
375
399
|
if node.member == "position_avg_price":
|
|
376
|
-
|
|
400
|
+
# Pine returns `na` when the position is flat
|
|
401
|
+
# (position_size == 0); the engine field is 0.0 when flat.
|
|
402
|
+
# Guard so the common `na(strategy.position_avg_price)`
|
|
403
|
+
# flat/in-position idiom is not silently defeated.
|
|
404
|
+
return ("(signed_position_size() == 0.0 ? na<double>() "
|
|
405
|
+
": position_entry_price_)")
|
|
377
406
|
if node.member == "position_entry_name":
|
|
378
407
|
return "position_entry_name()"
|
|
379
408
|
# Trade counts
|
|
@@ -478,13 +507,34 @@ class ExprVisitor:
|
|
|
478
507
|
(node.member in TA_IMPLICIT_COMPUTE_FULL and node.member in TA_COMPUTE_ARGS and TA_COMPUTE_ARGS[node.member] == [])
|
|
479
508
|
or node.member == "vwap"
|
|
480
509
|
):
|
|
481
|
-
# Find the matching call site
|
|
482
|
-
|
|
510
|
+
# Find the matching call site. Skip sites pruned as dead
|
|
511
|
+
# code (their owner function is never called): a dead site's
|
|
512
|
+
# member declaration is never emitted (see base.py
|
|
513
|
+
# ``_dead_ta_indices`` / emit_top.py member-decl guard), so
|
|
514
|
+
# binding a LIVE bare property read to one references an
|
|
515
|
+
# undeclared member. Regression: nightowlxtrader-azt — a live
|
|
516
|
+
# ``ta.vwap`` (top-level + inside request.security) resolved
|
|
517
|
+
# to dead ``f5``'s first-in-order ``_ta_vwap_10`` and emitted
|
|
518
|
+
# ``use of undeclared identifier '_ta_vwap_10'``. A live read
|
|
519
|
+
# must bind to a live site.
|
|
520
|
+
for _i, site in enumerate(self.ctx.ta_call_sites):
|
|
521
|
+
if _i in self._dead_ta_indices:
|
|
522
|
+
continue
|
|
483
523
|
ta_short = site.class_name.split("::")[-1].lower()
|
|
484
524
|
if site.member_name.startswith(f"_ta_{node.member}_"):
|
|
485
525
|
if node.member == "vwap":
|
|
486
|
-
return
|
|
487
|
-
|
|
526
|
+
return (
|
|
527
|
+
f"(history_advances_new_bar() ? {site.member_name}.compute("
|
|
528
|
+
"current_bar_.close, current_bar_.volume, current_bar_.timestamp) "
|
|
529
|
+
f": {site.member_name}.recompute(current_bar_.close, "
|
|
530
|
+
"current_bar_.volume, current_bar_.timestamp))"
|
|
531
|
+
)
|
|
532
|
+
return (
|
|
533
|
+
f"(history_advances_new_bar() ? {site.member_name}.compute("
|
|
534
|
+
f"{TA_IMPLICIT_COMPUTE_FULL[node.member]}) : "
|
|
535
|
+
f"{site.member_name}.recompute("
|
|
536
|
+
f"{TA_IMPLICIT_COMPUTE_FULL[node.member]}))"
|
|
537
|
+
)
|
|
488
538
|
# No registered call site for this TA property read —
|
|
489
539
|
# the old fallback emitted std::string("<name>"), a
|
|
490
540
|
# silent type mismatch. Reject loudly instead.
|
|
@@ -724,11 +774,95 @@ class ExprVisitor:
|
|
|
724
774
|
return f'std::string("{node.member}")'
|
|
725
775
|
return f"{obj}.{node.member}"
|
|
726
776
|
|
|
777
|
+
def _operand_na_kind(self, node, cpp_type: str) -> str | None:
|
|
778
|
+
"""Classify a relational operand by the ``na`` sentinel it can carry.
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
|
|
782
|
+
* ``"int"`` — an ``int``/``int64_t`` expression that can hold the
|
|
783
|
+
``INT_MIN`` sentinel. Because that sentinel is a *finite* very-negative
|
|
784
|
+
integer (not NaN), naive C++ diverges from Pine's falsy-on-na rule for
|
|
785
|
+
EVERY relational — ordered (``<`` ``>`` ``<=`` ``>=``) and equality
|
|
786
|
+
(``==`` ``!=``) alike.
|
|
787
|
+
* ``"float"`` — a ``double`` expression that can hold NaN. IEEE already
|
|
788
|
+
yields ``false`` for ``==`` ``<`` ``>`` ``<=`` ``>=`` against NaN
|
|
789
|
+
(matching Pine's falsy), so the ONLY diverging float cell is ``!=``
|
|
790
|
+
(IEEE ``NaN != x`` is true; Pine is falsy).
|
|
791
|
+
* ``None`` — provably not na (numeric/bool literal, inlined
|
|
792
|
+
compile-time constant) or a non-scalar type with no ``is_na`` overload;
|
|
793
|
+
the naive emission is already correct.
|
|
794
|
+
"""
|
|
795
|
+
if cpp_type not in _NA_SCALAR_CPP:
|
|
796
|
+
return None
|
|
797
|
+
# Literals are never na.
|
|
798
|
+
if isinstance(node, (NumberLiteral, BoolLiteral)):
|
|
799
|
+
return None
|
|
800
|
+
# A bare ``na`` lowers to ``na<double>()`` — a real NaN, i.e. it IS na.
|
|
801
|
+
if self._is_na_expr(node):
|
|
802
|
+
return "float"
|
|
803
|
+
# Inlined compile-time constants (non-input known vars) never hold na.
|
|
804
|
+
if (isinstance(node, Identifier)
|
|
805
|
+
and node.name in self._known_vars
|
|
806
|
+
and node.name not in self._input_backed_vars):
|
|
807
|
+
return None
|
|
808
|
+
return "int" if cpp_type in ("int", "int64_t") else "float"
|
|
809
|
+
|
|
810
|
+
def _emit_na_relational(self, op: str, left: str, right: str) -> str:
|
|
811
|
+
"""Emit an na-aware relational: ``false`` when either operand is ``na``.
|
|
812
|
+
|
|
813
|
+
Mirrors the ``nz()`` lambda idiom (``visit_call``): each operand is
|
|
814
|
+
hoisted to a temporary so a stateful operand expression is evaluated
|
|
815
|
+
exactly once (no double-step), then compared only when neither side is
|
|
816
|
+
na. ``is_na`` resolves via the emitted ``using namespace pineforge;``
|
|
817
|
+
(``double`` -> ``isnan``; integral -> ``== numeric_limits<T>::min()``).
|
|
818
|
+
"""
|
|
819
|
+
return (f"([&]{{ auto _pna_l = ({left}); auto _pna_r = ({right}); "
|
|
820
|
+
f"return !is_na(_pna_l) && !is_na(_pna_r) && "
|
|
821
|
+
f"(_pna_l {op} _pna_r); }}())")
|
|
822
|
+
|
|
823
|
+
def _lower_relational(self, op: str, left_node, right_node,
|
|
824
|
+
left_cpp: str, right_cpp: str) -> str:
|
|
825
|
+
"""Lower a Pine relational to C++, applying KI-71 na-aware wrapping.
|
|
826
|
+
|
|
827
|
+
Shared by ``_visit_binop`` and the ``request.security`` expression
|
|
828
|
+
builder so EVERY relational emission site honours Pine's falsy-on-na
|
|
829
|
+
rule. Wraps only the diverging cells: any na-capable *integer* operand
|
|
830
|
+
(the INT_MIN sentinel poisons all six operators) or a ``!=`` with an
|
|
831
|
+
na-capable *float* operand (the sole IEEE-diverging float cell). Pure
|
|
832
|
+
``double`` ``==`` ``<`` ``>`` ``<=`` ``>=`` keep the naive emission —
|
|
833
|
+
IEEE is already falsy-on-NaN there, so wrapping would be a pure no-op.
|
|
834
|
+
Non-relational ``op`` (or non-scalar operands with no ``is_na``) fall
|
|
835
|
+
through to the naive ``(left op right)`` form unchanged.
|
|
836
|
+
"""
|
|
837
|
+
if op in _RELATIONAL_OPS:
|
|
838
|
+
lt = self._infer_type(left_node)
|
|
839
|
+
rt = self._infer_type(right_node)
|
|
840
|
+
if lt in _NA_SCALAR_CPP and rt in _NA_SCALAR_CPP:
|
|
841
|
+
lk = self._operand_na_kind(left_node, lt)
|
|
842
|
+
rk = self._operand_na_kind(right_node, rt)
|
|
843
|
+
int_na = "int" in (lk, rk)
|
|
844
|
+
float_na = "float" in (lk, rk)
|
|
845
|
+
if int_na or (op == "!=" and float_na):
|
|
846
|
+
return self._emit_na_relational(op, left_cpp, right_cpp)
|
|
847
|
+
return f"({left_cpp} {op} {right_cpp})"
|
|
848
|
+
|
|
727
849
|
def _visit_binop(self, node: BinOp) -> str:
|
|
728
850
|
left = self._visit_expr(node.left)
|
|
729
851
|
right = self._visit_expr(node.right)
|
|
730
852
|
cpp_ops = {"and": "&&", "or": "||"}
|
|
731
853
|
op = cpp_ops.get(node.op, node.op)
|
|
854
|
+
if node.op == "+":
|
|
855
|
+
lt = self._infer_type(node.left)
|
|
856
|
+
rt = self._infer_type(node.right)
|
|
857
|
+
if lt == "std::string" or rt == "std::string":
|
|
858
|
+
def _as_string(rendered, inferred):
|
|
859
|
+
if inferred == "std::string":
|
|
860
|
+
return rendered
|
|
861
|
+
if inferred == "bool":
|
|
862
|
+
return f'(({rendered}) ? std::string("true") : std::string("false"))'
|
|
863
|
+
return f"std::to_string({rendered})"
|
|
864
|
+
|
|
865
|
+
return f"({_as_string(left, lt)} + {_as_string(right, rt)})"
|
|
732
866
|
# PineScript % works on floats — use std::fmod in C++
|
|
733
867
|
if node.op == "%":
|
|
734
868
|
return f"std::fmod((double)({left}), (double)({right}))"
|
|
@@ -738,7 +872,7 @@ class ExprVisitor:
|
|
|
738
872
|
# Ref: https://www.tradingview.com/pine-script-docs/concepts/operators/
|
|
739
873
|
if node.op == "/":
|
|
740
874
|
return f"((double)({left}) / (double)({right}))"
|
|
741
|
-
return
|
|
875
|
+
return self._lower_relational(op, node.left, node.right, left, right)
|
|
742
876
|
|
|
743
877
|
def _visit_unaryop(self, node: UnaryOp) -> str:
|
|
744
878
|
operand = self._visit_expr(node.operand)
|
|
@@ -784,26 +918,26 @@ class ExprVisitor:
|
|
|
784
918
|
# ``ta.highest(high, 10)[1]`` or ``f()[2]``. In Pine the call yields a
|
|
785
919
|
# series, so ``[k]`` reads its value k bars ago — but the call lowers to
|
|
786
920
|
# a freshly-computed C++ scalar, and ``scalar[k]`` is not subscriptable.
|
|
787
|
-
# Materialize the result into a
|
|
921
|
+
# Materialize the result into a checkpoint-owned class-member
|
|
788
922
|
# ``Series<T>`` that pushes (new bar) / updates (intrabar) the value
|
|
789
923
|
# exactly once per evaluation — same semantics as every other series in
|
|
790
924
|
# the strategy — and read ``[k]`` off it. The inner call is emitted once
|
|
791
|
-
# so its own stateful indicator is not double-stepped
|
|
792
|
-
#
|
|
793
|
-
#
|
|
925
|
+
# so its own stateful indicator is not double-stepped. The deterministic
|
|
926
|
+
# member identity includes the emitted UDF variant; separate Pine call
|
|
927
|
+
# sites never share history, and on_bar clears every synthetic member at
|
|
928
|
+
# run-start even when this particular expression is conditional.
|
|
794
929
|
if isinstance(node.object, FuncCall):
|
|
795
930
|
inner = self._visit_expr(node.object)
|
|
796
931
|
cpp_t = self._infer_type(node.object)
|
|
797
932
|
if cpp_t not in ("double", "int", "bool"):
|
|
798
933
|
cpp_t = "double"
|
|
934
|
+
member = self._inline_history_member("hist_call", node)
|
|
799
935
|
return (
|
|
800
936
|
f"([&]() -> {cpp_t} {{ "
|
|
801
|
-
f"static thread_local Series<{cpp_t}> _hist_call; "
|
|
802
|
-
f"if (is_first_tick_ && bar_index_ == 0) _hist_call.clear(); "
|
|
803
937
|
f"{cpp_t} _hv = ({inner}); "
|
|
804
|
-
f"if (
|
|
805
|
-
f"else
|
|
806
|
-
f"return
|
|
938
|
+
f"if (history_advances_new_bar()) {member}.push(_hv); "
|
|
939
|
+
f"else {member}.update(_hv); "
|
|
940
|
+
f"return {member}[(int)({idx})]; }}())"
|
|
807
941
|
)
|
|
808
942
|
obj = self._visit_expr(node.object)
|
|
809
943
|
# If subscripting a non-series variable (e.g., function parameter),
|