@pineforge/codegen-pyodide 0.8.0 → 0.8.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/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +453 -15
- package/pineforge_codegen/analyzer/call_handlers.py +200 -11
- package/pineforge_codegen/analyzer/contracts.py +62 -0
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +509 -44
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +286 -27
- package/pineforge_codegen/codegen/input.py +14 -0
- package/pineforge_codegen/codegen/security.py +294 -13
- package/pineforge_codegen/codegen/tables.py +59 -3
- package/pineforge_codegen/codegen/types.py +369 -19
- package/pineforge_codegen/codegen/visit_call.py +147 -22
- package/pineforge_codegen/codegen/visit_expr.py +54 -0
- package/pineforge_codegen/codegen/visit_stmt.py +161 -14
- package/pineforge_codegen/parser.py +56 -23
- package/pineforge_codegen/support_checker.py +281 -17
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +2 -8
- package/pineforge_codegen-0.8.0.tar.gz +0 -0
|
@@ -576,39 +576,64 @@ class Parser:
|
|
|
576
576
|
|
|
577
577
|
# -- Function definition --
|
|
578
578
|
|
|
579
|
+
def _parse_param_type_annotation(self) -> str | None:
|
|
580
|
+
"""Consume an optional function-parameter type annotation and return it
|
|
581
|
+
as a canonical hint string ('float', 'string', 'array<line>', 'pivot',
|
|
582
|
+
'chart.point', ...), or ``None`` for a bare untyped param.
|
|
583
|
+
|
|
584
|
+
Leaves the parser positioned at the parameter name. Supports the Pine
|
|
585
|
+
qualifier prefixes (``series`` / ``simple`` / ``const`` — consumed but
|
|
586
|
+
not part of the C++ type), builtin types, user-defined / drawing type
|
|
587
|
+
names, and the ``T[]`` postfix-array shorthand (normalised to
|
|
588
|
+
``array<T>`` by ``_parse_type_hint_string``).
|
|
589
|
+
"""
|
|
590
|
+
TYPE_TOKENS = {TokenType.TYPE_INT, TokenType.TYPE_FLOAT,
|
|
591
|
+
TokenType.TYPE_BOOL, TokenType.TYPE_STRING}
|
|
592
|
+
# Optional qualifiers — they do not affect the C++ param type.
|
|
593
|
+
while self._check(TokenType.IDENT) and self._current().value in (
|
|
594
|
+
"series", "simple", "const",
|
|
595
|
+
):
|
|
596
|
+
self._advance()
|
|
597
|
+
# Is there a type annotation before the parameter name? A builtin type
|
|
598
|
+
# token always is; an IDENT is a type only if followed by another IDENT
|
|
599
|
+
# (``line ln``), by ``[`` (``line[] arr``), or by ``<`` (the generic
|
|
600
|
+
# collection syntax ``array<float> xs`` / ``matrix<float> m`` /
|
|
601
|
+
# ``map<string,float> mp``). Without the ``<`` case the generic type is
|
|
602
|
+
# mis-consumed as the parameter name and the whole function definition
|
|
603
|
+
# silently fails to parse (its body leaks to top-level scope).
|
|
604
|
+
has_type = False
|
|
605
|
+
if self._current().type in TYPE_TOKENS:
|
|
606
|
+
has_type = True
|
|
607
|
+
elif self._check(TokenType.IDENT):
|
|
608
|
+
nxt = self._peek().type
|
|
609
|
+
if nxt in (TokenType.IDENT, TokenType.LBRACKET, TokenType.LT):
|
|
610
|
+
has_type = True
|
|
611
|
+
if not has_type:
|
|
612
|
+
return None
|
|
613
|
+
return self._parse_type_hint_string()
|
|
614
|
+
|
|
579
615
|
def _parse_func_def(self) -> FuncDef:
|
|
580
616
|
"""Parse: name(param1, param2) => expr_or_block"""
|
|
581
617
|
start_tok = self._current()
|
|
582
618
|
name = self._consume(TokenType.IDENT).value
|
|
583
619
|
self._consume(TokenType.LPAREN)
|
|
584
|
-
TYPE_TOKENS = {TokenType.TYPE_INT, TokenType.TYPE_FLOAT,
|
|
585
|
-
TokenType.TYPE_BOOL, TokenType.TYPE_STRING}
|
|
586
620
|
params = []
|
|
621
|
+
param_type_hints: list = []
|
|
622
|
+
param_defaults: list = []
|
|
587
623
|
while not self._check(TokenType.RPAREN):
|
|
588
|
-
# Consume optional
|
|
589
|
-
#
|
|
590
|
-
#
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
"simple",
|
|
594
|
-
"const",
|
|
595
|
-
):
|
|
596
|
-
self._advance()
|
|
597
|
-
# Handle optional type annotation: type param (e.g., int len, float src,
|
|
598
|
-
# string s). Built-in types are dedicated tokens; user-defined types are
|
|
599
|
-
# IDENTs and handled by the "next is IDENT" check below.
|
|
600
|
-
if self._current().type in TYPE_TOKENS:
|
|
601
|
-
self._advance() # skip the type annotation
|
|
624
|
+
# Consume the optional type annotation (builtin / user / drawing /
|
|
625
|
+
# ``T[]``), returning the canonical hint string. Handles ``float[] arr``,
|
|
626
|
+
# ``line[] ln``, ``color c``, ``SDZone z``, ``string tf``, as well as
|
|
627
|
+
# the untyped bare-name case.
|
|
628
|
+
hint = self._parse_param_type_annotation()
|
|
602
629
|
param_name = self._consume(TokenType.IDENT).value
|
|
603
|
-
|
|
604
|
-
# 'param_name' was actually a (user-defined) type name parsed as IDENT,
|
|
605
|
-
# next is the real name.
|
|
606
|
-
param_name = self._consume(TokenType.IDENT).value
|
|
607
|
-
# Skip default value: param = expr
|
|
630
|
+
pdefault = None
|
|
608
631
|
if self._check(TokenType.EQUALS):
|
|
609
632
|
self._advance() # consume '='
|
|
610
|
-
self._parse_expression() #
|
|
633
|
+
pdefault = self._parse_expression() # default value
|
|
611
634
|
params.append(param_name)
|
|
635
|
+
param_type_hints.append(hint)
|
|
636
|
+
param_defaults.append(pdefault)
|
|
612
637
|
self._match(TokenType.COMMA)
|
|
613
638
|
self._consume(TokenType.RPAREN)
|
|
614
639
|
self._skip_newlines()
|
|
@@ -625,6 +650,13 @@ class Parser:
|
|
|
625
650
|
expr = self._parse_expression()
|
|
626
651
|
node = FuncDef(name=name, params=params, body=[ExprStmt(expr=expr)], is_single_expr=True)
|
|
627
652
|
|
|
653
|
+
# Record per-param type hints + defaults (mirrors _parse_method_def) so
|
|
654
|
+
# the analyzer can type UDT/string/array params and the codegen can emit
|
|
655
|
+
# them with the correct C++ type (``pivot hi``, ``std::string s``).
|
|
656
|
+
node.annotations = {
|
|
657
|
+
"param_type_hints": param_type_hints,
|
|
658
|
+
"param_defaults": param_defaults,
|
|
659
|
+
}
|
|
628
660
|
return self._set_loc(node, start_tok)
|
|
629
661
|
|
|
630
662
|
def _parse_type_or_enum_decl(self):
|
|
@@ -701,7 +733,8 @@ class Parser:
|
|
|
701
733
|
if self._current().type in TYPE_KEYWORDS:
|
|
702
734
|
param_type = self._parse_type_hint_string()
|
|
703
735
|
elif (self._current().type == TokenType.IDENT
|
|
704
|
-
and self._peek().type
|
|
736
|
+
and self._peek().type in (TokenType.IDENT, TokenType.LBRACKET, TokenType.LT)):
|
|
737
|
+
# ``line ln`` / ``float[] arr`` / ``array<float> xs`` typed param.
|
|
705
738
|
param_type = self._parse_type_hint_string()
|
|
706
739
|
p = self._consume(TokenType.IDENT).value
|
|
707
740
|
pdefault = None
|
|
@@ -50,11 +50,15 @@ from .tv_input_choices import INPUT_SOURCE_SERIES_IDS
|
|
|
50
50
|
from .analyzer import TA_CLASS_MAP
|
|
51
51
|
from .codegen import (
|
|
52
52
|
MATH_FUNC_MAP, STR_FUNC_MAP,
|
|
53
|
-
ARRAY_METHODS, MAP_METHODS, MATRIX_METHODS,
|
|
53
|
+
ARRAY_METHODS, ARRAY_DRAWING_NEW_CTORS, MAP_METHODS, MATRIX_METHODS,
|
|
54
54
|
SYMINFO_MEMBER_MAP, COLOR_CONST_MAP,
|
|
55
55
|
SKIP_FUNC_NAMES, SKIP_NAMESPACES,
|
|
56
56
|
BAR_BUILTINS, BAR_FIELDS,
|
|
57
57
|
)
|
|
58
|
+
from .codegen.drawing import (
|
|
59
|
+
LINE_METHODS, LINE_NOOP, BOX_METHODS, BOX_NOOP,
|
|
60
|
+
LABEL_METHODS, LABEL_NOOP, LINEFILL_METHODS, LINEFILL_NOOP,
|
|
61
|
+
)
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
# ---------------------------------------------------------------------------
|
|
@@ -74,10 +78,38 @@ SUPPORTED_MATH: frozenset[str] = frozenset(
|
|
|
74
78
|
)
|
|
75
79
|
SUPPORTED_STR: frozenset[str] = frozenset(set(STR_FUNC_MAP) | {"format_time"})
|
|
76
80
|
SUPPORTED_INPUT: frozenset[str] = frozenset(sigs.INPUT_FUNCTIONS)
|
|
77
|
-
SUPPORTED_ARRAY: frozenset[str] = frozenset(set(ARRAY_METHODS) | {"new", "new_float", "new_int", "new_bool", "new_string", "from"})
|
|
81
|
+
SUPPORTED_ARRAY: frozenset[str] = frozenset(set(ARRAY_METHODS) | {"new", "new_float", "new_int", "new_bool", "new_string", "from"} | set(ARRAY_DRAWING_NEW_CTORS))
|
|
78
82
|
SUPPORTED_MAP: frozenset[str] = frozenset(set(MAP_METHODS) | {"new"})
|
|
79
83
|
SUPPORTED_MATRIX: frozenset[str] = frozenset(set(MATRIX_METHODS) | {"new"})
|
|
80
84
|
SUPPORTED_SYMINFO: frozenset[str] = frozenset(SYMINFO_MEMBER_MAP)
|
|
85
|
+
# Drawing-objects-as-data (spec §4.5). Geometry methods are REAL (route to the
|
|
86
|
+
# per-type arena); *_NOOP visual setters are accepted no-ops (Level.WARNING).
|
|
87
|
+
# Any line/box/label/linefill method NOT in these sets rejects loudly so an
|
|
88
|
+
# unknown drawing method is not silently emitted. ``table``/``polyline`` are
|
|
89
|
+
# intentionally absent — they stay in SKIP_NAMESPACES (no-op, never reclassified).
|
|
90
|
+
SUPPORTED_LINE: frozenset[str] = frozenset(LINE_METHODS | LINE_NOOP | {"new"})
|
|
91
|
+
SUPPORTED_BOX: frozenset[str] = frozenset(BOX_METHODS | BOX_NOOP | {"new"})
|
|
92
|
+
SUPPORTED_LABEL: frozenset[str] = frozenset(LABEL_METHODS | LABEL_NOOP | {"new"})
|
|
93
|
+
SUPPORTED_LINEFILL: frozenset[str] = frozenset(LINEFILL_METHODS | LINEFILL_NOOP | {"new"})
|
|
94
|
+
SUPPORTED_CHART_POINT: frozenset[str] = frozenset(
|
|
95
|
+
{"new", "now", "from_index", "from_time", "copy"}
|
|
96
|
+
)
|
|
97
|
+
# Visual no-op drawing setters (warn, don't reject) keyed by namespace.
|
|
98
|
+
_DRAWING_NOOP_BY_NS: dict[str, frozenset[str]] = {
|
|
99
|
+
"line": LINE_NOOP, "box": BOX_NOOP, "label": LABEL_NOOP, "linefill": LINEFILL_NOOP,
|
|
100
|
+
}
|
|
101
|
+
_DRAWING_SUPPORTED_BY_NS: dict[str, frozenset[str]] = {
|
|
102
|
+
"line": SUPPORTED_LINE, "box": SUPPORTED_BOX,
|
|
103
|
+
"label": SUPPORTED_LABEL, "linefill": SUPPORTED_LINEFILL,
|
|
104
|
+
}
|
|
105
|
+
_DRAWING_TYPE_NAMES: frozenset[str] = frozenset({"line", "box", "label", "linefill"})
|
|
106
|
+
# Scalar visual-container types whose *method* calls (``panel.cell(...)``,
|
|
107
|
+
# ``ln.set_xy1(...)``) legitimately carry visual-constant args (``text.align_*``,
|
|
108
|
+
# ``size.*``). A method on a receiver of one of these types — including a
|
|
109
|
+
# table/box/line/label/linefill PARAMETER — is a visual sink, so its argument
|
|
110
|
+
# subtree is visited with constant-namespace reads allowed (mirrors the
|
|
111
|
+
# namespace-form ``table.cell``/``label.new`` handling).
|
|
112
|
+
_VISUAL_CONTAINER_TYPES: frozenset[str] = _DRAWING_TYPE_NAMES | frozenset({"table"})
|
|
81
113
|
SUPPORTED_COLOR_CONST: frozenset[str] = frozenset(COLOR_CONST_MAP)
|
|
82
114
|
SUPPORTED_COLOR_FUNC: frozenset[str] = frozenset({"new", "rgb", "r", "g", "b", "t"})
|
|
83
115
|
# Cosmetic color builders with no backtest-logic effect. Warned (not rejected);
|
|
@@ -100,11 +132,10 @@ HARD_REJECT_FUNC: dict[str, str] = {
|
|
|
100
132
|
"request.seed": "External seed data feeds not available in PineForge.",
|
|
101
133
|
"request.quandl": "External Quandl data not available in PineForge.",
|
|
102
134
|
"request.currency_rate": "Currency conversion data not available in PineForge.",
|
|
103
|
-
# ticker.* chart-type modifiers and cross-symbol constructors — hard reject
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
),
|
|
135
|
+
# ticker.* chart-type modifiers and cross-symbol constructors — hard reject.
|
|
136
|
+
# NOTE: ticker.heikinashi is handled contextually in _visit_FuncCall (allowed
|
|
137
|
+
# for the chart's own symbol — the engine applies the HA candle transform
|
|
138
|
+
# inside the same-symbol security eval — rejected for cross-symbol).
|
|
108
139
|
"ticker.renko": (
|
|
109
140
|
"ticker.renko() chart-type modifier / cross-symbol construction not supported — "
|
|
110
141
|
"PineForge runs same-symbol MTF only via request.security with the chart's own tickerid."
|
|
@@ -146,17 +177,23 @@ HARD_REJECT_NAMESPACE: dict[str, str] = {
|
|
|
146
177
|
# DIVERGENT_VARS_ERROR is a SUBSET that is escalated to ERROR (rejected): these
|
|
147
178
|
# are silent MIS-ALIASES, not merely data-window divergences. They return a
|
|
148
179
|
# plausible value that is the WRONG quantity (last_bar_index -> current bar
|
|
149
|
-
# index
|
|
150
|
-
#
|
|
180
|
+
# index) and that value flows directly into trade logic, so the backtest would
|
|
181
|
+
# be silently wrong. A WARNING is not enough.
|
|
182
|
+
#
|
|
183
|
+
# NOTE: the bare ``time_close`` variable is NOT divergent — codegen lowers it to
|
|
184
|
+
# the engine's ``time_close()`` accessor, which returns the true bar-close
|
|
185
|
+
# timestamp (bar open + chart-timeframe duration via ``pine_time_close``),
|
|
186
|
+
# deterministic and TV-faithful. It was previously mis-listed here as a bar-open
|
|
187
|
+
# alias; that was stale. (The session-aware ``time_close(...)`` FUNCTION is a
|
|
188
|
+
# separate supported builtin handled in visit_call.)
|
|
151
189
|
DIVERGENT_VARS: dict[str, str] = {
|
|
152
190
|
"bar_index": "bar_index depends on the data window; PineForge and TradingView produce different values for the same script.",
|
|
153
191
|
"last_bar_index": "last_bar_index is aliased to the CURRENT bar index in PineForge codegen (not the index of the last bar); backtest would be silently wrong — rejected.",
|
|
154
192
|
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time.",
|
|
155
|
-
"time_close": "time_close is aliased to the bar OPEN timestamp in PineForge; it does not represent the bar close time; backtest would be silently wrong — rejected.",
|
|
156
193
|
}
|
|
157
194
|
|
|
158
195
|
# Subset of DIVERGENT_VARS escalated from WARNING to ERROR (see comment above).
|
|
159
|
-
DIVERGENT_VARS_ERROR: frozenset[str] = frozenset({"last_bar_index"
|
|
196
|
+
DIVERGENT_VARS_ERROR: frozenset[str] = frozenset({"last_bar_index"})
|
|
160
197
|
|
|
161
198
|
BARSTATE_APPROX_VARS: dict[str, str] = {
|
|
162
199
|
"barstate.islast": "barstate.islast is always false in PineForge batch backtests.",
|
|
@@ -411,6 +448,22 @@ class SupportChecker:
|
|
|
411
448
|
self._user_enums: set[str] = set()
|
|
412
449
|
self._user_funcs: set[str] = set()
|
|
413
450
|
self._user_methods: set[str] = set()
|
|
451
|
+
# Drawing lifecycle gap guard (spec L.3/U.7): only bare identifiers are
|
|
452
|
+
# promoted to Series<T> by analyzer/base.py::_visit_Subscript. A history
|
|
453
|
+
# read on a drawing handle nested in a UDT field (``p.a[1]``) or on a
|
|
454
|
+
# tuple element known to be a drawing handle (``la[1]`` after
|
|
455
|
+
# ``[la, lb] = makePair()``) would otherwise silently read the current
|
|
456
|
+
# bar. Track enough lightweight declaration information to reject those
|
|
457
|
+
# shapes before codegen.
|
|
458
|
+
self._udt_drawing_fields: dict[str, set[str]] = {}
|
|
459
|
+
self._var_udt_types: dict[str, str] = {}
|
|
460
|
+
# Names (vars and function params) declared as a scalar visual-container
|
|
461
|
+
# type (table/box/line/label/linefill). A method call on one of these
|
|
462
|
+
# (``panel.cell(...)``) is a visual sink whose args may carry visual
|
|
463
|
+
# constants, so it routes through ``_visit_children_const_ok``.
|
|
464
|
+
self._visual_container_vars: set[str] = set()
|
|
465
|
+
self._drawing_tuple_vars: set[str] = set()
|
|
466
|
+
self._func_tuple_drawing_returns: dict[str, list[bool]] = {}
|
|
414
467
|
# Track whether we are inside an if/ternary condition expression.
|
|
415
468
|
self._in_conditional_depth: int = 0
|
|
416
469
|
# Track whether we are inside an argument subtree that legitimately
|
|
@@ -425,6 +478,12 @@ class SupportChecker:
|
|
|
425
478
|
# bare ``time_close`` variable) must NOT be flagged as a divergent
|
|
426
479
|
# variable read. Populated as _visit_FuncCall descends into children.
|
|
427
480
|
self._callee_node_ids: set[int] = set()
|
|
481
|
+
# Declaration value of each scalar variable (``name = <expr>``), so the
|
|
482
|
+
# request.security symbol check can resolve an aliased symbol through
|
|
483
|
+
# its definition — e.g. ``haTicker = ticker.heikinashi(syminfo.tickerid)``
|
|
484
|
+
# then ``request.security(haTicker, ...)``, or ``reqSym = cond ? other :
|
|
485
|
+
# syminfo.tickerid``. First binding wins (closest to declaration).
|
|
486
|
+
self._scalar_defs: dict[str, ASTNode] = {}
|
|
428
487
|
|
|
429
488
|
# -- Public API --
|
|
430
489
|
|
|
@@ -446,12 +505,83 @@ class SupportChecker:
|
|
|
446
505
|
for stmt in ast.body:
|
|
447
506
|
if isinstance(stmt, TypeDecl):
|
|
448
507
|
self._user_types.add(stmt.name)
|
|
508
|
+
drawing_fields = {
|
|
509
|
+
field.name
|
|
510
|
+
for field in stmt.fields
|
|
511
|
+
if self._type_name_contains_drawing(field.type_name)
|
|
512
|
+
}
|
|
513
|
+
if drawing_fields:
|
|
514
|
+
self._udt_drawing_fields[stmt.name] = drawing_fields
|
|
449
515
|
elif isinstance(stmt, EnumDecl):
|
|
450
516
|
self._user_enums.add(stmt.name)
|
|
451
517
|
elif isinstance(stmt, FuncDef):
|
|
452
518
|
self._user_funcs.add(stmt.name)
|
|
519
|
+
tuple_returns = self._single_expr_tuple_drawing_mask(stmt)
|
|
520
|
+
if tuple_returns:
|
|
521
|
+
self._func_tuple_drawing_returns[stmt.name] = tuple_returns
|
|
522
|
+
self._collect_visual_container_params(stmt)
|
|
453
523
|
elif isinstance(stmt, MethodDef):
|
|
454
524
|
self._user_methods.add(stmt.name)
|
|
525
|
+
self._collect_visual_container_params(stmt)
|
|
526
|
+
|
|
527
|
+
def _collect_visual_container_params(self, fn) -> None:
|
|
528
|
+
"""Register a function's parameters that are declared with a scalar
|
|
529
|
+
visual-container type (``table panel``, ``line ln``) so method calls on
|
|
530
|
+
them inside the body (``panel.cell(...)``) are treated as visual sinks.
|
|
531
|
+
"""
|
|
532
|
+
hints = (getattr(fn, "annotations", None) or {}).get("param_type_hints") or []
|
|
533
|
+
for i, pname in enumerate(getattr(fn, "params", []) or []):
|
|
534
|
+
hint = hints[i] if i < len(hints) else None
|
|
535
|
+
if hint and str(hint).replace(" ", "") in _VISUAL_CONTAINER_TYPES:
|
|
536
|
+
self._visual_container_vars.add(pname)
|
|
537
|
+
|
|
538
|
+
@staticmethod
|
|
539
|
+
def _type_name_contains_drawing(type_name: str | None) -> bool:
|
|
540
|
+
if not type_name:
|
|
541
|
+
return False
|
|
542
|
+
compact = str(type_name).replace(" ", "")
|
|
543
|
+
if compact.endswith("[]"):
|
|
544
|
+
compact = f"array<{compact[:-2]}>"
|
|
545
|
+
if compact in _DRAWING_TYPE_NAMES:
|
|
546
|
+
return True
|
|
547
|
+
return any(f"<{drawing}>" in compact for drawing in _DRAWING_TYPE_NAMES)
|
|
548
|
+
|
|
549
|
+
@staticmethod
|
|
550
|
+
def _is_literal_zero_subscript(node: Subscript) -> bool:
|
|
551
|
+
return isinstance(node.index, NumberLiteral) and node.index.value == 0
|
|
552
|
+
|
|
553
|
+
def _expr_is_drawing_handle(self, expr: ASTNode | None) -> bool:
|
|
554
|
+
if isinstance(expr, FuncCall):
|
|
555
|
+
ns, name = _qualified_name(expr.callee)
|
|
556
|
+
if ns in _DRAWING_TYPE_NAMES and name in {"new", "copy"}:
|
|
557
|
+
return True
|
|
558
|
+
if ns == "linefill" and name in {"get_line1", "get_line2"}:
|
|
559
|
+
return True
|
|
560
|
+
if isinstance(expr, Identifier):
|
|
561
|
+
return expr.name in self._drawing_tuple_vars
|
|
562
|
+
if isinstance(expr, Ternary):
|
|
563
|
+
return self._expr_is_drawing_handle(expr.true_val) or self._expr_is_drawing_handle(expr.false_val)
|
|
564
|
+
return False
|
|
565
|
+
|
|
566
|
+
def _single_expr_tuple_drawing_mask(self, fn: FuncDef) -> list[bool] | None:
|
|
567
|
+
if not fn.is_single_expr or len(fn.body) != 1:
|
|
568
|
+
return None
|
|
569
|
+
stmt = fn.body[0]
|
|
570
|
+
expr = stmt.expr if isinstance(stmt, ExprStmt) else None
|
|
571
|
+
if not isinstance(expr, TupleLiteral):
|
|
572
|
+
return None
|
|
573
|
+
mask = [self._expr_is_drawing_handle(item) for item in expr.elements]
|
|
574
|
+
return mask if any(mask) else None
|
|
575
|
+
|
|
576
|
+
def _tuple_assign_drawing_mask(self, value: ASTNode | None) -> list[bool] | None:
|
|
577
|
+
if isinstance(value, TupleLiteral):
|
|
578
|
+
mask = [self._expr_is_drawing_handle(item) for item in value.elements]
|
|
579
|
+
return mask if any(mask) else None
|
|
580
|
+
if isinstance(value, FuncCall):
|
|
581
|
+
ns, name = _qualified_name(value.callee)
|
|
582
|
+
if ns is None and name in self._func_tuple_drawing_returns:
|
|
583
|
+
return self._func_tuple_drawing_returns[name]
|
|
584
|
+
return None
|
|
455
585
|
|
|
456
586
|
# -- Diagnostic emission --
|
|
457
587
|
|
|
@@ -639,6 +769,24 @@ class SupportChecker:
|
|
|
639
769
|
self._visit_children_const_ok(node)
|
|
640
770
|
|
|
641
771
|
def _visit_VarDecl(self, node: VarDecl) -> None:
|
|
772
|
+
if node.name and node.value is not None:
|
|
773
|
+
self._scalar_defs.setdefault(node.name, node.value)
|
|
774
|
+
if node.type_hint in self._user_types:
|
|
775
|
+
self._var_udt_types[node.name] = node.type_hint
|
|
776
|
+
elif isinstance(node.value, FuncCall):
|
|
777
|
+
ns, name = _qualified_name(node.value.callee)
|
|
778
|
+
if name == "new" and ns in self._user_types:
|
|
779
|
+
self._var_udt_types[node.name] = ns
|
|
780
|
+
# Track scalar visual-container vars (``var table dash = table.new(...)``)
|
|
781
|
+
# so a direct ``dash.cell(..., text.align_left)`` is treated as a visual
|
|
782
|
+
# sink (mirrors the table/box/line/label/linefill PARAM tracking).
|
|
783
|
+
decl_hint = str(node.type_hint).replace(" ", "") if node.type_hint else None
|
|
784
|
+
if decl_hint in _VISUAL_CONTAINER_TYPES:
|
|
785
|
+
self._visual_container_vars.add(node.name)
|
|
786
|
+
elif isinstance(node.value, FuncCall):
|
|
787
|
+
vns, vname = _qualified_name(node.value.callee)
|
|
788
|
+
if vname == "new" and vns in _VISUAL_CONTAINER_TYPES:
|
|
789
|
+
self._visual_container_vars.add(node.name)
|
|
642
790
|
if node.is_varip:
|
|
643
791
|
self._err(
|
|
644
792
|
node,
|
|
@@ -655,6 +803,11 @@ class SupportChecker:
|
|
|
655
803
|
self._visit_children(node)
|
|
656
804
|
|
|
657
805
|
def _visit_TupleAssign(self, node: TupleAssign) -> None:
|
|
806
|
+
drawing_mask = self._tuple_assign_drawing_mask(node.value)
|
|
807
|
+
if drawing_mask:
|
|
808
|
+
for idx, name in enumerate(node.names):
|
|
809
|
+
if idx < len(drawing_mask) and drawing_mask[idx]:
|
|
810
|
+
self._drawing_tuple_vars.add(name)
|
|
658
811
|
if isinstance(node.value, FuncCall):
|
|
659
812
|
ns, name = _qualified_name(node.value.callee)
|
|
660
813
|
if ns == "ta" and name == "stoch":
|
|
@@ -665,6 +818,40 @@ class SupportChecker:
|
|
|
665
818
|
)
|
|
666
819
|
self._visit_children(node)
|
|
667
820
|
|
|
821
|
+
def _visit_Subscript(self, node: Subscript) -> None:
|
|
822
|
+
# Best-effort defensive guard for the silent-wrong shapes called out in
|
|
823
|
+
# spec L.3/U.7. This is not a full expression type checker; it catches
|
|
824
|
+
# the known dangerous surface (UDT vars with drawing fields, and tuple
|
|
825
|
+
# destructured drawing handles) without trying to prove every nested or
|
|
826
|
+
# parameter-passed UDT expression. Literal [0] is current-bar in Pine and
|
|
827
|
+
# safe to leave to codegen's existing current-value behavior.
|
|
828
|
+
if self._is_literal_zero_subscript(node):
|
|
829
|
+
self._visit_children(node)
|
|
830
|
+
return
|
|
831
|
+
if isinstance(node.object, MemberAccess) and isinstance(node.object.object, Identifier):
|
|
832
|
+
var_name = node.object.object.name
|
|
833
|
+
udt_name = self._var_udt_types.get(var_name)
|
|
834
|
+
if udt_name is not None and node.object.member in self._udt_drawing_fields.get(udt_name, set()):
|
|
835
|
+
self._err(
|
|
836
|
+
node,
|
|
837
|
+
"History references on UDT drawing fields are not supported in PineForge.",
|
|
838
|
+
hint=(
|
|
839
|
+
f"{var_name}.{node.object.member}[...] would be read as the current-bar field, "
|
|
840
|
+
"not prior-bar history. Copy the field to a bare drawing variable first, "
|
|
841
|
+
"or remove the history reference."
|
|
842
|
+
),
|
|
843
|
+
)
|
|
844
|
+
elif isinstance(node.object, Identifier) and node.object.name in self._drawing_tuple_vars:
|
|
845
|
+
self._err(
|
|
846
|
+
node,
|
|
847
|
+
"History references on tuple-destructured drawing handles are not supported in PineForge.",
|
|
848
|
+
hint=(
|
|
849
|
+
f"{node.object.name}[...] is a drawing handle from tuple destructuring; "
|
|
850
|
+
"PineForge cannot historize that tuple element safely."
|
|
851
|
+
),
|
|
852
|
+
)
|
|
853
|
+
self._visit_children(node)
|
|
854
|
+
|
|
668
855
|
def _visit_FuncCall(self, node: FuncCall) -> None:
|
|
669
856
|
ns, name = _qualified_name(node.callee)
|
|
670
857
|
|
|
@@ -686,6 +873,25 @@ class SupportChecker:
|
|
|
686
873
|
self._visit_children(node)
|
|
687
874
|
return
|
|
688
875
|
|
|
876
|
+
# Heikin-Ashi: supported for the chart's OWN symbol only. The engine
|
|
877
|
+
# applies the HA candle transform inside the same-symbol security eval
|
|
878
|
+
# (request.security(ticker.heikinashi(syminfo.tickerid), ...)); a
|
|
879
|
+
# cross-symbol HA construction (an alternate symbol's candles) is not.
|
|
880
|
+
if full == "ticker.heikinashi":
|
|
881
|
+
arg0 = node.args[0] if node.args else node.kwargs.get("symbol")
|
|
882
|
+
if arg0 is None or not self._is_current_symbol_expr(arg0):
|
|
883
|
+
self._err(
|
|
884
|
+
node,
|
|
885
|
+
"ticker.heikinashi(...) is supported only for the chart's own symbol.",
|
|
886
|
+
hint=(
|
|
887
|
+
"Pass syminfo.tickerid: PineForge applies the Heikin-Ashi candle "
|
|
888
|
+
"transform to the chart symbol inside request.security, but cannot "
|
|
889
|
+
"load an alternate symbol's HA candles."
|
|
890
|
+
),
|
|
891
|
+
)
|
|
892
|
+
self._visit_children(node)
|
|
893
|
+
return
|
|
894
|
+
|
|
689
895
|
# Hard rejects by namespace (e.g. ticker.*).
|
|
690
896
|
if ns is not None and ns in HARD_REJECT_NAMESPACE:
|
|
691
897
|
self._err(
|
|
@@ -942,6 +1148,34 @@ class SupportChecker:
|
|
|
942
1148
|
self._visit_children(node)
|
|
943
1149
|
return
|
|
944
1150
|
|
|
1151
|
+
# Drawing-objects-as-data (line/box/label/linefill). Geometry methods
|
|
1152
|
+
# are REAL data; *_NOOP visual setters are accepted no-ops (warned);
|
|
1153
|
+
# any OTHER drawing method rejects loudly. Args carry constant-namespace
|
|
1154
|
+
# members (xloc.*, yloc.*, text.align_*, style consts), so children are
|
|
1155
|
+
# visited with const reads allowed. NOTE: user methods on a drawing var
|
|
1156
|
+
# (egoigor ``ln.slope()``) have ns="ln" (the receiver), NOT a DRAWING_NS,
|
|
1157
|
+
# so they are never gated here.
|
|
1158
|
+
if ns in _DRAWING_SUPPORTED_BY_NS:
|
|
1159
|
+
if name not in _DRAWING_SUPPORTED_BY_NS[ns]:
|
|
1160
|
+
self._err(node, f"{ns}.{name}(...) is not implemented in PineForge runtime.")
|
|
1161
|
+
self._visit_children(node)
|
|
1162
|
+
return
|
|
1163
|
+
if name in _DRAWING_NOOP_BY_NS[ns]:
|
|
1164
|
+
self._warn(
|
|
1165
|
+
node,
|
|
1166
|
+
f"{ns}.{name}(...) is a visual no-op in PineForge backtests "
|
|
1167
|
+
f"(drawing geometry is data; color/style/size are dropped).",
|
|
1168
|
+
)
|
|
1169
|
+
self._visit_children_const_ok(node)
|
|
1170
|
+
return
|
|
1171
|
+
if ns == "chart.point":
|
|
1172
|
+
if name not in SUPPORTED_CHART_POINT:
|
|
1173
|
+
self._err(node, f"chart.point.{name}(...) is not implemented in PineForge runtime.")
|
|
1174
|
+
self._visit_children(node)
|
|
1175
|
+
return
|
|
1176
|
+
self._visit_children_const_ok(node)
|
|
1177
|
+
return
|
|
1178
|
+
|
|
945
1179
|
# Drawing / charting / alert namespaces — codegen drops silently. Warn,
|
|
946
1180
|
# don't error: many strategies include these for the TradingView UI.
|
|
947
1181
|
# Their argument subtrees legitimately carry constant-namespace
|
|
@@ -962,6 +1196,17 @@ class SupportChecker:
|
|
|
962
1196
|
self._visit_children_const_ok(node)
|
|
963
1197
|
return
|
|
964
1198
|
|
|
1199
|
+
# Method call on a scalar visual-container receiver (``panel.cell(...)``
|
|
1200
|
+
# where ``panel`` is a ``table`` param, ``ln.set_xy1(...)`` on a ``line``
|
|
1201
|
+
# var). ``ns`` is the receiver variable/param name, not a namespace, so
|
|
1202
|
+
# the namespace-form drawing/table branches above did not fire. These are
|
|
1203
|
+
# visual sinks whose args legitimately carry visual constants
|
|
1204
|
+
# (``text.align_*``, ``size.*``), so visit children with const reads
|
|
1205
|
+
# allowed instead of tripping the free-expression const-namespace reject.
|
|
1206
|
+
if ns is not None and ns in self._visual_container_vars:
|
|
1207
|
+
self._visit_children_const_ok(node)
|
|
1208
|
+
return
|
|
1209
|
+
|
|
965
1210
|
self._visit_children(node)
|
|
966
1211
|
|
|
967
1212
|
def _visit_Identifier(self, node: Identifier) -> None:
|
|
@@ -1238,19 +1483,38 @@ class SupportChecker:
|
|
|
1238
1483
|
return False
|
|
1239
1484
|
return node.member in allowed
|
|
1240
1485
|
|
|
1241
|
-
def _is_current_symbol_expr(self, node: ASTNode) -> bool:
|
|
1486
|
+
def _is_current_symbol_expr(self, node: ASTNode, _seen: set[str] | None = None) -> bool:
|
|
1487
|
+
if _seen is None:
|
|
1488
|
+
_seen = set()
|
|
1242
1489
|
chain = _resolve_member_chain(node)
|
|
1243
1490
|
if chain in SECURITY_CURRENT_SYMBOL_NAMES:
|
|
1244
1491
|
return True
|
|
1245
|
-
# ticker.inherit(symbol, ...)
|
|
1246
|
-
#
|
|
1492
|
+
# ticker.inherit(symbol, ...) / ticker.standard(symbol) are passthrough,
|
|
1493
|
+
# and ticker.heikinashi(symbol) is the chart's OWN symbol with a causal
|
|
1494
|
+
# Heikin-Ashi candle transform (the engine applies it inside the security
|
|
1495
|
+
# eval via register_security_eval's heikinashi flag — no alternate symbol
|
|
1496
|
+
# is loaded). All allow a current-symbol first argument; a non-current
|
|
1497
|
+
# arg is genuine cross-symbol construction and still falls through.
|
|
1247
1498
|
if isinstance(node, FuncCall):
|
|
1248
1499
|
ns, fname = _qualified_name(node.callee)
|
|
1249
|
-
if ns == "ticker" and fname in ("inherit", "standard"):
|
|
1250
|
-
if node.args and self._is_current_symbol_expr(node.args[0]):
|
|
1500
|
+
if ns == "ticker" and fname in ("inherit", "standard", "heikinashi"):
|
|
1501
|
+
if node.args and self._is_current_symbol_expr(node.args[0], _seen):
|
|
1251
1502
|
return True
|
|
1252
|
-
if "symbol" in node.kwargs and self._is_current_symbol_expr(node.kwargs["symbol"]):
|
|
1503
|
+
if "symbol" in node.kwargs and self._is_current_symbol_expr(node.kwargs["symbol"], _seen):
|
|
1253
1504
|
return True
|
|
1505
|
+
# A ternary symbol (``cond ? other : syminfo.tickerid``) resolves to the
|
|
1506
|
+
# chart symbol whenever EITHER branch does. PineForge only ever loads the
|
|
1507
|
+
# chart symbol, so this is exact at any config where the chosen branch is
|
|
1508
|
+
# the chart symbol; an unconditional cross-symbol expression still errors.
|
|
1509
|
+
if isinstance(node, Ternary):
|
|
1510
|
+
return (self._is_current_symbol_expr(node.true_val, _seen)
|
|
1511
|
+
or self._is_current_symbol_expr(node.false_val, _seen))
|
|
1512
|
+
# Def-use: resolve a bare identifier through its declaration value so an
|
|
1513
|
+
# aliased symbol is accepted (``haTicker = ticker.heikinashi(...)`` then
|
|
1514
|
+
# ``request.security(haTicker, ...)``). Name-cycle-guarded.
|
|
1515
|
+
if isinstance(node, Identifier) and node.name in self._scalar_defs and node.name not in _seen:
|
|
1516
|
+
_seen.add(node.name)
|
|
1517
|
+
return self._is_current_symbol_expr(self._scalar_defs[node.name], _seen)
|
|
1254
1518
|
return False
|
|
1255
1519
|
|
|
1256
1520
|
# -- Pine timeframe-literal validation --
|
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codegen": "0.8.
|
|
2
|
+
"codegen": "0.8.1",
|
|
3
3
|
"pyodide": "314.0.0",
|
|
4
4
|
"python": "3.14.0",
|
|
5
5
|
"emscripten": "emscripten_5_0_3",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "85ac44383967dd8c9825a276661eab568b16ecb89fcb929a3b9b638b6a4f371c"
|
|
7
7
|
}
|
package/tables.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"CODEGEN_VERSION": "0.8.
|
|
2
|
+
"CODEGEN_VERSION": "0.8.1",
|
|
3
3
|
"TA_CLASS_MAP_KEYS": [
|
|
4
4
|
"sma",
|
|
5
5
|
"ema",
|
|
@@ -321,12 +321,8 @@
|
|
|
321
321
|
"plotshape"
|
|
322
322
|
],
|
|
323
323
|
"SKIP_NAMESPACES": [
|
|
324
|
-
"box",
|
|
325
324
|
"chart",
|
|
326
325
|
"display",
|
|
327
|
-
"label",
|
|
328
|
-
"line",
|
|
329
|
-
"linefill",
|
|
330
326
|
"polyline",
|
|
331
327
|
"position",
|
|
332
328
|
"size",
|
|
@@ -1438,7 +1434,6 @@
|
|
|
1438
1434
|
"request.seed": "External seed data feeds not available in PineForge.",
|
|
1439
1435
|
"request.quandl": "External Quandl data not available in PineForge.",
|
|
1440
1436
|
"request.currency_rate": "Currency conversion data not available in PineForge.",
|
|
1441
|
-
"ticker.heikinashi": "ticker.heikinashi() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
1442
1437
|
"ticker.renko": "ticker.renko() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
1443
1438
|
"ticker.kagi": "ticker.kagi() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
1444
1439
|
"ticker.linebreak": "ticker.linebreak() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
@@ -1453,8 +1448,7 @@
|
|
|
1453
1448
|
"DIVERGENT_VARS": {
|
|
1454
1449
|
"bar_index": "bar_index depends on the data window; PineForge and TradingView produce different values for the same script.",
|
|
1455
1450
|
"last_bar_index": "last_bar_index is aliased to the CURRENT bar index in PineForge codegen (not the index of the last bar); backtest would be silently wrong — rejected.",
|
|
1456
|
-
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time."
|
|
1457
|
-
"time_close": "time_close is aliased to the bar OPEN timestamp in PineForge; it does not represent the bar close time; backtest would be silently wrong — rejected."
|
|
1451
|
+
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time."
|
|
1458
1452
|
},
|
|
1459
1453
|
"BARSTATE_APPROX_VARS": {
|
|
1460
1454
|
"barstate.islast": "barstate.islast is always false in PineForge batch backtests.",
|
|
Binary file
|