@pineforge/codegen-pyodide 0.10.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) 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 +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  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 +896 -156
  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 +946 -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 +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  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-1.0.0.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.tar.gz +0 -0
@@ -68,8 +68,11 @@ from typing import Any
68
68
  from ..ast_nodes import (
69
69
  ASTNode, Assignment, BinOp, BoolLiteral, ExprStmt, FuncCall, Identifier,
70
70
  IfStmt, MemberAccess, NumberLiteral, StringLiteral, Subscript, SwitchStmt,
71
- Ternary, TupleLiteral, UnaryOp, VarDecl,
71
+ Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
72
72
  )
73
+ from ..method_binding import bind_function_defaults
74
+ from ..external_requests import FEED_LOWERING, LOWERING_ANNOTATION
75
+ from ..security_contexts import CONTEXT_ANNOTATION, DEAD_ANNOTATION
73
76
  from ..symbols import PineType
74
77
  from .. import signatures as sigs
75
78
  from .. import tv_input_choices as tv_in
@@ -77,9 +80,23 @@ from .contracts import FixnanCallSite, FuncInfo, SecurityCallInfo, TACallSite
77
80
  from .tables import (
78
81
  BAR_FIELDS, TA_CLASS_MAP, TA_MULTI_CTOR, TA_NO_CTOR, TA_PERIOD_ARG,
79
82
  TA_TUPLE_RETURNS, TA_TUPLE_ELEMENT_COUNTS, TA_COMPUTE_ARGS,
83
+ TA_LENGTH_ONLY_DEFAULT_SOURCE,
80
84
  )
81
85
 
82
86
 
87
+ # Element types a request.security helper tuple may carry (see
88
+ # ``_handle_request_call``); an ``na`` element is stored as a double like every
89
+ # numeric one. An element the definition leaves untyped is typed at the call
90
+ # (``_security_callsite_tuple_types``) or refused.
91
+ _SECURITY_TUPLE_ELEMENT_TYPES = frozenset({
92
+ PineType.INT, PineType.FLOAT, PineType.BOOL, PineType.STRING, PineType.NA,
93
+ })
94
+ _HINT_TYPES = {
95
+ "int": PineType.INT, "float": PineType.FLOAT, "bool": PineType.BOOL,
96
+ "string": PineType.STRING,
97
+ }
98
+
99
+
83
100
  class CallHandlers:
84
101
  """``_handle_*_call`` dispatch + bookkeeping for analyzer call-sites.
85
102
 
@@ -385,6 +402,57 @@ class CallHandlers:
385
402
  merged.pop()
386
403
  return merged
387
404
 
405
+ def _is_constant_bool_literal(self, node, value: bool, _seen=frozenset()) -> bool:
406
+ """Small analyzer-side counterpart of the support checker's constant
407
+ bool test. The analyzer already records global expression bindings and
408
+ global reassignments; use those records so a one-time alias of true or
409
+ false follows the same route as the literal spelling."""
410
+ if isinstance(node, BoolLiteral):
411
+ return node.value is value
412
+ if isinstance(node, Identifier) and node.name not in _seen:
413
+ if node.name in getattr(self, "_global_reassigned_names", set()):
414
+ return False
415
+ expr = self._global_expr_map.get(node.name)
416
+ return expr is not None and self._is_constant_bool_literal(
417
+ expr, value, _seen | {node.name}
418
+ )
419
+ return False
420
+
421
+ def _constant_string_literal(self, node, _seen=frozenset()) -> str | None:
422
+ """Resolve a never-reassigned global string literal for pivot routing."""
423
+ if isinstance(node, StringLiteral):
424
+ return node.value
425
+ if isinstance(node, Identifier) and node.name not in _seen:
426
+ if node.name in getattr(self, "_global_reassigned_names", set()):
427
+ return None
428
+ expr = self._global_expr_map.get(node.name)
429
+ if expr is not None:
430
+ return self._constant_string_literal(expr, _seen | {node.name})
431
+ return None
432
+
433
+ def _is_daily_vwap_anchor(self, node, _seen=frozenset()) -> bool:
434
+ """Recognize PineForge's established default VWAP anchor."""
435
+ if isinstance(node, FuncCall):
436
+ callee = node.callee
437
+ if (
438
+ isinstance(callee, MemberAccess)
439
+ and isinstance(callee.object, Identifier)
440
+ and callee.object.name == "timeframe"
441
+ and callee.member == "change"
442
+ and len(node.args) + len(node.kwargs) == 1
443
+ ):
444
+ tf = node.args[0] if node.args else node.kwargs.get("timeframe")
445
+ return isinstance(tf, StringLiteral) and tf.value in {"D", "1D"}
446
+ return False
447
+ if isinstance(node, Identifier) and node.name not in _seen:
448
+ if node.name in getattr(self, "_global_reassigned_names", set()):
449
+ return False
450
+ expr = self._global_expr_map.get(node.name)
451
+ return expr is not None and self._is_daily_vwap_anchor(
452
+ expr, _seen | {node.name}
453
+ )
454
+ return False
455
+
388
456
  def _handle_ta_call(self, func_name: str, node: FuncCall) -> PineType:
