@pineforge/codegen-pyodide 0.10.3 → 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.
Files changed (51) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +767 -77
  7. package/pineforge_codegen/analyzer/call_handlers.py +268 -42
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +907 -159
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +961 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1198 -101
  25. package/pineforge_codegen/codegen/tables.py +205 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +2055 -62
  28. package/pineforge_codegen/codegen/visit_call.py +929 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +765 -63
  30. package/pineforge_codegen/codegen/visit_stmt.py +610 -52
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen/symbols.py +4 -2
  47. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  48. package/release.json +2 -2
  49. package/tables.json +23 -21
  50. package/transpile.worker.mjs +24 -16
  51. package/pineforge_codegen-0.10.3.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
- and not isinstance(receiver_root, Identifier)
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) -> str:
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
- return self._visit_expr(value_node)
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
- return self._visit_expr(arg_node)
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
- if self._active_var_remap and safe in self._active_var_remap:
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
- if param_names and node.kwargs:
1331
- margs = _merge_kwargs(
1332
- node.args, node.kwargs, param_names, self._visit_expr
1333
- )
1334
- else:
1335
- margs = [self._visit_expr(a) for a in node.args]
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
- # na(x) -> is_na(x)
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
- return f"([&]{{ auto _nz_v = ({x}); return is_na(_nz_v) ? ({y}) : _nz_v; }}())"
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
- if param_names and node.kwargs:
1529
- args = _merge_kwargs(node.args, node.kwargs, param_names, self._visit_expr)
1530
- else:
1531
- args = [self._visit_expr(a) for a in node.args]
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,18 +2003,26 @@ 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":
1556
2010
  if func_name in ("new", "new_float", "new_int", "new_bool", "new_string") or func_name in ARRAY_DRAWING_NEW_CTORS:
1557
2011
  spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
2012
+ # The constructor of a wide int array (``_wide_int_array_names``)
2013
+ # must spell the declared ``std::vector<int64_t>``: the
2014
+ # declaration site names its target here.
2015
+ target = getattr(self, "_array_ctor_target_name", None)
2016
+ if target is not None:
2017
+ spec = self._widen_array_spec_for_name(target, spec)
1558
2018
  cpp_type = self._type_spec_to_cpp(spec)
1559
2019
  elem_spec = spec.element if spec.element is not None else TypeSpec.primitive("float")
1560
2020
  init_default = self._default_for_spec(elem_spec)
1561
2021
  if node.args:
1562
2022
  size_arg = self._visit_expr(node.args[0])
1563
2023
  if len(node.args) > 1:
2024
+ self._warn_narrow_int_element(node.args[1], spec,
2025
+ node.args[1])
1564
2026
  init_val = self._array_init_value_expr(elem_spec, node.args[1])
1565
2027
  else:
1566
2028
  init_val = init_default
@@ -1568,7 +2030,28 @@ class CallVisitor:
1568
2030
  return f"{cpp_type}()"
1569
2031
  if func_name == "from":
1570
2032
  spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
1571
- elems = ", ".join(self._visit_expr(a) for a in node.args)
2033
+ target = getattr(self, "_array_ctor_target_name", None)
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
2047
+ spec = self._widen_array_spec_for_name(target, spec)
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
+ )
1572
2055
  return f"{self._type_spec_to_cpp(spec)}{{{elems}}}"
1573
2056
  # Method calls: array.method(arr, args...)
1574
2057
  if func_name in ARRAY_METHODS and (
@@ -1580,7 +2063,7 @@ class CallVisitor:
1580
2063
  arr = self._visit_expr(all_nodes[0])
1581
2064
  spec = self._type_spec_from_expr(all_nodes[0])
1582
2065
  rest = self._array_method_args(func_name, all_nodes[1:], spec)
1583
- return self._array_method_expr(arr, func_name, rest, spec)
2066
+ return self._array_method_expr(arr, func_name, rest, spec, node)
1584
2067
  return "0"
1585
2068
 
1586
2069
  # color.* calls
@@ -1690,21 +2173,26 @@ class CallVisitor:
1690
2173
  if item["expr_node"] is ltf_expr_node:
1691
2174
  return f"_req_sec_lower_tf_{item['sec_id']}"
1692
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)})")
1693
2181
  # All other request.* functions
