@pineforge/codegen-pyodide 0.8.0 → 0.9.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 +797 -45
- package/pineforge_codegen/analyzer/call_handlers.py +368 -68
- package/pineforge_codegen/analyzer/contracts.py +91 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +1613 -111
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +536 -45
- package/pineforge_codegen/codegen/input.py +31 -0
- package/pineforge_codegen/codegen/security.py +791 -39
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +115 -10
- package/pineforge_codegen/codegen/types.py +478 -46
- package/pineforge_codegen/codegen/visit_call.py +331 -75
- package/pineforge_codegen/codegen/visit_expr.py +116 -14
- package/pineforge_codegen/codegen/visit_stmt.py +208 -23
- package/pineforge_codegen/parser.py +129 -29
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +320 -19
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -10
- package/pineforge_codegen-0.8.0.tar.gz +0 -0
|
@@ -136,15 +136,20 @@ from ..ast_nodes import (
|
|
|
136
136
|
FuncCall,
|
|
137
137
|
Identifier,
|
|
138
138
|
MemberAccess,
|
|
139
|
+
NaLiteral,
|
|
139
140
|
TupleLiteral,
|
|
140
141
|
StringLiteral,
|
|
141
142
|
)
|
|
142
143
|
from ..symbols import TypeSpec
|
|
143
144
|
from .. import signatures as sigs
|
|
145
|
+
from .drawing import ALL_DRAWING_METHODS
|
|
144
146
|
from .tables import (
|
|
147
|
+
ARRAY_DRAWING_NEW_CTORS,
|
|
145
148
|
ARRAY_METHODS,
|
|
146
149
|
BAR_FIELDS,
|
|
147
150
|
BAR_SERIES_PUSH,
|
|
151
|
+
DRAWING_NS,
|
|
152
|
+
DRAWING_TYPE_TO_CPP,
|
|
148
153
|
MAP_METHODS,
|
|
149
154
|
MATH_FUNC_MAP,
|
|
150
155
|
MATRIX_METHODS,
|
|
@@ -156,6 +161,7 @@ from .tables import (
|
|
|
156
161
|
SKIP_VAR_TYPES,
|
|
157
162
|
STR_FUNC_MAP,
|
|
158
163
|
TIME_FIELD_EXPRS,
|
|
164
|
+
_math_minmax_na_expr,
|
|
159
165
|
_merge_kwargs,
|
|
160
166
|
_merge_kwargs_with_defaults,
|
|
161
167
|
tz_time_field_lambda,
|
|
@@ -170,9 +176,21 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
|
170
176
|
"MMM DD YYYY ..." forms. A dateString without a time zone is GMT+0 per
|
|
171
177
|
the Pine reference. Returns None when the string cannot be parsed.
|
|
172
178
|
"""
|
|
173
|
-
|
|
179
|
+
import re
|
|
180
|
+
from datetime import datetime, timezone, timedelta
|
|
174
181
|
|
|
175
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)))
|
|
176
194
|
dt = None
|
|
177
195
|
try:
|
|
178
196
|
dt = datetime.fromisoformat(txt)
|
|
@@ -182,6 +200,7 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
|
182
200
|
"%d %b %Y %H:%M:%S", "%d %b %Y %H:%M", "%d %b %Y",
|
|
183
201
|
"%b %d %Y %H:%M:%S %z", "%b %d %Y %H:%M %z",
|
|
184
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",
|
|
185
204
|
):
|
|
186
205
|
try:
|
|
187
206
|
dt = datetime.strptime(txt, fmt)
|
|
@@ -191,7 +210,7 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
|
191
210
|
if dt is None:
|
|
192
211
|
return None
|
|
193
212
|
if dt.tzinfo is None:
|
|
194
|
-
dt = dt.replace(tzinfo=timezone.utc)
|
|
213
|
+
dt = dt.replace(tzinfo=(tzoff or timezone.utc))
|
|
195
214
|
return int(dt.timestamp() * 1000)
|
|
196
215
|
|
|
197
216
|
|
|
@@ -205,6 +224,63 @@ class CallVisitor:
|
|
|
205
224
|
# Function-call dispatch
|
|
206
225
|
# ------------------------------------------------------------------
|
|
207
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
|
+
|
|
208
284
|
def _visit_func_call(self, node: FuncCall) -> str:
|
|
209
285
|
callee = node.callee
|
|
210
286
|
if isinstance(callee, MemberAccess):
|
|
@@ -213,7 +289,7 @@ class CallVisitor:
|
|
|
213
289
|
mk = f"{recv_spec.name}.{callee.member}"
|
|
214
290
|
fi_u = self._func_info_map.get(mk)
|
|
215
291
|
if fi_u is not None and getattr(fi_u, "is_udt_method", False):
|
|
216
|
-
fn_cpp = self.
|
|
292
|
+
fn_cpp = self._udt_method_call_emit_name(fi_u, node)
|
|
217
293
|
recv_e = self._visit_expr(callee.object)
|
|
218
294
|
param_names = list(fi_u.node.params[1:]) if fi_u.node else []
|
|
219
295
|
# Drop the leading ``self`` slot from param_defaults so the
|
|
@@ -226,6 +302,32 @@ class CallVisitor:
|
|
|
226
302
|
)
|
|
227
303
|
rest = [self._visit_expr(a) for a in rest_nodes]
|
|
228
304
|
return f"{fn_cpp}({', '.join([recv_e] + rest)})"
|
|
305
|
+
|
|
306
|
+
# Drawing method dispatch (spec §4.3 / L.1). A KNOWN drawing method on a
|
|
307
|
+
# receiver that resolves to a drawing udt — gated on the METHOD NAME
|
|
308
|
+
# FIRST so a user method (egoigor's ``ln.slope()``, already routed by the
|
|
309
|
+
# block above) is never captured here. This single check covers all
|
|
310
|
+
# receiver shapes (identifier ``ln.set_x2(v)``, obj.field
|
|
311
|
+
# ``d.fld.set_y2(v)``, and arbitrary-expr ``d.upln.get(0).delete()``), so
|
|
312
|
+
# it precedes the obj.field.method / identifier branches below AND the
|
|
313
|
+
# generic ``delete`` -> ``_delete_`` rewrites + _resolve_callee.
|
|
314
|
+
if isinstance(callee, MemberAccess) and callee.member in ALL_DRAWING_METHODS:
|
|
315
|
+
recv_spec = self._type_spec_from_expr(callee.object)
|
|
316
|
+
if (recv_spec is not None and recv_spec.kind == "udt"
|
|
317
|
+
and recv_spec.name in DRAWING_TYPE_TO_CPP):
|
|
318
|
+
return self._emit_drawing_method(
|
|
319
|
+
recv_spec.name, callee.member, callee.object,
|
|
320
|
+
list(node.args), node,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# chart.point.now/new/from_index/from_time/copy — REAL data (a ChartPoint
|
|
324
|
+
# aggregate). Routed here BEFORE the obj.field.method receiver logic,
|
|
325
|
+
# which would otherwise mis-treat ``chart.point`` as a receiver object
|
|
326
|
+
# and raise on the ``chart.point`` member read (chart ∈ SKIP_NAMESPACES).
|
|
327
|
+
if self._is_chart_point_callee(callee):
|
|
328
|
+
cp_func, _cp_ns = self._resolve_callee(callee)
|
|
329
|
+
return self._emit_chart_point(cp_func, node)
|
|
330
|
+
|
|
229
331
|
# obj.field.method(args) — must not lower to namespace::method (loses receiver chain).
|
|
230
332
|
if isinstance(callee, MemberAccess):
|
|
231
333
|
obj = callee.object
|
|
@@ -240,7 +342,9 @@ class CallVisitor:
|
|
|
240
342
|
meth = callee.member
|
|
241
343
|
raw_args = [self._visit_expr(a) for a in node.args]
|
|
242
344
|
if recv_spec is not None and recv_spec.kind == "array" and meth in ARRAY_METHODS:
|
|
243
|
-
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
|
+
)
|
|
244
348
|
if recv_spec is not None and recv_spec.kind == "map" and meth in MAP_METHODS:
|
|
245
349
|
return self._map_method_expr(recv, meth, raw_args, recv_spec)
|
|
246
350
|
args = ", ".join(raw_args)
|
|
@@ -264,7 +368,9 @@ class CallVisitor:
|
|
|
264
368
|
return self._map_method_expr(m, meth_raw, margs, self._map_spec_for_name(oname))
|
|
265
369
|
if oname in self._array_vars and meth_raw in ARRAY_METHODS:
|
|
266
370
|
arr = self._safe_name(oname)
|
|
267
|
-
margs =
|
|
371
|
+
margs = self._array_method_args(
|
|
372
|
+
meth_raw, node.args, self._array_spec_for_name(oname)
|
|
373
|
+
)
|
|
268
374
|
return self._array_method_expr(arr, meth_raw, margs, self._array_spec_for_name(oname))
|
|
269
375
|
if oname in self._matrix_specs and meth_raw in MATRIX_METHODS:
|
|
270
376
|
arr = self._safe_name(oname)
|
|
@@ -293,7 +399,7 @@ class CallVisitor:
|
|
|
293
399
|
mk = f"{udt_t}.{meth_raw}"
|
|
294
400
|
fi_u = self._func_info_map.get(mk)
|
|
295
401
|
if fi_u is not None and getattr(fi_u, "is_udt_method", False):
|
|
296
|
-
fn_cpp = self.
|
|
402
|
+
fn_cpp = self._udt_method_call_emit_name(fi_u, node)
|
|
297
403
|
recv_e = self._visit_expr(obj)
|
|
298
404
|
param_names = list(fi_u.node.params[1:]) if fi_u.node else []
|
|
299
405
|
# Drop the leading ``self`` slot so param_defaults
|
|
@@ -321,10 +427,22 @@ class CallVisitor:
|
|
|
321
427
|
return f"is_na({args})"
|
|
322
428
|
|
|
323
429
|
# nz(x) / nz(x, y)
|
|
430
|
+
#
|
|
431
|
+
# x's emitted C++ source is substituted into the surrounding
|
|
432
|
+
# expression, so it must be evaluated EXACTLY ONCE: when x is a
|
|
433
|
+
# stateful call (e.g. a ta.* site lowered to `.compute()`/
|
|
434
|
+
# `.recompute()`), naively embedding {x} twice — once for the
|
|
435
|
+
# is_na() check, once for the non-na branch — invokes that call
|
|
436
|
+
# twice per bar, silently corrupting the indicator's internal state
|
|
437
|
+
# (e.g. nz(ta.sma(v, 50), v) becomes an effective 25-bar SMA: every
|
|
438
|
+
# bar is pushed into the ring buffer twice). An immediately-invoked
|
|
439
|
+
# lambda hoists x into a local `auto` so it is computed once and
|
|
440
|
+
# both branches read the same value; `[&]` is safe here since the
|
|
441
|
+
# lambda is called synchronously and discarded, never escaping.
|
|
324
442
|
if func_name == "nz" and namespace is None:
|
|
325
443
|
x = self._visit_expr(node.args[0])
|
|
326
444
|
y = self._visit_expr(node.args[1]) if len(node.args) > 1 else "0.0"
|
|
327
|
-
return f"(
|
|
445
|
+
return f"([&]{{ auto _nz_v = ({x}); return is_na(_nz_v) ? ({y}) : _nz_v; }}())"
|
|
328
446
|
|
|
329
447
|
# fixnan(x) -> persistent state
|
|
330
448
|
if func_name == "fixnan" and namespace is None:
|
|
@@ -345,11 +463,19 @@ class CallVisitor:
|
|
|
345
463
|
if site is not None:
|
|
346
464
|
compute_args = self._ta_compute_args_for_site(site)
|
|
347
465
|
ta_mem = self._ta_member_name(site)
|
|
348
|
-
|
|
466
|
+
uses_precalc = self._ta_site_uses_precalc(site)
|
|
467
|
+
if getattr(self, "_precalc_loop_active", False) and uses_precalc:
|
|
349
468
|
return f"_precalc_{ta_mem}[i]"
|
|
350
|
-
if
|
|
351
|
-
return
|
|
352
|
-
|
|
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
|
+
)
|
|
353
479
|
|
|
354
480
|
# math.* calls
|
|
355
481
|
if namespace == "math":
|
|
@@ -409,18 +535,23 @@ class CallVisitor:
|
|
|
409
535
|
# Array method syntax: arr.push(val) where namespace is the array variable name
|
|
410
536
|
if namespace is not None and namespace in self._array_vars and func_name in ARRAY_METHODS:
|
|
411
537
|
arr = self._safe_name(namespace)
|
|
412
|
-
|
|
413
|
-
|
|
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)
|
|
414
541
|
|
|
415
542
|
# Array operations — emit proper C++ vector operations
|
|
416
543
|
if namespace == "array":
|
|
417
|
-
if func_name in ("new", "new_float", "new_int", "new_bool", "new_string"):
|
|
544
|
+
if func_name in ("new", "new_float", "new_int", "new_bool", "new_string") or func_name in ARRAY_DRAWING_NEW_CTORS:
|
|
418
545
|
spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
|
|
419
546
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
420
|
-
|
|
547
|
+
elem_spec = spec.element if spec.element is not None else TypeSpec.primitive("float")
|
|
548
|
+
init_default = self._default_for_spec(elem_spec)
|
|
421
549
|
if node.args:
|
|
422
550
|
size_arg = self._visit_expr(node.args[0])
|
|
423
|
-
|
|
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
|
|
424
555
|
return f"{cpp_type}((size_t)({size_arg}), {init_val})"
|
|
425
556
|
return f"{cpp_type}()"
|
|
426
557
|
if func_name == "from":
|
|
@@ -430,8 +561,8 @@ class CallVisitor:
|
|
|
430
561
|
# Method calls: array.method(arr, args...)
|
|
431
562
|
if func_name in ARRAY_METHODS and node.args:
|
|
432
563
|
arr = self._visit_expr(node.args[0])
|
|
433
|
-
rest = [self._visit_expr(a) for a in node.args[1:]]
|
|
434
564
|
spec = self._type_spec_from_expr(node.args[0])
|
|
565
|
+
rest = self._array_method_args(func_name, node.args[1:], spec)
|
|
435
566
|
return self._array_method_expr(arr, func_name, rest, spec)
|
|
436
567
|
return "0"
|
|
437
568
|
|
|
@@ -446,6 +577,16 @@ class CallVisitor:
|
|
|
446
577
|
if namespace is None and func_name == "color" and func_name not in self._func_names:
|
|
447
578
|
return "0"
|
|
448
579
|
|
|
580
|
+
# Drawing-objects-as-data namespace-functional form (spec §4.3 form 1):
|
|
581
|
+
# line.new(...) / line.get_y2(ln) / box.set_top(b, v) / linefill.new(...)
|
|
582
|
+
# MUST precede the SKIP_NAMESPACES early-return (these namespaces were
|
|
583
|
+
# removed from SKIP_NAMESPACES). chart.point.* resolves to namespace
|
|
584
|
+
# "chart", so it is matched by callee shape instead.
|
|
585
|
+
if namespace in DRAWING_NS:
|
|
586
|
+
return self._emit_drawing_namespace_call(namespace, func_name, node)
|
|
587
|
+
if self._is_chart_point_callee(callee):
|
|
588
|
+
return self._emit_chart_point(func_name, node)
|
|
589
|
+
|
|
449
590
|
# Skip visual/unsupported namespace calls
|
|
450
591
|
if namespace in SKIP_NAMESPACES or namespace in SKIP_VAR_TYPES:
|
|
451
592
|
return "0"
|
|
@@ -468,19 +609,34 @@ class CallVisitor:
|
|
|
468
609
|
all_args.append(None)
|
|
469
610
|
all_args[i] = node.kwargs[pname]
|
|
470
611
|
|
|
471
|
-
# Find matching security call ID
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
612
|
+
# Find matching security call ID. A request.security whose
|
|
613
|
+
# timeframe is a UDF parameter called from multiple sites with
|
|
614
|
+
# multiple distinct literal timeframes is registered as N
|
|
615
|
+
# CLONES (one SecurityCallInfo per call site, same source
|
|
616
|
+
# expr_node identity, distinct sec_id/callsite_idx — see
|
|
617
|
+
# Analyzer._check_mixed_callsite_security_tf). All clones
|
|
618
|
+
# match the identity check below identically, so when more
|
|
619
|
+
# than one matches, disambiguate by which call-site clone's
|
|
620
|
+
# function body is currently being emitted
|
|
621
|
+
# (self._active_call_site_idx, set by _emit_func_def while
|
|
622
|
+
# walking that exact clone's body).
|
|
623
|
+
candidates = [
|
|
624
|
+
item for item in self._security_calls
|
|
625
|
+
if not item.get("is_lower_tf_array")
|
|
626
|
+
and (exprn := item["expr_node"]) is not None
|
|
627
|
+
and (len(all_args) > 2 and exprn is all_args[2])
|
|
628
|
+
]
|
|
629
|
+
chosen = None
|
|
630
|
+
if len(candidates) == 1:
|
|
631
|
+
chosen = candidates[0]
|
|
632
|
+
elif len(candidates) > 1:
|
|
633
|
+
chosen = next(
|
|
634
|
+
(c for c in candidates
|
|
635
|
+
if c.get("callsite_idx") == self._active_call_site_idx),
|
|
636
|
+
candidates[0],
|
|
637
|
+
)
|
|
638
|
+
sec_id = chosen["sec_id"] if chosen else None
|
|
639
|
+
expr_node = chosen["expr_node"] if chosen else None
|
|
484
640
|
|
|
485
641
|
if sec_id is not None and expr_node is not None:
|
|
486
642
|
if isinstance(expr_node, TupleLiteral):
|
|
@@ -522,9 +678,12 @@ class CallVisitor:
|
|
|
522
678
|
|
|
523
679
|
# ticker.* calls
|
|
524
680
|
if namespace == "ticker":
|
|
525
|
-
# ticker.inherit(symbol, ...)
|
|
526
|
-
#
|
|
527
|
-
|
|
681
|
+
# ticker.inherit(symbol, ...) / ticker.standard(symbol) — passthrough,
|
|
682
|
+
# and ticker.heikinashi(symbol) — same-symbol HA: emit the symbol
|
|
683
|
+
# argument unchanged. The runtime HA candle transform is applied by
|
|
684
|
+
# the engine via register_security_eval's heikinashi flag, so the
|
|
685
|
+
# ticker value itself just needs to be the (string) chart symbol.
|
|
686
|
+
if func_name in ("inherit", "standard", "heikinashi"):
|
|
528
687
|
if node.args:
|
|
529
688
|
return self._visit_expr(node.args[0])
|
|
530
689
|
if "symbol" in node.kwargs:
|
|
@@ -595,11 +754,12 @@ class CallVisitor:
|
|
|
595
754
|
# mutex-guarded setenv+localtime_r block as the 2-arg
|
|
596
755
|
# form.
|
|
597
756
|
tz_arg = "syminfo_.timezone"
|
|
598
|
-
#
|
|
599
|
-
#
|
|
600
|
-
#
|
|
601
|
-
#
|
|
602
|
-
|
|
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})"
|
|
603
763
|
|
|
604
764
|
# time(timeframe) or time(timeframe, session[, tz])
|
|
605
765
|
if func_name == "time" and namespace is None and (node.args or node.kwargs):
|
|
@@ -629,6 +789,8 @@ class CallVisitor:
|
|
|
629
789
|
is_tz_first = True
|
|
630
790
|
elif isinstance(node.args[0], StringLiteral):
|
|
631
791
|
is_tz_first = True
|
|
792
|
+
elif self._infer_type(node.args[0]) == "std::string":
|
|
793
|
+
is_tz_first = True
|
|
632
794
|
|
|
633
795
|
if is_tz_first:
|
|
634
796
|
# A single string argument is the timestamp(dateString)
|
|
@@ -678,7 +840,7 @@ class CallVisitor:
|
|
|
678
840
|
sc = args[6] if len(args) > 6 else "0"
|
|
679
841
|
return (
|
|
680
842
|
f"[&]() -> int64_t {{ "
|
|
681
|
-
f"std::string _tz = ({tz}); "
|
|
843
|
+
f"std::string _tz = pineforge::normalize_timezone_for_posix(({tz})); "
|
|
682
844
|
f"int _yr = ({yr}); int _mo = ({mo}); int _dy = ({dy}); "
|
|
683
845
|
f"int _hr = ({hr}); int _min = ({mn}); int _sc = ({sc}); "
|
|
684
846
|
f"static thread_local std::string _last_tz; "
|
|
@@ -690,6 +852,7 @@ class CallVisitor:
|
|
|
690
852
|
f"struct tm t = {{}}; "
|
|
691
853
|
f"t.tm_year = _yr - 1900; t.tm_mon = _mo - 1; "
|
|
692
854
|
f"t.tm_mday = _dy; t.tm_hour = _hr; t.tm_min = _min; t.tm_sec = _sc; "
|
|
855
|
+
f"t.tm_isdst = -1; "
|
|
693
856
|
f"int64_t _res; "
|
|
694
857
|
f"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") {{ "
|
|
695
858
|
f"_res = (int64_t)timegm(&t) * 1000; "
|
|
@@ -770,7 +933,15 @@ class CallVisitor:
|
|
|
770
933
|
if func_name == "float" and namespace is None and node.args:
|
|
771
934
|
return f"(double)({self._visit_expr(node.args[0])})"
|
|
772
935
|
if func_name == "bool" and namespace is None and node.args:
|
|
773
|
-
|
|
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
|
+
)
|
|
774
945
|
if func_name == "string" and namespace is None and node.args:
|
|
775
946
|
# Pine string(x) cast — same emission as str.tostring(x), with
|
|
776
947
|
# string passthrough and TV-style "true"/"false" for bools
|
|
@@ -915,10 +1086,19 @@ class CallVisitor:
|
|
|
915
1086
|
elif f.default:
|
|
916
1087
|
val = self._visit_expr(f.default)
|
|
917
1088
|
if val is not None:
|
|
918
|
-
# Fix narrowing:
|
|
1089
|
+
# Fix narrowing: brace-init (``T{.field = v}``) disallows
|
|
1090
|
+
# narrowing. Pine ``int`` UDT fields are emitted as
|
|
1091
|
+
# ``int64_t`` (see base.py) but are initialised from
|
|
1092
|
+
# ``na<double>()`` / doubles in places, so cast to the
|
|
1093
|
+
# field's type. ``na<double>()`` for an int field → 0.
|
|
919
1094
|
f_cpp_type = self._type_spec_to_cpp(field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name))
|
|
920
|
-
if f_cpp_type == "int"
|
|
921
|
-
|
|
1095
|
+
if f_cpp_type == "int":
|
|
1096
|
+
f_cpp_type = "int64_t"
|
|
1097
|
+
if f_cpp_type == "int64_t":
|
|
1098
|
+
if "na<double>" in val:
|
|
1099
|
+
val = val.replace("na<double>()", "na<int64_t>()")
|
|
1100
|
+
else:
|
|
1101
|
+
val = f"(int64_t)({val})"
|
|
922
1102
|
field_inits.append(f".{f.name} = {val}")
|
|
923
1103
|
# Mark the constructed object non-na (the struct's ``__pf_na`` is the
|
|
924
1104
|
# last declared field, so this designator stays in declaration order).
|
|
@@ -981,17 +1161,32 @@ class CallVisitor:
|
|
|
981
1161
|
|
|
982
1162
|
def _visit_arg_for_series(arg_node, arg_idx):
|
|
983
1163
|
"""Visit a function argument, returning Series ref for series params."""
|
|
984
|
-
if arg_idx in _func_series_param_indices
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
safe
|
|
994
|
-
|
|
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
|
+
)
|
|
995
1190
|
return self._visit_expr(arg_node)
|
|
996
1191
|
|
|
997
1192
|
if node.kwargs:
|
|
@@ -1012,6 +1207,17 @@ class CallVisitor:
|
|
|
1012
1207
|
all_args.extend(self._visit_expr(v) for v in node.kwargs.values())
|
|
1013
1208
|
else:
|
|
1014
1209
|
all_args = [_visit_arg_for_series(a, i) for i, a in enumerate(node.args)]
|
|
1210
|
+
# Drawing-style/visual CONSTANT passed positionally into a user function's
|
|
1211
|
+
# ``string`` parameter: ``label.style_*`` / ``size.*`` / other
|
|
1212
|
+
# DRAWING_STYLE_NS members lower to the bare token ``"0"`` (they only ever
|
|
1213
|
+
# feed dropped visual kwargs). Bound to a ``std::string`` parameter, that
|
|
1214
|
+
# ``0`` constructs ``std::string((char const*)0)`` at the call site -> a
|
|
1215
|
+
# null-pointer ``strlen`` crash at runtime. Coerce such args to
|
|
1216
|
+
# ``std::string("")`` so the (inert, visual-only) value is a valid empty
|
|
1217
|
+
# string. Only touches user functions with a known string param and an
|
|
1218
|
+
# arg that is exactly such a drawing-style constant read.
|
|
1219
|
+
if namespace is None and func_name in self._func_names:
|
|
1220
|
+
self._coerce_drawing_style_string_args(func_name, node.args, all_args)
|
|
1015
1221
|
# Default args (parser does not store defaults): isInSession(sess, res = timeframe.period)
|
|
1016
1222
|
if namespace is None and func_name in self._func_names:
|
|
1017
1223
|
fi = self._func_info_map.get(func_name)
|
|
@@ -1023,7 +1229,16 @@ class CallVisitor:
|
|
|
1023
1229
|
emit_name = self._func_safe_name(func_name) if func_name in self._func_names else func_name
|
|
1024
1230
|
# Per-call-site variant: if this function has TA/series calls, call the correct variant
|
|
1025
1231
|
cs_info = self.ctx.func_call_cs_map.get(id(node))
|
|
1026
|
-
|
|
1232
|
+
dispatch_key = (self._current_instance_name, id(node))
|
|
1233
|
+
if dispatch_key in self._instance_dispatch:
|
|
1234
|
+
# Context-sensitive (call-path) dispatch: the instance pre-pass
|
|
1235
|
+
# resolved this nested stateful-helper call to the clone bound to
|
|
1236
|
+
# THIS enclosing path's members (see _build_func_instances). This
|
|
1237
|
+
# is authoritative — it supersedes the textual-cs threading below,
|
|
1238
|
+
# which conflates a callee's own call sites with the enclosing
|
|
1239
|
+
# function's call sites for helpers reached through >1 path.
|
|
1240
|
+
emit_name = self._instance_dispatch[dispatch_key]
|
|
1241
|
+
elif self._active_call_site_idx is not None and cs_info is not None:
|
|
1027
1242
|
# Inside a per-call-site variant: override the cs_map index with
|
|
1028
1243
|
# the parent's active call-site index. This ensures sub-functions
|
|
1029
1244
|
# called from ma_cs6() use their _cs6 variant, not _cs0.
|
|
@@ -1040,10 +1255,62 @@ class CallVisitor:
|
|
|
1040
1255
|
emit_name = f"{self._func_safe_name(func_name)}_cs{self._active_call_site_idx}"
|
|
1041
1256
|
return f"{prefix}{emit_name}({', '.join(all_args)})"
|
|
1042
1257
|
|
|
1258
|
+
def _coerce_drawing_style_string_args(self, func_name, arg_nodes, all_args) -> None:
|
|
1259
|
+
"""In-place coerce positional args bound to a ``std::string`` user-function
|
|
1260
|
+
parameter that lowered to the bare token ``"0"`` from a drawing-style /
|
|
1261
|
+
visual constant (``label.style_*`` etc.). Such a literal ``0`` binds as
|
|
1262
|
+
``std::string((char const*)0)`` and segfaults on first use. Replace with
|
|
1263
|
+
``std::string("")`` (the value is visual-only and inert in a backtest)."""
|
|
1264
|
+
from .tables import DRAWING_STYLE_NS
|
|
1265
|
+
fi = self._func_info_map.get(func_name)
|
|
1266
|
+
if not fi or not getattr(fi, "node", None) or not fi.node.params:
|
|
1267
|
+
return
|
|
1268
|
+
specs = getattr(fi, "param_type_specs", []) or []
|
|
1269
|
+
for i, arg in enumerate(arg_nodes):
|
|
1270
|
+
if i >= len(all_args) or all_args[i] != "0":
|
|
1271
|
+
continue
|
|
1272
|
+
# Only when the destination parameter is a string.
|
|
1273
|
+
spec = specs[i] if i < len(specs) else None
|
|
1274
|
+
is_string_param = spec is not None and getattr(spec, "kind", None) == "primitive" \
|
|
1275
|
+
and getattr(spec, "name", None) == "string"
|
|
1276
|
+
if not is_string_param:
|
|
1277
|
+
continue
|
|
1278
|
+
# Only when the source really is a drawing-style/visual constant read
|
|
1279
|
+
# (so we never silently turn a numeric ``0`` into an empty string).
|
|
1280
|
+
if (isinstance(arg, MemberAccess) and isinstance(arg.object, Identifier)
|
|
1281
|
+
and arg.object.name in DRAWING_STYLE_NS):
|
|
1282
|
+
all_args[i] = 'std::string("")'
|
|
1283
|
+
|
|
1043
1284
|
def _visit_fixnan(self, node: FuncCall) -> str:
|
|
1044
1285
|
"""Emit fixnan with persistent state member."""
|
|
1045
|
-
|
|
1046
|
-
|
|
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}"
|
|
1047
1314
|
x = self._visit_expr(node.args[0])
|
|
1048
1315
|
return f"(is_na({x}) ? {member} : ({member} = {x}))"
|
|
1049
1316
|
|
|
@@ -1127,17 +1394,17 @@ class CallVisitor:
|
|
|
1127
1394
|
qty_val = self._visit_expr(qty_n) if qty_n else "na<double>()"
|
|
1128
1395
|
comment = self._visit_expr(comment_n) if comment_n is not None else '""'
|
|
1129
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>()"
|
|
1130
1399
|
|
|
1131
1400
|
if profit_n and not limit_n:
|
|
1132
|
-
|
|
1133
|
-
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)
|
|
1134
1402
|
if loss_n and not stop_n:
|
|
1135
|
-
|
|
1136
|
-
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)
|
|
1137
1404
|
|
|
1138
1405
|
return (f"strategy_exit({exit_id}, {from_id}, {limit_val}, {stop_val}, "
|
|
1139
1406
|
f"{trail_pts}, {trail_off}, {trail_pr}, {qty_pct}, {comment}, "
|
|
1140
|
-
f"{qty_val}, {oca_val})")
|
|
1407
|
+
f"{qty_val}, {oca_val}, {profit_ticks}, {loss_ticks})")
|
|
1141
1408
|
close_comment = self._visit_expr(comment_n) if comment_n is not None else '""'
|
|
1142
1409
|
return f"strategy_close({exit_id}, {close_comment})"
|
|
1143
1410
|
|
|
@@ -1358,23 +1625,12 @@ class CallVisitor:
|
|
|
1358
1625
|
if func_name == "avg" and len(args) > 2:
|
|
1359
1626
|
sum_expr = " + ".join(f"(double)({a})" for a in args)
|
|
1360
1627
|
return f"(({sum_expr}) / {len(args)}.0)"
|
|
1361
|
-
if func_name
|
|
1362
|
-
|
|
1363
|
-
for a in args[2:]:
|
|
1364
|
-
result = f"std::max({result}, (double)({a}))"
|
|
1365
|
-
return result
|
|
1366
|
-
if func_name == "min" and len(args) > 2:
|
|
1367
|
-
result = f"std::min((double)({args[0]}), (double)({args[1]}))"
|
|
1368
|
-
for a in args[2:]:
|
|
1369
|
-
result = f"std::min({result}, (double)({a}))"
|
|
1370
|
-
return result
|
|
1628
|
+
if func_name in ("min", "max"):
|
|
1629
|
+
return _math_minmax_na_expr(func_name, args)
|
|
1371
1630
|
if func_name in MATH_FUNC_MAP:
|
|
1372
1631
|
mapped = MATH_FUNC_MAP[func_name]
|
|
1373
1632
|
if "{0}" in mapped:
|
|
1374
1633
|
return mapped.format(*args)
|
|
1375
|
-
# std::min/std::max require same types — cast to double
|
|
1376
|
-
if func_name in ("min", "max") and len(args) == 2:
|
|
1377
|
-
return f"{mapped}((double)({args[0]}), (double)({args[1]}))"
|
|
1378
1634
|
return f"{mapped}({', '.join(args)})"
|
|
1379
1635
|
# Unknown math.* — safe fallback
|
|
1380
1636
|
return f"0.0 /* unsupported: math.{func_name} */"
|