389
457
  """Handle ta.* function calls."""
390
458
  # Visit all args for side effects (series detection, etc.)
@@ -393,22 +461,10 @@ class CallHandlers:
393
461
  for val in node.kwargs.values():
394
462
  self._visit(val)
395
463
 
396
- # ta.pivot_point_levels is a free runtime function (not a stateful
397
- # indicator), but its codegen lowers to use `_s_high[1]`, `_s_low[1]`,
398
- # `_s_close[1]` so the pivot is calculated from the PREVIOUS bar's
399
- # HLC (matching Pine v6 semantics where `developing` defaults to
400
- # false). Register the bar-field history series here so that the
401
- # codegen emits the corresponding `Series<double> _s_high/...` members
402
- # and pushes them at the top of every on_bar tick.
403
- if func_name == "pivot_point_levels":
404
- self._series_bar_fields.add("high")
405
- self._series_bar_fields.add("low")
406
- self._series_bar_fields.add("close")
407
- return PineType.FLOAT # actual array<float> handled by type inference
408
-
409
464
  # ta.vwap(source, anchor, stdev_mult) → 3-arg bands form.
410
465
  # When called with 3 args (or anchor/stdev_mult kwargs), remap to the
411
466
  # internal "vwap_bands" key which maps to ta::VWAPBands (returns tuple).
467
+ merged_v = None
412
468
  if func_name == "vwap":
413
469
  param_names_v = ["source", "anchor", "stdev_mult"]
414
470
  merged_v = list(node.args)
@@ -421,11 +477,27 @@ class CallHandlers:
421
477
  if len(merged_v) >= 3:
422
478
  func_name = "vwap_bands"
423
479
 
424
- if func_name not in TA_CLASS_MAP:
480
+ # An explicit anchor, including timeframe.change("1D"/"D"), is a
481
+ # Pine series condition. TA1's anchored classes must see it on
482
+ # every bar; only the omitted-anchor spelling keeps the historical
483
+ # session-day VWAP class.
484
+ anchor = merged_v[1] if len(merged_v) > 1 else None
485
+ if anchor is not None:
486
+ func_name = (
487
+ "vwap_anchored_bands"
488
+ if len(merged_v) >= 3
489
+ else "vwap_anchored"
490
+ )
491
+
492
+ if func_name not in TA_CLASS_MAP and func_name != "pivot_point_levels":
425
493
  return PineType.FLOAT
426
494
 
427
- # Merge positional + kwargs into a unified arg list
428
- all_args = self._merge_ta_args(func_name, node)
495
+ # Merge positional + kwargs into a unified arg list. The band form has
496
+ # no signature of its own: its arguments are ``ta.vwap``'s, merged above.
497
+ if func_name in ("vwap_bands", "vwap_anchored", "vwap_anchored_bands") and merged_v is not None:
498
+ all_args = merged_v
499
+ else:
500
+ all_args = self._merge_ta_args(func_name, node)
429
501
 
430
502
  # ta.tr(handle_na) — TV v6 default for handle_na is false. When the
431
503
  # caller omits the arg, inject the explicit ``false`` so the C++
@@ -448,15 +520,55 @@ class CallHandlers:
448
520
  self._series_bar_fields.add(field)
449
521
  all_args = [default_src]
450
522
 