1694
2182
  return "na<double>()"
1695
2183
 
1696
2184
  # ticker.* calls
1697
2185
  if namespace == "ticker":
1698
- # ticker.inherit(symbol, ...) / ticker.standard(symbol) — passthrough,
1699
- # and ticker.heikinashi(symbol) — same-symbol HA: emit the symbol
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
1700
2189
  # argument unchanged. The runtime HA candle transform is applied by
1701
2190
  # the engine via register_security_eval's heikinashi flag, so the
1702
2191
  # ticker value itself just needs to be the (string) chart symbol.
1703
2192
  if func_name in ("inherit", "standard", "heikinashi"):
1704
- if node.args:
1705
- return self._visit_expr(node.args[0])
1706
- if "symbol" in node.kwargs:
1707
- 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)
1708
2196
  # All other ticker.* calls are hard-rejected by support_checker;
1709
2197
  # emit empty string as safe fallback if they somehow reach codegen.
1710
2198
  return 'std::string("")'
@@ -1757,8 +2245,11 @@ class CallVisitor:
1757
2245
  and (node.args or node.kwargs)
1758
2246
  ):
1759
2247
  params = sigs.get_param_names(None, func_name)
1760
- args = _merge_kwargs(node.args, node.kwargs, params, self._visit_expr)
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
1761
2251
  ts_arg = args[0] if args else "current_bar_.timestamp"
2252
+ ts_arg = self._coerce_int_slot(ts_arg, ts_node, "int64_t")
1762
2253
  tz_arg = args[1] if len(args) > 1 else None
1763
2254
  field_expr = TIME_FIELD_EXPRS[func_name]
1764
2255
  if tz_arg is None:
@@ -1776,7 +2267,22 @@ class CallVisitor:
1776
2267
  # free of the per-call setenv+tzset churn (KI-35). field_expr is unused
1777
2268
  # now (the engine applies the Pine offsets internally).
1778
2269
  del field_expr
1779
- return f"pine_{func_name}((int64_t)({ts_arg}), {tz_arg})"
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
+ )
1780
2286
 
1781
2287
  # time(timeframe) or time(timeframe, session[, tz])
1782
2288
  #
@@ -1805,6 +2311,12 @@ class CallVisitor:
1805
2311
  # to nothing against the base engine (legacy 5-arg call), so the same
1806
2312
  # generated.cpp builds in every lab-experiment cell.
1807
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)
1808
2320
  if func_name == "time" and namespace is None and (node.args or node.kwargs):
1809
2321
  args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names(None, "time"), self._visit_expr)
1810
2322
  tf_e = args[0] if len(args) > 0 else 'script_tf_'
@@ -1877,13 +2389,18 @@ class CallVisitor:
1877
2389
  "month, day[, hour, minute, second]).",
1878
2390
  )
1879
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
+ ]
1880
2397
  tz = args[0]
1881
- yr = args[1] if len(args) > 1 else "1970"
1882
- mo = args[2] if len(args) > 2 else "1"
1883
- dy = args[3] if len(args) > 3 else "1"
1884
- hr = args[4] if len(args) > 4 else "0"
1885
- mn = args[5] if len(args) > 5 else "0"
1886
- sc = args[6] if len(args) > 6 else "0"
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"
1887
2404
  return _timestamp_calendar_lambda(tz, yr, mo, dy, hr, mn, sc)
1888
2405
  else:
1889
2406
  # Numeric form requires year, month, and day (hour/minute/
@@ -1902,7 +2419,10 @@ class CallVisitor:
1902
2419
  "[, hour, minute, second]); the dateString "
