@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.
- package/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +767 -77
- package/pineforge_codegen/analyzer/call_handlers.py +268 -42
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +907 -159
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +961 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1198 -101
- package/pineforge_codegen/codegen/tables.py +205 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +2055 -62
- package/pineforge_codegen/codegen/visit_call.py +929 -131
- package/pineforge_codegen/codegen/visit_expr.py +765 -63
- package/pineforge_codegen/codegen/visit_stmt.py +610 -52
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen/symbols.py +4 -2
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.3.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
|
-
|
|
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
|
-
|
|
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++
|
|
@@ -437,20 +509,66 @@ class CallHandlers:
|
|
|
437
509
|
all_args = [default_arg]
|
|
438
510
|
|
|
439
511
|
if func_name == "vwap" and not all_args:
|
|
440
|
-
|
|
512
|
+
# The bare ``ta.vwap`` property is the VWAP of hlc3 (Pine v6
|
|
513
|
+
# reference: "It uses hlc3 as its source series"); TradingView's
|
|
514
|
+
# own read equals hlc3 on a daily bar (lab tv
|
|
515
|
+
# notrade-session-vwap-f1d, NYSE:F 1D 2025-07-01..08-31, 42/42,
|
|
516
|
+
# 2026-09-05), where the engine used to read the bar's close.
|
|
517
|
+
default_src = Identifier(name="hlc3")
|
|
441
518
|
self._visit(default_src)
|
|
442
|
-
|
|
519
|
+
for field in ("high", "low", "close"):
|
|
520
|
+
self._series_bar_fields.add(field)
|
|
443
521
|
all_args = [default_src]
|
|
444
522
|
|
|
445
|
-
#
|
|
446
|
-
#
|
|
447
|
-
#
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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)
|
|
451
569
|
self._visit(default_src)
|
|
452
|
-
self._series_bar_fields.add(
|
|
453
|
-
all_args = [default_src, all_args[
|
|
570
|
+
self._series_bar_fields.add(default_source)
|
|
571
|
+
all_args = [default_src, all_args[-1]]
|
|
454
572
|
|
|
455
573
|
self._ta_counter += 1
|
|
456
574
|
class_name = TA_CLASS_MAP[func_name]
|
|
@@ -460,8 +578,8 @@ class CallHandlers:
|
|
|
460
578
|
# vwap_bands special dispatch: ta.vwap(source, anchor, stdev_mult)
|
|
461
579
|
# ctor receives stdev_mult only; compute receives source only.
|
|
462
580
|
# The anchor arg (index 1) is the Pine-level "when to reset" series;
|
|
463
|
-
# our VWAPBands wrapper
|
|
464
|
-
# anchor
|
|
581
|
+
# our VWAPBands wrapper resets on the symbol's session day, the
|
|
582
|
+
# default anchor, which is the only one the support checker admits.
|
|
465
583
|
if func_name == "vwap_bands":
|
|
466
584
|
ctor_args: list[str] = []
|
|
467
585
|
if len(all_args) >= 3 and all_args[2] is not None:
|
|
@@ -486,6 +604,7 @@ class CallHandlers:
|
|
|
486
604
|
|
|
487
605
|
# Determine constructor args
|
|
488
606
|
ctor_args: list[str] = []
|
|
607
|
+
ctor_nodes: list = []
|
|
489
608
|
effective_multi_ctor = TA_MULTI_CTOR.copy()
|
|
490
609
|
if func_name in ("pivothigh", "pivotlow") and len(all_args) == 3:
|
|
491
610
|
effective_multi_ctor[func_name] = [1, 2]
|
|
@@ -496,10 +615,12 @@ class CallHandlers:
|
|
|
496
615
|
for idx in effective_multi_ctor[func_name]:
|
|
497
616
|
if idx < len(all_args) and all_args[idx] is not None:
|
|
498
617
|
ctor_args.append(self._expr_to_str(all_args[idx]))
|
|
618
|
+
ctor_nodes.append(all_args[idx])
|
|
499
619
|
elif func_name in TA_PERIOD_ARG:
|
|
500
620
|
idx = TA_PERIOD_ARG[func_name]
|
|
501
621
|
if idx < len(all_args) and all_args[idx] is not None:
|
|
502
622
|
ctor_args.append(self._expr_to_str(all_args[idx]))
|
|
623
|
+
ctor_nodes.append(all_args[idx])
|
|
503
624
|
|
|
504
625
|
# Determine compute args (all args that aren't ctor args)
|
|
505
626
|
compute_args: list = []
|
|
@@ -528,6 +649,7 @@ class CallHandlers:
|
|
|
528
649
|
node=node,
|
|
529
650
|
is_static=is_static,
|
|
530
651
|
owner_func=(self._enclosing_func_names[-1] if self._enclosing_func_names else None),
|
|
652
|
+
ctor_nodes=ctor_nodes,
|
|
531
653
|
)
|
|
532
654
|
self._ta_call_sites.append(site)
|
|
533
655
|
self._ta_member_names.add(site.member_name)
|
|
@@ -553,6 +675,59 @@ class CallHandlers:
|
|
|
553
675
|
return self._security_symbol_is_heikinashi(self._global_expr_map[node.name], _seen)
|
|
554
676
|
return False
|
|
555
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
|
+
|
|
556
731
|
def _handle_request_call(self, func_name: str, node: FuncCall) -> PineType:
|
|
557
732
|
"""Handle request.* function calls."""
|
|
558
733
|
if func_name == "security":
|
|
@@ -578,8 +753,9 @@ class CallHandlers:
|
|
|
578
753
|
|
|
579
754
|
# Track TA sites created by the expression
|
|
580
755
|
ta_start = len(self._ta_call_sites)
|
|
756
|
+
expr_type = None
|
|
581
757
|
if expr_node is not None:
|
|
582
|
-
self._visit(expr_node)
|
|
758
|
+
expr_type = self._visit(expr_node)
|
|
583
759
|
ta_end = len(self._ta_call_sites)
|
|
584
760
|
security_ta_range = (ta_start, ta_end) if ta_end > ta_start else None
|
|
585
761
|
|
|
@@ -606,26 +782,30 @@ class CallHandlers:
|
|
|
606
782
|
if self._func_returns_tuple.get(expr_func, False):
|
|
607
783
|
tuple_size = self._func_tuple_element_count.get(expr_func, 0)
|
|
608
784
|
tuple_types = self._func_tuple_element_types.get(expr_func, ())
|
|
609
|
-
|
|
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 = (
|
|
610
795
|
tuple_size >= 2
|
|
611
796
|
and len(tuple_types) == tuple_size
|
|
612
797
|
and all(
|
|
613
|
-
item in
|
|
798
|
+
item in _SECURITY_TUPLE_ELEMENT_TYPES
|
|
614
799
|
for item in tuple_types
|
|
615
800
|
)
|
|
616
801
|
)
|
|
617
|
-
|
|
618
|
-
tuple_size >= 2
|
|
619
|
-
and len(tuple_types) == tuple_size
|
|
620
|
-
and all(item == PineType.BOOL for item in tuple_types)
|
|
621
|
-
)
|
|
622
|
-
if not (numeric_tuple or bool_tuple):
|
|
802
|
+
if not scalar_tuple:
|
|
623
803
|
inferred_types = ", ".join(
|
|
624
804
|
item.value for item in tuple_types
|
|
625
805
|
) or "unknown"
|
|
626
806
|
self._error(
|
|
627
807
|
"request.security tuple-return helpers support two or more "
|
|
628
|
-
"
|
|
808
|
+
"int, float, bool or string elements; inferred "
|
|
629
809
|
f"{tuple_size} element(s) [{inferred_types}]",
|
|
630
810
|
expr_node.loc,
|
|
631
811
|
)
|
|
@@ -651,10 +831,16 @@ class CallHandlers:
|
|
|
651
831
|
lookahead_node = all_args[4] if len(all_args) > 4 else None
|
|
652
832
|
|
|
653
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"]
|
|
654
841
|
# Heikin-Ashi same-symbol read: request.security(ticker.heikinashi(
|
|
655
842
|
# syminfo.tickerid), ...) (directly or via a global alias). The engine
|
|
656
843
|
# applies the HA candle transform inside the security eval.
|
|
657
|
-
symbol_node = all_args[0] if all_args else None
|
|
658
844
|
heikinashi = self._security_symbol_is_heikinashi(symbol_node)
|
|
659
845
|
# Capture the user function (if any) whose body contains this call,
|
|
660
846
|
# so the codegen can resolve a parameter ``tf`` via the call sites.
|
|
@@ -662,6 +848,14 @@ class CallHandlers:
|
|
|
662
848
|
containing_func = scope_name[5:] if scope_name.startswith("func_") else ""
|
|
663
849
|
if returns_tuple and tuple_element_types:
|
|
664
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
|
|
665
859
|
self._security_calls.append(SecurityCallInfo(
|
|
666
860
|
sec_id=sec_id,
|
|
667
861
|
timeframe=tf_node,
|
|
@@ -676,9 +870,15 @@ class CallHandlers:
|
|
|
676
870
|
depends_on_mutable_globals=bool(mutable_globals),
|
|
677
871
|
mutable_globals=mutable_globals,
|
|
678
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"),
|
|
679
879
|
))
|
|
680
880
|
|
|
681
|
-
return PineType.FLOAT
|
|
881
|
+
return PineType.STRING if string_result else PineType.FLOAT
|
|
682
882
|
|
|
683
883
|
if func_name == "security_lower_tf":
|
|
684
884
|
return self._handle_request_security_lower_tf(node)
|
|
@@ -782,9 +982,11 @@ class CallHandlers:
|
|
|
782
982
|
sec_id = len(self._security_calls)
|
|
783
983
|
|
|
784
984
|
mutable_globals = tuple(sorted(self._collect_security_mutable_globals(expr_node)))
|
|
985
|
+
notes = node.annotations or {}
|
|
986
|
+
context = notes.get(CONTEXT_ANNOTATION)
|
|
785
987
|
self._security_calls.append(SecurityCallInfo(
|
|
786
988
|
sec_id=sec_id,
|
|
787
|
-
timeframe=tf_node,
|
|
989
|
+
timeframe=context["timeframe"] if context is not None else tf_node,
|
|
788
990
|
expression=expr_node,
|
|
789
991
|
returns_tuple=False,
|
|
790
992
|
tuple_size=0,
|
|
@@ -794,6 +996,8 @@ class CallHandlers:
|
|
|
794
996
|
depends_on_mutable_globals=bool(mutable_globals),
|
|
795
997
|
mutable_globals=mutable_globals,
|
|
796
998
|
is_lower_tf_array=True,
|
|
999
|
+
context_resolved=context is not None,
|
|
1000
|
+
dead=bool(notes.get(DEAD_ANNOTATION)),
|
|
797
1001
|
))
|
|
798
1002
|
|
|
799
1003
|
# ``request.security_lower_tf`` returns an array; the value-level
|
|
@@ -1276,6 +1480,9 @@ class CallHandlers:
|
|
|
1276
1480
|
and isinstance(node.callee, MemberAccess)
|
|
1277
1481
|
):
|
|
1278
1482
|
positional_args.insert(0, node.callee.object)
|
|
1483
|
+
param_defaults = list(
|
|
1484
|
+
(func_def.annotations or {}).get("param_defaults", ())
|
|
1485
|
+
)
|
|
1279
1486
|
for p_idx, param_name in enumerate(func_def.params):
|
|
1280
1487
|
if p_idx < len(positional_args):
|
|
1281
1488
|
param_arg_map[param_name] = self._expr_to_str(
|
|
@@ -1285,6 +1492,13 @@ class CallHandlers:
|
|
|
1285
1492
|
param_arg_map[param_name] = self._expr_to_str(
|
|
1286
1493
|
node.kwargs[param_name]
|
|
1287
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
|
+
)
|
|
1288
1502
|
|
|
1289
1503
|
if func_name in self._func_ta_ranges:
|
|
1290
1504
|
start, end = self._func_ta_ranges[func_name]
|
|
@@ -1451,6 +1665,7 @@ class CallHandlers:
|
|
|
1451
1665
|
node=orig.node,
|
|
1452
1666
|
is_static=orig.is_static,
|
|
1453
1667
|
owner_func=func_name,
|
|
1668
|
+
ctor_nodes=orig.ctor_nodes[:],
|
|
1454
1669
|
)
|
|
1455
1670
|
selected_ta_indices[i] = len(self._ta_call_sites)
|
|
1456
1671
|
self._ta_call_sites.append(cloned)
|
|
@@ -1529,6 +1744,17 @@ class CallHandlers:
|
|
|
1529
1744
|
visited_types[id(arg)] = self._visit(arg)
|
|
1530
1745
|
for arg in node.kwargs.values():
|
|
1531
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)
|
|
1532
1758
|
|
|
1533
1759
|
param_types = [
|
|
1534
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,
|
|
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
|
|
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 =
|
|
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):
|