@pineforge/codegen-pyodide 1.0.0 → 1.0.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 +189 -1
- package/pineforge_codegen/analyzer/contracts.py +3 -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/collection_history.py +200 -0
- package/pineforge_codegen/codegen/emit_top.py +32 -5
- package/pineforge_codegen/codegen/helpers.py +5 -1
- 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/parser.py +19 -4
- package/pineforge_codegen/support_checker.py +37 -0
- package/pineforge_codegen-1.0.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/pineforge_codegen-1.0.0.tar.gz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pineforge/codegen-pyodide",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
|
@@ -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()
|
|
@@ -4345,6 +4358,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4345
4358
|
self._symbols.define(sym)
|
|
4346
4359
|
setattr(sym, "_pf_decl_node_id", id(node))
|
|
4347
4360
|
setattr(sym, "_pf_decl_binding_name", node.name)
|
|
4361
|
+
self._note_member_writer(sym, node.name, node)
|
|
4348
4362
|
if (type_spec is None
|
|
4349
4363
|
and self._collection_scope_stack
|
|
4350
4364
|
and self._block_node_stack
|
|
@@ -4647,6 +4661,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
4647
4661
|
self._symbols.define(sym)
|
|
4648
4662
|
setattr(sym, "_pf_decl_node_id", id(node))
|
|
4649
4663
|
setattr(sym, "_pf_decl_binding_name", name)
|
|
4664
|
+
self._note_member_writer(sym, name, node)
|
|
4650
4665
|
|
|
4651
4666
|
if (self._collection_scope_stack
|
|
4652
4667
|
and self._block_node_stack
|
|
@@ -6174,6 +6189,19 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6174
6189
|
if self._type_spec_contains_map(object_spec):
|
|
6175
6190
|
assert object_spec is not None
|
|
6176
6191
|
self._reject_unsupported_map_history(object_spec, node)
|
|
6192
|
+
if (object_spec is not None
|
|
6193
|
+
and object_spec.kind in ("array", "matrix")):
|
|
6194
|
+
# A parameter given an array or a matrix: its history is a
|
|
6195
|
+
# parameter's, which PineForge does not keep; the earlier
|
|
6196
|
+
# lowering stays where it compiled (collection_history.py).
|
|
6197
|
+
# A selection of one is an expression's history.
|
|
6198
|
+
checker = self._history_checker()
|
|
6199
|
+
if isinstance(node.object, Identifier):
|
|
6200
|
+
decision = checker.decide_parameter_history(node, object_spec)
|
|
6201
|
+
if decision.outcome == REFUSED:
|
|
6202
|
+
self._collection_history_raise(decision, node)
|
|
6203
|
+
else:
|
|
6204
|
+
self._refuse_collection_expression_history(node, object_spec)
|
|
6177
6205
|
|
|
6178
6206
|
# Propagate concrete caller specs through wrapper calls. Each edge is
|
|
6179
6207
|
# identity-keyed from the definition pass, so a local that shadows a
|
|
@@ -6428,8 +6456,10 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6428
6456
|
def _visit_Subscript(self, node: Subscript) -> PineType:
|
|
6429
6457
|
obj_type = self._visit(node.object)
|
|
6430
6458
|
self._visit(node.index)
|
|
6459
|
+
self._refuse_collection_field_history(node)
|
|
6431
6460
|
|
|
6432
6461
|
object_spec = self._history_receiver_type_spec(node.object)
|
|
6462
|
+
self._refuse_collection_expression_history(node, object_spec)
|
|
6433
6463
|
if self._type_spec_contains_map(object_spec):
|
|
6434
6464
|
assert object_spec is not None
|
|
6435
6465
|
self._reject_unsupported_map_history(object_spec, node)
|
|
@@ -6461,7 +6491,17 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6461
6491
|
"Series buffer for that binding.",
|
|
6462
6492
|
node.loc,
|
|
6463
6493
|
)
|
|
6464
|
-
|
|
6494
|
+
# An array's or a matrix's history: copies kept beside
|
|
6495
|
+
# the variable, not a Series of it (collection_history.py);
|
|
6496
|
+
# a legacy matrix parameter's read keeps the registration
|
|
6497
|
+
# below, as it had.
|
|
6498
|
+
keeps_series = not (
|
|
6499
|
+
getattr(sym, "type_spec", None) is not None
|
|
6500
|
+
and sym.type_spec.kind in ("array", "matrix")
|
|
6501
|
+
and not self._note_collection_history_read(node, name, sym)
|
|
6502
|
+
)
|
|
6503
|
+
if keeps_series and (getattr(sym, "type_spec", None) is None
|
|
6504
|
+
or sym.type_spec.kind not in ("array", "map")):
|
|
6465
6505
|
exact_member = getattr(sym, "_pf_var_member_name", None)
|
|
6466
6506
|
if exact_member is not None:
|
|
6467
6507
|
self._series_var_members.add(exact_member)
|
|
@@ -6510,6 +6550,150 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6510
6550
|
|
|
6511
6551
|
return obj_type
|
|
6512
6552
|
|
|
6553
|
+
def _refuse_collection_field_history(self, node: Subscript) -> None:
|
|
6554
|
+
"""The history of an object's array, matrix or map field: TradingView
|
|
6555
|
+
refuses ``h.xs[1]`` and ``(h.xs)[1]`` alike ("Cannot use the
|
|
6556
|
+
history-referencing operator on fields of user-defined types",
|
|
6557
|
+
CE10290: fixtures/array_history_tv ahist_field_noparen,
|
|
6558
|
+
ahist_field_paren, ahist_mfield) and takes ``(h[1]).xs``. The C++
|
|
6559
|
+
indexed the field's current collection."""
|
|
6560
|
+
field = node.object
|
|
6561
|
+
if not isinstance(field, MemberAccess):
|
|
6562
|
+
return
|
|
6563
|
+
owner = self._history_receiver_type_spec(field.object)
|
|
6564
|
+
if (owner is None or owner.kind != "udt"
|
|
6565
|
+
or owner.name not in self._udt_fields):
|
|
6566
|
+
return
|
|
6567
|
+
spec = (self._udt_field_type_specs.get(owner.name) or {}).get(field.member)
|
|
6568
|
+
if spec is None or spec.kind not in ("array", "matrix", "map"):
|
|
6569
|
+
return
|
|
6570
|
+
receiver = (field.object.name if isinstance(field.object, Identifier)
|
|
6571
|
+
else "object")
|
|
6572
|
+
raise CompileError([Diagnostic(
|
|
6573
|
+
level=Level.ERROR,
|
|
6574
|
+
phase=Phase.ANALYZER,
|
|
6575
|
+
location=node.loc or SourceLocation(self._filename, 1, 1, 1),
|
|
6576
|
+
message=(f"{receiver}.{field.member}[...]: TradingView refuses the "
|
|
6577
|
+
"history-referencing operator on fields of user-defined "
|
|
6578
|
+
"types (CE10290)."),
|
|
6579
|
+
hint=(f"Read the field of the object's history: "
|
|
6580
|
+
f"({receiver}[1]).{field.member}."),
|
|
6581
|
+
)])
|
|
6582
|
+
|
|
6583
|
+
def _history_checker(self) -> CollectionHistoryChecker:
|
|
6584
|
+
"""The script's array and matrix history checker, built when the
|
|
6585
|
+
visit first meets such a read."""
|
|
6586
|
+
if self._collection_history_checker is None:
|
|
6587
|
+
self._collection_history_checker = CollectionHistoryChecker(
|
|
6588
|
+
self._ast, self._collection_history_name_kind)
|
|
6589
|
+
return self._collection_history_checker
|
|
6590
|
+
|
|
6591
|
+
def _collection_history_name_kind(self, name: str) -> str | None:
|
|
6592
|
+
"""``array`` / ``matrix`` / ``map`` / ``scalar`` for a script
|
|
6593
|
+
variable, else None (collection_history.CollectionHistoryChecker)."""
|
|
6594
|
+
sym = self._symbols.global_scope.symbols.get(name)
|
|
6595
|
+
if sym is None:
|
|
6596
|
+
return None
|
|
6597
|
+
spec = getattr(sym, "type_spec", None)
|
|
6598
|
+
if spec is not None and spec.kind in ("array", "matrix", "map"):
|
|
6599
|
+
return spec.kind
|
|
6600
|
+
if sym.pine_type in (PineType.INT, PineType.FLOAT, PineType.BOOL,
|
|
6601
|
+
PineType.STRING, PineType.COLOR):
|
|
6602
|
+
return "scalar"
|
|
6603
|
+
return None
|
|
6604
|
+
|
|
6605
|
+
def _note_member_writer(self, sym, name: str, node) -> None:
|
|
6606
|
+
"""A declaration writing the class member of its name (the script's
|
|
6607
|
+
top level and its blocks): a sibling block's declaration of the name
|
|
6608
|
+
closes another one's array history before it writes
|
|
6609
|
+
(codegen/collection_history.py)."""
|
|
6610
|
+
if sym.scope == "global":
|
|
6611
|
+
self._member_writer_decls.setdefault(name, set()).add(id(node))
|
|
6612
|
+
|
|
6613
|
+
def _collection_history_raise(self, decision, node) -> None:
|
|
6614
|
+
raise CompileError([Diagnostic(
|
|
6615
|
+
level=Level.ERROR,
|
|
6616
|
+
phase=Phase.ANALYZER,
|
|
6617
|
+
location=(getattr(decision.node, "loc", None) or node.loc
|
|
6618
|
+
or SourceLocation(self._filename, 1, 1, 1)),
|
|
6619
|
+
message=decision.message,
|
|
6620
|
+
hint=decision.hint,
|
|
6621
|
+
)])
|
|
6622
|
+
|
|
6623
|
+
def _refuse_collection_expression_history(self, node: Subscript, spec) -> None:
|
|
6624
|
+
"""The history of an expression whose value is an array or a matrix
|
|
6625
|
+
(a call's result, a selection): TradingView reads the copy the
|
|
6626
|
+
expression produced k executions back (fixtures/array_history_tv
|
|
6627
|
+
ahist_fn ``c1``), which PineForge does not keep. A use whose earlier
|
|
6628
|
+
lowering compiled keeps it; one only a collection fits never compiled
|
|
6629
|
+
and is refused (collection_history.py)."""
|
|
6630
|
+
if (isinstance(node.object, Identifier) or spec is None
|
|
6631
|
+
or spec.kind not in ("array", "matrix")):
|
|
6632
|
+
return
|
|
6633
|
+
checker = self._history_checker()
|
|
6634
|
+
decided = checker.decision_for(node) is not None
|
|
6635
|
+
decision = checker.decide_expression(node, spec)
|
|
6636
|
+
if decision.outcome == REFUSED:
|
|
6637
|
+
self._collection_history_raise(decision, node)
|
|
6638
|
+
if decision.outcome == LEGACY and decision.use == "element" and not decided:
|
|
6639
|
+
self._warn(
|
|
6640
|
+
"(...)[...] reads an element of the selected current array in "
|
|
6641
|
+
"PineForge: no history of a selection is kept, where TradingView "
|
|
6642
|
+
"reads the array it selected that many bars back.",
|
|
6643
|
+
node.loc,
|
|
6644
|
+
)
|
|
6645
|
+
|
|
6646
|
+
def _note_collection_history_read(self, node: Subscript, name: str, sym) -> bool:
|
|
6647
|
+
"""Decide ``a[k]`` of an array or a matrix variable
|
|
6648
|
+
(collection_history.py): annotate a supported read, refuse a refused
|
|
6649
|
+
one, and keep a legacy one's earlier lowering. True where that
|
|
6650
|
+
lowering registered a series (a matrix parameter's), which the caller
|
|
6651
|
+
then does as it did."""
|
|
6652
|
+
decl_node_id = getattr(sym, "_pf_decl_node_id", None)
|
|
6653
|
+
read = HistoryRead(
|
|
6654
|
+
node=node,
|
|
6655
|
+
name=name,
|
|
6656
|
+
kind=sym.type_spec.kind,
|
|
6657
|
+
spec=sym.type_spec,
|
|
6658
|
+
in_callable=bool(self._enclosing_func_names),
|
|
6659
|
+
is_parameter=sym.scope != "global" and decl_node_id is None,
|
|
6660
|
+
is_global=sym.scope == "global",
|
|
6661
|
+
is_var=bool(sym.is_var),
|
|
6662
|
+
decl_node_id=decl_node_id,
|
|
6663
|
+
scope_name=sym.scope or "",
|
|
6664
|
+
)
|
|
6665
|
+
decision = self._history_checker().decide(read)
|
|
6666
|
+
if node.annotations and COLLECTION_HISTORY_ANNOTATION in node.annotations:
|
|
6667
|
+
node.annotations = dict(node.annotations)
|
|
6668
|
+
del node.annotations[COLLECTION_HISTORY_ANNOTATION]
|
|
6669
|
+
if decision.outcome == REFUSED:
|
|
6670
|
+
self._collection_history_raise(decision, node)
|
|
6671
|
+
if decision.outcome == SUPPORTED:
|
|
6672
|
+
node.annotations = dict(node.annotations or {})
|
|
6673
|
+
node.annotations[COLLECTION_HISTORY_ANNOTATION] = {
|
|
6674
|
+
"var": name, "kind": read.kind, "use": decision.use,
|
|
6675
|
+
"spec": read.spec, "member": None,
|
|
6676
|
+
}
|
|
6677
|
+
if not any(r.node is node for r in self._collection_history_reads[-1:]):
|
|
6678
|
+
self._collection_history_reads.append(read)
|
|
6679
|
+
return False
|
|
6680
|
+
return decision.outcome == LEGACY and decision.use == "series"
|
|
6681
|
+
|
|
6682
|
+
def _collection_history_is_supported(self, node: Subscript) -> bool:
|
|
6683
|
+
"""Whether the checker supports the history read ``node`` (decided
|
|
6684
|
+
where the visit met it; a read not met yet keeps its old typing)."""
|
|
6685
|
+
checker = self._collection_history_checker
|
|
6686
|
+
decision = checker.decision_for(node) if checker is not None else None
|
|
6687
|
+
return decision is not None and decision.outcome == SUPPORTED
|
|
6688
|
+
|
|
6689
|
+
def _check_collection_history(self) -> dict:
|
|
6690
|
+
"""The declarations whose history the supported reads need, keyed
|
|
6691
|
+
by their members (pineforge_codegen/collection_history.py)."""
|
|
6692
|
+
if not self._collection_history_reads:
|
|
6693
|
+
return {}
|
|
6694
|
+
return self._history_checker().history_variables(
|
|
6695
|
+
self._collection_history_reads, self._member_writer_decls)
|
|
6696
|
+
|
|
6513
6697
|
def _note_function_global_history_read(
|
|
6514
6698
|
self, node: Subscript, name: str, sym) -> None:
|
|
6515
6699
|
"""Record ``x[k]`` in a plain UDF body on a script variable.
|
|
@@ -6807,6 +6991,10 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
6807
6991
|
if owner_spec is not None and owner_spec.kind == "udt" and owner_spec.name:
|
|
6808
6992
|
field_spec = (self._udt_field_type_specs.get(owner_spec.name) or {}).get(node.member)
|
|
6809
6993
|
if field_spec is not None:
|
|
6994
|
+
# The receiver is read too: ``(o[1]).inner.v`` reads the
|
|
6995
|
+
# history of ``o`` (fixtures/udt_history_tv udth_ref),
|
|
6996
|
+
# which only its visit registers.
|
|
6997
|
+
self._visit(node.object)
|
|
6810
6998
|
return self._type_hint_to_pine(str(field_spec))
|
|
6811
6999
|
self._visit(node.object)
|
|
6812
7000
|
# strategy.closedtrades.* and strategy.opentrades.* return types
|
|
@@ -337,6 +337,9 @@ class AnalyzerContext:
|
|
|
337
337
|
# per read. TradingView keeps that history per call site, so codegen gives
|
|
338
338
|
# each emitted body its own chart-clocked buffer of the variable.
|
|
339
339
|
func_global_history_reads: dict = field(default_factory=dict)
|
|
340
|
+
# Array and matrix variables whose history the script reads, by name
|
|
341
|
+
# (pineforge_codegen/collection_history.py HistoryVariable).
|
|
342
|
+
collection_history: dict = field(default_factory=dict)
|
|
340
343
|
func_global_history_nodes: dict = field(default_factory=dict)
|
|
341
344
|
# FuncDef owner -> declaration-bound non-persistent history-local names.
|
|
342
345
|
# 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
|
# ---------------------------------------------------------------------------
|
|
@@ -145,6 +145,7 @@ TA_TUPLE_RESULT_TYPES = {
|
|
|
145
145
|
|
|
146
146
|
# CPP_RESERVED + the NamingHelper mixin are pulled in from helpers.py so the
|
|
147
147
|
# small naming/walk utilities can be shared with future visitor mixins.
|
|
148
|
+
from .collection_history import CollectionHistoryEmitter
|
|
148
149
|
from .helpers import (
|
|
149
150
|
CPP_RESERVED, INLINE_HISTORY_KINDS, SESSION_FLAG_MEMBERS, NamingHelper,
|
|
150
151
|
cpp_code_only, na_preserving_int_cast, pine_truth_cast,
|
|
@@ -194,7 +195,7 @@ from .drawing import DrawingVisitor
|
|
|
194
195
|
# CodeGen class
|
|
195
196
|
# ---------------------------------------------------------------------------
|
|
196
197
|
|
|
197
|
-
class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, DrawingVisitor, NamingHelper):
|
|
198
|
+
class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, DrawingVisitor, CollectionHistoryEmitter, NamingHelper):
|
|
198
199
|
"""Generate C++ from an AnalyzerContext (visitor pattern).
|
|
199
200
|
|
|
200
201
|
Mixin chain (Python MRO is left-to-right; method names are
|
|
@@ -1945,7 +1946,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1945
1946
|
)
|
|
1946
1947
|
elif (orig_safe in self._series_var_member_names
|
|
1947
1948
|
or vname in self.ctx.series_vars):
|
|
1948
|
-
|
|
1949
|
+
# A history-read var object keeps its references, as
|
|
1950
|
+
# its base member does.
|
|
1951
|
+
udt_t = udt_spec.name if udt_spec is not None else member_udt_type
|
|
1952
|
+
element = (self._safe_name(udt_t) if udt_t in self._udt_defs
|
|
1953
|
+
else cpp_type)
|
|
1954
|
+
lines.append(f" Series<{element}> {cloned_safe}{series_suffix};")
|
|
1949
1955
|
elif collection_spec is not None:
|
|
1950
1956
|
lines.append(
|
|
1951
1957
|
f" {self._type_spec_to_cpp(collection_spec)} {cloned_safe};"
|
|
@@ -1987,8 +1993,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1987
1993
|
lines.append(f" {cpp_type} {cloned_safe};")
|
|
1988
1994
|
return
|
|
1989
1995
|
# Non-var series var
|
|
1990
|
-
|
|
1991
|
-
|
|
1996
|
+
raw_names = [n for n in self.ctx.series_vars if self._safe_name(n) == orig_safe]
|
|
1997
|
+
if raw_names:
|
|
1998
|
+
# A history-read object or drawing (a receiver ``this`` spelled
|
|
1999
|
+
# ``pf_safe_this``) holds handles, as its base member does.
|
|
2000
|
+
handles = {self._series_handle_cpp_type(n) for n in raw_names}
|
|
2001
|
+
cpp_type = (handles.pop() if len(handles) == 1 and None not in handles
|
|
2002
|
+
else self._series_type_for(orig_safe))
|
|
1992
2003
|
lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
|
|
1993
2004
|
else:
|
|
1994
2005
|
lines.append(f" double {cloned_safe} = 0.0;")
|
|
@@ -2112,6 +2123,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2112
2123
|
declared_spec = self._type_spec_from_hint_name(stmt.type_hint)
|
|
2113
2124
|
elif isinstance(stmt.value, (Ternary, IfStmt, SwitchStmt)):
|
|
2114
2125
|
declared_spec = self._type_spec_from_expr(stmt.value)
|
|
2126
|
+
elif self._roots_at_collection_history(stmt.value):
|
|
2127
|
+
# A copy of an array's or a matrix's history, or a built-in's
|
|
2128
|
+
# result on one (``pm = m[1]``, ``c = (m[1]).copy()``).
|
|
2129
|
+
declared_spec = self._type_spec_from_expr(stmt.value)
|
|
2130
|
+
if declared_spec is not None and declared_spec.kind == "array":
|
|
2131
|
+
self._array_vars.add(stmt.name)
|
|
2132
|
+
self._collection_types.setdefault(stmt.name, declared_spec)
|
|
2133
|
+
continue
|
|
2115
2134
|
else:
|
|
2116
2135
|
continue
|
|
2117
2136
|
if declared_spec is None or declared_spec.kind != "matrix":
|
|
@@ -2225,10 +2244,17 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2225
2244
|
|
|
2226
2245
|
Walks chained ``FuncCall`` receivers (e.g. ``m.transpose().copy()``)
|
|
2227
2246
|
until it finds an ``Identifier`` so the source matrix's TypeSpec can
|
|
2228
|
-
be propagated through fluent call chains.
|
|
2247
|
+
be propagated through fluent call chains. A history read (``m[1]``)
|
|
2248
|
+
is the variable's: its copy has the variable's type.
|
|
2229
2249
|
"""
|
|
2230
2250
|
if not isinstance(call_node, FuncCall):
|
|
2231
2251
|
return None
|
|
2252
|
+
|
|
2253
|
+
def unwrap(node):
|
|
2254
|
+
if isinstance(node, Subscript) and isinstance(node.object, Identifier):
|
|
2255
|
+
return node.object
|
|
2256
|
+
return node
|
|
2257
|
+
|
|
2232
2258
|
callee = call_node.callee
|
|
2233
2259
|
# Method form: m.method(...) — possibly chained: m.foo().bar()
|
|
2234
2260
|
if isinstance(callee, MemberAccess):
|
|
@@ -2240,12 +2266,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2240
2266
|
obj = inner_callee.object
|
|
2241
2267
|
else:
|
|
2242
2268
|
break
|
|
2269
|
+
obj = unwrap(obj)
|
|
2243
2270
|
if isinstance(obj, Identifier):
|
|
2244
2271
|
if obj.name != "matrix":
|
|
2245
2272
|
return obj.name
|
|
2246
2273
|
# matrix.method(m, ...) functional form
|
|
2247
2274
|
if call_node.args:
|
|
2248
|
-
first = call_node.args[0]
|
|
2275
|
+
first = unwrap(call_node.args[0])
|
|
2249
2276
|
if isinstance(first, Identifier):
|
|
2250
2277
|
return first.name
|
|
2251
2278
|
return None
|
|
@@ -3623,13 +3650,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3623
3650
|
if len(states) > 1
|
|
3624
3651
|
}
|
|
3625
3652
|
|
|
3653
|
+
# A history parameter holding a user-defined object or a drawing keeps
|
|
3654
|
+
# the references its calls passed (``_series_param_element_cpp_type``).
|
|
3655
|
+
handle_cpp_types = {
|
|
3656
|
+
*DRAWING_TYPE_TO_CPP.values(),
|
|
3657
|
+
*(self._safe_name(name) for name in self._udt_defs),
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3626
3660
|
def register_one(
|
|
3627
3661
|
kind: str,
|
|
3628
3662
|
source_key: tuple,
|
|
3629
3663
|
cpp_type: str,
|
|
3630
3664
|
context: str | None,
|
|
3631
3665
|
) -> None:
|
|
3632
|
-
if cpp_type not in ("double", "int", "int64_t", "bool")
|
|
3666
|
+
if (cpp_type not in ("double", "int", "int64_t", "bool")
|
|
3667
|
+
and cpp_type not in handle_cpp_types):
|
|
3633
3668
|
cpp_type = "double"
|
|
3634
3669
|
key = (kind, *source_key, context)
|
|
3635
3670
|
if key in self._inline_history_member_by_key:
|
|
@@ -3721,6 +3756,27 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3721
3756
|
lexical.update(self._func_collection_types.get(owner, {}))
|
|
3722
3757
|
return lexical
|
|
3723
3758
|
|
|
3759
|
+
def owner_typed(owner: str | None, compute):
|
|
3760
|
+
"""``compute()`` with the owner's parameter types bound, as the
|
|
3761
|
+
emitter binds them: a history read of a typed parameter's field
|
|
3762
|
+
(``f(Outer p) => p.inner[1]``) types its Series by the field."""
|
|
3763
|
+
previous = self.__dict__.get("_current_func_param_specs")
|
|
3764
|
+
self._current_func_param_specs = owner_lexical_specs(owner)
|
|
3765
|
+
try:
|
|
3766
|
+
return compute()
|
|
3767
|
+
finally:
|
|
3768
|
+
if previous is None:
|
|
3769
|
+
self.__dict__.pop("_current_func_param_specs", None)
|
|
3770
|
+
else:
|
|
3771
|
+
self._current_func_param_specs = previous
|
|
3772
|
+
|
|
3773
|
+
def history_cpp_type(expr, owner: str | None) -> str:
|
|
3774
|
+
"""``_history_value_cpp_type`` of ``expr``, a reference's handle
|
|
3775
|
+
resolved with the owner's parameters bound; any other value keeps
|
|
3776
|
+
the inference every earlier build registered."""
|
|
3777
|
+
return (owner_typed(owner, lambda: self._reference_cpp_type(expr))
|
|
3778
|
+
or self._infer_type(expr))
|
|
3779
|
+
|
|
3724
3780
|
def plain_udf_info(call: FuncCall):
|
|
3725
3781
|
if not isinstance(call.callee, Identifier):
|
|
3726
3782
|
return None
|
|
@@ -3846,7 +3902,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3846
3902
|
owner = owner_by_node.get(id(node))
|
|
3847
3903
|
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
3848
3904
|
register(
|
|
3849
|
-
"hist_call", (id(node),),
|
|
3905
|
+
"hist_call", (id(node),), history_cpp_type(node.object, owner),
|
|
3906
|
+
owner,
|
|
3850
3907
|
)
|
|
3851
3908
|
elif (isinstance(node, Subscript)
|
|
3852
3909
|
and self._is_session_flag(node.object)):
|
|
@@ -3865,9 +3922,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3865
3922
|
self._session_call_flags.setdefault(owner, set()).add(flag)
|
|
3866
3923
|
register("session_call", (owner, flag), "bool", owner)
|
|
3867
3924
|
elif (isinstance(node, Subscript)
|
|
3868
|
-
and self._is_compound_history_object(node.object)
|
|
3925
|
+
and (self._is_compound_history_object(node.object)
|
|
3926
|
+
or (isinstance(node.object, (Ternary, MemberAccess))
|
|
3927
|
+
and owner_typed(owner, lambda: self._reference_cpp_type(
|
|
3928
|
+
node.object)) is not None))):
|
|
3869
3929
|
register(
|
|
3870
|
-
"hist_call", (id(node),),
|
|
3930
|
+
"hist_call", (id(node),), history_cpp_type(node.object, owner),
|
|
3931
|
+
owner,
|
|
3871
3932
|
)
|
|
3872
3933
|
|
|
3873
3934
|
if not isinstance(node, FuncCall):
|
|
@@ -4021,6 +4082,38 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4021
4082
|
("fn_global_hist", id(fi.node), name, self._current_instance_name)
|
|
4022
4083
|
)
|
|
4023
4084
|
|
|
4085
|
+
def _reference_cpp_type(self, expr) -> str | None:
|
|
4086
|
+
"""The handle type of an expression whose value is a user-defined
|
|
4087
|
+
object or a drawing reference (``Cell``, ``Box``), else None."""
|
|
4088
|
+
spec = self._type_spec_from_expr(expr)
|
|
4089
|
+
if (spec is None or spec.kind != "udt"
|
|
4090
|
+
or not (spec.name in DRAWING_TYPE_TO_CPP
|
|
4091
|
+
or spec.name in self._udt_defs)):
|
|
4092
|
+
return None
|
|
4093
|
+
return DRAWING_TYPE_TO_CPP.get(spec.name) or self._safe_name(spec.name)
|
|
4094
|
+
|
|
4095
|
+
def _registered_history_handle(self, node: Subscript) -> str | None:
|
|
4096
|
+
"""The handle type of the synthetic history Series the prepass
|
|
4097
|
+
registered for ``node`` (``_prepare_inline_history_members``), else
|
|
4098
|
+
None: emission reads the type the member was declared with."""
|
|
4099
|
+
member = self._inline_history_member_by_key.get(
|
|
4100
|
+
("hist_call", id(node), self._current_instance_name))
|
|
4101
|
+
if member is None:
|
|
4102
|
+
return None
|
|
4103
|
+
handles = {*DRAWING_TYPE_TO_CPP.values(),
|
|
4104
|
+
*(self._safe_name(name) for name in self._udt_defs)}
|
|
4105
|
+
for info in self._inline_history_members:
|
|
4106
|
+
if info["member_name"] == member:
|
|
4107
|
+
return info["cpp_type"] if info["cpp_type"] in handles else None
|
|
4108
|
+
return None
|
|
4109
|
+
|
|
4110
|
+
def _history_value_cpp_type(self, expr) -> str:
|
|
4111
|
+
"""The element type of the synthetic Series that keeps ``expr``'s
|
|
4112
|
+
history: a reference's handle (TradingView's history of such an
|
|
4113
|
+
expression is the reference it produced at its previous evaluation:
|
|
4114
|
+
fixtures/udt_history_tv udth_expr), else the scalar it infers."""
|
|
4115
|
+
return self._reference_cpp_type(expr) or self._infer_type(expr)
|
|
4116
|
+
|
|
4024
4117
|
def _is_compound_history_object(self, node) -> bool:
|
|
4025
4118
|
"""Whether ``node[k]`` is history on an operator expression or a
|
|
4026
4119
|
``session.*`` flag.
|
|
@@ -4038,6 +4131,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4038
4131
|
"""
|
|
4039
4132
|
if self._is_session_flag(node):
|
|
4040
4133
|
return True
|
|
4134
|
+
if (isinstance(node, (Ternary, MemberAccess))
|
|
4135
|
+
and self._reference_cpp_type(node) is not None):
|
|
4136
|
+
# ``(c ? a : b)[1]`` and an object-typed field's ``o.inner[1]``:
|
|
4137
|
+
# the reference the expression produced at its previous
|
|
4138
|
+
# evaluation (fixtures/udt_history_tv udth_expr).
|
|
4139
|
+
return True
|
|
4041
4140
|
return (isinstance(node, (BinOp, UnaryOp, Ternary))
|
|
4042
4141
|
and self._infer_type(node) in ("double", "int", "int64_t", "bool"))
|
|
4043
4142
|
|
|
@@ -4614,6 +4713,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4614
4713
|
# per-callsite instances are declared below inside GeneratedStrategy
|
|
4615
4714
|
# and therefore join the automatic COOF checkpoint inventory.
|
|
4616
4715
|
self._emit_lazy_source_clock_helper(lines)
|
|
4716
|
+
# The history of an array or a matrix variable: its support types
|
|
4717
|
+
# (pineforge_codegen/collection_history.py), emitted only for a script
|
|
4718
|
+
# that reads one.
|
|
4719
|
+
self._emit_collection_history_helper(lines)
|
|
4617
4720
|
# The chart-bar session type, inserted here once the class is lowered
|
|
4618
4721
|
# and known to call it (codegen/session_market.py).
|
|
4619
4722
|
_session_market_at = len(lines)
|
|
@@ -4847,9 +4950,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4847
4950
|
continue
|
|
4848
4951
|
callable_udt_spec = self._callable_var_udt_spec(name)
|
|
4849
4952
|
if callable_udt_spec is not None:
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4953
|
+
handle_cpp = self._type_spec_to_cpp(callable_udt_spec)
|
|
4954
|
+
if safe in self._series_var_member_names:
|
|
4955
|
+
# A history-read var object: its history holds the
|
|
4956
|
+
# references it held (``_series_handle_cpp_type``).
|
|
4957
|
+
lines.append(f" Series<{handle_cpp}> {safe}{_mbb};")
|
|
4958
|
+
else:
|
|
4959
|
+
lines.append(f" {handle_cpp} {safe};")
|
|
4853
4960
|
continue
|
|
4854
4961
|
# Detect array vars from init expression. Guard the substring
|
|
4855
4962
|
# heuristic against a UDT constructor that merely WRAPS array.new /
|
|
@@ -4941,7 +5048,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4941
5048
|
udt_type = udt_name
|
|
4942
5049
|
break
|
|
4943
5050
|
if udt_type:
|
|
4944
|
-
|
|
5051
|
+
if safe in self._series_var_member_names:
|
|
5052
|
+
# A history-read var object: its history holds the
|
|
5053
|
+
# references it held (``_series_handle_cpp_type``).
|
|
5054
|
+
lines.append(
|
|
5055
|
+
f" Series<{self._safe_name(udt_type)}> {safe}{_mbb};")
|
|
5056
|
+
else:
|
|
5057
|
+
lines.append(f" {self._safe_name(udt_type)} {safe};")
|
|
4945
5058
|
continue
|
|
4946
5059
|
cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
4947
5060
|
# Promote int->int64_t when init RHS is an int64-returning builtin
|
|
@@ -5150,6 +5263,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
5150
5263
|
lines.append(" DrawingArena<LabelRec> _pf_labels_{_PF_DRAWING_UNBOUNDED};")
|
|
5151
5264
|
lines.append(" DrawingArena<LinefillRec> _pf_linefills_{_PF_DRAWING_UNBOUNDED};")
|
|
5152
5265
|
|
|
5266
|
+
# 8e. The copies an array or a matrix variable's executions left
|
|
5267
|
+
# (pineforge_codegen/collection_history.py).
|
|
5268
|
+
for variable in self._collection_history_variables():
|
|
5269
|
+
lines.append(
|
|
5270
|
+
f" _PFCollectionHistory<{self._collection_history_cpp_type(variable)}> "
|
|
5271
|
+
f"{self._collection_history_member(variable)}"
|
|
5272
|
+
f"{self._collection_history_capacity(variable)};"
|
|
5273
|
+
)
|
|
5274
|
+
|
|
5153
5275
|
# 9. _var_initialized flag
|
|
5154
5276
|
if self.ctx.var_members:
|
|
5155
5277
|
lines.append(" bool _var_initialized = false;")
|