@pineforge/codegen-pyodide 1.0.0 → 1.1.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/glue.py +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +24 -9
- package/pineforge_codegen/analyzer/base.py +190 -1
- package/pineforge_codegen/analyzer/call_handlers.py +2 -0
- package/pineforge_codegen/analyzer/contracts.py +6 -0
- package/pineforge_codegen/analyzer/types.py +16 -5
- package/pineforge_codegen/ast_nodes.py +7 -0
- package/pineforge_codegen/codegen/base.py +136 -14
- package/pineforge_codegen/codegen/checked_settings.py +272 -0
- package/pineforge_codegen/codegen/collection_history.py +200 -0
- package/pineforge_codegen/codegen/emit_top.py +76 -14
- package/pineforge_codegen/codegen/helpers.py +12 -2
- package/pineforge_codegen/codegen/host_members.py +8 -0
- package/pineforge_codegen/codegen/input.py +36 -3
- package/pineforge_codegen/codegen/security.py +56 -1
- package/pineforge_codegen/codegen/ta.py +65 -1
- package/pineforge_codegen/codegen/types.py +136 -8
- package/pineforge_codegen/codegen/visit_call.py +92 -3
- package/pineforge_codegen/codegen/visit_expr.py +202 -3
- package/pineforge_codegen/codegen/visit_stmt.py +19 -4
- package/pineforge_codegen/collection_history.py +1595 -0
- package/pineforge_codegen/external_requests.py +3 -0
- package/pineforge_codegen/parser.py +19 -4
- package/pineforge_codegen/request_discovery.py +506 -0
- package/pineforge_codegen/support_checker.py +37 -0
- package/pineforge_codegen-1.1.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +2 -1
- package/pineforge_codegen-1.0.0.tar.gz +0 -0
package/glue.py
CHANGED
|
@@ -40,4 +40,5 @@ def transpile_json(source: str) -> str:
|
|
|
40
40
|
# A script that transpiled carries its warnings in the same entry format.
|
|
41
41
|
return json.dumps({"ok": True, "cpp": full["cpp"], "inputs": full["inputs"],
|
|
42
42
|
"strategyParams": full["strategyParams"],
|
|
43
|
-
"diagnostics": _diagnostic_entries(full["diagnostics"])
|
|
43
|
+
"diagnostics": _diagnostic_entries(full["diagnostics"]),
|
|
44
|
+
"requests": full["requests"]})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pineforge/codegen-pyodide",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Gate-validated Pyodide payload for the PineScript v6 -> C++ transpiler: archive (run in Pyodide), unpacked source, introspected tables, and release metadata.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -13,6 +13,7 @@ from .finite_ta_length import expand_finite_choice_extrema_lengths
|
|
|
13
13
|
from .library_inline import inline_libraries
|
|
14
14
|
from .limits import TimeBudget, check_ast_depth, check_source_size, ensure_recursion_headroom
|
|
15
15
|
from .pragmas import extract_pf_trace_pragmas
|
|
16
|
+
from .request_discovery import discover_requests, request_sites
|
|
16
17
|
from .security_contexts import specialize_security_contexts
|
|
17
18
|
from .block_locals import rename_block_locals
|
|
18
19
|
from .support_checker import check_support as _support_diagnostics
|
|
@@ -52,7 +53,9 @@ def _generate(pine_source: str, check_support: bool, filename: str,
|
|
|
52
53
|
the member of its name cannot hold gets a name of its own, and the
|
|
53
54
|
pipeline runs again (``block_locals``).
|
|
54
55
|
|
|
55
|
-
Returns ``(codegen, ctx, cpp, support_diagnostics)
|
|
56
|
+
Returns ``(codegen, ctx, cpp, support_diagnostics, sites)``: ``sites``
|
|
57
|
+
are the requests of another symbol's data the support checker lowered
|
|
58
|
+
(``request_discovery.request_sites``)."""
|
|
56
59
|
budget = None
|
|
57
60
|
clones: frozenset[str] = frozenset()
|
|
58
61
|
renamed: frozenset = frozenset()
|
|
@@ -67,6 +70,7 @@ def _generate(pine_source: str, check_support: bool, filename: str,
|
|
|
67
70
|
if any(d.level == Level.ERROR for d in support_diagnostics):
|
|
68
71
|
raise CompileError(support_diagnostics)
|
|
69
72
|
budget.check(phase=Phase.ANALYZER)
|
|
73
|
+
sites = request_sites(ast)
|
|
70
74
|
ast = lower_no_data_requests(ast)
|
|
71
75
|
ast = specialize_security_contexts(ast, filename=filename)
|
|
72
76
|
ast = bind_builtin_keywords(expand_finite_choice_extrema_lengths(ast))
|
|
@@ -86,18 +90,18 @@ def _generate(pine_source: str, check_support: bool, filename: str,
|
|
|
86
90
|
apart = gen.block_locals_needing_names - renamed
|
|
87
91
|
if apart:
|
|
88
92
|
renamed |= apart
|
|
89
|
-
del ast, ctx, gen, cpp
|
|
93
|
+
del ast, ctx, gen, cpp, sites
|
|
90
94
|
continue
|
|
91
95
|
needed = gen.session_functions_needing_clones
|
|
92
96
|
if not needed:
|
|
93
|
-
return gen, ctx, cpp, support_diagnostics
|
|
97
|
+
return gen, ctx, cpp, support_diagnostics, sites
|
|
94
98
|
# Only uncloned functions ask, so the set grows each time and the
|
|
95
99
|
# loop ends by the time every function that reads a flag at an
|
|
96
100
|
# offset is cloned.
|
|
97
101
|
if needed <= clones:
|
|
98
102
|
raise AssertionError("a cloned function asked for session clones again")
|
|
99
103
|
clones |= needed
|
|
100
|
-
del ast, ctx, gen, cpp
|
|
104
|
+
del ast, ctx, gen, cpp, sites
|
|
101
105
|
|
|
102
106
|
|
|
103
107
|
def transpile(pine_source: str, *, check_support: bool = True, filename: str = "<input>",
|
|
@@ -140,7 +144,8 @@ def transpile(pine_source: str, *, check_support: bool = True, filename: str = "
|
|
|
140
144
|
Returns:
|
|
141
145
|
Generated C++ source string.
|
|
142
146
|
"""
|
|
143
|
-
_gen, _ctx, cpp, _support = _generate(pine_source, check_support, filename,
|
|
147
|
+
_gen, _ctx, cpp, _support, _sites = _generate(pine_source, check_support, filename,
|
|
148
|
+
libraries)
|
|
144
149
|
return cpp
|
|
145
150
|
|
|
146
151
|
|
|
@@ -161,26 +166,36 @@ def transpile_full(pine_source: str, *, check_support: bool = True,
|
|
|
161
166
|
source order). Each has ``title`` / ``type`` /
|
|
162
167
|
``default`` and optionally ``min`` / ``max`` / ``step`` / ``options``
|
|
163
168
|
(omitted when the corresponding signature argument is absent or
|
|
164
|
-
references a non-const value).
|
|
165
|
-
:meth:`CodeGen.extract_input_manifest`.
|
|
169
|
+
references a non-const value); an ``input.symbol`` entry also has
|
|
170
|
+
``kind: "symbol"``. See :meth:`CodeGen.extract_input_manifest`.
|
|
166
171
|
- ``strategyParams``: the literal ``strategy(...)`` kwargs the analyzer
|
|
167
172
|
surfaced (e.g. ``initial_capital``, ``pyramiding``).
|
|
168
173
|
- ``diagnostics``: the warnings (:class:`~pineforge_codegen.errors.Diagnostic`,
|
|
169
174
|
``Level.WARNING``) the support checker and the analyzer raised for a
|
|
170
175
|
script that transpiled -- e.g. an approximated ``ta.vwap`` anchor. An
|
|
171
176
|
error still raises ``CompileError``, which carries the warnings too.
|
|
177
|
+
- ``requests``: every request site that reads another symbol's feed,
|
|
178
|
+
by line: its symbol and timeframe as registration computes them
|
|
179
|
+
before the first bar (``literal`` / ``input`` / ``computed`` /
|
|
180
|
+
``unresolvable``; ``literal`` / ``chart`` / ``input`` / ``computed``),
|
|
181
|
+
``lookahead``, ``gaps`` and ``ignore_invalid_symbol``. A site lowered
|
|
182
|
+
to ``na`` (its value reaches display sinks only) or in a helper
|
|
183
|
+
nothing reaches is not listed. See
|
|
184
|
+
:mod:`pineforge_codegen.request_discovery`.
|
|
172
185
|
|
|
173
186
|
Args mirror :func:`transpile`.
|
|
174
187
|
|
|
175
188
|
Returns:
|
|
176
189
|
``{"cpp": str, "inputs": list[dict], "strategyParams": dict,
|
|
177
|
-
"diagnostics": list[Diagnostic]}``.
|
|
190
|
+
"diagnostics": list[Diagnostic], "requests": list[dict]}``.
|
|
178
191
|
"""
|
|
179
|
-
gen, ctx, cpp, support_diagnostics = _generate(
|
|
192
|
+
gen, ctx, cpp, support_diagnostics, sites = _generate(
|
|
193
|
+
pine_source, check_support, filename, libraries)
|
|
180
194
|
return {
|
|
181
195
|
"cpp": cpp,
|
|
182
196
|
"inputs": gen.extract_input_manifest(),
|
|
183
197
|
"strategyParams": dict(ctx.strategy_params),
|
|
184
198
|
"diagnostics": [d for d in (*support_diagnostics, *ctx.diagnostics)
|
|
185
199
|
if d.level == Level.WARNING],
|
|
200
|
+
"requests": discover_requests(gen, ctx, sites),
|
|
186
201
|
}
|
|
@@ -32,6 +32,10 @@ from ..errors import SourceLocation, Diagnostic, CompileError, Level, Phase
|
|
|
32
32
|
from ..limits import TimeBudget, iter_ast_nodes
|
|
33
33
|
from ..security_contexts import PASS_WARNINGS_ANNOTATION, context_key, reads_bar_series
|
|
34
34
|
from ..session_reads import emitted_session_reads
|
|
35
|
+
from ..collection_history import (
|
|
36
|
+
COLLECTION_HISTORY_ANNOTATION, LEGACY, REFUSED, SUPPORTED,
|
|
37
|
+
CollectionHistoryChecker, HistoryRead,
|
|
38
|
+
)
|
|
35
39
|
from ..method_binding import (
|
|
36
40
|
BoundMethodArgs,
|
|
37
41
|
MethodBindError,
|
|
@@ -431,6 +435,13 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
431
435
|
# pass can tell borrowed clones apart from a dead function's own
|
|
432
436
|
# sites (see contracts.TACallSite.owner_func).
|
|
433
437
|
self._enclosing_func_names: list[str] = []
|
|
438
|
+
# Array and matrix history reads (``a[1]``), each decided where the
|
|
439
|
+
# visit meets it (pineforge_codegen/collection_history.py), and the
|
|
440
|
+
# declarations writing each class member, by name (a sibling block's
|
|
441
|
+
# declaration closes another one's history).
|
|
442
|
+
self._collection_history_reads: list[HistoryRead] = []
|
|
443
|
+
self._collection_history_checker: CollectionHistoryChecker | None = None
|
|
444
|
+
self._member_writer_decls: dict[str, set[int]] = {}
|
|
434
445
|
# Exact TA targets borrowed through nested callable edges while visiting
|
|
435
446
|
# the current callable body. Constructor templates are tracked
|
|
436
447
|
# separately in ``_func_ta_ctor_args`` because state ownership also
|
|
@@ -552,6 +563,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
552
563
|
self._ensure_pine_v6()
|
|
553
564
|
self._check_direct_terminal_array_element_callee_shadows()
|
|
554
565
|
self._visit(self._ast)
|
|
566
|
+
collection_history = self._check_collection_history()
|
|
555
567
|
self._check_direct_terminal_array_temporary_cycles()
|
|
556
568
|
self._register_resolved_direct_terminal_array_forward_calls()
|
|
557
569
|
self._refresh_direct_terminal_array_temporary_returns()
|
|
@@ -652,6 +664,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
652
664
|
for owner, names in self._func_var_storage_names.items()
|
|
653
665
|
},
|
|
654
666
|
func_series_vars=self._func_series_vars,
|
|
667
|
+
collection_history=collection_history,
|
|
655
668
|
func_global_history_reads={
|
|
656
669
|
owner: list(names)
|
|
657
670
|
for owner, names in self._func_global_history_reads.items()
|
|
@@ -3784,6 +3797,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
3784
3797
|
symbol=sec.symbol,
|
|
3785
3798
|
foreign=sec.foreign,
|
|
3786
3799
|
ignore_invalid=sec.ignore_invalid,
|
|
3800
|
+
loc=sec.loc,
|
|
3787
3801
|
)
|
|
3788
3802
|
new_calls.append(clone)
|
|
3789
3803
|
next_sec_id += 1
|
|
@@ -4345,6 +4359,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4345
4359
|
self._symbols.define(sym)
|
|
4346
4360
|
setattr(sym, "_pf_decl_node_id", id(node))
|
|
4347
4361
|
setattr(sym, "_pf_decl_binding_name", node.name)
|
|
4362
|
+
self._note_member_writer(sym, node.name, node)
|
|
4348
4363
|
if (type_spec is None
|
|
4349
4364
|
and self._collection_scope_stack
|
|
4350
4365
|
and self._block_node_stack
|
|
@@ -4647,6 +4662,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4647
4662
|
self._symbols.define(sym)
|
|
4648
4663
|
setattr(sym, "_pf_decl_node_id", id(node))
|
|
4649
4664
|
setattr(sym, "_pf_decl_binding_name", name)
|
|
4665
|
+
self._note_member_writer(sym, name, node)
|
|
4650
4666
|
|
|
4651
4667
|
if (self._collection_scope_stack
|
|
4652
4668
|
and self._block_node_stack
|
|
@@ -6174,6 +6190,19 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6174
6190
|
if self._type_spec_contains_map(object_spec):
|
|
6175
6191
|
assert object_spec is not None
|
|
6176
6192
|
self._reject_unsupported_map_history(object_spec, node)
|
|
6193
|
+
if (object_spec is not None
|
|
6194
|
+
and object_spec.kind in ("array", "matrix")):
|
|
6195
|
+
# A parameter given an array or a matrix: its history is a
|
|
6196
|
+
# parameter's, which PineForge does not keep; the earlier
|
|
6197
|
+
# lowering stays where it compiled (collection_history.py).
|
|
6198
|
+
# A selection of one is an expression's history.
|
|
6199
|
+
checker = self._history_checker()
|
|
6200
|
+
if isinstance(node.object, Identifier):
|
|
6201
|
+
decision = checker.decide_parameter_history(node, object_spec)
|
|
6202
|
+
if decision.outcome == REFUSED:
|
|
6203
|
+
self._collection_history_raise(decision, node)
|
|
6204
|
+
else:
|
|
6205
|
+
self._refuse_collection_expression_history(node, object_spec)
|
|
6177
6206
|
|
|
6178
6207
|
# Propagate concrete caller specs through wrapper calls. Each edge is
|
|
6179
6208
|
# identity-keyed from the definition pass, so a local that shadows a
|
|
@@ -6428,8 +6457,10 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6428
6457
|
def _visit_Subscript(self, node: Subscript) -> PineType:
|
|
6429
6458
|
obj_type = self._visit(node.object)
|
|
6430
6459
|
self._visit(node.index)
|
|
6460
|
+
self._refuse_collection_field_history(node)
|
|
6431
6461
|
|
|
6432
6462
|
object_spec = self._history_receiver_type_spec(node.object)
|
|
6463
|
+
self._refuse_collection_expression_history(node, object_spec)
|
|
6433
6464
|
if self._type_spec_contains_map(object_spec):
|
|
6434
6465
|
assert object_spec is not None
|
|
6435
6466
|
self._reject_unsupported_map_history(object_spec, node)
|
|
@@ -6461,7 +6492,17 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6461
6492
|
"Series buffer for that binding.",
|
|
6462
6493
|
node.loc,
|
|
6463
6494
|
)
|
|
6464
|
-
|
|
6495
|
+
# An array's or a matrix's history: copies kept beside
|
|
6496
|
+
# the variable, not a Series of it (collection_history.py);
|
|
6497
|
+
# a legacy matrix parameter's read keeps the registration
|
|
6498
|
+
# below, as it had.
|
|
6499
|
+
keeps_series = not (
|
|
6500
|
+
getattr(sym, "type_spec", None) is not None
|
|
6501
|
+
and sym.type_spec.kind in ("array", "matrix")
|
|
6502
|
+
and not self._note_collection_history_read(node, name, sym)
|
|
6503
|
+
)
|
|
6504
|
+
if keeps_series and (getattr(sym, "type_spec", None) is None
|
|
6505
|
+
or sym.type_spec.kind not in ("array", "map")):
|
|
6465
6506
|
exact_member = getattr(sym, "_pf_var_member_name", None)
|
|
6466
6507
|
if exact_member is not None:
|
|
6467
6508
|
self._series_var_members.add(exact_member)
|
|
@@ -6510,6 +6551,150 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6510
6551
|
|
|
6511
6552
|
return obj_type
|
|
6512
6553
|
|
|
6554
|
+
def _refuse_collection_field_history(self, node: Subscript) -> None:
|
|
6555
|
+
"""The history of an object's array, matrix or map field: TradingView
|
|
6556
|
+
refuses ``h.xs[1]`` and ``(h.xs)[1]`` alike ("Cannot use the
|
|
6557
|
+
history-referencing operator on fields of user-defined types",
|
|
6558
|
+
CE10290: fixtures/array_history_tv ahist_field_noparen,
|
|
6559
|
+
ahist_field_paren, ahist_mfield) and takes ``(h[1]).xs``. The C++
|
|
6560
|
+
indexed the field's current collection."""
|
|
6561
|
+
field = node.object
|
|
6562
|
+
if not isinstance(field, MemberAccess):
|
|
6563
|
+
return
|
|
6564
|
+
owner = self._history_receiver_type_spec(field.object)
|
|
6565
|
+
if (owner is None or owner.kind != "udt"
|
|
6566
|
+
or owner.name not in self._udt_fields):
|
|
6567
|
+
return
|
|
6568
|
+
spec = (self._udt_field_type_specs.get(owner.name) or {}).get(field.member)
|
|
6569
|
+
if spec is None or spec.kind not in ("array", "matrix", "map"):
|
|
6570
|
+
return
|
|
6571
|
+
receiver = (field.object.name if isinstance(field.object, Identifier)
|
|
6572
|
+
else "object")
|
|
6573
|
+
raise CompileError([Diagnostic(
|
|
6574
|
+
level=Level.ERROR,
|
|
6575
|
+
phase=Phase.ANALYZER,
|
|
6576
|
+
location=node.loc or SourceLocation(self._filename, 1, 1, 1),
|
|
6577
|
+
message=(f"{receiver}.{field.member}[...]: TradingView refuses the "
|
|
6578
|
+
"history-referencing operator on fields of user-defined "
|
|
6579
|
+
"types (CE10290)."),
|
|
6580
|
+
hint=(f"Read the field of the object's history: "
|
|
6581
|
+
f"({receiver}[1]).{field.member}."),
|
|
6582
|
+
)])
|
|
6583
|
+
|
|
6584
|
+
def _history_checker(self) -> CollectionHistoryChecker:
|
|
6585
|
+
"""The script's array and matrix history checker, built when the
|
|
6586
|
+
visit first meets such a read."""
|
|
6587
|
+
if self._collection_history_checker is None:
|
|
6588
|
+
self._collection_history_checker = CollectionHistoryChecker(
|
|
6589
|
+
self._ast, self._collection_history_name_kind)
|
|
6590
|
+
return self._collection_history_checker
|
|
6591
|
+
|
|
6592
|
+
def _collection_history_name_kind(self, name: str) -> str | None:
|
|
6593
|
+
"""``array`` / ``matrix`` / ``map`` / ``scalar`` for a script
|
|
6594
|
+
variable, else None (collection_history.CollectionHistoryChecker)."""
|
|
6595
|
+
sym = self._symbols.global_scope.symbols.get(name)
|
|
6596
|
+
if sym is None:
|
|
6597
|
+
return None
|
|
6598
|
+
spec = getattr(sym, "type_spec", None)
|
|
6599
|
+
if spec is not None and spec.kind in ("array", "matrix", "map"):
|
|
6600
|
+
return spec.kind
|
|
6601
|
+
if sym.pine_type in (PineType.INT, PineType.FLOAT, PineType.BOOL,
|
|
6602
|
+
PineType.STRING, PineType.COLOR):
|
|
6603
|
+
return "scalar"
|
|
6604
|
+
return None
|
|
6605
|
+
|
|
6606
|
+
def _note_member_writer(self, sym, name: str, node) -> None:
|
|
6607
|
+
"""A declaration writing the class member of its name (the script's
|
|
6608
|
+
top level and its blocks): a sibling block's declaration of the name
|
|
6609
|
+
closes another one's array history before it writes
|
|
6610
|
+
(codegen/collection_history.py)."""
|
|
6611
|
+
if sym.scope == "global":
|
|
6612
|
+
self._member_writer_decls.setdefault(name, set()).add(id(node))
|
|
6613
|
+
|
|
6614
|
+
def _collection_history_raise(self, decision, node) -> None:
|
|
6615
|
+
raise CompileError([Diagnostic(
|
|
6616
|
+
level=Level.ERROR,
|
|
6617
|
+
phase=Phase.ANALYZER,
|
|
6618
|
+
location=(getattr(decision.node, "loc", None) or node.loc
|
|
6619
|
+
or SourceLocation(self._filename, 1, 1, 1)),
|
|
6620
|
+
message=decision.message,
|
|
6621
|
+
hint=decision.hint,
|
|
6622
|
+
)])
|
|
6623
|
+
|
|
6624
|
+
def _refuse_collection_expression_history(self, node: Subscript, spec) -> None:
|
|
6625
|
+
"""The history of an expression whose value is an array or a matrix
|
|
6626
|
+
(a call's result, a selection): TradingView reads the copy the
|
|
6627
|
+
expression produced k executions back (fixtures/array_history_tv
|
|
6628
|
+
ahist_fn ``c1``), which PineForge does not keep. A use whose earlier
|
|
6629
|
+
lowering compiled keeps it; one only a collection fits never compiled
|
|
6630
|
+
and is refused (collection_history.py)."""
|
|
6631
|
+
if (isinstance(node.object, Identifier) or spec is None
|
|
6632
|
+
or spec.kind not in ("array", "matrix")):
|
|
6633
|
+
return
|
|
6634
|
+
checker = self._history_checker()
|
|
6635
|
+
decided = checker.decision_for(node) is not None
|
|
6636
|
+
decision = checker.decide_expression(node, spec)
|
|
6637
|
+
if decision.outcome == REFUSED:
|
|
6638
|
+
self._collection_history_raise(decision, node)
|
|
6639
|
+
if decision.outcome == LEGACY and decision.use == "element" and not decided:
|
|
6640
|
+
self._warn(
|
|
6641
|
+
"(...)[...] reads an element of the selected current array in "
|
|
6642
|
+
"PineForge: no history of a selection is kept, where TradingView "
|
|
6643
|
+
"reads the array it selected that many bars back.",
|
|
6644
|
+
node.loc,
|
|
6645
|
+
)
|
|
6646
|
+
|
|
6647
|
+
def _note_collection_history_read(self, node: Subscript, name: str, sym) -> bool:
|
|
6648
|
+
"""Decide ``a[k]`` of an array or a matrix variable
|
|
6649
|
+
(collection_history.py): annotate a supported read, refuse a refused
|
|
6650
|
+
one, and keep a legacy one's earlier lowering. True where that
|
|
6651
|
+
lowering registered a series (a matrix parameter's), which the caller
|
|
6652
|
+
then does as it did."""
|
|
6653
|
+
decl_node_id = getattr(sym, "_pf_decl_node_id", None)
|
|
6654
|
+
read = HistoryRead(
|
|
6655
|
+
node=node,
|
|
6656
|
+
name=name,
|
|
6657
|
+
kind=sym.type_spec.kind,
|
|
6658
|
+
spec=sym.type_spec,
|
|
6659
|
+
in_callable=bool(self._enclosing_func_names),
|
|
6660
|
+
is_parameter=sym.scope != "global" and decl_node_id is None,
|
|
6661
|
+
is_global=sym.scope == "global",
|
|
6662
|
+
is_var=bool(sym.is_var),
|
|
6663
|
+
decl_node_id=decl_node_id,
|
|
6664
|
+
scope_name=sym.scope or "",
|
|
6665
|
+
)
|
|
6666
|
+
decision = self._history_checker().decide(read)
|
|
6667
|
+
if node.annotations and COLLECTION_HISTORY_ANNOTATION in node.annotations:
|
|
6668
|
+
node.annotations = dict(node.annotations)
|
|
6669
|
+
del node.annotations[COLLECTION_HISTORY_ANNOTATION]
|
|
6670
|
+
if decision.outcome == REFUSED:
|
|
6671
|
+
self._collection_history_raise(decision, node)
|
|
6672
|
+
if decision.outcome == SUPPORTED:
|
|
6673
|
+
node.annotations = dict(node.annotations or {})
|
|
6674
|
+
node.annotations[COLLECTION_HISTORY_ANNOTATION] = {
|
|
6675
|
+
"var": name, "kind": read.kind, "use": decision.use,
|
|
6676
|
+
"spec": read.spec, "member": None,
|
|
6677
|
+
}
|
|
6678
|
+
if not any(r.node is node for r in self._collection_history_reads[-1:]):
|
|
6679
|
+
self._collection_history_reads.append(read)
|
|
6680
|
+
return False
|
|
6681
|
+
return decision.outcome == LEGACY and decision.use == "series"
|
|
6682
|
+
|
|
6683
|
+
def _collection_history_is_supported(self, node: Subscript) -> bool:
|
|
6684
|
+
"""Whether the checker supports the history read ``node`` (decided
|
|
6685
|
+
where the visit met it; a read not met yet keeps its old typing)."""
|
|
6686
|
+
checker = self._collection_history_checker
|
|
6687
|
+
decision = checker.decision_for(node) if checker is not None else None
|
|
6688
|
+
return decision is not None and decision.outcome == SUPPORTED
|
|
6689
|
+
|
|
6690
|
+
def _check_collection_history(self) -> dict:
|
|
6691
|
+
"""The declarations whose history the supported reads need, keyed
|
|
6692
|
+
by their members (pineforge_codegen/collection_history.py)."""
|
|
6693
|
+
if not self._collection_history_reads:
|
|
6694
|
+
return {}
|
|
6695
|
+
return self._history_checker().history_variables(
|
|
6696
|
+
self._collection_history_reads, self._member_writer_decls)
|
|
6697
|
+
|
|
6513
6698
|
def _note_function_global_history_read(
|
|
6514
6699
|
self, node: Subscript, name: str, sym) -> None:
|
|
6515
6700
|
"""Record ``x[k]`` in a plain UDF body on a script variable.
|
|
@@ -6807,6 +6992,10 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6807
6992
|
if owner_spec is not None and owner_spec.kind == "udt" and owner_spec.name:
|
|
6808
6993
|
field_spec = (self._udt_field_type_specs.get(owner_spec.name) or {}).get(node.member)
|
|
6809
6994
|
if field_spec is not None:
|
|
6995
|
+
# The receiver is read too: ``(o[1]).inner.v`` reads the
|
|
6996
|
+
# history of ``o`` (fixtures/udt_history_tv udth_ref),
|
|
6997
|
+
# which only its visit registers.
|
|
6998
|
+
self._visit(node.object)
|
|
6810
6999
|
return self._type_hint_to_pine(str(field_spec))
|
|
6811
7000
|
self._visit(node.object)
|
|
6812
7001
|
# strategy.closedtrades.* and strategy.opentrades.* return types
|
|
@@ -876,6 +876,7 @@ class CallHandlers:
|
|
|
876
876
|
dead=bool(notes.get(DEAD_ANNOTATION)),
|
|
877
877
|
foreign=notes.get(LOWERING_ANNOTATION) == FEED_LOWERING,
|
|
878
878
|
ignore_invalid=node.kwargs.get("ignore_invalid_symbol"),
|
|
879
|
+
loc=node.loc,
|
|
879
880
|
))
|
|
880
881
|
|
|
881
882
|
return PineType.STRING if string_result else PineType.FLOAT
|
|
@@ -998,6 +999,7 @@ class CallHandlers:
|
|
|
998
999
|
is_lower_tf_array=True,
|
|
999
1000
|
context_resolved=context is not None,
|
|
1000
1001
|
dead=bool(notes.get(DEAD_ANNOTATION)),
|
|
1002
|
+
loc=node.loc,
|
|
1001
1003
|
))
|
|
1002
1004
|
|
|
1003
1005
|
# ``request.security_lower_tf`` returns an array; the value-level
|
|
@@ -196,6 +196,9 @@ class SecurityCallInfo:
|
|
|
196
196
|
# ``symbol`` string and ``ignore_invalid_symbol`` (the node, or None).
|
|
197
197
|
foreign: bool = False
|
|
198
198
|
ignore_invalid: Any = None
|
|
199
|
+
# The request call's source location (``request_discovery`` names the
|
|
200
|
+
# site by its line).
|
|
201
|
+
loc: Any = None
|
|
199
202
|
|
|
200
203
|
|
|
201
204
|
@dataclass
|
|
@@ -337,6 +340,9 @@ class AnalyzerContext:
|
|
|
337
340
|
# per read. TradingView keeps that history per call site, so codegen gives
|
|
338
341
|
# each emitted body its own chart-clocked buffer of the variable.
|
|
339
342
|
func_global_history_reads: dict = field(default_factory=dict)
|
|
343
|
+
# Array and matrix variables whose history the script reads, by name
|
|
344
|
+
# (pineforge_codegen/collection_history.py HistoryVariable).
|
|
345
|
+
collection_history: dict = field(default_factory=dict)
|
|
340
346
|
func_global_history_nodes: dict = field(default_factory=dict)
|
|
341
347
|
# FuncDef owner -> declaration-bound non-persistent history-local names.
|
|
342
348
|
# Parameters are deliberately excluded so a same-named qualified
|
|
@@ -347,14 +347,25 @@ class TypeHelper:
|
|
|
347
347
|
if isinstance(value, Subscript):
|
|
348
348
|
# Pine's history operator preserves the value type: a
|
|
349
349
|
# ``Series<line>`` read such as ``h[1]`` is a scalar ``line``
|
|
350
|
-
# handle
|
|
351
|
-
#
|
|
352
|
-
#
|
|
353
|
-
# PineType in
|
|
350
|
+
# handle and ``c[1]`` of a user-defined object the reference ``c``
|
|
351
|
+
# held (fixtures/udt_history_tv), not the legacy numeric fallback.
|
|
352
|
+
# Collection subscripts have separate array/map semantics and
|
|
353
|
+
# primitive history inference already flows through PineType in
|
|
354
|
+
# ``_visit_Subscript``.
|
|
354
355
|
receiver_spec = self._type_spec_from_expr(value.object)
|
|
355
356
|
if (receiver_spec is not None
|
|
356
357
|
and receiver_spec.kind == "udt"
|
|
357
|
-
and receiver_spec.name in _DRAWING_TYPE_NAMES
|
|
358
|
+
and (receiver_spec.name in _DRAWING_TYPE_NAMES
|
|
359
|
+
or receiver_spec.name in self._udt_fields)):
|
|
360
|
+
return receiver_spec
|
|
361
|
+
if (receiver_spec is not None
|
|
362
|
+
and receiver_spec.kind in ("array", "matrix")
|
|
363
|
+
and isinstance(value.object, Identifier)
|
|
364
|
+
and self._collection_history_is_supported(value)):
|
|
365
|
+
# An array's or a matrix's history is a collection of its
|
|
366
|
+
# type (fixtures/array_history_tv; pineforge_codegen/
|
|
367
|
+
# collection_history.py); a legacy read keeps the element
|
|
368
|
+
# typing it had.
|
|
358
369
|
return receiver_spec
|
|
359
370
|
return None
|
|
360
371
|
if isinstance(value, IfStmt):
|
|
@@ -6,6 +6,13 @@ from typing import Any
|
|
|
6
6
|
from pineforge_codegen.errors import SourceLocation
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
# A MemberAccess (a field, or a method call's callee) written straight after a
|
|
10
|
+
# history reference, ``c[1].v`` / ``b[1].get_top()``: TradingView refuses it
|
|
11
|
+
# (CE10011 / CE10010) and takes ``(c[1]).v``, which parses to the same nodes.
|
|
12
|
+
# Set by the parser, refused by the support checker.
|
|
13
|
+
HISTORY_MEMBER_ANNOTATION = "pf_history_member"
|
|
14
|
+
|
|
15
|
+
|
|
9
16
|
# ---------------------------------------------------------------------------
|
|
10
17
|
# Base class
|
|
11
18
|
# ---------------------------------------------------------------------------
|