1903
2420
  "overload is not supported in PineForge.",
1904
2421
  )
1905
- args = [self._visit_expr(a) for a in merged]
2422
+ args = [
2423
+ self._coerce_int_slot(self._visit_expr(a), a, "int")
2424
+ for a in merged
2425
+ ]
1906
2426
  yr = args[0]
1907
2427
  mo = args[1] if len(args) > 1 else "1"
1908
2428
  dy = args[2] if len(args) > 2 else "1"
@@ -1932,37 +2452,55 @@ class CallVisitor:
1932
2452
  # Type cast functions: int(x), float(x), bool(x), string(x)
1933
2453
  if func_name == "int" and namespace is None and node.args:
1934
2454
  # Pine int(na) → na (int form). Evaluate once, propagate na via
1935
- # 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.
1936
2458
  x = self._visit_expr(node.args[0])
1937
- return (f"[&](){{ double _pf_v = (double)({x}); "
1938
- f"return is_na(_pf_v) ? na<int>() : (int)_pf_v; }}()")
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)
1939
2466
  if func_name == "float" and namespace is None and node.args:
1940
2467
  return f"(double)({self._visit_expr(node.args[0])})"
1941
2468
  if func_name == "bool" and namespace is None and node.args:
1942
2469
  # Pine v6 bools are two-state. Explicit bool(int/float) treats na
1943
2470
  # like false, while a raw C++ cast would make NaN truthy.
1944
2471
  x = self._visit_expr(node.args[0])
1945
- return (
1946
- f"[&](){{ auto _pf_v = ({x}); "
1947
- f"using _pf_t = std::decay_t<decltype(_pf_v)>; "
1948
- f"if constexpr (std::is_same_v<_pf_t, bool>) {{ return _pf_v; }} "
1949
- f"else {{ return is_na(_pf_v) ? false : (bool)_pf_v; }} }}()"
1950
- )
2472
+ return pine_truth_cast(x)
1951
2473
  if func_name == "string" and namespace is None and node.args:
1952
2474
  # Pine string(x) cast — same emission as str.tostring(x), with
1953
2475
  # string passthrough and TV-style "true"/"false" for bools
1954
2476
  # (std::to_string would reject strings / render bools as 0/1).
1955
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>()"
1956
2484
  inferred = self._infer_type(arg)
1957
2485
  if inferred == "std::string":
1958
2486
  return self._visit_expr(arg)
1959
2487
  if inferred == "bool":
2488
+ if self._pine_v5_body:
2489
+ self._refuse_v5_bool_na_observer(node, "string()", [arg])
1960
2490
  visited = self._visit_expr(arg)
1961
2491
  return f'(({visited}) ? std::string("true") : std::string("false"))'
1962
2492
  return self._visit_str_call("tostring", node)
1963
2493
 
1964
2494
  # ta.pivot_point_levels — free function, not a stateful indicator
1965
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
+ )
1966
2504
  if node.kwargs:
1967
2505
  args = _merge_kwargs(
1968
2506
  node.args,
@@ -2015,7 +2553,13 @@ class CallVisitor:
2015
2553
  init = args_e[2] if len(args_e) > 2 else "0.0"
2016
2554
  return f"PineMatrix::new_({rows}, {cols}, {init})"
2017
2555
  cpp_t = self._type_spec_to_cpp(elem_spec)
2018
- init = args_e[2] if len(args_e) > 2 else self._default_for_spec(elem_spec)
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
+ )
2019
2563
  return f"PineGenericMatrix<{cpp_t}>::new_({rows}, {cols}, {init})"
2020
2564
  if func_name in MATRIX_METHODS and node.args:
2021
2565
  from ..ast_nodes import Identifier as _Ident
@@ -2035,10 +2579,16 @@ class CallVisitor:
2035
2579
  self._check_matrix_method_allowed(func_name, recv_spec, node)
2036
2580
  obj = self._visit_expr(node.args[0])
