@pineforge/codegen-pyodide 0.10.4 → 1.0.0-rc.1
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/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +754 -76
- package/pineforge_codegen/analyzer/call_handlers.py +260 -40
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +896 -156
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +946 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1188 -100
- package/pineforge_codegen/codegen/tables.py +193 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +1882 -78
- package/pineforge_codegen/codegen/visit_call.py +920 -131
- package/pineforge_codegen/codegen/visit_expr.py +738 -57
- package/pineforge_codegen/codegen/visit_stmt.py +595 -49
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.4.tar.gz +0 -0
|
@@ -134,22 +134,36 @@ from __future__ import annotations
|
|
|
134
134
|
|
|
135
135
|
from ..ast_nodes import (
|
|
136
136
|
ASTNode,
|
|
137
|
+
Assignment,
|
|
138
|
+
BoolLiteral,
|
|
139
|
+
ColorLiteral,
|
|
140
|
+
ExprStmt,
|
|
137
141
|
FuncCall,
|
|
138
142
|
Identifier,
|
|
139
143
|
MemberAccess,
|
|
140
144
|
NaLiteral,
|
|
145
|
+
NumberLiteral,
|
|
146
|
+
Ternary,
|
|
141
147
|
TupleLiteral,
|
|
142
148
|
StringLiteral,
|
|
143
149
|
VarDecl,
|
|
144
150
|
)
|
|
151
|
+
from ..external_requests import RECORDED_KEY_ANNOTATION
|
|
152
|
+
from ..limits import iter_ast_nodes
|
|
153
|
+
from ..security_contexts import ticker_symbol_arg
|
|
145
154
|
from ..symbols import TypeSpec, method_receiver_type_name
|
|
146
155
|
from ..method_binding import (
|
|
147
156
|
MethodBindError,
|
|
157
|
+
bind_function_defaults,
|
|
148
158
|
bind_method_call,
|
|
149
159
|
signature_from_callable,
|
|
150
160
|
)
|
|
151
161
|
from .. import signatures as sigs
|
|
152
162
|
from .drawing import ALL_DRAWING_METHODS
|
|
163
|
+
from .helpers import (
|
|
164
|
+
color_alpha_cast, cpp_is_plain_read, evaluate_args_once,
|
|
165
|
+
na_preserving_int_cast, pine_truth_cast,
|
|
166
|
+
)
|
|
153
167
|
from .tables import (
|
|
154
168
|
ARRAY_DRAWING_NEW_CTORS,
|
|
155
169
|
ARRAY_METHODS,
|
|
@@ -164,15 +178,15 @@ from .tables import (
|
|
|
164
178
|
MATRIX_METHODS,
|
|
165
179
|
MATRIX_METHOD_KWARGS,
|
|
166
180
|
MATRIX_NUMERIC_ONLY,
|
|
167
|
-
MATRIX_SORT_ALLOWED_GENERIC_ELEMS,
|
|
168
181
|
SKIP_FUNC_NAMES,
|
|
169
182
|
SKIP_NAMESPACES,
|
|
170
183
|
SKIP_VAR_TYPES,
|
|
184
|
+
STR_ARGS_READ_REPEATEDLY,
|
|
171
185
|
STR_FUNC_MAP,
|
|
172
186
|
TIME_FIELD_EXPRS,
|
|
173
187
|
_math_minmax_na_expr,
|
|
188
|
+
_math_round_digits_expr,
|
|
174
189
|
_merge_kwargs,
|
|
175
|
-
tz_time_field_lambda,
|
|
176
190
|
)
|
|
177
191
|
|
|
178
192
|
|
|
@@ -271,6 +285,14 @@ def _timestamp_calendar_lambda(tz: str, yr: str, mo: str, dy: str,
|
|
|
271
285
|
)
|
|
272
286
|
|
|
273
287
|
|
|
288
|
+
def _whole_transparency(node) -> bool:
|
|
289
|
+
"""A color's transparency written as an integer literal from 0 to 100,
|
|
290
|
+
which the historic ``(int)`` conversion leaves as it is
|
|
291
|
+
(``color_alpha_cast``'s ``whole``)."""
|
|
292
|
+
return (isinstance(node, NumberLiteral) and isinstance(node.value, int)
|
|
293
|
+
and 0 <= node.value <= 100)
|
|
294
|
+
|
|
295
|
+
|
|
274
296
|
class CallVisitor:
|
|
275
297
|
"""Function-call dispatch visitor methods shared across the codegen.
|
|
276
298
|
|
|
@@ -303,6 +325,8 @@ class CallVisitor:
|
|
|
303
325
|
def _typed_user_method_info(self, receiver, member: str):
|
|
304
326
|
"""Resolve an exact typed user method before any builtin method."""
|
|
305
327
|
receiver_spec = self._type_spec_from_expr(receiver)
|
|
328
|
+
if receiver_spec is None and isinstance(receiver, Identifier):
|
|
329
|
+
receiver_spec = self._scalar_receiver_spec(receiver.name, member)
|
|
306
330
|
receiver_name = method_receiver_type_name(receiver_spec)
|
|
307
331
|
if receiver_name is None:
|
|
308
332
|
return receiver_spec, None
|
|
@@ -314,6 +338,138 @@ class CallVisitor:
|
|
|
314
338
|
return receiver_spec, None
|
|
315
339
|
return receiver_spec, method_info
|
|
316
340
|
|
|
341
|
+
_SCALAR_RECEIVER_TYPES = {
|
|
342
|
+
"double": ("float",), "int": ("int",), "bool": ("bool",),
|
|
343
|
+
"std::string": ("string",), "int64_t": ("int", "color"),
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
def _scalar_receiver_spec(self, name: str, member: str):
|
|
347
|
+
"""The primitive spec of a scalar a typed method is called on, where
|
|
348
|
+
``_type_spec_from_expr`` has only its tombstone: a global scalar is
|
|
349
|
+
recorded as bound to no user type, which hid its own primitive type
|
|
350
|
+
(``s5 = ta.ema(close, 5)``; ``s5.m()`` was emitted raw, as a member
|
|
351
|
+
call on a double). A local or parameter reads its C++ type, a global
|
|
352
|
+
its symbol; an ``int64_t`` is whichever of ``int`` and ``color`` has
|
|
353
|
+
the method."""
|
|
354
|
+
cpp = (getattr(self, "_current_func_local_types", {}).get(name)
|
|
355
|
+
or getattr(self, "_current_func_param_types", {}).get(name))
|
|
356
|
+
if cpp is not None:
|
|
357
|
+
candidates = self._SCALAR_RECEIVER_TYPES.get(cpp, ())
|
|
358
|
+
else:
|
|
359
|
+
if getattr(self, "_global_udt_types", {}).get(name, "absent") is not None:
|
|
360
|
+
return None
|
|
361
|
+
sym = self._variable_symbol(name)
|
|
362
|
+
candidates = (sym.pine_type.value,) if sym is not None else ()
|
|
363
|
+
found = [kind for kind in candidates
|
|
364
|
+
if kind in ("float", "int", "bool", "string", "color")
|
|
365
|
+
and f"{kind}.{member}" in self._func_info_map]
|
|
366
|
+
return TypeSpec.primitive(found[0]) if len(found) == 1 else None
|
|
367
|
+
|
|
368
|
+
_TIME_PARAMS = ("timeframe", "session", "timezone", "bars_back", "timeframe_bars_back")
|
|
369
|
+
|
|
370
|
+
def _bind_time_call(self, node: FuncCall) -> dict:
|
|
371
|
+
"""``time()`` / ``time_close()`` bound to Pine v6's overloads:
|
|
372
|
+
``(timeframe, bars_back, timeframe_bars_back)``, ``(timeframe,
|
|
373
|
+
session, bars_back, timeframe_bars_back)`` and ``(timeframe,
|
|
374
|
+
session, timezone, bars_back, timeframe_bars_back)``. An offset sits
|
|
375
|
+
where the longer form has a string, so a numeric second or third
|
|
376
|
+
argument names the shorter form (``time("", "", -1)`` is the next
|
|
377
|
+
bar's open, ``time("60", -1)`` the hour of the next bar)."""
|
|
378
|
+
def numeric(arg) -> bool:
|
|
379
|
+
return self._infer_type(arg) in ("int", "int64_t", "double")
|
|
380
|
+
|
|
381
|
+
bound = dict.fromkeys(self._TIME_PARAMS)
|
|
382
|
+
positional = list(node.args)
|
|
383
|
+
if positional:
|
|
384
|
+
bound["timeframe"] = positional[0]
|
|
385
|
+
rest = positional[1:]
|
|
386
|
+
if rest and numeric(rest[0]):
|
|
387
|
+
order = ("bars_back", "timeframe_bars_back")
|
|
388
|
+
elif len(rest) >= 2 and numeric(rest[1]):
|
|
389
|
+
order = ("session", "bars_back", "timeframe_bars_back")
|
|
390
|
+
else:
|
|
391
|
+
order = ("session", "timezone", "bars_back", "timeframe_bars_back")
|
|
392
|
+
for name, arg in zip(order, rest):
|
|
393
|
+
bound[name] = arg
|
|
394
|
+
for name, arg in node.kwargs.items():
|
|
395
|
+
if name in bound:
|
|
396
|
+
bound[name] = arg
|
|
397
|
+
return bound
|
|
398
|
+
|
|
399
|
+
def _time_offset_call(self, func_name: str, node: FuncCall) -> str | None:
|
|
400
|
+
"""Lower ``time()`` / ``time_close()`` reading another bar.
|
|
401
|
+
|
|
402
|
+
A ``bars_back`` / ``timeframe_bars_back`` that is not a literal 0 reads
|
|
403
|
+
another bar's time: the host's ``pine_time_offset`` steps the chart's
|
|
404
|
+
bars and the requested timeframe's (a future bar included, as
|
|
405
|
+
TradingView reads it: lab tv tapes te_time_bb_chart / te_time_bb_tf,
|
|
406
|
+
tests/fixtures/time_bars_back). None when the call reads the current
|
|
407
|
+
bar -- no offset, or literal-0 offsets -- for the plain lowering."""
|
|
408
|
+
bound = self._bind_time_call(node)
|
|
409
|
+
offsets = [bound[name] for name in ("bars_back", "timeframe_bars_back")
|
|
410
|
+
if bound[name] is not None]
|
|
411
|
+
if all(isinstance(o, NumberLiteral) and o.value == 0 for o in offsets):
|
|
412
|
+
return None
|
|
413
|
+
if self._security_payload_depth:
|
|
414
|
+
self._codegen_error(
|
|
415
|
+
node,
|
|
416
|
+
f"{func_name}() with bars_back / timeframe_bars_back inside "
|
|
417
|
+
"request.security is not supported",
|
|
418
|
+
hint=f"Read {func_name}() with its offset outside request.security().",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def text(name: str, default: str) -> str:
|
|
422
|
+
arg = bound[name]
|
|
423
|
+
return self._visit_expr(arg) if arg is not None else default
|
|
424
|
+
|
|
425
|
+
def offset(name: str) -> str:
|
|
426
|
+
arg = bound[name]
|
|
427
|
+
if arg is None:
|
|
428
|
+
return "0"
|
|
429
|
+
return self._coerce_int_slot(f"({self._visit_expr(arg)})", arg, "int")
|
|
430
|
+
|
|
431
|
+
close = "true" if func_name == "time_close" else "false"
|
|
432
|
+
empty = 'std::string("")'
|
|
433
|
+
return (
|
|
434
|
+
f"pine_time_offset(current_bar_.timestamp, {offset('bars_back')}, "
|
|
435
|
+
f"{text('timeframe', 'script_tf_')}, {text('session', empty)}, "
|
|
436
|
+
f"{text('timezone', empty)}, {offset('timeframe_bars_back')}, {close})"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def _check_time_bars_back(self, func_name: str, node: FuncCall) -> FuncCall:
|
|
440
|
+
"""``time()`` / ``time_close()`` reading the current bar, spelled for
|
|
441
|
+
the plain lowering: its timeframe, session and timezone in order,
|
|
442
|
+
literal-0 offsets dropped (``time_close("", 0)`` read the 0 as its
|
|
443
|
+
session)."""
|
|
444
|
+
bound = self._bind_time_call(node)
|
|
445
|
+
if all(bound[name] is None for name in ("bars_back", "timeframe_bars_back")):
|
|
446
|
+
return node
|
|
447
|
+
positional = []
|
|
448
|
+
kwargs = {}
|
|
449
|
+
for name in ("timeframe", "session", "timezone"):
|
|
450
|
+
if bound[name] is None:
|
|
451
|
+
continue
|
|
452
|
+
if len(positional) == ("timeframe", "session", "timezone").index(name):
|
|
453
|
+
positional.append(bound[name])
|
|
454
|
+
else:
|
|
455
|
+
kwargs[name] = bound[name]
|
|
456
|
+
return FuncCall(callee=node.callee, args=positional, kwargs=kwargs,
|
|
457
|
+
loc=node.loc, annotations=node.annotations)
|
|
458
|
+
|
|
459
|
+
def _user_call_args_with_defaults(self, func_name: str, node: FuncCall):
|
|
460
|
+
"""A plain user-function call's arguments with omitted parameters
|
|
461
|
+
filled from their declared defaults (``bind_function_defaults``);
|
|
462
|
+
``None`` when the established binding applies."""
|
|
463
|
+
fi = self._func_info_map.get(func_name)
|
|
464
|
+
if (fi is None or fi.node is None or not fi.node.params
|
|
465
|
+
or getattr(fi, "is_udt_method", False)):
|
|
466
|
+
return None
|
|
467
|
+
return bind_function_defaults(
|
|
468
|
+
fi.node.params,
|
|
469
|
+
(fi.node.annotations or {}).get("param_defaults", ()),
|
|
470
|
+
node,
|
|
471
|
+
)
|
|
472
|
+
|
|
317
473
|
def _bind_typed_method_args(self, method_info, node: FuncCall):
|
|
318
474
|
"""Bind typed method arguments through the analyzer-shared rules."""
|
|
319
475
|
|
|
@@ -387,6 +543,12 @@ class CallVisitor:
|
|
|
387
543
|
)
|
|
388
544
|
for index, arg in enumerate(rest_nodes)
|
|
389
545
|
]
|
|
546
|
+
method_cs = self._callable_target_callsite_idx(method_info, node)
|
|
547
|
+
for index, arg in enumerate(rest_nodes, start=1):
|
|
548
|
+
if self._func_param_is_bool(method_info, index, method_cs):
|
|
549
|
+
rest_cpp[index - 1] = self._coerce_bool_expr(
|
|
550
|
+
rest_cpp[index - 1], arg
|
|
551
|
+
)
|
|
390
552
|
|
|
391
553
|
receiver_root = receiver_node
|
|
392
554
|
while isinstance(receiver_root, MemberAccess):
|
|
@@ -398,6 +560,11 @@ class CallVisitor:
|
|
|
398
560
|
and receiver_spec.name not in self._udt_defs
|
|
399
561
|
)
|
|
400
562
|
)
|
|
563
|
+
# A temporary receiver is evaluated before the arguments in Pine. C++
|
|
564
|
+
# leaves the order of call arguments unspecified (GCC evaluates them
|
|
565
|
+
# right to left), so stage the call whenever an argument could observe
|
|
566
|
+
# the receiver's evaluation.
|
|
567
|
+
receiver_is_temporary = not isinstance(receiver_root, Identifier)
|
|
401
568
|
return self._ordered_user_call_expr(
|
|
402
569
|
fn_cpp,
|
|
403
570
|
[receiver_node, *rest_nodes],
|
|
@@ -406,10 +573,17 @@ class CallVisitor:
|
|
|
406
573
|
receiver_node,
|
|
407
574
|
*binding.evaluation_order,
|
|
408
575
|
],
|
|
409
|
-
force_stage=(
|
|
576
|
+
force_stage=(receiver_is_temporary and (
|
|
410
577
|
receiver_passes_by_reference
|
|
411
|
-
|
|
412
|
-
|
|
578
|
+
or any(
|
|
579
|
+
not isinstance(arg, (
|
|
580
|
+
NumberLiteral, StringLiteral, BoolLiteral, NaLiteral,
|
|
581
|
+
ColorLiteral,
|
|
582
|
+
))
|
|
583
|
+
for arg in rest_nodes
|
|
584
|
+
)
|
|
585
|
+
)) or self._binds_fresh_array_to_reference(
|
|
586
|
+
method_info, rest_nodes, first_param=1),
|
|
413
587
|
)
|
|
414
588
|
|
|
415
589
|
def _callable_target_callsite_idx(self, fi, node: FuncCall) -> int | None:
|
|
@@ -443,7 +617,8 @@ class CallVisitor:
|
|
|
443
617
|
+ ">(_pf_series_raw); }())"
|
|
444
618
|
)
|
|
445
619
|
|
|
446
|
-
def _array_init_value_expr(self, elem_spec: TypeSpec | None, value_node
|
|
620
|
+
def _array_init_value_expr(self, elem_spec: TypeSpec | None, value_node,
|
|
621
|
+
store: bool = True) -> str:
|
|
447
622
|
if isinstance(value_node, NaLiteral):
|
|
448
623
|
if elem_spec is not None and elem_spec.kind == "udt":
|
|
449
624
|
return self._default_for_spec(elem_spec)
|
|
@@ -451,7 +626,28 @@ class CallVisitor:
|
|
|
451
626
|
if cpp_type in ("double", "int", "int64_t", "bool", "std::string"):
|
|
452
627
|
return f"na<{cpp_type}>()"
|
|
453
628
|
return self._default_for_spec(elem_spec)
|
|
454
|
-
|
|
629
|
+
rendered = self._visit_expr(value_node)
|
|
630
|
+
if (elem_spec is not None and elem_spec.kind == "primitive"
|
|
631
|
+
and elem_spec.name == "bool"):
|
|
632
|
+
return self._coerce_bool_expr(rendered, value_node)
|
|
633
|
+
if (store and elem_spec is not None and elem_spec.kind == "primitive"
|
|
634
|
+
and elem_spec.name == "int"
|
|
635
|
+
and (self._holds_wide_int_constant(value_node)
|
|
636
|
+
or self._expr_returns_wide_int(value_node, None, set(), None))):
|
|
637
|
+
# A 64-bit value stored in a 32-bit element (an int array only an
|
|
638
|
+
# epoch widens, ``_wide_int_array_names``; a matrix<int>): the
|
|
639
|
+
# crossing keeps na (quirk 9), and a braced initializer does not
|
|
640
|
+
# narrow implicitly.
|
|
641
|
+
return na_preserving_int_cast(rendered, "int")
|
|
642
|
+
if (store and elem_spec is not None and elem_spec.kind == "primitive"
|
|
643
|
+
and elem_spec.name == "int64"
|
|
644
|
+
and self._holds_na_aware_wide_double(value_node)
|
|
645
|
+
and self._emitted_value_is_double(value_node)):
|
|
646
|
+
# A 64-bit product over an operand that can be na is a double
|
|
647
|
+
# (``_wide_int_arith_cpp``): an epoch array's element narrows it
|
|
648
|
+
# na-preserving too.
|
|
649
|
+
return na_preserving_int_cast(rendered, "int64_t")
|
|
650
|
+
return rendered
|
|
455
651
|
|
|
456
652
|
def _array_method_args(
|
|
457
653
|
self, method: str, arg_nodes: list, spec: TypeSpec | None,
|
|
@@ -474,8 +670,14 @@ class CallVisitor:
|
|
|
474
670
|
"binary_search_leftmost": {0},
|
|
475
671
|
"binary_search_rightmost": {0},
|
|
476
672
|
}.get(method, set())
|
|
673
|
+
stores = method in ("set", "push", "unshift", "insert", "fill")
|
|
674
|
+
if stores:
|
|
675
|
+
for idx in value_arg_indexes:
|
|
676
|
+
if idx < len(arg_nodes):
|
|
677
|
+
self._warn_narrow_int_element(arg_nodes[idx], spec,
|
|
678
|
+
arg_nodes[idx])
|
|
477
679
|
return [
|
|
478
|
-
self._array_init_value_expr(elem_spec, arg)
|
|
680
|
+
self._array_init_value_expr(elem_spec, arg, stores)
|
|
479
681
|
if idx in value_arg_indexes
|
|
480
682
|
else self._visit_expr(arg)
|
|
481
683
|
for idx, arg in enumerate(arg_nodes)
|
|
@@ -488,6 +690,39 @@ class CallVisitor:
|
|
|
488
690
|
return list(node.args)
|
|
489
691
|
return _merge_kwargs(node.args, node.kwargs, param_names, lambda arg: arg)
|
|
490
692
|
|
|
693
|
+
def _matrix_bool_value_args(
|
|
694
|
+
self, method: str, args: list[str], arg_nodes: list,
|
|
695
|
+
spec: TypeSpec | None,
|
|
696
|
+
) -> list[str]:
|
|
697
|
+
"""Apply Pine truthiness to values stored in ``matrix<bool>``."""
|
|
698
|
+
value_at = {"set": 2, "fill": 0}.get(method)
|
|
699
|
+
if value_at is not None and value_at < len(arg_nodes):
|
|
700
|
+
self._warn_narrow_int_element(arg_nodes[value_at], spec,
|
|
701
|
+
arg_nodes[value_at])
|
|
702
|
+
if (spec is not None and spec.kind == "matrix"
|
|
703
|
+
and spec.element is not None
|
|
704
|
+
and spec.element.kind == "primitive"
|
|
705
|
+
and spec.element.name == "int"
|
|
706
|
+
and value_at < len(args)
|
|
707
|
+
and (self._holds_wide_int_constant(arg_nodes[value_at])
|
|
708
|
+
or self._expr_returns_wide_int(
|
|
709
|
+
arg_nodes[value_at], None, set(), None))):
|
|
710
|
+
# A 64-bit value (a double where an operand can be na,
|
|
711
|
+
# ``_wide_int_arith_cpp``) narrows na-preserving into the int
|
|
712
|
+
# element (quirk 9), as an int array's element does.
|
|
713
|
+
args[value_at] = na_preserving_int_cast(args[value_at], "int")
|
|
714
|
+
if (spec is None or spec.kind != "matrix"
|
|
715
|
+
or spec.element is None
|
|
716
|
+
or spec.element.kind != "primitive"
|
|
717
|
+
or spec.element.name != "bool"):
|
|
718
|
+
return args
|
|
719
|
+
value_index = {"set": 2, "fill": 0}.get(method)
|
|
720
|
+
if value_index is not None and value_index < len(args):
|
|
721
|
+
args[value_index] = self._coerce_bool_expr(
|
|
722
|
+
args[value_index], arg_nodes[value_index]
|
|
723
|
+
)
|
|
724
|
+
return args
|
|
725
|
+
|
|
491
726
|
def _array_function_arg_nodes(self, method: str, node: FuncCall) -> list:
|
|
492
727
|
"""Merge ``array.method(id=..., ...)`` arguments in signature order."""
|
|
493
728
|
if method == "copy":
|
|
@@ -672,8 +907,53 @@ class CallVisitor:
|
|
|
672
907
|
hint=f"Use the established positional form: {signature}.",
|
|
673
908
|
)
|
|
674
909
|
|
|
910
|
+
if method == "put":
|
|
911
|
+
receiver = (bound[0] if functional
|
|
912
|
+
else getattr(node.callee, "object", None))
|
|
913
|
+
self._warn_narrow_int_element(
|
|
914
|
+
node, self._type_spec_from_expr(receiver) if receiver is not None
|
|
915
|
+
else None, bound[-1])
|
|
675
916
|
return bound
|
|
676
917
|
|
|
918
|
+
def _warn_narrow_int_element(self, node, spec, value_node) -> None:
|
|
919
|
+
"""Warn once where a ``map<..., int>`` value, a ``matrix<int>``
|
|
920
|
+
element or an ``array<int>`` element (of an array no epoch widens,
|
|
921
|
+
``_wide_int_array_names``) receives a value that can leave int32 (a
|
|
922
|
+
constant past it, ``_holds_wide_int_constant``, or a 64-bit value,
|
|
923
|
+
``_expr_returns_wide_int``): the handle stores a C++ ``int``, where
|
|
924
|
+
TradingView's ``int`` is 64-bit. The element type also types the
|
|
925
|
+
handle's parameters and its ``values()`` / ``keys()`` / ``row()`` /
|
|
926
|
+
``col()`` arrays (an array's, the declared ``array<int>`` parameters
|
|
927
|
+
and fields it is passed to), so it keeps the width it compiles
|
|
928
|
+
with."""
|
|
929
|
+
if spec is None or value_node is None:
|
|
930
|
+
return
|
|
931
|
+
element = (spec.value if spec.kind == "map"
|
|
932
|
+
else spec.element if spec.kind in ("matrix", "array")
|
|
933
|
+
else None)
|
|
934
|
+
if (element is None or element.kind != "primitive"
|
|
935
|
+
or element.name != "int"):
|
|
936
|
+
return
|
|
937
|
+
if not (self._holds_wide_int_constant(value_node)
|
|
938
|
+
or self._expr_returns_wide_int(value_node, None, set(), None)):
|
|
939
|
+
return
|
|
940
|
+
warned = getattr(self, "_narrow_int_element_warned", None)
|
|
941
|
+
if warned is None:
|
|
942
|
+
warned = self._narrow_int_element_warned = set()
|
|
943
|
+
if id(node) in warned:
|
|
944
|
+
return
|
|
945
|
+
warned.add(id(node))
|
|
946
|
+
kind = {"map": "map<..., int> value", "matrix": "matrix<int> element",
|
|
947
|
+
"array": "array<int> element"}[spec.kind]
|
|
948
|
+
self._codegen_warning(
|
|
949
|
+
node,
|
|
950
|
+
f"{'An' if spec.kind == 'array' else 'A'} {kind} is stored as a "
|
|
951
|
+
"32-bit int in PineForge; this value can "
|
|
952
|
+
"exceed int32, where TradingView's int is 64-bit, and is truncated "
|
|
953
|
+
"(or read as na) there.",
|
|
954
|
+
hint="Declare the values float to keep every digit.",
|
|
955
|
+
)
|
|
956
|
+
|
|
677
957
|
def _map_param_method_expr(
|
|
678
958
|
self, map_expr: str, method: str, arg_nodes: list, spec: TypeSpec,
|
|
679
959
|
) -> str:
|
|
@@ -686,6 +966,15 @@ class CallVisitor:
|
|
|
686
966
|
extending temporary receiver lifetime through the operation.
|
|
687
967
|
"""
|
|
688
968
|
args = [self._visit_expr(arg) for arg in arg_nodes]
|
|
969
|
+
if (method == "put" and arg_nodes and spec is not None
|
|
970
|
+
and spec.kind == "map" and spec.value is not None
|
|
971
|
+
and spec.value.kind == "primitive" and spec.value.name == "int"
|
|
972
|
+
and (self._holds_wide_int_constant(arg_nodes[-1])
|
|
973
|
+
or self._expr_returns_wide_int(arg_nodes[-1], None, set(), None))):
|
|
974
|
+
# A 64-bit value (a double where an operand can be na,
|
|
975
|
+
# ``_wide_int_arith_cpp``) narrows na-preserving into the int
|
|
976
|
+
# value (quirk 9), as an int array's element does.
|
|
977
|
+
args[-1] = na_preserving_int_cast(args[-1], "int")
|
|
689
978
|
occupied = "\n".join((map_expr, *args))
|
|
690
979
|
counter = getattr(self, "_map_param_arg_counter", 0)
|
|
691
980
|
bindings: list[tuple[str, str]] = []
|
|
@@ -901,6 +1190,122 @@ class CallVisitor:
|
|
|
901
1190
|
result.update(self._func_collection_types.get(func_info.name, {}))
|
|
902
1191
|
return result
|
|
903
1192
|
|
|
1193
|
+
# Builtins that return an array no variable holds: ``matrix.row(m, i)``
|
|
1194
|
+
# is a copy of the row, ``array.copy(a)`` of the array.
|
|
1195
|
+
_FRESH_ARRAY_CALLS = frozenset({
|
|
1196
|
+
("array", "from"), ("array", "copy"), ("array", "sort_indices"),
|
|
1197
|
+
("array", "abs"), ("array", "standardize"), ("matrix", "row"),
|
|
1198
|
+
("matrix", "col"), ("matrix", "eigenvalues"), ("str", "split"),
|
|
1199
|
+
("map", "keys"), ("map", "values"),
|
|
1200
|
+
})
|
|
1201
|
+
_FRESH_ARRAY_METHODS = {
|
|
1202
|
+
"array": frozenset({"copy", "sort_indices", "abs", "standardize"}),
|
|
1203
|
+
"matrix": frozenset({"row", "col", "eigenvalues"}),
|
|
1204
|
+
"map": frozenset({"keys", "values"}),
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
def _array_arg_kind(self, node, depth: int = 0) -> str:
|
|
1208
|
+
"""``"fresh"`` for an argument that is an array no variable holds (a
|
|
1209
|
+
new or copied array, a matrix row, a user function's new array),
|
|
1210
|
+
``"alias"`` for a user function's array that a variable may hold (it
|
|
1211
|
+
returns its argument or a global) or a selection of a held and a
|
|
1212
|
+
fresh array, and ``"keep"`` for every other argument, which keeps the
|
|
1213
|
+
lowering it had: a variable, a field, ``na``, any other built-in."""
|
|
1214
|
+
if isinstance(node, Ternary):
|
|
1215
|
+
kinds = {self._array_arg_kind(node.true_val, depth),
|
|
1216
|
+
self._array_arg_kind(node.false_val, depth)}
|
|
1217
|
+
if "alias" in kinds or len(kinds) > 1:
|
|
1218
|
+
# A C++ conditional of a held and a fresh array copies the
|
|
1219
|
+
# held one; TradingView passes that array itself.
|
|
1220
|
+
return "alias"
|
|
1221
|
+
return kinds.pop()
|
|
1222
|
+
if not isinstance(node, FuncCall) or depth > 8:
|
|
1223
|
+
return "keep"
|
|
1224
|
+
callee = node.callee
|
|
1225
|
+
if isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier):
|
|
1226
|
+
key = (callee.object.name, callee.member)
|
|
1227
|
+
if key in self._FRESH_ARRAY_CALLS or (
|
|
1228
|
+
key[0] == "array" and key[1].startswith("new")):
|
|
1229
|
+
return "fresh"
|
|
1230
|
+
info = None
|
|
1231
|
+
if isinstance(callee, Identifier):
|
|
1232
|
+
info = self._func_info_map.get(callee.name)
|
|
1233
|
+
elif isinstance(callee, MemberAccess):
|
|
1234
|
+
spec = self._type_spec_from_expr(callee.object)
|
|
1235
|
+
fresh = self._FRESH_ARRAY_METHODS.get(spec.kind if spec is not None else "")
|
|
1236
|
+
if fresh is not None:
|
|
1237
|
+
return "fresh" if callee.member in fresh else "keep"
|
|
1238
|
+
receiver = method_receiver_type_name(spec) if spec is not None else None
|
|
1239
|
+
info = self._func_info_map.get(f"{receiver}.{callee.member}") if receiver else None
|
|
1240
|
+
if info is None:
|
|
1241
|
+
return "keep"
|
|
1242
|
+
return self._returned_array_kind(info, depth + 1)
|
|
1243
|
+
|
|
1244
|
+
def _returned_array_kind(self, func_info, depth: int) -> str:
|
|
1245
|
+
"""``"fresh"`` when a user function returns an array it creates: its
|
|
1246
|
+
value is a fresh array, or a local declared as one and never
|
|
1247
|
+
reassigned; ``"alias"`` otherwise (a user function's array reaches
|
|
1248
|
+
its caller as a C++ temporary, which no ``T&`` took)."""
|
|
1249
|
+
fdef = getattr(func_info, "node", None)
|
|
1250
|
+
body = list(getattr(fdef, "body", None) or ())
|
|
1251
|
+
if not body:
|
|
1252
|
+
return "alias"
|
|
1253
|
+
last = body[-1]
|
|
1254
|
+
value = (last.expr if isinstance(last, ExprStmt)
|
|
1255
|
+
else last.value if isinstance(last, VarDecl) else None)
|
|
1256
|
+
if isinstance(value, Identifier):
|
|
1257
|
+
decls = [s for s in body if isinstance(s, VarDecl) and s.name == value.name]
|
|
1258
|
+
reassigned = any(
|
|
1259
|
+
isinstance(n, Assignment) and isinstance(n.target, Identifier)
|
|
1260
|
+
and n.target.name == value.name
|
|
1261
|
+
for stmt in body for n, _depth in iter_ast_nodes(stmt))
|
|
1262
|
+
if (len(decls) != 1 or reassigned
|
|
1263
|
+
or value.name in (getattr(fdef, "params", None) or ())):
|
|
1264
|
+
return "alias"
|
|
1265
|
+
value = decls[0].value
|
|
1266
|
+
if not isinstance(value, (FuncCall, Ternary)):
|
|
1267
|
+
return "alias"
|
|
1268
|
+
return "fresh" if self._array_arg_kind(value, depth) == "fresh" else "alias"
|
|
1269
|
+
|
|
1270
|
+
def _binds_fresh_array_to_reference(self, func_info, arg_nodes: list,
|
|
1271
|
+
first_param: int = 0) -> bool:
|
|
1272
|
+
"""Whether a call passes a fresh array to a parameter its callee takes
|
|
1273
|
+
as ``std::vector<T>&``. Pine arrays are references: a callee's
|
|
1274
|
+
changes to a variable's array reach the caller, so the parameter stays
|
|
1275
|
+
a reference. A fresh array is a C++ temporary, which binds to no
|
|
1276
|
+
``T&``; staging the call (``_ordered_user_call_expr``) binds it to a
|
|
1277
|
+
named forwarding reference, and the callee's changes vanish with it,
|
|
1278
|
+
as TradingView's do. An array a variable may hold that reaches the
|
|
1279
|
+
call as a temporary (a function returning the array it was given) is
|
|
1280
|
+
refused: PineForge would pass a copy of it, and it never compiled."""
|
|
1281
|
+
if func_info is None:
|
|
1282
|
+
return False
|
|
1283
|
+
specs = list(getattr(func_info, "param_type_specs", ()) or ())
|
|
1284
|
+
params = (list(func_info.node.params)
|
|
1285
|
+
if getattr(func_info, "node", None) is not None else [])
|
|
1286
|
+
series = self.ctx.func_series_vars.get(func_info.name, set())
|
|
1287
|
+
stage = False
|
|
1288
|
+
for offset, arg in enumerate(arg_nodes):
|
|
1289
|
+
index = first_param + offset
|
|
1290
|
+
spec = specs[index] if index < len(specs) else None
|
|
1291
|
+
if (arg is None or spec is None or spec.kind != "array"
|
|
1292
|
+
or (index < len(params) and params[index] in series)):
|
|
1293
|
+
continue
|
|
1294
|
+
kind = self._array_arg_kind(arg)
|
|
1295
|
+
if kind == "alias":
|
|
1296
|
+
param = params[index] if index < len(params) else f"#{index + 1}"
|
|
1297
|
+
self._codegen_error(
|
|
1298
|
+
arg,
|
|
1299
|
+
f"an array a call returns is passed to parameter '{param}' of "
|
|
1300
|
+
f"'{func_info.name}', which takes the array itself: PineForge "
|
|
1301
|
+
"would pass a copy of it",
|
|
1302
|
+
hint="TradingView passes the array a function returns as itself, so "
|
|
1303
|
+
"the callee's changes to it reach every variable holding it; "
|
|
1304
|
+
"pass a variable holding the array instead.",
|
|
1305
|
+
)
|
|
1306
|
+
stage = stage or kind == "fresh"
|
|
1307
|
+
return stage
|
|
1308
|
+
|
|
904
1309
|
def _ordered_user_call_expr(
|
|
905
1310
|
self,
|
|
906
1311
|
call_head: str,
|
|
@@ -987,7 +1392,17 @@ class CallVisitor:
|
|
|
987
1392
|
)
|
|
988
1393
|
method_series = self.ctx.func_series_vars.get(func_info.name, set())
|
|
989
1394
|
if param_name not in method_series:
|
|
990
|
-
|
|
1395
|
+
cpp = self._visit_expr(arg_node)
|
|
1396
|
+
int_cpp = self._func_param_int_cpp_type(
|
|
1397
|
+
func_info, param_index,
|
|
1398
|
+
self._callable_target_callsite_idx(func_info, call_node))
|
|
1399
|
+
if (int_cpp is not None
|
|
1400
|
+
and self._holds_na_aware_wide_double(arg_node)
|
|
1401
|
+
and self._emitted_value_is_double(arg_node)):
|
|
1402
|
+
# A double-valued 64-bit product (``_wide_int_arith_cpp``)
|
|
1403
|
+
# narrows na-preserving into an integer parameter (quirk 9).
|
|
1404
|
+
cpp = na_preserving_int_cast(cpp, int_cpp)
|
|
1405
|
+
return cpp
|
|
991
1406
|
expected_cpp_type = self._series_param_element_cpp_type(
|
|
992
1407
|
func_info,
|
|
993
1408
|
param_index,
|
|
@@ -1010,8 +1425,7 @@ class CallVisitor:
|
|
|
1010
1425
|
== expected_cpp_type
|
|
1011
1426
|
):
|
|
1012
1427
|
return safe
|
|
1013
|
-
|
|
1014
|
-
safe = self._active_var_remap[safe]
|
|
1428
|
+
safe = self._call_site_var_name(arg_node, safe)
|
|
1015
1429
|
if (
|
|
1016
1430
|
not is_current_series_param
|
|
1017
1431
|
and self._binding_is_series(arg_name, safe)
|
|
@@ -1133,7 +1547,9 @@ class CallVisitor:
|
|
|
1133
1547
|
# function-return receiver is a C++ rvalue; bind it to a
|
|
1134
1548
|
# named forwarding-reference lambda parameter first so
|
|
1135
1549
|
# the method sees a valid lvalue for the full call.
|
|
1136
|
-
force_stage=stage_receiver
|
|
1550
|
+
force_stage=stage_receiver
|
|
1551
|
+
or self._binds_fresh_array_to_reference(
|
|
1552
|
+
fi_u, rest_nodes, first_param=1),
|
|
1137
1553
|
)
|
|
1138
1554
|
|
|
1139
1555
|
# Drawing method dispatch (spec §4.3 / L.1). A KNOWN drawing method on a
|
|
@@ -1166,7 +1582,7 @@ class CallVisitor:
|
|
|
1166
1582
|
recv = self._visit_expr(callee.object)
|
|
1167
1583
|
arg_nodes = self._array_method_arg_nodes(callee.member, node)
|
|
1168
1584
|
args = self._array_method_args(callee.member, arg_nodes, recv_spec)
|
|
1169
|
-
return self._array_method_expr(recv, callee.member, args, recv_spec)
|
|
1585
|
+
return self._array_method_expr(recv, callee.member, args, recv_spec, node)
|
|
1170
1586
|
|
|
1171
1587
|
# chart.point.now/new/from_index/from_time/copy — REAL data (a ChartPoint
|
|
1172
1588
|
# aggregate). Routed here BEFORE the obj.field.method receiver logic,
|
|
@@ -1222,7 +1638,7 @@ class CallVisitor:
|
|
|
1222
1638
|
recv,
|
|
1223
1639
|
meth,
|
|
1224
1640
|
self._array_method_args(meth, arg_nodes, recv_spec),
|
|
1225
|
-
recv_spec,
|
|
1641
|
+
recv_spec, node,
|
|
1226
1642
|
)
|
|
1227
1643
|
args = ", ".join(raw_args)
|
|
1228
1644
|
if meth == "delete":
|
|
@@ -1270,7 +1686,7 @@ class CallVisitor:
|
|
|
1270
1686
|
meth_raw, arg_nodes, param_spec
|
|
1271
1687
|
)
|
|
1272
1688
|
return self._array_method_expr(
|
|
1273
|
-
arr, meth_raw, margs, param_spec
|
|
1689
|
+
arr, meth_raw, margs, param_spec, node
|
|
1274
1690
|
)
|
|
1275
1691
|
if (
|
|
1276
1692
|
param_spec is not None
|
|
@@ -1318,7 +1734,7 @@ class CallVisitor:
|
|
|
1318
1734
|
margs = self._array_method_args(
|
|
1319
1735
|
meth_raw, arg_nodes, recv_spec
|
|
1320
1736
|
)
|
|
1321
|
-
return self._array_method_expr(arr, meth_raw, margs, recv_spec)
|
|
1737
|
+
return self._array_method_expr(arr, meth_raw, margs, recv_spec, node)
|
|
1322
1738
|
if (
|
|
1323
1739
|
recv_spec is not None
|
|
1324
1740
|
and recv_spec.kind == "matrix"
|
|
@@ -1327,12 +1743,16 @@ class CallVisitor:
|
|
|
1327
1743
|
arr = self._collection_receiver_expr(oname)
|
|
1328
1744
|
self._check_matrix_method_allowed(meth_raw, recv_spec, node)
|
|
1329
1745
|
param_names = MATRIX_METHOD_KWARGS.get(meth_raw)
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1746
|
+
raw_args = (
|
|
1747
|
+
_merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
|
|
1748
|
+
if param_names and node.kwargs else list(node.args)
|
|
1749
|
+
)
|
|
1750
|
+
margs = self._matrix_bool_value_args(
|
|
1751
|
+
meth_raw,
|
|
1752
|
+
[self._visit_expr(a) for a in raw_args],
|
|
1753
|
+
raw_args,
|
|
1754
|
+
recv_spec,
|
|
1755
|
+
)
|
|
1336
1756
|
fn = MATRIX_METHODS[meth_raw]
|
|
1337
1757
|
try:
|
|
1338
1758
|
return fn(arr, margs)
|
|
@@ -1370,6 +1790,8 @@ class CallVisitor:
|
|
|
1370
1790
|
obj,
|
|
1371
1791
|
*binding.evaluation_order,
|
|
1372
1792
|
],
|
|
1793
|
+
force_stage=self._binds_fresh_array_to_reference(
|
|
1794
|
+
fi_u, rest_nodes, first_param=1),
|
|
1373
1795
|
)
|
|
1374
1796
|
args = ", ".join(self._visit_expr(a) for a in node.args)
|
|
1375
1797
|
recv = self._visit_expr(obj)
|
|
@@ -1380,8 +1802,27 @@ class CallVisitor:
|
|
|
1380
1802
|
|
|
1381
1803
|
func_name, namespace = self._resolve_callee(callee)
|
|
1382
1804
|
|
|
1383
|
-
|
|
1805
|
+
if (func_name in ("na", "nz", "fixnan") and namespace is None
|
|
1806
|
+
and self._pine_v5_body and node.args):
|
|
1807
|
+
self._refuse_v5_bool_na_observer(node, f"{func_name}()", node.args[:1])
|
|
1808
|
+
|
|
1809
|
+
# na(x) -> is_na(x); a drawing asks its arena (codegen/drawing.py).
|
|
1384
1810
|
if func_name == "na" and namespace is None:
|
|
1811
|
+
if (len(node.args) == 1 and not node.kwargs
|
|
1812
|
+
and self._infer_type(node.args[0]) == "std::string"):
|
|
1813
|
+
# A string na is stored as the empty string (the engine's
|
|
1814
|
+
# ``na<std::string>()``), which has no ``is_na`` overload.
|
|
1815
|
+
self._codegen_warning(
|
|
1816
|
+
node,
|
|
1817
|
+
"na() of a string reads the empty string as na: PineForge "
|
|
1818
|
+
"stores a string na as \"\", so an authored empty string "
|
|
1819
|
+
"is na here, while TradingView tells the two apart.",
|
|
1820
|
+
)
|
|
1821
|
+
return f"({self._visit_expr(node.args[0])}).empty()"
|
|
1822
|
+
if len(node.args) == 1:
|
|
1823
|
+
drawing = self._drawing_na_expr(node.args[0])
|
|
1824
|
+
if drawing is not None:
|
|
1825
|
+
return drawing
|
|
1385
1826
|
args = ", ".join(self._visit_expr(a) for a in node.args)
|
|
1386
1827
|
return f"is_na({args})"
|
|
1387
1828
|
|
|
@@ -1398,10 +1839,17 @@ class CallVisitor:
|
|
|
1398
1839
|
# lambda hoists x into a local `auto` so it is computed once and
|
|
1399
1840
|
# both branches read the same value; `[&]` is safe here since the
|
|
1400
1841
|
# lambda is called synchronously and discarded, never escaping.
|
|
1842
|
+
# Pine evaluates the replacement y on every call too, na source or
|
|
1843
|
+
# not, so a y that is more than a plain read is bound as well: in the
|
|
1844
|
+
# ternary it ran only on the bars whose x was na, and a stateful call
|
|
1845
|
+
# in it skipped every other bar.
|
|
1401
1846
|
if func_name == "nz" and namespace is None:
|
|
1402
1847
|
x = self._visit_expr(node.args[0])
|
|
1403
1848
|
y = self._visit_expr(node.args[1]) if len(node.args) > 1 else "0.0"
|
|
1404
|
-
|
|
1849
|
+
if cpp_is_plain_read(y):
|
|
1850
|
+
return f"([&]{{ auto _nz_v = ({x}); return is_na(_nz_v) ? ({y}) : _nz_v; }}())"
|
|
1851
|
+
return (f"([&]{{ auto _nz_v = ({x}); auto _nz_y = ({y}); "
|
|
1852
|
+
f"return is_na(_nz_v) ? _nz_y : _nz_v; }}())")
|
|
1405
1853
|
|
|
1406
1854
|
# fixnan(x) -> persistent state
|
|
1407
1855
|
if func_name == "fixnan" and namespace is None:
|
|
@@ -1525,10 +1973,16 @@ class CallVisitor:
|
|
|
1525
1973
|
arr = self._collection_receiver_expr(namespace)
|
|
1526
1974
|
self._check_matrix_method_allowed(func_name, namespace_spec, node)
|
|
1527
1975
|
param_names = MATRIX_METHOD_KWARGS.get(func_name)
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1976
|
+
raw_args = (
|
|
1977
|
+
_merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
|
|
1978
|
+
if param_names and node.kwargs else list(node.args)
|
|
1979
|
+
)
|
|
1980
|
+
args = self._matrix_bool_value_args(
|
|
1981
|
+
func_name,
|
|
1982
|
+
[self._visit_expr(a) for a in raw_args],
|
|
1983
|
+
raw_args,
|
|
1984
|
+
namespace_spec,
|
|
1985
|
+
)
|
|
1532
1986
|
fn = MATRIX_METHODS[func_name]
|
|
1533
1987
|
try:
|
|
1534
1988
|
return fn(arr, args)
|
|
@@ -1549,7 +2003,7 @@ class CallVisitor:
|
|
|
1549
2003
|
spec = namespace_spec
|
|
1550
2004
|
arg_nodes = self._array_method_arg_nodes(func_name, node)
|
|
1551
2005
|
args = self._array_method_args(func_name, arg_nodes, spec)
|
|
1552
|
-
return self._array_method_expr(arr, func_name, args, spec)
|
|
2006
|
+
return self._array_method_expr(arr, func_name, args, spec, node)
|
|
1553
2007
|
|
|
1554
2008
|
# Array operations — emit proper C++ vector operations
|
|
1555
2009
|
if namespace == "array":
|
|
@@ -1567,6 +2021,8 @@ class CallVisitor:
|
|
|
1567
2021
|
if node.args:
|
|
1568
2022
|
size_arg = self._visit_expr(node.args[0])
|
|
1569
2023
|
if len(node.args) > 1:
|
|
2024
|
+
self._warn_narrow_int_element(node.args[1], spec,
|
|
2025
|
+
node.args[1])
|
|
1570
2026
|
init_val = self._array_init_value_expr(elem_spec, node.args[1])
|
|
1571
2027
|
else:
|
|
1572
2028
|
init_val = init_default
|
|
@@ -1576,8 +2032,26 @@ class CallVisitor:
|
|
|
1576
2032
|
spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
|
|
1577
2033
|
target = getattr(self, "_array_ctor_target_name", None)
|
|
1578
2034
|
if target is not None:
|
|
2035
|
+
# The declaration's own element type wins over the
|
|
2036
|
+
# argument inference, which cannot see every scalar's
|
|
2037
|
+
# family (a ``color`` variable reads as float there):
|
|
2038
|
+
# ``color[] cs = array.from(c1, c2)`` declares
|
|
2039
|
+
# ``std::vector<int64_t>`` and must construct one.
|
|
2040
|
+
declared = self._collection_spec_for_name(target)
|
|
2041
|
+
if (declared is not None and declared.kind == "array"
|
|
2042
|
+
and declared.element is not None
|
|
2043
|
+
and declared.element.kind == "primitive"
|
|
2044
|
+
and spec.kind == "array"
|
|
2045
|
+
and declared.element != spec.element):
|
|
2046
|
+
spec = declared
|
|
1579
2047
|
spec = self._widen_array_spec_for_name(target, spec)
|
|
1580
|
-
|
|
2048
|
+
elem_spec = spec.element
|
|
2049
|
+
for a in node.args:
|
|
2050
|
+
self._warn_narrow_int_element(a, spec, a)
|
|
2051
|
+
elems = ", ".join(
|
|
2052
|
+
self._array_init_value_expr(elem_spec, a)
|
|
2053
|
+
for a in node.args
|
|
2054
|
+
)
|
|
1581
2055
|
return f"{self._type_spec_to_cpp(spec)}{{{elems}}}"
|
|
1582
2056
|
# Method calls: array.method(arr, args...)
|
|
1583
2057
|
if func_name in ARRAY_METHODS and (
|
|
@@ -1589,7 +2063,7 @@ class CallVisitor:
|
|
|
1589
2063
|
arr = self._visit_expr(all_nodes[0])
|
|
1590
2064
|
spec = self._type_spec_from_expr(all_nodes[0])
|
|
1591
2065
|
rest = self._array_method_args(func_name, all_nodes[1:], spec)
|
|
1592
|
-
return self._array_method_expr(arr, func_name, rest, spec)
|
|
2066
|
+
return self._array_method_expr(arr, func_name, rest, spec, node)
|
|
1593
2067
|
return "0"
|
|
1594
2068
|
|
|
1595
2069
|
# color.* calls
|
|
@@ -1699,21 +2173,26 @@ class CallVisitor:
|
|
|
1699
2173
|
if item["expr_node"] is ltf_expr_node:
|
|
1700
2174
|
return f"_req_sec_lower_tf_{item['sec_id']}"
|
|
1701
2175
|
return "std::vector<double>{}"
|
|
2176
|
+
# request.earnings / dividends / splits / financial reading the
|
|
2177
|
+
# series recorded under their key (external_requests).
|
|
2178
|
+
if RECORDED_KEY_ANNOTATION in (node.annotations or {}):
|
|
2179
|
+
return (f"_pf_recorded({self._recorded_key_expr(node)}, "
|
|
2180
|
+
f"_pf_rec_missing_{self._recorded_site(node)})")
|
|
1702
2181
|
# All other request.* functions
|
|
1703
2182
|
return "na<double>()"
|
|
1704
2183
|
|
|
1705
2184
|
# ticker.* calls
|
|
1706
2185
|
if namespace == "ticker":
|
|
1707
|
-
# ticker.inherit(
|
|
1708
|
-
#
|
|
2186
|
+
# ticker.inherit(from_tickerid, symbol) / ticker.standard(symbol) —
|
|
2187
|
+
# passthrough of the symbol (inherit names it second), and
|
|
2188
|
+
# ticker.heikinashi(symbol) — same-symbol HA: emit the symbol
|
|
1709
2189
|
# argument unchanged. The runtime HA candle transform is applied by
|
|
1710
2190
|
# the engine via register_security_eval's heikinashi flag, so the
|
|
1711
2191
|
# ticker value itself just needs to be the (string) chart symbol.
|
|
1712
2192
|
if func_name in ("inherit", "standard", "heikinashi"):
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
return self._visit_expr(node.kwargs["symbol"])
|
|
2193
|
+
symbol = ticker_symbol_arg(node)
|
|
2194
|
+
if symbol is not None:
|
|
2195
|
+
return self._visit_expr(symbol)
|
|
1717
2196
|
# All other ticker.* calls are hard-rejected by support_checker;
|
|
1718
2197
|
# emit empty string as safe fallback if they somehow reach codegen.
|
|
1719
2198
|
return 'std::string("")'
|
|
@@ -1766,8 +2245,11 @@ class CallVisitor:
|
|
|
1766
2245
|
and (node.args or node.kwargs)
|
|
1767
2246
|
):
|
|
1768
2247
|
params = sigs.get_param_names(None, func_name)
|
|
1769
|
-
|
|
2248
|
+
arg_nodes = _merge_kwargs(node.args, node.kwargs, params, lambda a: a)
|
|
2249
|
+
args = [self._visit_expr(a) for a in arg_nodes]
|
|
2250
|
+
ts_node = arg_nodes[0] if arg_nodes else None
|
|
1770
2251
|
ts_arg = args[0] if args else "current_bar_.timestamp"
|
|
2252
|
+
ts_arg = self._coerce_int_slot(ts_arg, ts_node, "int64_t")
|
|
1771
2253
|
tz_arg = args[1] if len(args) > 1 else None
|
|
1772
2254
|
field_expr = TIME_FIELD_EXPRS[func_name]
|
|
1773
2255
|
if tz_arg is None:
|
|
@@ -1785,7 +2267,22 @@ class CallVisitor:
|
|
|
1785
2267
|
# free of the per-call setenv+tzset churn (KI-35). field_expr is unused
|
|
1786
2268
|
# now (the engine applies the Pine offsets internally).
|
|
1787
2269
|
del field_expr
|
|
1788
|
-
|
|
2270
|
+
if func_name == "weekofyear":
|
|
2271
|
+
# A covered TradingView tape reports week 1 for an na
|
|
2272
|
+
# timestamp in both UTC and America/New_York; the engine's
|
|
2273
|
+
# epoch-zero week helper returns 0 / 52 respectively.
|
|
2274
|
+
return (
|
|
2275
|
+
f"([&](){{ int64_t _pf_calendar_ts = ({ts_arg}); "
|
|
2276
|
+
f"auto _pf_calendar_tz = ({tz_arg}); "
|
|
2277
|
+
f"if (is_na(_pf_calendar_ts)) return 1; "
|
|
2278
|
+
f"return pine_weekofyear(_pf_calendar_ts, "
|
|
2279
|
+
f"_pf_calendar_tz); }}())"
|
|
2280
|
+
)
|
|
2281
|
+
return (
|
|
2282
|
+
f"([&](){{ int64_t _pf_calendar_ts = ({ts_arg}); "
|
|
2283
|
+
f"return pine_{func_name}(is_na(_pf_calendar_ts) ? "
|
|
2284
|
+
f"int64_t(0) : _pf_calendar_ts, {tz_arg}); }}())"
|
|
2285
|
+
)
|
|
1789
2286
|
|
|
1790
2287
|
# time(timeframe) or time(timeframe, session[, tz])
|
|
1791
2288
|
#
|
|
@@ -1814,6 +2311,12 @@ class CallVisitor:
|
|
|
1814
2311
|
# to nothing against the base engine (legacy 5-arg call), so the same
|
|
1815
2312
|
# generated.cpp builds in every lab-experiment cell.
|
|
1816
2313
|
#
|
|
2314
|
+
if (func_name in ("time", "time_close") and namespace is None
|
|
2315
|
+
and (node.args or node.kwargs)):
|
|
2316
|
+
lowered = self._time_offset_call(func_name, node)
|
|
2317
|
+
if lowered is not None:
|
|
2318
|
+
return lowered
|
|
2319
|
+
node = self._check_time_bars_back(func_name, node)
|
|
1817
2320
|
if func_name == "time" and namespace is None and (node.args or node.kwargs):
|
|
1818
2321
|
args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names(None, "time"), self._visit_expr)
|
|
1819
2322
|
tf_e = args[0] if len(args) > 0 else 'script_tf_'
|
|
@@ -1886,13 +2389,18 @@ class CallVisitor:
|
|
|
1886
2389
|
"month, day[, hour, minute, second]).",
|
|
1887
2390
|
)
|
|
1888
2391
|
args = [self._visit_expr(a) for a in node.args]
|
|
2392
|
+
# Every calendar field lands in an ``int`` inside the lambda.
|
|
2393
|
+
fields = [
|
|
2394
|
+
self._coerce_int_slot(a, node.args[i], "int")
|
|
2395
|
+
for i, a in enumerate(args)
|
|
2396
|
+
]
|
|
1889
2397
|
tz = args[0]
|
|
1890
|
-
yr =
|
|
1891
|
-
mo =
|
|
1892
|
-
dy =
|
|
1893
|
-
hr =
|
|
1894
|
-
mn =
|
|
1895
|
-
sc =
|
|
2398
|
+
yr = fields[1] if len(fields) > 1 else "1970"
|
|
2399
|
+
mo = fields[2] if len(fields) > 2 else "1"
|
|
2400
|
+
dy = fields[3] if len(fields) > 3 else "1"
|
|
2401
|
+
hr = fields[4] if len(fields) > 4 else "0"
|
|
2402
|
+
mn = fields[5] if len(fields) > 5 else "0"
|
|
2403
|
+
sc = fields[6] if len(fields) > 6 else "0"
|
|
1896
2404
|
return _timestamp_calendar_lambda(tz, yr, mo, dy, hr, mn, sc)
|
|
1897
2405
|
else:
|
|
1898
2406
|
# Numeric form requires year, month, and day (hour/minute/
|
|
@@ -1911,7 +2419,10 @@ class CallVisitor:
|
|
|
1911
2419
|
"[, hour, minute, second]); the dateString "
|
|
1912
2420
|
"overload is not supported in PineForge.",
|
|
1913
2421
|
)
|
|
1914
|
-
args = [
|
|
2422
|
+
args = [
|
|
2423
|
+
self._coerce_int_slot(self._visit_expr(a), a, "int")
|
|
2424
|
+
for a in merged
|
|
2425
|
+
]
|
|
1915
2426
|
yr = args[0]
|
|
1916
2427
|
mo = args[1] if len(args) > 1 else "1"
|
|
1917
2428
|
dy = args[2] if len(args) > 2 else "1"
|
|
@@ -1941,37 +2452,55 @@ class CallVisitor:
|
|
|
1941
2452
|
# Type cast functions: int(x), float(x), bool(x), string(x)
|
|
1942
2453
|
if func_name == "int" and namespace is None and node.args:
|
|
1943
2454
|
# Pine int(na) → na (int form). Evaluate once, propagate na via
|
|
1944
|
-
# the engine's int sentinel instead of collapsing NaN to 0.
|
|
2455
|
+
# the engine's int sentinel instead of collapsing NaN to 0. Pine
|
|
2456
|
+
# int is 64-bit: int(time) keeps the epoch, so a wide argument
|
|
2457
|
+
# (``_wide_int_provenance``) casts to int64_t.
|
|
1945
2458
|
x = self._visit_expr(node.args[0])
|
|
1946
|
-
|
|
1947
|
-
|
|
2459
|
+
owner = self._func_info_map.get(
|
|
2460
|
+
getattr(self, "_active_func_name", "") or "")
|
|
2461
|
+
if self._expr_returns_wide_int(
|
|
2462
|
+
node.args[0], owner, set(),
|
|
2463
|
+
getattr(self, "_active_call_site_idx", None)):
|
|
2464
|
+
return na_preserving_int_cast(x, "int64_t")
|
|
2465
|
+
return na_preserving_int_cast(x)
|
|
1948
2466
|
if func_name == "float" and namespace is None and node.args:
|
|
1949
2467
|
return f"(double)({self._visit_expr(node.args[0])})"
|
|
1950
2468
|
if func_name == "bool" and namespace is None and node.args:
|
|
1951
2469
|
# Pine v6 bools are two-state. Explicit bool(int/float) treats na
|
|
1952
2470
|
# like false, while a raw C++ cast would make NaN truthy.
|
|
1953
2471
|
x = self._visit_expr(node.args[0])
|
|
1954
|
-
return (
|
|
1955
|
-
f"[&](){{ auto _pf_v = ({x}); "
|
|
1956
|
-
f"using _pf_t = std::decay_t<decltype(_pf_v)>; "
|
|
1957
|
-
f"if constexpr (std::is_same_v<_pf_t, bool>) {{ return _pf_v; }} "
|
|
1958
|
-
f"else {{ return is_na(_pf_v) ? false : (bool)_pf_v; }} }}()"
|
|
1959
|
-
)
|
|
2472
|
+
return pine_truth_cast(x)
|
|
1960
2473
|
if func_name == "string" and namespace is None and node.args:
|
|
1961
2474
|
# Pine string(x) cast — same emission as str.tostring(x), with
|
|
1962
2475
|
# string passthrough and TV-style "true"/"false" for bools
|
|
1963
2476
|
# (std::to_string would reject strings / render bools as 0/1).
|
|
1964
2477
|
arg = node.args[0]
|
|
2478
|
+
if self._is_na_expr(arg):
|
|
2479
|
+
# ``string(na)`` types an ``na`` as a string (``[string(na),
|
|
2480
|
+
# string(na)]`` arms of a tuple): the string na, not
|
|
2481
|
+
# ``str.tostring(na)`` ("NaN"), whose formatter a script
|
|
2482
|
+
# without str.tostring does not declare.
|
|
2483
|
+
return "na<std::string>()"
|
|
1965
2484
|
inferred = self._infer_type(arg)
|
|
1966
2485
|
if inferred == "std::string":
|
|
1967
2486
|
return self._visit_expr(arg)
|
|
1968
2487
|
if inferred == "bool":
|
|
2488
|
+
if self._pine_v5_body:
|
|
2489
|
+
self._refuse_v5_bool_na_observer(node, "string()", [arg])
|
|
1969
2490
|
visited = self._visit_expr(arg)
|
|
1970
2491
|
return f'(({visited}) ? std::string("true") : std::string("false"))'
|
|
1971
2492
|
return self._visit_str_call("tostring", node)
|
|
1972
2493
|
|
|
1973
2494
|
# ta.pivot_point_levels — free function, not a stateful indicator
|
|
1974
2495
|
if namespace == "ta" and func_name == "pivot_point_levels":
|
|
2496
|
+
routed_site = self._get_ta_site(node)
|
|
2497
|
+
if routed_site is not None:
|
|
2498
|
+
compute_args = self._ta_compute_args_for_site(routed_site)
|
|
2499
|
+
ta_mem = self._ta_member_name(routed_site)
|
|
2500
|
+
return (
|
|
2501
|
+
f"(history_advances_new_bar() ? {ta_mem}.compute({compute_args}) "
|
|
2502
|
+
f": {ta_mem}.recompute({compute_args}))"
|
|
2503
|
+
)
|
|
1975
2504
|
if node.kwargs:
|
|
1976
2505
|
args = _merge_kwargs(
|
|
1977
2506
|
node.args,
|
|
@@ -2024,7 +2553,13 @@ class CallVisitor:
|
|
|
2024
2553
|
init = args_e[2] if len(args_e) > 2 else "0.0"
|
|
2025
2554
|
return f"PineMatrix::new_({rows}, {cols}, {init})"
|
|
2026
2555
|
cpp_t = self._type_spec_to_cpp(elem_spec)
|
|
2027
|
-
|
|
2556
|
+
if len(node.args) > 2:
|
|
2557
|
+
self._warn_narrow_int_element(
|
|
2558
|
+
node, TypeSpec.matrix(elem_spec), node.args[2])
|
|
2559
|
+
init = (
|
|
2560
|
+
self._array_init_value_expr(elem_spec, node.args[2])
|
|
2561
|
+
if len(args_e) > 2 else self._default_for_spec(elem_spec)
|
|
2562
|
+
)
|
|
2028
2563
|
return f"PineGenericMatrix<{cpp_t}>::new_({rows}, {cols}, {init})"
|
|
2029
2564
|
if func_name in MATRIX_METHODS and node.args:
|
|
2030
2565
|
from ..ast_nodes import Identifier as _Ident
|
|
@@ -2044,10 +2579,16 @@ class CallVisitor:
|
|
|
2044
2579
|
self._check_matrix_method_allowed(func_name, recv_spec, node)
|
|
2045
2580
|
obj = self._visit_expr(node.args[0])
|
|
2046
2581
|
param_names = MATRIX_METHOD_KWARGS.get(func_name)
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2582
|
+
raw_rest = (
|
|
2583
|
+
_merge_kwargs(node.args[1:], node.kwargs, param_names, lambda a: a)
|
|
2584
|
+
if param_names else list(node.args[1:])
|
|
2585
|
+
)
|
|
2586
|
+
rest = self._matrix_bool_value_args(
|
|
2587
|
+
func_name,
|
|
2588
|
+
[self._visit_expr(a) for a in raw_rest],
|
|
2589
|
+
raw_rest,
|
|
2590
|
+
self._type_spec_from_expr(node.args[0]),
|
|
2591
|
+
)
|
|
2051
2592
|
fn = MATRIX_METHODS[func_name]
|
|
2052
2593
|
try:
|
|
2053
2594
|
return fn(obj, rest)
|
|
@@ -2059,13 +2600,21 @@ class CallVisitor:
|
|
|
2059
2600
|
)
|
|
2060
2601
|
return "0.0"
|
|
2061
2602
|
|
|
2062
|
-
# log.* calls (log.error, log.warning, log.info)
|
|
2603
|
+
# log.* calls (log.error, log.warning, log.info). TradingView: "The
|
|
2604
|
+
# second overloads of all log.*() functions have the same parameter
|
|
2605
|
+
# signature and formatting behaviors as str.format()": log.info(fmt,
|
|
2606
|
+
# arg0, ...) logs str.format(fmt, arg0, ...). The one-argument
|
|
2607
|
+
# log.info(message) logs its message as is.
|
|
2063
2608
|
if namespace == "log":
|
|
2064
2609
|
log_funcs = {"info": "pine_log_info", "warning": "pine_log_warning", "error": "pine_log_error"}
|
|
2065
2610
|
if func_name in log_funcs:
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2611
|
+
message = (node.args[0] if node.args
|
|
2612
|
+
else node.kwargs.get("message", node.kwargs.get("formatString")))
|
|
2613
|
+
if message is None:
|
|
2614
|
+
return f'{log_funcs[func_name]}("")'
|
|
2615
|
+
if len(node.args) > 1:
|
|
2616
|
+
return f'{log_funcs[func_name]}({self._str_format_expr(message, node.args[1:])})'
|
|
2617
|
+
return f'{log_funcs[func_name]}({self._visit_expr(message)})'
|
|
2069
2618
|
return '"" /* unsupported log */'
|
|
2070
2619
|
|
|
2071
2620
|
# timeframe.* calls (e.g., timeframe.change) — not supported in single-TF backtest
|
|
@@ -2130,14 +2679,23 @@ class CallVisitor:
|
|
|
2130
2679
|
),
|
|
2131
2680
|
)
|
|
2132
2681
|
if f_cpp_type == "int64_t":
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2682
|
+
val = self._coerce_int_slot(val, value_node, "int64_t")
|
|
2683
|
+
if val == "na<double>()":
|
|
2684
|
+
val = "na<int64_t>()"
|
|
2685
|
+
elif f_cpp_type == "bool":
|
|
2686
|
+
val = self._coerce_bool_expr(val, value_node)
|
|
2137
2687
|
elif (self._is_nullable_collection_cpp_type(f_cpp_type)
|
|
2138
2688
|
and val == "na<double>()"):
|
|
2139
2689
|
val = f"{f_cpp_type}{{}}"
|
|
2140
|
-
|
|
2690
|
+
array_field = self._udt_array_field_cpp(
|
|
2691
|
+
field_specs.get(f.name)
|
|
2692
|
+
or self._type_spec_from_hint_name(f.type_name),
|
|
2693
|
+
namespace)
|
|
2694
|
+
if array_field is not None:
|
|
2695
|
+
val = (f"{array_field}()" if val == "na<double>()"
|
|
2696
|
+
else self._udt_array_field_value(
|
|
2697
|
+
array_field, value_node, val))
|
|
2698
|
+
field_inits.append(f".{self._safe_name(f.name)} = {val}")
|
|
2141
2699
|
return self._emit_udt_new_expr(namespace, field_inits)
|
|
2142
2700
|
|
|
2143
2701
|
# UDT copy: TypeName.copy(obj)
|
|
@@ -2236,8 +2794,7 @@ class CallVisitor:
|
|
|
2236
2794
|
== expected_cpp_type
|
|
2237
2795
|
):
|
|
2238
2796
|
return safe
|
|
2239
|
-
|
|
2240
|
-
safe = self._active_var_remap[safe]
|
|
2797
|
+
safe = self._call_site_var_name(arg_node, safe)
|
|
2241
2798
|
if (
|
|
2242
2799
|
not is_current_series_param
|
|
2243
2800
|
and self._binding_is_series(aname, safe)
|
|
@@ -2293,7 +2850,20 @@ class CallVisitor:
|
|
|
2293
2850
|
return self._visit_expr(arg_node)
|
|
2294
2851
|
|
|
2295
2852
|
ordered_arg_nodes: list = []
|
|
2296
|
-
|
|
2853
|
+
defaulted_arg_nodes = (
|
|
2854
|
+
self._user_call_args_with_defaults(func_name, node)
|
|
2855
|
+
if namespace is None and func_name in self._func_names
|
|
2856
|
+
else None
|
|
2857
|
+
)
|
|
2858
|
+
if defaulted_arg_nodes is not None:
|
|
2859
|
+
# Omitted parameters take their declared defaults, and keywords
|
|
2860
|
+
# land on their own parameters past any omitted one.
|
|
2861
|
+
ordered_arg_nodes = list(defaulted_arg_nodes)
|
|
2862
|
+
all_args = [
|
|
2863
|
+
_visit_arg_for_series(a, i)
|
|
2864
|
+
for i, a in enumerate(defaulted_arg_nodes)
|
|
2865
|
+
]
|
|
2866
|
+
elif node.kwargs:
|
|
2297
2867
|
# Try to resolve kwargs using FuncInfo params for user-defined functions
|
|
2298
2868
|
fi = self._func_info_map.get(func_name)
|
|
2299
2869
|
if fi and fi.node and fi.node.params:
|
|
@@ -2334,6 +2904,16 @@ class CallVisitor:
|
|
|
2334
2904
|
# arg that is exactly such a drawing-style constant read.
|
|
2335
2905
|
if namespace is None and func_name in self._func_names:
|
|
2336
2906
|
self._coerce_drawing_style_string_args(func_name, node.args, all_args)
|
|
2907
|
+
# A double-valued argument bound to an integer parameter narrows at
|
|
2908
|
+
# the call boundary, which is undefined for na.
|
|
2909
|
+
self._coerce_int_param_args(
|
|
2910
|
+
func_name, ordered_arg_nodes, all_args,
|
|
2911
|
+
self.ctx.func_call_cs_map.get(id(node), (None, None))[1],
|
|
2912
|
+
)
|
|
2913
|
+
self._coerce_bool_param_args(
|
|
2914
|
+
func_name, ordered_arg_nodes, all_args,
|
|
2915
|
+
self.ctx.func_call_cs_map.get(id(node), (None, None))[1],
|
|
2916
|
+
)
|
|
2337
2917
|
# Default args (parser does not store defaults): isInSession(sess, res = timeframe.period)
|
|
2338
2918
|
if namespace is None and func_name in self._func_names:
|
|
2339
2919
|
fi = self._func_info_map.get(func_name)
|
|
@@ -2377,9 +2957,41 @@ class CallVisitor:
|
|
|
2377
2957
|
ordered_arg_nodes,
|
|
2378
2958
|
all_args,
|
|
2379
2959
|
source_order_nodes=[*node.args, *node.kwargs.values()],
|
|
2960
|
+
force_stage=self._binds_fresh_array_to_reference(
|
|
2961
|
+
self._func_info_map.get(func_name), ordered_arg_nodes),
|
|
2380
2962
|
)
|
|
2381
2963
|
return f"{call_head}({', '.join(all_args)})"
|
|
2382
2964
|
|
|
2965
|
+
def _coerce_int_param_args(self, func_name, arg_nodes, all_args,
|
|
2966
|
+
call_site_idx) -> None:
|
|
2967
|
+
"""In-place narrow every argument bound to an integer parameter.
|
|
2968
|
+
|
|
2969
|
+
Pine ``color`` parameters are integers in the emitted signature too, so
|
|
2970
|
+
a ``na`` colour argument (``na<double>()``) is a NaN crossing into an
|
|
2971
|
+
``int`` — undefined, and it defeats ``is_na`` on the other side."""
|
|
2972
|
+
fi = self._func_info_map.get(func_name)
|
|
2973
|
+
if not fi or not getattr(fi, "node", None) or not fi.node.params:
|
|
2974
|
+
return
|
|
2975
|
+
for i, arg in enumerate(arg_nodes):
|
|
2976
|
+
if i >= len(all_args):
|
|
2977
|
+
break
|
|
2978
|
+
int_cpp = self._func_param_int_cpp_type(fi, i, call_site_idx)
|
|
2979
|
+
if int_cpp is None:
|
|
2980
|
+
continue
|
|
2981
|
+
all_args[i] = self._coerce_int_slot(all_args[i], arg, int_cpp)
|
|
2982
|
+
|
|
2983
|
+
def _coerce_bool_param_args(self, func_name, arg_nodes, all_args,
|
|
2984
|
+
call_site_idx) -> None:
|
|
2985
|
+
"""Apply Pine's false-for-``na`` rule at bool UDF parameters."""
|
|
2986
|
+
fi = self._func_info_map.get(func_name)
|
|
2987
|
+
if not fi or not getattr(fi, "node", None) or not fi.node.params:
|
|
2988
|
+
return
|
|
2989
|
+
for i, arg in enumerate(arg_nodes):
|
|
2990
|
+
if i >= len(all_args) or arg is None:
|
|
2991
|
+
continue
|
|
2992
|
+
if self._func_param_is_bool(fi, i, call_site_idx):
|
|
2993
|
+
all_args[i] = self._coerce_bool_expr(all_args[i], arg)
|
|
2994
|
+
|
|
2383
2995
|
def _coerce_drawing_style_string_args(self, func_name, arg_nodes, all_args) -> None:
|
|
2384
2996
|
"""In-place coerce positional args bound to a ``std::string`` user-function
|
|
2385
2997
|
parameter that lowered to the bare token ``"0"`` from a drawing-style /
|
|
@@ -2436,8 +3048,15 @@ class CallVisitor:
|
|
|
2436
3048
|
else:
|
|
2437
3049
|
self._fixnan_counter += 1
|
|
2438
3050
|
member = f"_prev_fixnan_{self._fixnan_counter}"
|
|
3051
|
+
# x is evaluated exactly once, as in ``nz``: a stateful call in it
|
|
3052
|
+
# (``fixnan(100 * ta.rma(plusDM, len) / tr)``, TradingView's DMI) used
|
|
3053
|
+
# to run twice per bar, once for the is_na() test and once for the
|
|
3054
|
+
# store, double-stepping the RMA.
|
|
2439
3055
|
x = self._visit_expr(node.args[0])
|
|
2440
|
-
return
|
|
3056
|
+
return (
|
|
3057
|
+
f"([&]{{ auto _fixnan_v = ({x}); "
|
|
3058
|
+
f"return is_na(_fixnan_v) ? {member} : ({member} = _fixnan_v); }}())"
|
|
3059
|
+
)
|
|
2441
3060
|
|
|
2442
3061
|
@staticmethod
|
|
2443
3062
|
def _strategy_close_callsite_token(node: FuncCall) -> str:
|
|
@@ -2470,7 +3089,13 @@ class CallVisitor:
|
|
|
2470
3089
|
if func_name == "entry":
|
|
2471
3090
|
p = self._resolve_func_args(node, "strategy.entry")
|
|
2472
3091
|
entry_id = self._visit_expr(p.get("id")) if "id" in p else '""'
|
|
2473
|
-
|
|
3092
|
+
direction_node = p.get("direction")
|
|
3093
|
+
direction = (
|
|
3094
|
+
self._coerce_bool_expr(
|
|
3095
|
+
self._visit_expr(direction_node), direction_node
|
|
3096
|
+
)
|
|
3097
|
+
if direction_node is not None else "true"
|
|
3098
|
+
)
|
|
2474
3099
|
stop = p.get("stop")
|
|
2475
3100
|
limit = p.get("limit")
|
|
2476
3101
|
qty = p.get("qty")
|
|
@@ -2501,14 +3126,26 @@ class CallVisitor:
|
|
|
2501
3126
|
comment = self._visit_expr(p.get("comment")) if p.get("comment") is not None else '""'
|
|
2502
3127
|
qty = self._visit_expr(p.get("qty")) if p.get("qty") is not None else "na<double>()"
|
|
2503
3128
|
qty_pct = self._visit_expr(p.get("qty_percent")) if p.get("qty_percent") is not None else "na<double>()"
|
|
2504
|
-
|
|
3129
|
+
immediately_node = p.get("immediately")
|
|
3130
|
+
immediately = (
|
|
3131
|
+
self._coerce_bool_expr(
|
|
3132
|
+
self._visit_expr(immediately_node), immediately_node
|
|
3133
|
+
)
|
|
3134
|
+
if immediately_node is not None else "false"
|
|
3135
|
+
)
|
|
2505
3136
|
callsite = self._strategy_close_callsite_token(node)
|
|
2506
3137
|
return f"strategy_close({close_id}, {comment}, {qty}, {qty_pct}, {immediately}, {callsite})"
|
|
2507
3138
|
|
|
2508
3139
|
if func_name == "close_all":
|
|
2509
3140
|
p = self._resolve_func_args(node, "strategy.close_all")
|
|
2510
3141
|
comment = self._visit_expr(p.get("comment")) if p.get("comment") is not None else '""'
|
|
2511
|
-
|
|
3142
|
+
immediately_node = p.get("immediately")
|
|
3143
|
+
immediately = (
|
|
3144
|
+
self._coerce_bool_expr(
|
|
3145
|
+
self._visit_expr(immediately_node), immediately_node
|
|
3146
|
+
)
|
|
3147
|
+
if immediately_node is not None else "false"
|
|
3148
|
+
)
|
|
2512
3149
|
# The engine's ID-less strategy_close path closes the complete
|
|
2513
3150
|
# position. Reuse it so close_all preserves the Pine order comment
|
|
2514
3151
|
# and same-tick fill flag instead of silently discarding both.
|
|
@@ -2555,8 +3192,8 @@ class CallVisitor:
|
|
|
2555
3192
|
return (f"strategy_exit({exit_id}, {from_id}, {limit_val}, {stop_val}, "
|
|
2556
3193
|
f"{trail_pts}, {trail_off}, {trail_pr}, {qty_pct}, {comment}, "
|
|
2557
3194
|
f"{qty_val}, {oca_val}, {profit_ticks}, {loss_ticks})")
|
|
2558
|
-
|
|
2559
|
-
return f"
|
|
3195
|
+
comment = self._visit_expr(comment_n) if comment_n is not None else '""'
|
|
3196
|
+
return f"strategy_exit_cancel_bracket({exit_id}, {from_id}, {comment})"
|
|
2560
3197
|
|
|
2561
3198
|
if func_name == "cancel":
|
|
2562
3199
|
p = self._resolve_func_args(node, "strategy.close") # same shape: id first
|
|
@@ -2569,7 +3206,13 @@ class CallVisitor:
|
|
|
2569
3206
|
if func_name == "order":
|
|
2570
3207
|
p = self._resolve_func_args(node, "strategy.order")
|
|
2571
3208
|
order_id = self._visit_expr(p.get("id")) if "id" in p else '""'
|
|
2572
|
-
|
|
3209
|
+
direction_node = p.get("direction")
|
|
3210
|
+
direction = (
|
|
3211
|
+
self._coerce_bool_expr(
|
|
3212
|
+
self._visit_expr(direction_node), direction_node
|
|
3213
|
+
)
|
|
3214
|
+
if direction_node is not None else "true"
|
|
3215
|
+
)
|
|
2573
3216
|
qty = self._visit_expr(p.get("qty")) if "qty" in p else "0"
|
|
2574
3217
|
limit_arg = self._visit_expr(p.get("limit")) if "limit" in p else "na<double>()"
|
|
2575
3218
|
stop_arg = self._visit_expr(p.get("stop")) if "stop" in p else "na<double>()"
|
|
@@ -2585,7 +3228,14 @@ class CallVisitor:
|
|
|
2585
3228
|
if isinstance(node.callee, MemberAccess):
|
|
2586
3229
|
inner = node.callee.object
|
|
2587
3230
|
if isinstance(inner, MemberAccess) and inner.member in ("closedtrades", "opentrades"):
|
|
2588
|
-
|
|
3231
|
+
# The engine's accessors take an ``int`` trade index; a
|
|
3232
|
+
# double-valued Pine expression narrows into it.
|
|
3233
|
+
idx = (
|
|
3234
|
+
self._coerce_int_slot(
|
|
3235
|
+
self._visit_expr(node.args[0]), node.args[0], "int",
|
|
3236
|
+
)
|
|
3237
|
+
if node.args else "0"
|
|
3238
|
+
)
|
|
2589
3239
|
is_open = inner.member == "opentrades"
|
|
2590
3240
|
# Open trades have no exit metadata in Pine
|
|
2591
3241
|
if is_open and func_name in (
|
|
@@ -2634,12 +3284,100 @@ class CallVisitor:
|
|
|
2634
3284
|
f"have rejected. Add a handler above or extend STRATEGY_FUNCTIONS."
|
|
2635
3285
|
)
|
|
2636
3286
|
|
|
3287
|
+
# TradingView's parameter order of color.from_gradient.
|
|
3288
|
+
_FROM_GRADIENT_PARAMS = (
|
|
3289
|
+
"value", "bottom_value", "top_value", "bottom_color", "top_color",
|
|
3290
|
+
)
|
|
3291
|
+
# TradingView's parameter names of the other color.* functions.
|
|
3292
|
+
_COLOR_PARAMS = {
|
|
3293
|
+
"new": ("color", "transp"),
|
|
3294
|
+
"rgb": ("red", "green", "blue", "transp"),
|
|
3295
|
+
"r": ("color",), "g": ("color",), "b": ("color",), "t": ("color",),
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
def _color_arg_nodes(self, func_name: str, node) -> list:
|
|
3299
|
+
"""The argument nodes of a ``color.*`` call in parameter order: the
|
|
3300
|
+
positional ones, then each keyword in its parameter's slot, up to the
|
|
3301
|
+
last one given (``color.new(transp = 40, color = c)`` is
|
|
3302
|
+
``color.new(c, 40)``). A slot nothing fills is None."""
|
|
3303
|
+
params = self._COLOR_PARAMS.get(func_name, ())
|
|
3304
|
+
bound = list(node.args)
|
|
3305
|
+
for index, name in enumerate(params):
|
|
3306
|
+
if index >= len(node.args) and name in node.kwargs:
|
|
3307
|
+
bound += [None] * (index + 1 - len(bound))
|
|
3308
|
+
bound[index] = node.kwargs[name]
|
|
3309
|
+
return bound
|
|
3310
|
+
|
|
3311
|
+
# Builtins whose call has no state and no side effect.
|
|
3312
|
+
_PURE_CALL_NAMESPACES = frozenset({"color", "math", "str"})
|
|
3313
|
+
_PURE_CALLS = frozenset({"na", "nz", "int", "float", "bool", "string"})
|
|
3314
|
+
|
|
3315
|
+
def _may_have_effects(self, node) -> bool:
|
|
3316
|
+
"""Whether evaluating ``node`` can change anything or keep state: it
|
|
3317
|
+
calls a user function, a ``ta.*`` / ``request.*`` builtin, an
|
|
3318
|
+
array or map method, ... -- anything but a pure builtin."""
|
|
3319
|
+
for sub in self._walk_ast(node):
|
|
3320
|
+
if isinstance(sub, FuncCall):
|
|
3321
|
+
func_name, namespace = self._resolve_callee(sub.callee)
|
|
3322
|
+
if namespace in self._PURE_CALL_NAMESPACES:
|
|
3323
|
+
continue
|
|
3324
|
+
if namespace is None and func_name in self._PURE_CALLS \
|
|
3325
|
+
and func_name not in self._func_names:
|
|
3326
|
+
continue
|
|
3327
|
+
return True
|
|
3328
|
+
return False
|
|
3329
|
+
|
|
2637
3330
|
def _visit_color_call(self, func_name: str, node) -> str:
|
|
2638
3331
|
"""Emit color.* calls as integer representations."""
|
|
2639
|
-
|
|
3332
|
+
if func_name == "from_gradient":
|
|
3333
|
+
# Visual only: the colour it picks reaches no order, so the call
|
|
3334
|
+
# yields the na colour. Its arguments still run once each, in
|
|
3335
|
+
# parameter order, as on TradingView -- a stateful call in one
|
|
3336
|
+
# (a user function keeping a series, a ta.* compute(), a counter)
|
|
3337
|
+
# advances on every bar (lab tv probe pf-oi-from-gradient-args).
|
|
3338
|
+
arguments = _merge_kwargs(node.args, node.kwargs,
|
|
3339
|
+
list(self._FROM_GRADIENT_PARAMS), lambda a: a)
|
|
3340
|
+
effects = []
|
|
3341
|
+
for argument in arguments:
|
|
3342
|
+
cpp = self._visit_expr(argument)
|
|
3343
|
+
if self._may_have_effects(argument) and not cpp_is_plain_read(cpp):
|
|
3344
|
+
effects.append(cpp)
|
|
3345
|
+
if not effects:
|
|
3346
|
+
return "0"
|
|
3347
|
+
return ("([&]() -> int64_t { "
|
|
3348
|
+
+ " ".join(f"(void)({cpp});" for cpp in effects)
|
|
3349
|
+
+ " return 0; }())")
|
|
3350
|
+
# Keyword arguments take their parameter's slot; TradingView evaluates
|
|
3351
|
+
# the arguments in parameter order, as the lowerings below do.
|
|
3352
|
+
arg_nodes = self._color_arg_nodes(func_name, node)
|
|
3353
|
+
if any(a is None for a in arg_nodes):
|
|
3354
|
+
return "0"
|
|
3355
|
+
args = [self._visit_expr(a) for a in arg_nodes]
|
|
2640
3356
|
if func_name == "new":
|
|
2641
3357
|
if len(args) >= 2:
|
|
2642
|
-
|
|
3358
|
+
base = self._coerce_int_slot(
|
|
3359
|
+
args[0], arg_nodes[0], "int64_t",
|
|
3360
|
+
)
|
|
3361
|
+
whole = _whole_transparency(arg_nodes[1])
|
|
3362
|
+
alpha = color_alpha_cast(args[1], whole)
|
|
3363
|
+
base_node = arg_nodes[0]
|
|
3364
|
+
if (isinstance(base_node, ColorLiteral)
|
|
3365
|
+
or (isinstance(base_node, MemberAccess)
|
|
3366
|
+
and isinstance(base_node.object, Identifier)
|
|
3367
|
+
and base_node.object.name == "color")):
|
|
3368
|
+
# A literal color is never na; preserve the compact
|
|
3369
|
+
# historical spelling (including input.color defaults).
|
|
3370
|
+
return f"pine_color::new_color({base}, {alpha})"
|
|
3371
|
+
# A color variable or ``na`` may carry the integer sentinel.
|
|
3372
|
+
# Evaluate both authored arguments once, then preserve that
|
|
3373
|
+
# sentinel instead of letting new_color mask it into a value.
|
|
3374
|
+
return (
|
|
3375
|
+
f"[&]() -> int64_t {{ auto _pf_color_base = ({base}); "
|
|
3376
|
+
f"auto _pf_color_alpha = ({args[1]}); "
|
|
3377
|
+
f"if (is_na(_pf_color_base)) return na<int64_t>(); "
|
|
3378
|
+
f"return pine_color::new_color(_pf_color_base, "
|
|
3379
|
+
f"{color_alpha_cast('_pf_color_alpha', whole)}); }}()"
|
|
3380
|
+
)
|
|
2643
3381
|
return "0"
|
|
2644
3382
|
if func_name in ("r", "g", "b", "t"):
|
|
2645
3383
|
if args:
|
|
@@ -2647,17 +3385,72 @@ class CallVisitor:
|
|
|
2647
3385
|
return "0"
|
|
2648
3386
|
if func_name == "rgb":
|
|
2649
3387
|
if len(args) >= 4:
|
|
2650
|
-
|
|
3388
|
+
channels = [
|
|
3389
|
+
self._coerce_int_slot(args[i], arg_nodes[i], "int64_t")
|
|
3390
|
+
for i in range(3)
|
|
3391
|
+
]
|
|
3392
|
+
alpha = color_alpha_cast(args[3], _whole_transparency(arg_nodes[3]))
|
|
3393
|
+
return (
|
|
3394
|
+
"pine_color::new_color(static_cast<int64_t>("
|
|
3395
|
+
f"(static_cast<uint64_t>({channels[0]}) & 0xFFULL) << 16 | "
|
|
3396
|
+
f"(static_cast<uint64_t>({channels[1]}) & 0xFFULL) << 8 | "
|
|
3397
|
+
f"(static_cast<uint64_t>({channels[2]}) & 0xFFULL)), {alpha})"
|
|
3398
|
+
)
|
|
2651
3399
|
elif len(args) >= 3:
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
3400
|
+
channels = [
|
|
3401
|
+
self._coerce_int_slot(args[i], arg_nodes[i], "int64_t")
|
|
3402
|
+
for i in range(3)
|
|
3403
|
+
]
|
|
3404
|
+
return (
|
|
3405
|
+
"pine_color::new_color(static_cast<int64_t>("
|
|
3406
|
+
f"(static_cast<uint64_t>({channels[0]}) & 0xFFULL) << 16 | "
|
|
3407
|
+
f"(static_cast<uint64_t>({channels[1]}) & 0xFFULL) << 8 | "
|
|
3408
|
+
f"(static_cast<uint64_t>({channels[2]}) & 0xFFULL)), 0)"
|
|
3409
|
+
)
|
|
2655
3410
|
return "0"
|
|
2656
3411
|
return "0"
|
|
2657
3412
|
|
|
3413
|
+
def _str_format_expr(self, fmt_node, arg_nodes) -> str:
|
|
3414
|
+
"""Shared TradingView MessageFormat lowering for str.format and log.*."""
|
|
3415
|
+
if self._pine_v5_body:
|
|
3416
|
+
self._refuse_v5_bool_na_observer(fmt_node, "str.format()", arg_nodes)
|
|
3417
|
+
fmt_arg = self._visit_expr(fmt_node)
|
|
3418
|
+
rest = [f"_PFTvFormatValue({self._visit_expr(orig)})" for orig in arg_nodes]
|
|
3419
|
+
vec = "{" + ", ".join(rest) + "}"
|
|
3420
|
+
return f'pine_str_format_tv({fmt_arg}, {vec})'
|
|
3421
|
+
|
|
2658
3422
|
def _visit_str_call(self, func_name: str, node) -> str:
|
|
2659
3423
|
args = _merge_kwargs(node.args, node.kwargs,
|
|
2660
3424
|
sigs.get_param_names("str", func_name), self._visit_expr)
|
|
3425
|
+
arg_nodes = _merge_kwargs(
|
|
3426
|
+
node.args, node.kwargs,
|
|
3427
|
+
sigs.get_param_names("str", func_name), lambda a: a,
|
|
3428
|
+
)
|
|
3429
|
+
|
|
3430
|
+
if func_name == "repeat":
|
|
3431
|
+
if len(args) < 2:
|
|
3432
|
+
self._codegen_warning(
|
|
3433
|
+
node,
|
|
3434
|
+
"str.repeat is missing its count argument; PineForge "
|
|
3435
|
+
"emits an empty string for this invalid TradingView call.",
|
|
3436
|
+
)
|
|
3437
|
+
return 'std::string("")'
|
|
3438
|
+
count_node = arg_nodes[1] if len(arg_nodes) > 1 else None
|
|
3439
|
+
if (count_node is not None
|
|
3440
|
+
and not isinstance(count_node, (NumberLiteral, BoolLiteral))):
|
|
3441
|
+
warned = getattr(self, "_warned_repeat_na_sites", None)
|
|
3442
|
+
if warned is None:
|
|
3443
|
+
warned = self._warned_repeat_na_sites = set()
|
|
3444
|
+
if id(node) not in warned:
|
|
3445
|
+
warned.add(id(node))
|
|
3446
|
+
self._codegen_warning(
|
|
3447
|
+
count_node,
|
|
3448
|
+
"str.repeat count can be na: PineForge returns an empty "
|
|
3449
|
+
"string for that value, while TradingView returns na "
|
|
3450
|
+
"string. The engine has no nullable string value.",
|
|
3451
|
+
hint="Check na(count) before calling str.repeat when "
|
|
3452
|
+
"the result affects strategy logic.",
|
|
3453
|
+
)
|
|
2661
3454
|
|
|
2662
3455
|
if func_name == "tostring":
|
|
2663
3456
|
# Pine: str.tostring(enumVar) → field title / IANA string, not the int index
|
|
@@ -2673,56 +3466,45 @@ class CallVisitor:
|
|
|
2673
3466
|
return (
|
|
2674
3467
|
f"pine_enum_str_at({et}_str_values, {n}, {var})"
|
|
2675
3468
|
)
|
|
3469
|
+
inferred = self._infer_type(val_arg) if val_arg is not None else ""
|
|
3470
|
+
if inferred == "std::string":
|
|
3471
|
+
return args[0] if args else 'std::string("")'
|
|
3472
|
+
if inferred == "bool":
|
|
3473
|
+
if self._pine_v5_body:
|
|
3474
|
+
self._refuse_v5_bool_na_observer(node, "str.tostring()", [val_arg])
|
|
3475
|
+
return (f'({args[0]} ? std::string("true") : '
|
|
3476
|
+
'std::string("false"))')
|
|
2676
3477
|
if len(args) >= 2:
|
|
2677
|
-
return f"
|
|
3478
|
+
return f"pine_str_tostring_tv({args[0]}, {args[1]}, syminfo_mintick_)"
|
|
2678
3479
|
if len(args) >= 1:
|
|
2679
|
-
return f"
|
|
3480
|
+
return f"pine_str_tostring_tv({args[0]}, std::string(), syminfo_mintick_)"
|
|
2680
3481
|
return 'std::string("")'
|
|
2681
3482
|
|
|
2682
3483
|
if func_name == "substring":
|
|
2683
3484
|
if len(args) == 3:
|
|
2684
|
-
|
|
3485
|
+
# begin_pos is read twice: evaluate it once.
|
|
3486
|
+
return evaluate_args_once(
|
|
3487
|
+
args, (1,), lambda a: f"{a[0]}.substr({a[1]}, {a[2]} - {a[1]})",
|
|
3488
|
+
"_pf_str_a")
|
|
2685
3489
|
elif len(args) == 2:
|
|
2686
3490
|
return f"{args[0]}.substr({args[1]})"
|
|
2687
3491
|
return 'std::string("")'
|
|
2688
3492
|
|
|
2689
3493
|
if func_name == "format":
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
# ``std::to_string`` wrap on a source-text-prefix heuristic
|
|
2695
|
-
# (``"`` / ``std::string`` / ``pine_str``), which mis-classified
|
|
2696
|
-
# any string-typed bare identifier or string-returning helper
|
|
2697
|
-
# call (e.g. ``str.tostring(x)`` bound to a variable). The type
|
|
2698
|
-
# check below uses the analyzer's inferred PineType instead so
|
|
2699
|
-
# ``std::string`` args pass through unchanged.
|
|
2700
|
-
if node.args:
|
|
2701
|
-
fmt_arg = self._visit_expr(node.args[0])
|
|
2702
|
-
rest = []
|
|
2703
|
-
for orig in node.args[1:]:
|
|
2704
|
-
visited = self._visit_expr(orig)
|
|
2705
|
-
inferred = self._infer_type(orig)
|
|
2706
|
-
if inferred == "std::string":
|
|
2707
|
-
rest.append(visited)
|
|
2708
|
-
continue
|
|
2709
|
-
# Booleans render as 0/1 via std::to_string; force the
|
|
2710
|
-
# TV-style "true"/"false" output so backtest logs and
|
|
2711
|
-
# alert messages line up with the TradingView side.
|
|
2712
|
-
if inferred == "bool":
|
|
2713
|
-
rest.append(
|
|
2714
|
-
f'({visited} ? std::string("true") : std::string("false"))'
|
|
2715
|
-
)
|
|
2716
|
-
continue
|
|
2717
|
-
rest.append(f'std::to_string({visited})')
|
|
2718
|
-
if rest:
|
|
2719
|
-
vec = "{" + ", ".join(rest) + "}"
|
|
2720
|
-
return f'pine_str_format({fmt_arg}, {vec})'
|
|
2721
|
-
return fmt_arg
|
|
3494
|
+
format_node = (node.args[0] if node.args
|
|
3495
|
+
else node.kwargs.get("formatString"))
|
|
3496
|
+
if format_node is not None:
|
|
3497
|
+
return self._str_format_expr(format_node, node.args[1:])
|
|
2722
3498
|
return 'std::string("")'
|
|
2723
3499
|
|
|
2724
3500
|
if func_name == "format_time":
|
|
2725
3501
|
ts = args[0] if args else "0"
|
|
3502
|
+
ts_node = node.args[0] if node.args else node.kwargs.get("time")
|
|
3503
|
+
if (ts_node is not None and self._holds_na_aware_wide_double(ts_node)
|
|
3504
|
+
and self._emitted_value_is_double(ts_node)):
|
|
3505
|
+
# A double-valued 64-bit product (``_wide_int_arith_cpp``)
|
|
3506
|
+
# narrows na-preserving into the int64_t timestamp (quirk 9).
|
|
3507
|
+
ts = na_preserving_int_cast(ts, "int64_t")
|
|
2726
3508
|
fmt = args[1] if len(args) > 1 else '"yyyy-MM-dd"'
|
|
2727
3509
|
tz = args[2] if len(args) > 2 else '"UTC"'
|
|
2728
3510
|
return f'pine_str_format_time({ts}, {fmt}, {tz})'
|
|
@@ -2732,20 +3514,27 @@ class CallVisitor:
|
|
|
2732
3514
|
# 4-arg form: replace the Nth occurrence (0-based, per Pine
|
|
2733
3515
|
# spec). Out-of-range / negative occurrence → original string.
|
|
2734
3516
|
return (
|
|
2735
|
-
f'[&](){{ std::string
|
|
2736
|
-
f'std::string
|
|
2737
|
-
f'if(
|
|
2738
|
-
f'
|
|
2739
|
-
f'
|
|
2740
|
-
f'
|
|
2741
|
-
f'
|
|
3517
|
+
f'[&](){{ std::string __pf_s={args[0]}; std::string __pf_t={args[1]}; '
|
|
3518
|
+
f'std::string __pf_r={args[2]}; int __pf_occ={self._coerce_int_slot(args[3], arg_nodes[3], "int")}; '
|
|
3519
|
+
f'if(is_na(__pf_occ)) __pf_occ=0; '
|
|
3520
|
+
f'if(__pf_t.empty()||__pf_occ<0) return __pf_s; '
|
|
3521
|
+
f'size_t __pf_p=0; int __pf_i=0; '
|
|
3522
|
+
f'while((__pf_p=__pf_s.find(__pf_t,__pf_p))!=std::string::npos){{ '
|
|
3523
|
+
f'if(__pf_i==__pf_occ){{ __pf_s.replace(__pf_p,__pf_t.length(),__pf_r); break; }} '
|
|
3524
|
+
f'__pf_p+=__pf_t.length(); __pf_i++; }} return __pf_s; }}()'
|
|
2742
3525
|
)
|
|
2743
3526
|
if len(args) >= 3:
|
|
2744
|
-
|
|
3527
|
+
# target is read twice: evaluate it once.
|
|
3528
|
+
return evaluate_args_once(
|
|
3529
|
+
args, (1,),
|
|
3530
|
+
lambda a: f'[&](){{ std::string __pf_s={a[0]}; auto __pf_p=__pf_s.find({a[1]}); if(__pf_p!=std::string::npos) __pf_s.replace(__pf_p,{a[1]}.length(),{a[2]}); return __pf_s; }}()',
|
|
3531
|
+
"_pf_str_a")
|
|
2745
3532
|
return 'std::string("")'
|
|
2746
3533
|
|
|
2747
3534
|
if func_name in STR_FUNC_MAP and STR_FUNC_MAP[func_name] is not None:
|
|
2748
|
-
return
|
|
3535
|
+
return evaluate_args_once(
|
|
3536
|
+
args, STR_ARGS_READ_REPEATEDLY.get(func_name, ()),
|
|
3537
|
+
STR_FUNC_MAP[func_name], "_pf_str_a")
|
|
2749
3538
|
|
|
2750
3539
|
return f'std::string("") /* unsupported: str.{func_name} */'
|
|
2751
3540
|
|
|
@@ -2753,7 +3542,7 @@ class CallVisitor:
|
|
|
2753
3542
|
args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names("math", func_name), self._visit_expr)
|
|
2754
3543
|
# Handle special cases first
|
|
2755
3544
|
if func_name == "round" and len(args) == 2:
|
|
2756
|
-
return
|
|
3545
|
+
return _math_round_digits_expr(args)
|
|
2757
3546
|
if func_name == "round_to_mintick":
|
|
2758
3547
|
# Engine method (engine.hpp): NaN- and mintick<=0-guarded,
|
|
2759
3548
|
# unlike the previous inlined unguarded std::round.
|