@pineforge/codegen-pyodide 0.10.0 → 0.10.2
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/__init__.py +3 -0
- package/pineforge_codegen/analyzer/base.py +3999 -140
- package/pineforge_codegen/analyzer/call_handlers.py +662 -48
- package/pineforge_codegen/analyzer/contracts.py +104 -7
- package/pineforge_codegen/analyzer/types.py +951 -14
- package/pineforge_codegen/codegen/base.py +2280 -228
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +747 -177
- package/pineforge_codegen/codegen/security.py +1546 -132
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +328 -80
- package/pineforge_codegen/codegen/types.py +1160 -227
- package/pineforge_codegen/codegen/visit_call.py +1191 -104
- package/pineforge_codegen/codegen/visit_expr.py +283 -48
- package/pineforge_codegen/codegen/visit_stmt.py +1137 -202
- package/pineforge_codegen/finite_ta_length.py +798 -0
- package/pineforge_codegen/method_binding.py +163 -0
- package/pineforge_codegen/parser.py +48 -9
- package/pineforge_codegen/signatures.py +4 -4
- package/pineforge_codegen/support_checker.py +262 -23
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.2.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.10.0.tar.gz +0 -0
|
@@ -10,14 +10,11 @@ Buckets:
|
|
|
10
10
|
* HARD_REJECT_FUNC / HARD_REJECT_NAMESPACE - calls that have no PineForge
|
|
11
11
|
semantics at all (e.g. ``request.financial``, ``ticker.*``).
|
|
12
12
|
* DIVERGENT_VARS - built-in variables whose PineForge value diverges from
|
|
13
|
-
TradingView.
|
|
14
|
-
data window, ``timenow`` is not
|
|
15
|
-
logging code that does not
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
they produce a plausible-looking but wrong value that flows straight into
|
|
19
|
-
trade logic, so a backtest would be silently wrong. Those are escalated to
|
|
20
|
-
ERROR (rejected) rather than merely warned.
|
|
13
|
+
TradingView. They are reported as WARNING (e.g. ``bar_index`` and
|
|
14
|
+
``last_bar_index`` depend on the fed data window, ``timenow`` is not
|
|
15
|
+
wall-clock) — these often appear in visual or logging code that does not
|
|
16
|
+
affect trade outcomes. DIVERGENT_VARS_ERROR is reserved for silent
|
|
17
|
+
mis-aliases severe enough to reject; it is currently empty.
|
|
21
18
|
* NOT_YET - calls the runtime could support but the transpiler does not yet
|
|
22
19
|
emit (e.g. ``max_bars_back``, bare ``barssince``).
|
|
23
20
|
* request.security - only ``symbol`` / ``timeframe`` / ``expression`` allowed,
|
|
@@ -81,6 +78,9 @@ SUPPORTED_INPUT: frozenset[str] = frozenset(sigs.INPUT_FUNCTIONS)
|
|
|
81
78
|
SUPPORTED_ARRAY: frozenset[str] = frozenset(set(ARRAY_METHODS) | {"new", "new_float", "new_int", "new_bool", "new_string", "from"} | set(ARRAY_DRAWING_NEW_CTORS))
|
|
82
79
|
SUPPORTED_MAP: frozenset[str] = frozenset(set(MAP_METHODS) | {"new"})
|
|
83
80
|
SUPPORTED_MATRIX: frozenset[str] = frozenset(set(MATRIX_METHODS) | {"new"})
|
|
81
|
+
_SUPPORTED_MAP_VALUE_TYPES: frozenset[str] = frozenset(
|
|
82
|
+
{"float", "int", "bool", "string"}
|
|
83
|
+
)
|
|
84
84
|
SUPPORTED_SYMINFO: frozenset[str] = frozenset(SYMINFO_MEMBER_MAP)
|
|
85
85
|
# Drawing-objects-as-data (spec §4.5). Geometry methods are REAL (route to the
|
|
86
86
|
# per-type arena); *_NOOP visual setters are accepted no-ops (Level.WARNING).
|
|
@@ -174,11 +174,11 @@ HARD_REJECT_NAMESPACE: dict[str, str] = {
|
|
|
174
174
|
# logging or visual logic that does not affect trade outcomes. The checker still
|
|
175
175
|
# flags divergence so users see the risk.
|
|
176
176
|
#
|
|
177
|
-
# DIVERGENT_VARS_ERROR is a SUBSET
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
177
|
+
# DIVERGENT_VARS_ERROR is a SUBSET reserved for silent MIS-ALIASES that must be
|
|
178
|
+
# escalated to ERROR (rejected), rather than merely warned. It is currently
|
|
179
|
+
# empty: last_bar_index lowers to the true final index of the fed data window,
|
|
180
|
+
# but that index can still differ from TradingView when the window does not
|
|
181
|
+
# cover the same chart history.
|
|
182
182
|
#
|
|
183
183
|
# NOTE: the bare ``time_close`` variable is NOT divergent — codegen lowers it to
|
|
184
184
|
# the engine's ``time_close()`` accessor, which returns the true bar-close
|
|
@@ -188,20 +188,32 @@ HARD_REJECT_NAMESPACE: dict[str, str] = {
|
|
|
188
188
|
# separate supported builtin handled in visit_call.)
|
|
189
189
|
DIVERGENT_VARS: dict[str, str] = {
|
|
190
190
|
"bar_index": "bar_index depends on the data window; PineForge and TradingView produce different values for the same script.",
|
|
191
|
-
"last_bar_index": "last_bar_index is
|
|
191
|
+
"last_bar_index": "last_bar_index is the final index of PineForge's fed data window; it can diverge from TradingView when the window does not cover the same chart history.",
|
|
192
192
|
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time.",
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
# Subset of DIVERGENT_VARS escalated from WARNING to ERROR (see comment above).
|
|
196
|
-
DIVERGENT_VARS_ERROR: frozenset[str] = frozenset(
|
|
196
|
+
DIVERGENT_VARS_ERROR: frozenset[str] = frozenset()
|
|
197
197
|
|
|
198
198
|
BARSTATE_APPROX_VARS: dict[str, str] = {
|
|
199
|
-
"barstate.islast":
|
|
199
|
+
"barstate.islast": (
|
|
200
|
+
"In direct batch chart-scope evaluation, barstate.islast is true only "
|
|
201
|
+
"on the final bar of PineForge's fed chart-data window. Inside a "
|
|
202
|
+
"request.security() history index, PineForge currently selects the "
|
|
203
|
+
"false branch; requested-context last-bar state is not modeled."
|
|
204
|
+
),
|
|
200
205
|
"barstate.ishistory": "barstate.ishistory is always true in PineForge batch backtests.",
|
|
201
206
|
"barstate.isrealtime": "barstate.isrealtime is always false in PineForge batch backtests.",
|
|
202
207
|
"barstate.isnew": "barstate.isnew follows PineForge first-tick execution state.",
|
|
203
208
|
"barstate.isconfirmed": "barstate.isconfirmed follows PineForge last-tick execution state.",
|
|
204
|
-
"barstate.islastconfirmedhistory":
|
|
209
|
+
"barstate.islastconfirmedhistory": (
|
|
210
|
+
"In direct historical batch chart-scope evaluation, PineForge "
|
|
211
|
+
"approximates barstate.islastconfirmedhistory as true only on the "
|
|
212
|
+
"final bar of the fed chart-data window. Inside a request.security() "
|
|
213
|
+
"history index, PineForge currently selects the false branch; "
|
|
214
|
+
"TradingView's requested-context and realtime-boundary semantics are "
|
|
215
|
+
"not modeled."
|
|
216
|
+
),
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
STRATEGY_UNSUPPORTED_PARAMS: dict[str, set[str]] = {
|
|
@@ -209,9 +221,13 @@ STRATEGY_UNSUPPORTED_PARAMS: dict[str, set[str]] = {
|
|
|
209
221
|
"order": {"comment", "alert_message", "disable_alert", "qty_type"},
|
|
210
222
|
"exit": {"comment_profit", "comment_loss", "comment_trailing", "alert_message", "alert_profit", "alert_loss", "alert_trailing", "disable_alert"},
|
|
211
223
|
"close": {"alert_message", "disable_alert"},
|
|
212
|
-
"close_all": {"
|
|
224
|
+
"close_all": {"alert_message", "disable_alert"},
|
|
213
225
|
}
|
|
214
226
|
|
|
227
|
+
# Alert delivery remains outside the backtest runtime contract. Keep these
|
|
228
|
+
# parameters warning-only so strategies that use webhook metadata still run;
|
|
229
|
+
# a follow-up must plumb alert events before codegen may claim support.
|
|
230
|
+
|
|
215
231
|
# strategy.closedtrades / strategy.opentrades accessor surfaces are NOT
|
|
216
232
|
# symmetric in Pine v6. opentrades has no exit_* fields (a trade has not
|
|
217
233
|
# closed yet). Both lack ``direction`` (Pine has ``size`` whose sign carries
|
|
@@ -461,6 +477,12 @@ class SupportChecker:
|
|
|
461
477
|
# shapes before codegen.
|
|
462
478
|
self._udt_drawing_fields: dict[str, set[str]] = {}
|
|
463
479
|
self._var_udt_types: dict[str, str] = {}
|
|
480
|
+
# PineMap IDs require runtime snapshots during COOF rollback. History
|
|
481
|
+
# buffers and generic matrices still value-copy their elements, so
|
|
482
|
+
# map-bearing shapes that would silently retain live aliases are gated
|
|
483
|
+
# until those containers have recursive checkpoint adapters.
|
|
484
|
+
self._udt_field_type_names: dict[str, dict[str, str]] = {}
|
|
485
|
+
self._map_bearing_udts: set[str] = set()
|
|
464
486
|
# Names (vars and function params) declared as a scalar visual-container
|
|
465
487
|
# type (table/box/line/label/linefill). A method call on one of these
|
|
466
488
|
# (``panel.cell(...)``) is a visual sink whose args may carry visual
|
|
@@ -494,11 +516,20 @@ class SupportChecker:
|
|
|
494
516
|
# then ``request.security(haTicker, ...)``, or ``reqSym = cond ? other :
|
|
495
517
|
# syminfo.tickerid``. First binding wins (closest to declaration).
|
|
496
518
|
self._scalar_defs: dict[str, ASTNode] = {}
|
|
519
|
+
# EVERY reassignment value bound to a scalar name (``name := <expr>``
|
|
520
|
+
# and the compound forms). A declaration alone does not pin the value a
|
|
521
|
+
# ``request.security`` symbol argument actually carries: the engine is
|
|
522
|
+
# registered with no symbol at all, so an accepted-but-divergent rebind
|
|
523
|
+
# silently backtests the CHART feed. Collected in a whole-AST pre-pass
|
|
524
|
+
# (below) as well as during the visit, so a rebind that lexically
|
|
525
|
+
# FOLLOWS the request.security call cannot slip past.
|
|
526
|
+
self._scalar_rebinds: dict[str, list[ASTNode]] = {}
|
|
497
527
|
|
|
498
528
|
# -- Public API --
|
|
499
529
|
|
|
500
530
|
def check(self) -> list[Diagnostic]:
|
|
501
531
|
self._collect_user_definitions(self._ast)
|
|
532
|
+
self._collect_scalar_rebinds(self._ast)
|
|
502
533
|
for stmt in self._ast.body:
|
|
503
534
|
self._visit(stmt)
|
|
504
535
|
return self._diagnostics
|
|
@@ -511,10 +542,50 @@ class SupportChecker:
|
|
|
511
542
|
|
|
512
543
|
# -- Setup --
|
|
513
544
|
|
|
545
|
+
def _record_scalar_rebind(self, node: Assignment) -> None:
|
|
546
|
+
"""Remember ``name := <expr>`` (and the compound forms) by name.
|
|
547
|
+
|
|
548
|
+
A compound op (``+=``, ``*=``, …) records its right-hand operand, which
|
|
549
|
+
is never a current-symbol expression — so a string-built symbol is
|
|
550
|
+
rejected by the same rule that rejects a plain divergent ``:=``.
|
|
551
|
+
"""
|
|
552
|
+
target = node.target
|
|
553
|
+
if not isinstance(target, Identifier) or node.value is None:
|
|
554
|
+
return
|
|
555
|
+
recorded = self._scalar_rebinds.setdefault(target.name, [])
|
|
556
|
+
if not any(value is node.value for value in recorded):
|
|
557
|
+
recorded.append(node.value)
|
|
558
|
+
|
|
559
|
+
def _collect_scalar_rebinds(self, node: ASTNode) -> None:
|
|
560
|
+
"""Pre-pass: collect every scalar rebind anywhere in the AST.
|
|
561
|
+
|
|
562
|
+
The visit walks top-level statements in source order, so a rebind that
|
|
563
|
+
FOLLOWS a ``request.security`` call would otherwise never be seen by
|
|
564
|
+
that call's symbol check. Collecting up front is strictly fail-closed:
|
|
565
|
+
it can only add rejections, never remove one.
|
|
566
|
+
"""
|
|
567
|
+
if isinstance(node, Assignment):
|
|
568
|
+
self._record_scalar_rebind(node)
|
|
569
|
+
for value in vars(node).values():
|
|
570
|
+
if isinstance(value, ASTNode):
|
|
571
|
+
self._collect_scalar_rebinds(value)
|
|
572
|
+
elif isinstance(value, list):
|
|
573
|
+
for item in value:
|
|
574
|
+
if isinstance(item, ASTNode):
|
|
575
|
+
self._collect_scalar_rebinds(item)
|
|
576
|
+
elif isinstance(value, dict):
|
|
577
|
+
for item in value.values():
|
|
578
|
+
if isinstance(item, ASTNode):
|
|
579
|
+
self._collect_scalar_rebinds(item)
|
|
580
|
+
|
|
514
581
|
def _collect_user_definitions(self, ast: Program) -> None:
|
|
515
582
|
for stmt in ast.body:
|
|
516
583
|
if isinstance(stmt, TypeDecl):
|
|
517
584
|
self._user_types.add(stmt.name)
|
|
585
|
+
self._udt_field_type_names[stmt.name] = {
|
|
586
|
+
field.name: str(field.type_name or "").replace(" ", "")
|
|
587
|
+
for field in stmt.fields
|
|
588
|
+
}
|
|
518
589
|
drawing_fields = {
|
|
519
590
|
field.name
|
|
520
591
|
for field in stmt.fields
|
|
@@ -536,6 +607,140 @@ class SupportChecker:
|
|
|
536
607
|
self._user_methods.add(stmt.name)
|
|
537
608
|
self._collect_visual_container_params(stmt)
|
|
538
609
|
|
|
610
|
+
# Resolve direct and transitively nested map-bearing UDTs. Pine type
|
|
611
|
+
# declarations are normally dependency ordered, but the fixed point
|
|
612
|
+
# also keeps this guard deterministic for synthetic test ASTs.
|
|
613
|
+
changed = True
|
|
614
|
+
while changed:
|
|
615
|
+
changed = False
|
|
616
|
+
for udt_name, fields in self._udt_field_type_names.items():
|
|
617
|
+
bearing_fields = {
|
|
618
|
+
field_name
|
|
619
|
+
for field_name, type_name in fields.items()
|
|
620
|
+
if "map<" in type_name
|
|
621
|
+
or any(
|
|
622
|
+
type_name == nested
|
|
623
|
+
or f"<{nested}>" in type_name
|
|
624
|
+
or f"<{nested}," in type_name
|
|
625
|
+
or f",{nested}>" in type_name
|
|
626
|
+
for nested in self._map_bearing_udts
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
if bearing_fields:
|
|
630
|
+
if udt_name not in self._map_bearing_udts:
|
|
631
|
+
self._map_bearing_udts.add(udt_name)
|
|
632
|
+
changed = True
|
|
633
|
+
|
|
634
|
+
# Constructor checks alone are insufficient: a typed ``na``
|
|
635
|
+
# declaration, UDT field, or unused callable parameter can introduce
|
|
636
|
+
# the same unsupported runtime shape without ever calling map.new() or
|
|
637
|
+
# matrix.new(). Validate every declared type boundary after the UDT
|
|
638
|
+
# fixed point is known so unsupported shapes fail with a Pine source
|
|
639
|
+
# diagnostic instead of a generated-C++ static_assert.
|
|
640
|
+
for stmt in ast.body:
|
|
641
|
+
if isinstance(stmt, TypeDecl):
|
|
642
|
+
for field in stmt.fields:
|
|
643
|
+
self._validate_declared_type_hint(
|
|
644
|
+
stmt,
|
|
645
|
+
field.type_name,
|
|
646
|
+
context=f"field '{stmt.name}.{field.name}'",
|
|
647
|
+
)
|
|
648
|
+
elif isinstance(stmt, (FuncDef, MethodDef)):
|
|
649
|
+
hints = (
|
|
650
|
+
(getattr(stmt, "annotations", None) or {})
|
|
651
|
+
.get("param_type_hints")
|
|
652
|
+
or []
|
|
653
|
+
)
|
|
654
|
+
for index, param_name in enumerate(stmt.params):
|
|
655
|
+
hint = hints[index] if index < len(hints) else None
|
|
656
|
+
if hint:
|
|
657
|
+
self._validate_declared_type_hint(
|
|
658
|
+
stmt,
|
|
659
|
+
hint,
|
|
660
|
+
context=f"parameter '{param_name}'",
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
@staticmethod
|
|
664
|
+
def _split_declared_type_args(text: str) -> list[str]:
|
|
665
|
+
"""Split ``K,V`` while preserving nested generic arguments."""
|
|
666
|
+
args: list[str] = []
|
|
667
|
+
current: list[str] = []
|
|
668
|
+
depth = 0
|
|
669
|
+
for char in text:
|
|
670
|
+
if char == "<":
|
|
671
|
+
depth += 1
|
|
672
|
+
elif char == ">":
|
|
673
|
+
depth -= 1
|
|
674
|
+
if char == "," and depth == 0:
|
|
675
|
+
args.append("".join(current))
|
|
676
|
+
current = []
|
|
677
|
+
else:
|
|
678
|
+
current.append(char)
|
|
679
|
+
if current:
|
|
680
|
+
args.append("".join(current))
|
|
681
|
+
return args
|
|
682
|
+
|
|
683
|
+
def _validate_declared_type_hint(
|
|
684
|
+
self,
|
|
685
|
+
node: ASTNode,
|
|
686
|
+
type_name: str | None,
|
|
687
|
+
*,
|
|
688
|
+
context: str,
|
|
689
|
+
) -> None:
|
|
690
|
+
"""Fail closed for runtime collection limits at every type boundary."""
|
|
691
|
+
if not type_name:
|
|
692
|
+
return
|
|
693
|
+
compact = str(type_name).replace(" ", "")
|
|
694
|
+
if compact.endswith("[]"):
|
|
695
|
+
compact = f"array<{compact[:-2]}>"
|
|
696
|
+
|
|
697
|
+
if compact.startswith("map<") and compact.endswith(">"):
|
|
698
|
+
parts = self._split_declared_type_args(compact[4:-1])
|
|
699
|
+
if len(parts) != 2:
|
|
700
|
+
return
|
|
701
|
+
key_type, value_type = parts
|
|
702
|
+
if key_type != "string":
|
|
703
|
+
self._err(
|
|
704
|
+
node,
|
|
705
|
+
f"{context}: map keys must be string in PineForge's "
|
|
706
|
+
"supported map subset.",
|
|
707
|
+
)
|
|
708
|
+
if value_type not in _SUPPORTED_MAP_VALUE_TYPES:
|
|
709
|
+
self._err(
|
|
710
|
+
node,
|
|
711
|
+
f"{context}: map values must be primitive in PineForge's "
|
|
712
|
+
"supported map subset.",
|
|
713
|
+
)
|
|
714
|
+
return
|
|
715
|
+
|
|
716
|
+
if compact.startswith("matrix<") and compact.endswith(">"):
|
|
717
|
+
element_type = compact[len("matrix<"):-1]
|
|
718
|
+
if element_type in self._map_bearing_udts:
|
|
719
|
+
self._err(
|
|
720
|
+
node,
|
|
721
|
+
f"matrix<{element_type}> is not supported when the UDT "
|
|
722
|
+
"contains a map field; rollback would retain live map "
|
|
723
|
+
"aliases.",
|
|
724
|
+
hint="Use array<UDT> for recursively checkpointed state.",
|
|
725
|
+
)
|
|
726
|
+
return
|
|
727
|
+
if "<" in element_type:
|
|
728
|
+
self._err(
|
|
729
|
+
node,
|
|
730
|
+
f"matrix<{element_type}>: nested collection element types "
|
|
731
|
+
"not supported in v1.",
|
|
732
|
+
)
|
|
733
|
+
return
|
|
734
|
+
|
|
735
|
+
# Arrays are recursively checkpointed and may contain valid maps, but
|
|
736
|
+
# the nested map still has the same key/value boundary.
|
|
737
|
+
if compact.startswith("array<") and compact.endswith(">"):
|
|
738
|
+
self._validate_declared_type_hint(
|
|
739
|
+
node,
|
|
740
|
+
compact[len("array<"):-1],
|
|
741
|
+
context=context,
|
|
742
|
+
)
|
|
743
|
+
|
|
539
744
|
def _collect_visual_container_params(self, fn) -> None:
|
|
540
745
|
"""Register a function's parameters that are declared with a scalar
|
|
541
746
|
visual-container type (``table panel``, ``line ln``) so method calls on
|
|
@@ -814,6 +1019,12 @@ class SupportChecker:
|
|
|
814
1019
|
# so a direct ``dash.cell(..., text.align_left)`` is treated as a visual
|
|
815
1020
|
# sink (mirrors the table/box/line/label/linefill PARAM tracking).
|
|
816
1021
|
decl_hint = str(node.type_hint).replace(" ", "") if node.type_hint else None
|
|
1022
|
+
if decl_hint:
|
|
1023
|
+
self._validate_declared_type_hint(
|
|
1024
|
+
node,
|
|
1025
|
+
decl_hint,
|
|
1026
|
+
context=f"variable '{node.name}'",
|
|
1027
|
+
)
|
|
817
1028
|
if decl_hint in _VISUAL_CONTAINER_TYPES:
|
|
818
1029
|
self._visual_container_vars.add(node.name)
|
|
819
1030
|
elif isinstance(node.value, FuncCall):
|
|
@@ -835,6 +1046,14 @@ class SupportChecker:
|
|
|
835
1046
|
)
|
|
836
1047
|
self._visit_children(node)
|
|
837
1048
|
|
|
1049
|
+
def _visit_Assignment(self, node: Assignment) -> None:
|
|
1050
|
+
# ``_scalar_defs`` records only the DECLARATION, so a later ``:=``
|
|
1051
|
+
# rebind used to be invisible to the request.security symbol check.
|
|
1052
|
+
# (``check`` also pre-collects these; recording here keeps the visit
|
|
1053
|
+
# self-consistent for any caller that drives visitors directly.)
|
|
1054
|
+
self._record_scalar_rebind(node)
|
|
1055
|
+
self._visit_children(node)
|
|
1056
|
+
|
|
838
1057
|
def _visit_TupleAssign(self, node: TupleAssign) -> None:
|
|
839
1058
|
drawing_mask = self._tuple_assign_drawing_mask(node.value)
|
|
840
1059
|
if drawing_mask:
|
|
@@ -1160,7 +1379,7 @@ class SupportChecker:
|
|
|
1160
1379
|
targs = [str(t).replace(" ", "") for t in targs]
|
|
1161
1380
|
if targs and targs[0] != "string":
|
|
1162
1381
|
self._err(node, "map keys must be string in PineForge's supported map subset.")
|
|
1163
|
-
if len(targs) > 1 and targs[1] not in
|
|
1382
|
+
if len(targs) > 1 and targs[1] not in _SUPPORTED_MAP_VALUE_TYPES:
|
|
1164
1383
|
self._err(node, "map values must be primitive in PineForge's supported map subset.")
|
|
1165
1384
|
if ns == "matrix" and name == "new":
|
|
1166
1385
|
targs = (getattr(node.callee, "annotations", None) or {}).get("template_args") or []
|
|
@@ -1172,6 +1391,15 @@ class SupportChecker:
|
|
|
1172
1391
|
self._visit_children(node)
|
|
1173
1392
|
return
|
|
1174
1393
|
allowed_prim = {"float", "int", "bool", "string", "color"}
|
|
1394
|
+
if t in self._map_bearing_udts:
|
|
1395
|
+
self._err(
|
|
1396
|
+
node,
|
|
1397
|
+
f"matrix<{t}> is not supported when the UDT contains "
|
|
1398
|
+
"a map field; rollback would retain live map aliases.",
|
|
1399
|
+
hint="Use array<UDT> for recursively checkpointed state.",
|
|
1400
|
+
)
|
|
1401
|
+
self._visit_children(node)
|
|
1402
|
+
return
|
|
1175
1403
|
if t not in allowed_prim and t not in self._user_types:
|
|
1176
1404
|
self._err(node, f"matrix<{t}> element type not supported. Allowed: float, int, bool, string, color, or a declared UDT.")
|
|
1177
1405
|
self._visit_children(node)
|
|
@@ -1548,10 +1776,21 @@ class SupportChecker:
|
|
|
1548
1776
|
or self._is_current_symbol_expr(node.false_val, _seen))
|
|
1549
1777
|
# Def-use: resolve a bare identifier through its declaration value so an
|
|
1550
1778
|
# aliased symbol is accepted (``haTicker = ticker.heikinashi(...)`` then
|
|
1551
|
-
# ``request.security(haTicker, ...)``).
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1779
|
+
# ``request.security(haTicker, ...)``). Every ``:=`` rebind of the name
|
|
1780
|
+
# must resolve to the chart symbol too: the declaration alone does not
|
|
1781
|
+
# pin the value the call actually receives. Name-cycle-guarded.
|
|
1782
|
+
if isinstance(node, Identifier) and node.name not in _seen:
|
|
1783
|
+
definition = self._scalar_defs.get(node.name)
|
|
1784
|
+
rebinds = self._scalar_rebinds.get(node.name)
|
|
1785
|
+
if definition is not None or rebinds:
|
|
1786
|
+
_seen.add(node.name)
|
|
1787
|
+
if rebinds and not all(
|
|
1788
|
+
self._is_current_symbol_expr(value, _seen) for value in rebinds
|
|
1789
|
+
):
|
|
1790
|
+
return False
|
|
1791
|
+
if definition is None:
|
|
1792
|
+
return False
|
|
1793
|
+
return self._is_current_symbol_expr(definition, _seen)
|
|
1555
1794
|
return False
|
|
1556
1795
|
|
|
1557
1796
|
# -- Pine timeframe-literal validation --
|
|
@@ -56,9 +56,74 @@ class TypeSpec:
|
|
|
56
56
|
return f"array<{self.element}>"
|
|
57
57
|
if self.kind == "map" and self.key is not None and self.value is not None:
|
|
58
58
|
return f"map<{self.key},{self.value}>"
|
|
59
|
+
if self.kind == "matrix" and self.element is not None:
|
|
60
|
+
return f"matrix<{self.element}>"
|
|
59
61
|
return self.name or self.kind
|
|
60
62
|
|
|
61
63
|
|
|
64
|
+
def method_receiver_type_name(spec: TypeSpec | None) -> str | None:
|
|
65
|
+
"""Canonical Pine type name used to key a user method receiver.
|
|
66
|
+
|
|
67
|
+
``MethodDef.type_name`` is the parser-normalized spelling (for example
|
|
68
|
+
``array<int>``). Calls are resolved from structured ``TypeSpec`` values,
|
|
69
|
+
so both sides need one lossless spelling across primitive, UDT, and
|
|
70
|
+
collection receivers. Incomplete collection specs stay unresolved rather
|
|
71
|
+
than accidentally colliding under a coarse ``array``/``map``/``matrix``
|
|
72
|
+
key.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
if spec is None:
|
|
76
|
+
return None
|
|
77
|
+
if spec.kind in {"primitive", "udt"}:
|
|
78
|
+
return spec.name
|
|
79
|
+
if spec.kind == "array" and spec.element is not None:
|
|
80
|
+
element = method_receiver_type_name(spec.element)
|
|
81
|
+
return f"array<{element}>" if element is not None else None
|
|
82
|
+
if spec.kind == "map" and spec.key is not None and spec.value is not None:
|
|
83
|
+
key = method_receiver_type_name(spec.key)
|
|
84
|
+
value = method_receiver_type_name(spec.value)
|
|
85
|
+
if key is not None and value is not None:
|
|
86
|
+
return f"map<{key},{value}>"
|
|
87
|
+
return None
|
|
88
|
+
if spec.kind == "matrix" and spec.element is not None:
|
|
89
|
+
element = method_receiver_type_name(spec.element)
|
|
90
|
+
return f"matrix<{element}>" if element is not None else None
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def method_receiver_cpp_token(
|
|
95
|
+
spec: TypeSpec | None,
|
|
96
|
+
fallback_name: str | None = None,
|
|
97
|
+
) -> str:
|
|
98
|
+
"""Return a stable C++ identifier fragment for a method receiver type.
|
|
99
|
+
|
|
100
|
+
Existing primitive, UDT, and drawing receiver spellings remain unchanged.
|
|
101
|
+
Generic Pine types cannot be embedded directly in an identifier, so their
|
|
102
|
+
structure is encoded recursively (``array<int>`` -> ``array_int``). The
|
|
103
|
+
fallback only supports older/synthetic ``FuncInfo`` records that lack the
|
|
104
|
+
structured receiver spec.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
if spec is not None:
|
|
108
|
+
if spec.kind in {"primitive", "udt"} and spec.name:
|
|
109
|
+
return spec.name
|
|
110
|
+
if spec.kind == "array" and spec.element is not None:
|
|
111
|
+
return f"array_{method_receiver_cpp_token(spec.element)}"
|
|
112
|
+
if spec.kind == "map" and spec.key is not None and spec.value is not None:
|
|
113
|
+
return (
|
|
114
|
+
f"map_{method_receiver_cpp_token(spec.key)}_"
|
|
115
|
+
f"{method_receiver_cpp_token(spec.value)}"
|
|
116
|
+
)
|
|
117
|
+
if spec.kind == "matrix" and spec.element is not None:
|
|
118
|
+
return f"matrix_{method_receiver_cpp_token(spec.element)}"
|
|
119
|
+
|
|
120
|
+
raw = fallback_name or "receiver"
|
|
121
|
+
token = "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in raw)
|
|
122
|
+
if token and token[0].isdigit():
|
|
123
|
+
token = f"type_{token}"
|
|
124
|
+
return token or "receiver"
|
|
125
|
+
|
|
126
|
+
|
|
62
127
|
@dataclass
|
|
63
128
|
class Symbol:
|
|
64
129
|
name: str
|
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codegen": "0.10.
|
|
2
|
+
"codegen": "0.10.2",
|
|
3
3
|
"pyodide": "314.0.0",
|
|
4
4
|
"python": "3.14.0",
|
|
5
5
|
"emscripten": "emscripten_5_0_3",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "97ebb8d0b14c32d3c88b9521c8f13150ee9cb9e3859227af970ec4dcbfa6bf71"
|
|
7
7
|
}
|
package/tables.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"CODEGEN_VERSION": "0.10.
|
|
2
|
+
"CODEGEN_VERSION": "0.10.2",
|
|
3
3
|
"TA_CLASS_MAP_KEYS": [
|
|
4
4
|
"sma",
|
|
5
5
|
"ema",
|
|
@@ -861,14 +861,14 @@
|
|
|
861
861
|
"qty",
|
|
862
862
|
"qty_percent",
|
|
863
863
|
"alert_message",
|
|
864
|
-
"
|
|
865
|
-
"
|
|
864
|
+
"immediately",
|
|
865
|
+
"disable_alert"
|
|
866
866
|
],
|
|
867
867
|
"strategy.close_all": [
|
|
868
868
|
"comment",
|
|
869
869
|
"alert_message",
|
|
870
|
-
"
|
|
871
|
-
"
|
|
870
|
+
"immediately",
|
|
871
|
+
"disable_alert"
|
|
872
872
|
],
|
|
873
873
|
"strategy.cancel": [
|
|
874
874
|
"id"
|
|
@@ -1449,16 +1449,16 @@
|
|
|
1449
1449
|
},
|
|
1450
1450
|
"DIVERGENT_VARS": {
|
|
1451
1451
|
"bar_index": "bar_index depends on the data window; PineForge and TradingView produce different values for the same script.",
|
|
1452
|
-
"last_bar_index": "last_bar_index is
|
|
1452
|
+
"last_bar_index": "last_bar_index is the final index of PineForge's fed data window; it can diverge from TradingView when the window does not cover the same chart history.",
|
|
1453
1453
|
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time."
|
|
1454
1454
|
},
|
|
1455
1455
|
"BARSTATE_APPROX_VARS": {
|
|
1456
|
-
"barstate.islast": "barstate.islast is
|
|
1456
|
+
"barstate.islast": "In direct batch chart-scope evaluation, barstate.islast is true only on the final bar of PineForge's fed chart-data window. Inside a request.security() history index, PineForge currently selects the false branch; requested-context last-bar state is not modeled.",
|
|
1457
1457
|
"barstate.ishistory": "barstate.ishistory is always true in PineForge batch backtests.",
|
|
1458
1458
|
"barstate.isrealtime": "barstate.isrealtime is always false in PineForge batch backtests.",
|
|
1459
1459
|
"barstate.isnew": "barstate.isnew follows PineForge first-tick execution state.",
|
|
1460
1460
|
"barstate.isconfirmed": "barstate.isconfirmed follows PineForge last-tick execution state.",
|
|
1461
|
-
"barstate.islastconfirmedhistory": "barstate.islastconfirmedhistory
|
|
1461
|
+
"barstate.islastconfirmedhistory": "In direct historical batch chart-scope evaluation, PineForge approximates barstate.islastconfirmedhistory as true only on the final bar of the fed chart-data window. Inside a request.security() history index, PineForge currently selects the false branch; TradingView's requested-context and realtime-boundary semantics are not modeled."
|
|
1462
1462
|
},
|
|
1463
1463
|
"STRATEGY_UNSUPPORTED_PARAMS": {
|
|
1464
1464
|
"entry": [
|
|
@@ -1487,9 +1487,7 @@
|
|
|
1487
1487
|
],
|
|
1488
1488
|
"close_all": [
|
|
1489
1489
|
"alert_message",
|
|
1490
|
-
"
|
|
1491
|
-
"disable_alert",
|
|
1492
|
-
"immediately"
|
|
1490
|
+
"disable_alert"
|
|
1493
1491
|
]
|
|
1494
1492
|
},
|
|
1495
1493
|
"CLOSED_TRADE_ACCESSOR_METHODS": [
|
|
Binary file
|