2037
2581
  param_names = MATRIX_METHOD_KWARGS.get(func_name)
2038
- if param_names:
2039
- rest = _merge_kwargs(node.args[1:], node.kwargs, param_names, self._visit_expr)
2040
- else:
2041
- rest = [self._visit_expr(a) for a in node.args[1:]]
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
+ )
2042
2592
  fn = MATRIX_METHODS[func_name]
2043
2593
  try:
2044
2594
  return fn(obj, rest)
@@ -2050,13 +2600,21 @@ class CallVisitor:
2050
2600
  )
2051
2601
  return "0.0"
2052
2602
 
2053
- # 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.
2054
2608
  if namespace == "log":
2055
2609
  log_funcs = {"info": "pine_log_info", "warning": "pine_log_warning", "error": "pine_log_error"}
2056
2610
  if func_name in log_funcs:
2057
- log_args = [self._visit_expr(a) for a in node.args]
2058
- msg_arg = log_args[0] if log_args else '""'
2059
- return f'{log_funcs[func_name]}({msg_arg})'
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)})'
2060
2618
  return '"" /* unsupported log */'
2061
2619
 
2062
2620
  # timeframe.* calls (e.g., timeframe.change) — not supported in single-TF backtest
@@ -2121,14 +2679,23 @@ class CallVisitor:
2121
2679
  ),
2122
2680
  )
2123
2681
  if f_cpp_type == "int64_t":
2124
- if "na<double>" in val:
2125
- val = val.replace("na<double>()", "na<int64_t>()")
2126
- else:
2127
- val = f"(int64_t)({val})"
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)
2128
2687
  elif (self._is_nullable_collection_cpp_type(f_cpp_type)
2129
2688
  and val == "na<double>()"):
2130
2689
  val = f"{f_cpp_type}{{}}"
2131
- field_inits.append(f".{f.name} = {val}")
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}")
2132
2699
  return self._emit_udt_new_expr(namespace, field_inits)
2133
2700
 
2134
2701
  # UDT copy: TypeName.copy(obj)
@@ -2227,8 +2794,7 @@ class CallVisitor:
2227
2794
  == expected_cpp_type
2228
2795
  ):
2229
2796
  return safe
2230
- if self._active_var_remap and safe in self._active_var_remap:
2231
- safe = self._active_var_remap[safe]
2797
+ safe = self._call_site_var_name(arg_node, safe)
2232
2798
  if (
2233
2799
  not is_current_series_param
2234
2800
  and self._binding_is_series(aname, safe)
@@ -2284,7 +2850,20 @@ class CallVisitor:
2284
2850
  return self._visit_expr(arg_node)
2285
2851
 
2286
2852
  ordered_arg_nodes: list = []
2287
- if node.kwargs:
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:
2288
2867
  # Try to resolve kwargs using FuncInfo params for user-defined functions
2289
2868
  fi = self._func_info_map.get(func_name)
2290
2869
  if fi and fi.node and fi.node.params:
@@ -2325,6 +2904,16 @@ class CallVisitor:
2325
2904
  # arg that is exactly such a drawing-style constant read.
2326
2905
  if namespace is None and func_name in self._func_names:
2327
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
+ )
2328
2917
  # Default args (parser does not store defaults): isInSession(sess, res = timeframe.period)
2329
2918
  if namespace is None and func_name in self._func_names:
2330
2919
  fi = self._func_info_map.get(func_name)
@@ -2368,9 +2957,41 @@ class CallVisitor:
2368
2957
  ordered_arg_nodes,
2369
2958
  all_args,
2370
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),
2371
2962
  )
2372
2963
  return f"{call_head}({', '.join(all_args)})"
2373
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
+
2374
2995
  def _coerce_drawing_style_string_args(self, func_name, arg_nodes, all_args) -> None:
2375
2996
  """In-place coerce positional args bound to a ``std::string`` user-function
2376
2997
  parameter that lowered to the bare token ``"0"`` from a drawing-style /
@@ -2427,8 +3048,15 @@ class CallVisitor:
2427
3048
  else:
2428
3049
  self._fixnan_counter += 1
2429
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.
2430
3055
  x = self._visit_expr(node.args[0])
2431
- return f"(is_na({x}) ? {member} : ({member} = {x}))"
3056
+ return (
3057
+ f"([&]{{ auto _fixnan_v = ({x}); "
3058
+ f"return is_na(_fixnan_v) ? {member} : ({member} = _fixnan_v); }}())"
3059
+ )
2432
3060
 
2433
3061
  @staticmethod
2434
3062
  def _strategy_close_callsite_token(node: FuncCall) -> str:
@@ -2461,7 +3089,13 @@ class CallVisitor:
2461
3089
  if func_name == "entry":
2462
3090
  p = self._resolve_func_args(node, "strategy.entry")
2463
3091
  entry_id = self._visit_expr(p.get("id")) if "id" in p else '""'
2464
- direction = self._visit_expr(p.get("direction")) if "direction" in p else "true"
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
+ )
2465
3099
  stop = p.get("stop")
2466
3100
  limit = p.get("limit")
2467
3101
  qty = p.get("qty")
@@ -2492,14 +3126,26 @@ class CallVisitor:
2492
3126
  comment = self._visit_expr(p.get("comment")) if p.get("comment") is not None else '""'
2493
3127
  qty = self._visit_expr(p.get("qty")) if p.get("qty") is not None else "na<double>()"
2494
3128
  qty_pct = self._visit_expr(p.get("qty_percent")) if p.get("qty_percent") is not None else "na<double>()"
2495
- immediately = self._visit_expr(p.get("immediately")) if p.get("immediately") is not None else "false"
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
+ )
2496
3136
  callsite = self._strategy_close_callsite_token(node)
2497
3137
  return f"strategy_close({close_id}, {comment}, {qty}, {qty_pct}, {immediately}, {callsite})"
2498
3138
 
2499
3139
  if func_name == "close_all":
2500
3140
  p = self._resolve_func_args(node, "strategy.close_all")
2501
3141
  comment = self._visit_expr(p.get("comment")) if p.get("comment") is not None else '""'
2502
- immediately = self._visit_expr(p.get("immediately")) if p.get("immediately") is not None else "false"
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
+ )
2503
3149
  # The engine's ID-less strategy_close path closes the complete
2504
3150
  # position. Reuse it so close_all preserves the Pine order comment
2505
3151
  # and same-tick fill flag instead of silently discarding both.
@@ -2546,8 +3192,8 @@ class CallVisitor:
2546
3192
  return (f"strategy_exit({exit_id}, {from_id}, {limit_val}, {stop_val}, "
2547
3193
  f"{trail_pts}, {trail_off}, {trail_pr}, {qty_pct}, {comment}, "
2548
3194
  f"{qty_val}, {oca_val}, {profit_ticks}, {loss_ticks})")
2549
- close_comment = self._visit_expr(comment_n) if comment_n is not None else '""'
2550
- return f"strategy_close({exit_id}, {close_comment})"
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})"
2551
3197
 
2552
3198
  if func_name == "cancel":
2553
3199
  p = self._resolve_func_args(node, "strategy.close") # same shape: id first
@@ -2560,7 +3206,13 @@ class CallVisitor:
2560
3206
  if func_name == "order":
2561
3207
  p = self._resolve_func_args(node, "strategy.order")
2562
3208
  order_id = self._visit_expr(p.get("id")) if "id" in p else '""'
2563
- direction = self._visit_expr(p.get("direction")) if "direction" in p else "true"
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
+ )
2564
3216
  qty = self._visit_expr(p.get("qty")) if "qty" in p else "0"
2565
3217
  limit_arg = self._visit_expr(p.get("limit")) if "limit" in p else "na<double>()"
2566
3218
  stop_arg = self._visit_expr(p.get("stop")) if "stop" in p else "na<double>()"
@@ -2576,7 +3228,14 @@ class CallVisitor:
2576
3228
  if isinstance(node.callee, MemberAccess):
2577
3229
  inner = node.callee.object
2578
3230
  if isinstance(inner, MemberAccess) and inner.member in ("closedtrades", "opentrades"):
2579
- idx = self._visit_expr(node.args[0]) if node.args else "0"
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
+ )
2580
3239
  is_open = inner.member == "opentrades"
2581
3240
  # Open trades have no exit metadata in Pine
2582
3241
  if is_open and func_name in (
@@ -2625,12 +3284,100 @@ class CallVisitor:
2625
3284
  f"have rejected. Add a handler above or extend STRATEGY_FUNCTIONS."
2626
3285
  )
2627
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
+
2628
3330
  def _visit_color_call(self, func_name: str, node) -> str:
2629
3331
  """Emit color.* calls as integer representations."""
2630
- args = [self._visit_expr(a) for a in node.args]
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]
2631
3356
  if func_name == "new":
2632
3357
  if len(args) >= 2:
2633
- return f'pine_color::new_color({args[0]}, (int)({args[1]}))'
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
+ )
2634
3381
  return "0"
2635
3382
  if func_name in ("r", "g", "b", "t"):
2636
3383
  if args:
@@ -2638,17 +3385,72 @@ class CallVisitor:
2638
3385
  return "0"
2639
3386
  if func_name == "rgb":
2640
3387
  if len(args) >= 4:
2641
- return f"pine_color::new_color(((int64_t)({args[0]}) << 16 | (int64_t)({args[1]}) << 8 | (int64_t)({args[2]})), (int)({args[3]}))"
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
+ )
2642
3399
  elif len(args) >= 3:
2643
- return f"pine_color::new_color(((int64_t)({args[0]}) << 16 | (int64_t)({args[1]}) << 8 | (int64_t)({args[2]})), 0)"
2644
- return "0"
2645
- if func_name == "from_gradient":
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
+ )
2646
3410
  return "0"
2647
3411
  return "0"
2648
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
+
2649
3422
  def _visit_str_call(self, func_name: str, node) -> str:
2650
3423
  args = _merge_kwargs(node.args, node.kwargs,
2651
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
+ )
2652
3454
 
2653
3455
  if func_name == "tostring":
2654
3456
  # Pine: str.tostring(enumVar) → field title / IANA string, not the int index
@@ -2664,56 +3466,45 @@ class CallVisitor:
2664
3466
  return (
2665
3467
  f"pine_enum_str_at({et}_str_values, {n}, {var})"
2666
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"))')
2667
3477
  if len(args) >= 2:
2668
- return f"pine_str_tostring({args[0]}, {args[1]}, syminfo_mintick_)"
3478
+ return f"pine_str_tostring_tv({args[0]}, {args[1]}, syminfo_mintick_)"
2669
3479
  if len(args) >= 1:
2670
- return f"std::to_string({args[0]})"
3480
+ return f"pine_str_tostring_tv({args[0]}, std::string(), syminfo_mintick_)"
2671
3481
  return 'std::string("")'
2672
3482
 
2673
3483
  if func_name == "substring":
2674
3484
  if len(args) == 3:
2675
- return f"{args[0]}.substr({args[1]}, {args[2]} - {args[1]})"
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")
2676
3489
  elif len(args) == 2:
2677
3490
  return f"{args[0]}.substr({args[1]})"
2678
3491
  return 'std::string("")'
2679
3492
 
2680
3493
  if func_name == "format":
2681
- # str.format is variadic: signature has only ``formatStr``; the
2682
- # remaining args are placeholder substitutions. The runtime
2683
- # ``pine_str_format(fmt, vector<string>)`` requires every arg
2684
- # already converted to ``std::string``. We previously gated the
2685
- # ``std::to_string`` wrap on a source-text-prefix heuristic
2686
- # (``"`` / ``std::string`` / ``pine_str``), which mis-classified
2687
- # any string-typed bare identifier or string-returning helper
2688
- # call (e.g. ``str.tostring(x)`` bound to a variable). The type
2689
- # check below uses the analyzer's inferred PineType instead so
2690
- # ``std::string`` args pass through unchanged.
2691
- if node.args:
2692
- fmt_arg = self._visit_expr(node.args[0])
2693
- rest = []
2694
- for orig in node.args[1:]:
2695
- visited = self._visit_expr(orig)
2696
- inferred = self._infer_type(orig)
2697
- if inferred == "std::string":
2698
- rest.append(visited)
2699
- continue
2700
- # Booleans render as 0/1 via std::to_string; force the
2701
- # TV-style "true"/"false" output so backtest logs and
2702
- # alert messages line up with the TradingView side.
2703
- if inferred == "bool":
2704
- rest.append(
2705
- f'({visited} ? std::string("true") : std::string("false"))'
2706
- )
2707
- continue
2708
- rest.append(f'std::to_string({visited})')
2709
- if rest:
2710
- vec = "{" + ", ".join(rest) + "}"
2711
- return f'pine_str_format({fmt_arg}, {vec})'
2712
- 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:])
2713
3498
  return 'std::string("")'
2714
3499
 
2715
3500
  if func_name == "format_time":
2716
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")
2717
3508
  fmt = args[1] if len(args) > 1 else '"yyyy-MM-dd"'
2718
3509
  tz = args[2] if len(args) > 2 else '"UTC"'
2719
3510
  return f'pine_str_format_time({ts}, {fmt}, {tz})'
@@ -2723,20 +3514,27 @@ class CallVisitor:
2723
3514
  # 4-arg form: replace the Nth occurrence (0-based, per Pine
2724
3515
  # spec). Out-of-range / negative occurrence → original string.
2725
3516
  return (
2726
- f'[&](){{ std::string s={args[0]}; std::string t={args[1]}; '
2727
- f'std::string r={args[2]}; int _occ=(int)({args[3]}); '
2728
- f'if(t.empty()||_occ<0) return s; '
2729
- f'size_t p=0; int _i=0; '
2730
- f'while((p=s.find(t,p))!=std::string::npos){{ '
2731
- f'if(_i==_occ){{ s.replace(p,t.length(),r); break; }} '
2732
- f'p+=t.length(); _i++; }} return s; }}()'
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; }}()'
2733
3525
  )
2734
3526
  if len(args) >= 3:
2735
- return f'[&](){{ std::string s={args[0]}; auto p=s.find({args[1]}); if(p!=std::string::npos) s.replace(p,{args[1]}.length(),{args[2]}); return s; }}()'
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")
2736
3532
  return 'std::string("")'
2737
3533
 
2738
3534
  if func_name in STR_FUNC_MAP and STR_FUNC_MAP[func_name] is not None:
2739
- return STR_FUNC_MAP[func_name](args)
3535
+ return evaluate_args_once(
3536
+ args, STR_ARGS_READ_REPEATEDLY.get(func_name, ()),
3537
+ STR_FUNC_MAP[func_name], "_pf_str_a")
2740
3538
 
2741
3539
  return f'std::string("") /* unsupported: str.{func_name} */'
2742
3540
 
@@ -2744,7 +3542,7 @@ class CallVisitor:
2744
3542
  args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names("math", func_name), self._visit_expr)
2745
3543
  # Handle special cases first
2746
3544
  if func_name == "round" and len(args) == 2:
2747
- return f"(std::round({args[0]} * std::pow(10.0, {args[1]})) / std::pow(10.0, {args[1]}))"
3545
+ return _math_round_digits_expr(args)
2748
3546
  if func_name == "round_to_mintick":
2749
3547
  # Engine method (engine.hpp): NaN- and mintick<=0-guarded,
2750
3548
  # unlike the previous inlined unguarded std::round.