451
- # Handle ta.highest(length) / ta.lowest(length) with 1 arg:
452
- # single arg is the length, source defaults to high/low respectively.
453
- # Remap so all_args = [default_source, length_arg].
454
- _DEFAULT_SOURCE = {"highest": "high", "lowest": "low"}
455
- if func_name in _DEFAULT_SOURCE and len(all_args) == 1:
456
- default_src = Identifier(name=_DEFAULT_SOURCE[func_name])
523
+ # ``pivot_point_levels(type, anchor, developing)`` retains the
524
+ # historical free-function lowering only for the exact spelling the
525
+ # free function computes: anchor=true on every bar and
526
+ # developing=false for the Fibonacci, Classic, and Camarilla types.
527
+ # The public TradingView pivot tape pins Traditional, Woodie, and DM
528
+ # to the TA1 formulas, including the high Traditional levels.
529
+ if func_name == "pivot_point_levels":
530
+ self._series_bar_fields.update(("high", "low", "close"))
531
+ all_args = self._merge_ta_args(func_name, node)
532
+ if len(all_args) >= 2 and len(all_args) < 3:
533
+ all_args.append(BoolLiteral(value=False))
534
+ exact = (
535
+ len(all_args) >= 3
536
+ and self._is_constant_bool_literal(all_args[1], True)
537
+ and self._is_constant_bool_literal(all_args[2], False)
538
+ and self._constant_string_literal(all_args[0]) in {
539
+ "Fibonacci", "Classic", "Camarilla"
540
+ }
541
+ )
542
+ if exact:
543
+ return PineType.FLOAT
544
+ self._ta_counter += 1
545
+ site = TACallSite(
546
+ member_name=f"_ta_pivot_point_levels_{self._ta_counter}",
547
+ class_name="_PFPivotPointLevels",
548
+ ctor_args=[],
549
+ compute_args=all_args[:3],
550
+ returns_tuple=False,
551
+ node=node,
552
+ is_static=False,
553
+ owner_func=(self._enclosing_func_names[-1]
554
+ if self._enclosing_func_names else None),
555
+ )
556
+ self._ta_call_sites.append(site)
557
+ self._ta_member_names.add(site.member_name)
558
+ return PineType.FLOAT # actual array<float> handled by type inference
559
+
560
+ # The one-arg forms ta.highest(length) / ta.lowest(length) /
561
+ # ta.highestbars(length) / ta.lowestbars(length), positional or
562
+ # ``length=``: the source defaults to high / low. Remap so all_args =
563
+ # [default_source, length_arg]; highestbars / lowestbars used to send
564
+ # the length to compute() as the source.
565
+ default_source = TA_LENGTH_ONLY_DEFAULT_SOURCE.get(func_name)
566
+ if default_source is not None and (
567
+ len(all_args) == 1 or (len(all_args) == 2 and all_args[0] is None)):
568
+ default_src = Identifier(name=default_source)
457
569
  self._visit(default_src)
458
- self._series_bar_fields.add(_DEFAULT_SOURCE[func_name])
459
- all_args = [default_src, all_args[0]]
570
+ self._series_bar_fields.add(default_source)
571
+ all_args = [default_src, all_args[-1]]
460
572
 
461
573
  self._ta_counter += 1
462
574
  class_name = TA_CLASS_MAP[func_name]
@@ -466,8 +578,8 @@ class CallHandlers:
466
578
  # vwap_bands special dispatch: ta.vwap(source, anchor, stdev_mult)
467
579
  # ctor receives stdev_mult only; compute receives source only.
468
580
  # The anchor arg (index 1) is the Pine-level "when to reset" series;
469
- # our VWAPBands wrapper uses UTC-day boundaries matching the daily
470
- # anchor default, so anchor is intentionally ignored in codegen.
581
+ # our VWAPBands wrapper resets on the symbol's session day, the
582
+ # default anchor, which is the only one the support checker admits.
471
583
  if func_name == "vwap_bands":
472
584
  ctor_args: list[str] = []
473
585
  if len(all_args) >= 3 and all_args[2] is not None:
@@ -492,6 +604,7 @@ class CallHandlers:
492
604
 
493
605
  # Determine constructor args
494
606
  ctor_args: list[str] = []
607
+ ctor_nodes: list = []
495
608
  effective_multi_ctor = TA_MULTI_CTOR.copy()
