@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.
- package/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +754 -76
- package/pineforge_codegen/analyzer/call_handlers.py +260 -40
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +896 -156
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +946 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1188 -100
- package/pineforge_codegen/codegen/tables.py +193 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +1882 -78
- package/pineforge_codegen/codegen/visit_call.py +920 -131
- package/pineforge_codegen/codegen/visit_expr.py +738 -57
- package/pineforge_codegen/codegen/visit_stmt.py +595 -49
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen-1.0.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.4.tar.gz +0 -0
|
@@ -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++
|
|
@@ -448,15 +520,55 @@ class CallHandlers:
|
|
|
448
520
|
self._series_bar_fields.add(field)
|
|
449
521
|
all_args = [default_src]
|
|
450
522
|
|
|
451
|
-
#
|
|
452
|
-
#
|
|
453
|
-
#
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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(
|
|
459
|
-
all_args = [default_src, all_args[
|
|
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
|
|
470
|
-
# 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.
|
|
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
|
-
|
|
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
|
|
798
|
+
item in _SECURITY_TUPLE_ELEMENT_TYPES
|
|
620
799
|
for item in tuple_types
|
|
621
800
|
)
|
|
622
801
|
)
|
|
623
|
-
|
|
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
|
-
"
|
|
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,
|
|
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):
|