496
609
  if func_name in ("pivothigh", "pivotlow") and len(all_args) == 3:
497
610
  effective_multi_ctor[func_name] = [1, 2]
@@ -502,10 +615,12 @@ class CallHandlers:
502
615
  for idx in effective_multi_ctor[func_name]:
503
616
  if idx < len(all_args) and all_args[idx] is not None:
504
617
  ctor_args.append(self._expr_to_str(all_args[idx]))
618
+ ctor_nodes.append(all_args[idx])
505
619
  elif func_name in TA_PERIOD_ARG:
506
620
  idx = TA_PERIOD_ARG[func_name]
507
621
  if idx < len(all_args) and all_args[idx] is not None:
508
622
  ctor_args.append(self._expr_to_str(all_args[idx]))
623
+ ctor_nodes.append(all_args[idx])
509
624
 
510
625
  # Determine compute args (all args that aren't ctor args)
511
626
  compute_args: list = []
@@ -534,6 +649,7 @@ class CallHandlers:
534
649
  node=node,
535
650
  is_static=is_static,
536
651
  owner_func=(self._enclosing_func_names[-1] if self._enclosing_func_names else None),
652
+ ctor_nodes=ctor_nodes,
537
653
  )
538
654
  self._ta_call_sites.append(site)
539
655
  self._ta_member_names.add(site.member_name)
@@ -559,6 +675,59 @@ class CallHandlers:
559
675
  return self._security_symbol_is_heikinashi(self._global_expr_map[node.name], _seen)
560
676
  return False
561
677
 
678
+ def _security_callsite_tuple_types(
679
+ self,
680
+ func_name: str,
681
+ call: FuncCall,
682
+ element_types: tuple[PineType, ...],
683
+ ) -> tuple[PineType, ...]:
684
+ """Type the elements a helper's definition leaves untyped from this
685
+ request.security call's arguments (``f(high, 10)``: ``src[k]`` is a
686
+ float), following the helper's top-level declarations. An element
687
+ still untyped keeps ``UNKNOWN`` and is refused."""
688
+ fdef = self._func_defs.get(func_name)
689
+ if fdef is None or not fdef.body:
690
+ return element_types
691
+ final = fdef.body[-1]
692
+ final = final.expr if isinstance(final, ExprStmt) else final
693
+ if not isinstance(final, TupleLiteral) or len(final.elements) != len(element_types):
694
+ return element_types
695
+
696
+ def arg_type(arg) -> PineType:
697
+ if isinstance(arg, Identifier):
698
+ if arg.name in BAR_FIELDS:
699
+ return PineType.FLOAT
700
+ if arg.name == "time":
701
+ return PineType.INT
702
+ sym = self._symbols.resolve(arg.name)
703
+ if sym is not None and sym.pine_type in _SECURITY_TUPLE_ELEMENT_TYPES:
704
+ return sym.pine_type
705
+ return self._callsite_primitive_expr_type(arg, {})
706
+
707
+ env: dict[str, PineType] = {}
708
+ for index, param in enumerate(fdef.params):
709
+ arg = call.kwargs.get(param)
710
+ if arg is None and index < len(call.args):
711
+ arg = call.args[index]
712
+ env[param] = arg_type(arg) if arg is not None else PineType.UNKNOWN
713
+ for stmt in fdef.body[:-1]:
714
+ if isinstance(stmt, VarDecl):
715
+ env[stmt.name] = (
716
+ _HINT_TYPES.get(stmt.type_hint, PineType.UNKNOWN)
717
+ if stmt.type_hint
718
+ else self._callsite_primitive_expr_type(stmt.value, env)
719
+ )
720
+ elif isinstance(stmt, TupleAssign):
721
+ env.update((name, PineType.UNKNOWN) for name in stmt.names)
722
+ refined = []
723
+ for known, element in zip(element_types, final.elements):
724
+ if known == PineType.UNKNOWN:
725
+ typed = self._callsite_primitive_expr_type(element, env)
726
+ if typed in (PineType.INT, PineType.FLOAT, PineType.BOOL, PineType.STRING):
727
+ known = typed
728
+ refined.append(known)
729
+ return tuple(refined)
730
+
562
731
  def _handle_request_call(self, func_name: str, node: FuncCall) -> PineType:
563
732
  """Handle request.* function calls."""
564
733
  if func_name == "security":
@@ -584,8 +753,9 @@ class CallHandlers:
584
753
 
585
754
  # Track TA sites created by the expression
586
755
  ta_start = len(self._ta_call_sites)
756
+ expr_type = None
587
757
  if expr_node is not None:
588
- self._visit(expr_node)
758
+ expr_type = self._visit(expr_node)
589
759
  ta_end = len(self._ta_call_sites)
590
760
  security_ta_range = (ta_start, ta_end) if ta_end > ta_start else None
591
761
 
@@ -612,26 +782,30 @@ class CallHandlers:
612
782
  if self._func_returns_tuple.get(expr_func, False):
613
783
  tuple_size = self._func_tuple_element_count.get(expr_func, 0)
614
784
  tuple_types = self._func_tuple_element_types.get(expr_func, ())
615
- numeric_tuple = (
785
+ if PineType.UNKNOWN in tuple_types:
786
+ tuple_types = self._security_callsite_tuple_types(
787
+ expr_func, expr_node, tuple_types
788
+ )
789
+ # TradingView returns a tuple of any mix of scalars. A
790
+ # bool element keeps a bool slot, a string element a
791
+ # string slot; every numeric element, and one whose type
792
+ # is not inferred (``src[k]`` of a parameter), keeps the
793
+ # double storage of the numeric family.
794
+ scalar_tuple = (
616
795
  tuple_size >= 2
617
796
  and len(tuple_types) == tuple_size
618
797
  and all(
619
- item in (PineType.INT, PineType.FLOAT)
798
+ item in _SECURITY_TUPLE_ELEMENT_TYPES
620
799
  for item in tuple_types
621
800
  )
622
801
  )
623
- bool_tuple = (
624
- tuple_size >= 2
625
- and len(tuple_types) == tuple_size
626
- and all(item == PineType.BOOL for item in tuple_types)
627
- )
628
- if not (numeric_tuple or bool_tuple):
802
+ if not scalar_tuple:
629
803
  inferred_types = ", ".join(
630
804
  item.value for item in tuple_types
631
805
  ) or "unknown"
632
806
  self._error(
633
807
  "request.security tuple-return helpers support two or more "
634
- "numeric int/float elements or homogeneous bool elements; inferred "
808
+ "int, float, bool or string elements; inferred "
635
809
  f"{tuple_size} element(s) [{inferred_types}]",
636
810
  expr_node.loc,
637
811
  )
@@ -657,10 +831,16 @@ class CallHandlers:
657
831
  lookahead_node = all_args[4] if len(all_args) > 4 else None
658
832
 
659
833
  mutable_globals = tuple(sorted(self._collect_security_mutable_globals(expr_node)))
834
+ # A helper request's symbol and timeframe, resolved through its
835
+ # call paths (``security_contexts``), register in its place.
836
+ notes = node.annotations or {}
837
+ context = notes.get(CONTEXT_ANNOTATION)
838
+ symbol_node = all_args[0] if all_args else None
839
+ if context is not None:
840
+ symbol_node, tf_node = context["symbol"], context["timeframe"]
660
841
  # Heikin-Ashi same-symbol read: request.security(ticker.heikinashi(
661
842
  # syminfo.tickerid), ...) (directly or via a global alias). The engine
662
843
  # applies the HA candle transform inside the security eval.
663
- symbol_node = all_args[0] if all_args else None
664
844
  heikinashi = self._security_symbol_is_heikinashi(symbol_node)
665
845
  # Capture the user function (if any) whose body contains this call,
666
846
  # so the codegen can resolve a parameter ``tf`` via the call sites.
@@ -668,6 +848,14 @@ class CallHandlers:
668
848
  containing_func = scope_name[5:] if scope_name.startswith("func_") else ""
669
849
  if returns_tuple and tuple_element_types:
670
850
  self._tuple_element_types_by_node[id(node)] = tuple_element_types
851
+ # A helper whose value is this request returns its tuple
852
+ # (``htf(sym, tf) => request.security(sym, tf, pack())``); a TA
853
+ # tuple's result struct is not one.
854
+ if returns_tuple and (isinstance(expr_node, TupleLiteral) or tuple_element_types):
855
+ self._security_tuple_shapes[id(node)] = tuple_size
856
+ # A string payload returns a string (TradingView's na string reads
857
+ # empty); every other scalar keeps the historical float result.
858
+ string_result = not returns_tuple and expr_type == PineType.STRING
671
859
  self._security_calls.append(SecurityCallInfo(
672
860
  sec_id=sec_id,
673
861
  timeframe=tf_node,
@@ -682,9 +870,15 @@ class CallHandlers:
682
870
  depends_on_mutable_globals=bool(mutable_globals),
683
871
  mutable_globals=mutable_globals,
684
872
  containing_func=containing_func,
873
+ string_result=string_result,
874
+ symbol=symbol_node,
875
+ context_resolved=context is not None,
876
+ dead=bool(notes.get(DEAD_ANNOTATION)),
877
+ foreign=notes.get(LOWERING_ANNOTATION) == FEED_LOWERING,
878
+ ignore_invalid=node.kwargs.get("ignore_invalid_symbol"),
685
879
  ))
686
880
 
687
- return PineType.FLOAT
881
+ return PineType.STRING if string_result else PineType.FLOAT
688
882
 
689
883
  if func_name == "security_lower_tf":
690
884
  return self._handle_request_security_lower_tf(node)
@@ -788,9 +982,11 @@ class CallHandlers:
788
982
  sec_id = len(self._security_calls)
789
983
 
790
984
  mutable_globals = tuple(sorted(self._collect_security_mutable_globals(expr_node)))
985
+ notes = node.annotations or {}
986
+ context = notes.get(CONTEXT_ANNOTATION)
791
987
  self._security_calls.append(SecurityCallInfo(
792
988
  sec_id=sec_id,
793
- timeframe=tf_node,
989
+ timeframe=context["timeframe"] if context is not None else tf_node,
794
990
  expression=expr_node,
795
991
  returns_tuple=False,
796
992
  tuple_size=0,
@@ -800,6 +996,8 @@ class CallHandlers:
800
996
  depends_on_mutable_globals=bool(mutable_globals),
801
997
  mutable_globals=mutable_globals,
802
998
  is_lower_tf_array=True,
999
+ context_resolved=context is not None,
1000
+ dead=bool(notes.get(DEAD_ANNOTATION)),
803
1001
  ))
804
1002
 
805
1003
  # ``request.security_lower_tf`` returns an array; the value-level
@@ -1282,6 +1480,9 @@ class CallHandlers:
1282
1480
  and isinstance(node.callee, MemberAccess)
1283
1481
  ):
1284
1482
  positional_args.insert(0, node.callee.object)
1483
+ param_defaults = list(
1484
+ (func_def.annotations or {}).get("param_defaults", ())
1485
+ )
1285
1486
  for p_idx, param_name in enumerate(func_def.params):
1286
1487
  if p_idx < len(positional_args):
1287
1488
  param_arg_map[param_name] = self._expr_to_str(
@@ -1291,6 +1492,13 @@ class CallHandlers:
1291
1492
  param_arg_map[param_name] = self._expr_to_str(
1292
1493
  node.kwargs[param_name]
1293
1494
  )
1495
+ elif (method_info is None
1496
+ and p_idx < len(param_defaults)
1497
+ and param_defaults[p_idx] is not None):
1498
+ # An omitted parameter reads its declared default.
1499
+ param_arg_map[param_name] = self._expr_to_str(
1500
+ param_defaults[p_idx]
1501
+ )
1294
1502
 
1295
1503
  if func_name in self._func_ta_ranges:
1296
1504
  start, end = self._func_ta_ranges[func_name]
@@ -1457,6 +1665,7 @@ class CallHandlers:
1457
1665
  node=orig.node,
1458
1666
  is_static=orig.is_static,
1459
1667
  owner_func=func_name,
1668
+ ctor_nodes=orig.ctor_nodes[:],
1460
1669
  )
1461
1670
  selected_ta_indices[i] = len(self._ta_call_sites)
1462
1671
  self._ta_call_sites.append(cloned)
@@ -1535,6 +1744,17 @@ class CallHandlers:
1535
1744
  visited_types[id(arg)] = self._visit(arg)
1536
1745
  for arg in node.kwargs.values():
1537
1746
  visited_types[id(arg)] = self._visit(arg)
1747
+ # An omitted parameter binds its declared default, as codegen passes it.
1748
+ defaulted = bind_function_defaults(
1749
+ func_def.params,
1750
+ (func_def.annotations or {}).get("param_defaults", ()),
1751
+ node,
1752
+ )
1753
+ if defaulted is not None:
1754
+ bound_args = list(defaulted)
1755
+ for arg in bound_args:
1756
+ if id(arg) not in visited_types:
1757
+ visited_types[id(arg)] = self._visit(arg)
1538
1758
 
1539
1759
  param_types = [
1540
1760
  visited_types.get(id(arg), PineType.UNKNOWN)
@@ -51,6 +51,12 @@ class TACallSite:
51
51
  # owning callee's emitted clone body referencing undeclared members
52
52
  # (regression: quantbyboji-nq-hma-midday ``_ta_change_*_cs1``).
53
53
  owner_func: str | None = None
54
+ # The AST nodes ``ctor_args`` were spelled from, in the same order (the
55
+ # nodes of the textual call: a clone keeps its template's). Empty for the
56
+ # shim sites that build their ctor args another way. The series- and
57
+ # simple-length lowering renders a length the constructor cannot take
58
+ # from these at the call site.
59
+ ctor_nodes: list = field(default_factory=list)
54
60
 
55
61
 
56
62
  @dataclass
@@ -174,6 +180,22 @@ class SecurityCallInfo:
174
180
  # body (``self._active_call_site_idx``). None for an ordinary
175
181
  # (non-cloned) security call.
176
182
  callsite_idx: int | None = None
183
+ # A scalar payload of string type: the result is a ``std::string``
184
+ # holding na (empty) until the first requested value.
185
+ string_result: bool = False
186
+ # The call's symbol argument (None when omitted).
187
+ symbol: Any = None
188
+ # ``timeframe`` and ``symbol`` hold the context
189
+ # ``security_contexts.specialize_security_contexts`` resolved through the
190
+ # helper call paths: expressions of globals, inputs and built-ins.
191
+ context_resolved: bool = False
192
+ # The call sits in a helper no top-level statement reaches: it never runs.
193
+ dead: bool = False
194
+ # Another symbol's request that reads the feed a requests manifest pins
195
+ # for it (``external_requests.FEED_LOWERING``): registered with the
196
+ # ``symbol`` string and ``ignore_invalid_symbol`` (the node, or None).
197
+ foreign: bool = False
198
+ ignore_invalid: Any = None
177
199
 
178
200
 
179
201
  @dataclass
@@ -268,6 +290,15 @@ class AnalyzerContext:
268
290
  # emits each call site's body — required for the per-clone
269
291
  # SecurityCallInfo.callsite_idx disambiguation in visit_call.py to work.
270
292
  func_security_clone_only: set = field(default_factory=set)
293
+ # Functions and methods that cannot keep a session.* flag's history per
294
+ # call site (name -> why): every method, and the functions a method, a
295
+ # request.security expression or a UDT field default reaches. Codegen
296
+ # refuses a session.<flag>[k] it emits in one of them.
297
+ session_history_unsafe: dict = field(default_factory=dict)
298
+ # Functions that read a flag at an offset this analysis did not clone:
299
+ # no per-call Series. A read of theirs the C++ holds asks
300
+ # pineforge_codegen._generate to clone them.
301
+ session_uncloned: frozenset = frozenset()
271
302
  # (func_name, cs_idx) -> {orig_member_name: cloned_member_name}. Populated by
272
303
  # the analyzer ONLY for clones whose default ``{base}_cs{cs_idx}`` name would
273
304
  # collide with a clone minted through another enclosing function; lets codegen
@@ -301,6 +332,12 @@ class AnalyzerContext:
301
332
  # global read remains lexical Pine, not the future local).
302
333
  func_var_storage_names: dict = field(default_factory=dict)
303
334
  func_series_vars: dict = field(default_factory=dict)
335
+ # Plain UDF -> script variables (and ``bar_index``) its body reads through
336
+ # history, in source order, and id(Subscript) -> (UDF, name, Subscript)
337
+ # per read. TradingView keeps that history per call site, so codegen gives
338
+ # each emitted body its own chart-clocked buffer of the variable.
339
+ func_global_history_reads: dict = field(default_factory=dict)
340
+ func_global_history_nodes: dict = field(default_factory=dict)
304
341
  # FuncDef owner -> declaration-bound non-persistent history-local names.
305
342
  # Parameters are deliberately excluded so a same-named qualified
306
343
  # persistent member does not manufacture a bogus raw/clone Series.
@@ -27,9 +27,11 @@ from __future__ import annotations
27
27
 
28
28
  from ..ast_nodes import (
29
29
  ASTNode, BinOp, BoolLiteral, FuncCall, Identifier, MemberAccess,
30
- NaLiteral, NumberLiteral, StringLiteral, Subscript, Ternary, UnaryOp,
30
+ NaLiteral, NumberLiteral, StringLiteral, Subscript, Ternary, TupleLiteral,
31
+ UnaryOp,
31
32
  )
32
33
  from ..errors import CompileError, Diagnostic, Level, Phase, SourceLocation
34
+ from ..pine_spelling import input_binding_names, is_input_call, pine_string_literal
33
35
  from .. import tv_input_choices as tv_in
34
36
  from .tables import BAR_FIELDS
35
37
 
@@ -96,6 +98,18 @@ class DiagnosticsHelper:
96
98
  # would otherwise silently reassociate under C++ precedence.
97
99
  _NONATOMIC_EXPR_NODES = (BinOp, UnaryOp, Ternary)
98
100
 
101
+ def _input_binding_name(self, node: FuncCall) -> str | None:
102
+ """The declared name an untitled global-scope input call is keyed by
103
+ (``pine_spelling.input_binding_names``), or None. Every input takes its
104
+ title as argument #2 or ``title=``."""
105
+ if len(node.args) > 1 or "title" in node.kwargs:
106
+ return None
107
+ names = getattr(self, "_input_binding_names_cache", None)
108
+ if names is None:
109
+ names = input_binding_names(self._ast.body)
110
+ self._input_binding_names_cache = names
111
+ return names.get(id(node))
112
+
99
113
  def _operand_to_str(self, node: ASTNode) -> str:
100
114
  s = self._expr_to_str(node)
101
115
  if isinstance(node, self._NONATOMIC_EXPR_NODES):
@@ -108,7 +122,7 @@ class DiagnosticsHelper:
108
122
  if isinstance(node, NumberLiteral):
109
123
  return str(node.value)
110
124
  if isinstance(node, StringLiteral):
111
- return f'"{node.value}"'
125
+ return pine_string_literal(node.value)
112
126
  if isinstance(node, BoolLiteral):
113
127
  return "true" if node.value else "false"
114
128
  if isinstance(node, NaLiteral):
@@ -122,9 +136,21 @@ class DiagnosticsHelper:
122
136
  if isinstance(node, UnaryOp):
123
137
  return f"{node.op}{self._operand_to_str(node.operand)}"
124
138
  if isinstance(node, FuncCall):
125
- args = ", ".join(self._expr_to_str(a) for a in node.args)
139
+ args = [self._expr_to_str(a) for a in node.args]
140
+ if is_input_call(node):
141
+ # The codegen reads an inline input's title, defval and bounds
142
+ # back from this spelling; without its keyword arguments
143
+ # ``input.int(defval=9, title="fast")`` would become
144
+ # ``input.int()``. An untitled call a declaration names (a
145
+ # ``var`` initializer's) is keyed by that name, spelled title=.
146
+ args += [f"{k}={self._expr_to_str(v)}" for k, v in node.kwargs.items()]
147
+ bound = self._input_binding_name(node)
148
+ if bound is not None:
149
+ args.append(f"title={pine_string_literal(bound)}")
126
150
  callee_str = self._expr_to_str(node.callee)
127
- return f"{callee_str}({args})"
151
+ return f"{callee_str}({', '.join(args)})"
152
+ if isinstance(node, TupleLiteral):
153
+ return "[" + ", ".join(self._expr_to_str(e) for e in node.elements) + "]"
128
154
  if isinstance(node, Subscript):
129
155
  return f"{self._expr_to_str(node.object)}[{self._expr_to_str(node.index)}]"
130
156
  if isinstance(node, Ternary):