@pineforge/codegen-pyodide 0.7.6 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +453 -15
- package/pineforge_codegen/analyzer/call_handlers.py +200 -11
- package/pineforge_codegen/analyzer/contracts.py +62 -0
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +532 -51
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +331 -30
- package/pineforge_codegen/codegen/helpers.py +26 -1
- package/pineforge_codegen/codegen/input.py +39 -2
- package/pineforge_codegen/codegen/security.py +321 -9
- package/pineforge_codegen/codegen/ta.py +4 -1
- package/pineforge_codegen/codegen/tables.py +63 -7
- package/pineforge_codegen/codegen/types.py +383 -21
- package/pineforge_codegen/codegen/visit_call.py +159 -22
- package/pineforge_codegen/codegen/visit_expr.py +91 -3
- package/pineforge_codegen/codegen/visit_stmt.py +188 -26
- package/pineforge_codegen/lexer.py +68 -3
- package/pineforge_codegen/parser.py +106 -49
- package/pineforge_codegen/support_checker.py +361 -37
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +4 -18
- package/pineforge_codegen-0.7.6.tar.gz +0 -0
|
@@ -50,11 +50,15 @@ from .tv_input_choices import INPUT_SOURCE_SERIES_IDS
|
|
|
50
50
|
from .analyzer import TA_CLASS_MAP
|
|
51
51
|
from .codegen import (
|
|
52
52
|
MATH_FUNC_MAP, STR_FUNC_MAP,
|
|
53
|
-
ARRAY_METHODS, MAP_METHODS, MATRIX_METHODS,
|
|
53
|
+
ARRAY_METHODS, ARRAY_DRAWING_NEW_CTORS, MAP_METHODS, MATRIX_METHODS,
|
|
54
54
|
SYMINFO_MEMBER_MAP, COLOR_CONST_MAP,
|
|
55
55
|
SKIP_FUNC_NAMES, SKIP_NAMESPACES,
|
|
56
56
|
BAR_BUILTINS, BAR_FIELDS,
|
|
57
57
|
)
|
|
58
|
+
from .codegen.drawing import (
|
|
59
|
+
LINE_METHODS, LINE_NOOP, BOX_METHODS, BOX_NOOP,
|
|
60
|
+
LABEL_METHODS, LABEL_NOOP, LINEFILL_METHODS, LINEFILL_NOOP,
|
|
61
|
+
)
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
# ---------------------------------------------------------------------------
|
|
@@ -74,12 +78,44 @@ SUPPORTED_MATH: frozenset[str] = frozenset(
|
|
|
74
78
|
)
|
|
75
79
|
SUPPORTED_STR: frozenset[str] = frozenset(set(STR_FUNC_MAP) | {"format_time"})
|
|
76
80
|
SUPPORTED_INPUT: frozenset[str] = frozenset(sigs.INPUT_FUNCTIONS)
|
|
77
|
-
SUPPORTED_ARRAY: frozenset[str] = frozenset(set(ARRAY_METHODS) | {"new", "new_float", "new_int", "new_bool", "new_string", "from"})
|
|
81
|
+
SUPPORTED_ARRAY: frozenset[str] = frozenset(set(ARRAY_METHODS) | {"new", "new_float", "new_int", "new_bool", "new_string", "from"} | set(ARRAY_DRAWING_NEW_CTORS))
|
|
78
82
|
SUPPORTED_MAP: frozenset[str] = frozenset(set(MAP_METHODS) | {"new"})
|
|
79
83
|
SUPPORTED_MATRIX: frozenset[str] = frozenset(set(MATRIX_METHODS) | {"new"})
|
|
80
84
|
SUPPORTED_SYMINFO: frozenset[str] = frozenset(SYMINFO_MEMBER_MAP)
|
|
85
|
+
# Drawing-objects-as-data (spec §4.5). Geometry methods are REAL (route to the
|
|
86
|
+
# per-type arena); *_NOOP visual setters are accepted no-ops (Level.WARNING).
|
|
87
|
+
# Any line/box/label/linefill method NOT in these sets rejects loudly so an
|
|
88
|
+
# unknown drawing method is not silently emitted. ``table``/``polyline`` are
|
|
89
|
+
# intentionally absent — they stay in SKIP_NAMESPACES (no-op, never reclassified).
|
|
90
|
+
SUPPORTED_LINE: frozenset[str] = frozenset(LINE_METHODS | LINE_NOOP | {"new"})
|
|
91
|
+
SUPPORTED_BOX: frozenset[str] = frozenset(BOX_METHODS | BOX_NOOP | {"new"})
|
|
92
|
+
SUPPORTED_LABEL: frozenset[str] = frozenset(LABEL_METHODS | LABEL_NOOP | {"new"})
|
|
93
|
+
SUPPORTED_LINEFILL: frozenset[str] = frozenset(LINEFILL_METHODS | LINEFILL_NOOP | {"new"})
|
|
94
|
+
SUPPORTED_CHART_POINT: frozenset[str] = frozenset(
|
|
95
|
+
{"new", "now", "from_index", "from_time", "copy"}
|
|
96
|
+
)
|
|
97
|
+
# Visual no-op drawing setters (warn, don't reject) keyed by namespace.
|
|
98
|
+
_DRAWING_NOOP_BY_NS: dict[str, frozenset[str]] = {
|
|
99
|
+
"line": LINE_NOOP, "box": BOX_NOOP, "label": LABEL_NOOP, "linefill": LINEFILL_NOOP,
|
|
100
|
+
}
|
|
101
|
+
_DRAWING_SUPPORTED_BY_NS: dict[str, frozenset[str]] = {
|
|
102
|
+
"line": SUPPORTED_LINE, "box": SUPPORTED_BOX,
|
|
103
|
+
"label": SUPPORTED_LABEL, "linefill": SUPPORTED_LINEFILL,
|
|
104
|
+
}
|
|
105
|
+
_DRAWING_TYPE_NAMES: frozenset[str] = frozenset({"line", "box", "label", "linefill"})
|
|
106
|
+
# Scalar visual-container types whose *method* calls (``panel.cell(...)``,
|
|
107
|
+
# ``ln.set_xy1(...)``) legitimately carry visual-constant args (``text.align_*``,
|
|
108
|
+
# ``size.*``). A method on a receiver of one of these types — including a
|
|
109
|
+
# table/box/line/label/linefill PARAMETER — is a visual sink, so its argument
|
|
110
|
+
# subtree is visited with constant-namespace reads allowed (mirrors the
|
|
111
|
+
# namespace-form ``table.cell``/``label.new`` handling).
|
|
112
|
+
_VISUAL_CONTAINER_TYPES: frozenset[str] = _DRAWING_TYPE_NAMES | frozenset({"table"})
|
|
81
113
|
SUPPORTED_COLOR_CONST: frozenset[str] = frozenset(COLOR_CONST_MAP)
|
|
82
114
|
SUPPORTED_COLOR_FUNC: frozenset[str] = frozenset({"new", "rgb", "r", "g", "b", "t"})
|
|
115
|
+
# Cosmetic color builders with no backtest-logic effect. Warned (not rejected);
|
|
116
|
+
# codegen emits a benign default color (0 = na color). color.from_gradient is a
|
|
117
|
+
# charting/plot helper that only tints visual output.
|
|
118
|
+
COSMETIC_COLOR_FUNC: frozenset[str] = frozenset({"from_gradient"})
|
|
83
119
|
SUPPORTED_TIMEFRAME_FUNC: frozenset[str] = frozenset({"change", "in_seconds"})
|
|
84
120
|
SUPPORTED_RUNTIME_FUNC: frozenset[str] = frozenset({"error"})
|
|
85
121
|
# log.* helpers wired into pine_log_{info,warning,error} by codegen/visit_call.
|
|
@@ -96,12 +132,10 @@ HARD_REJECT_FUNC: dict[str, str] = {
|
|
|
96
132
|
"request.seed": "External seed data feeds not available in PineForge.",
|
|
97
133
|
"request.quandl": "External Quandl data not available in PineForge.",
|
|
98
134
|
"request.currency_rate": "Currency conversion data not available in PineForge.",
|
|
99
|
-
|
|
100
|
-
# ticker
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"PineForge runs same-symbol MTF only via request.security with the chart's own tickerid."
|
|
104
|
-
),
|
|
135
|
+
# ticker.* chart-type modifiers and cross-symbol constructors — hard reject.
|
|
136
|
+
# NOTE: ticker.heikinashi is handled contextually in _visit_FuncCall (allowed
|
|
137
|
+
# for the chart's own symbol — the engine applies the HA candle transform
|
|
138
|
+
# inside the same-symbol security eval — rejected for cross-symbol).
|
|
105
139
|
"ticker.renko": (
|
|
106
140
|
"ticker.renko() chart-type modifier / cross-symbol construction not supported — "
|
|
107
141
|
"PineForge runs same-symbol MTF only via request.security with the chart's own tickerid."
|
|
@@ -143,17 +177,23 @@ HARD_REJECT_NAMESPACE: dict[str, str] = {
|
|
|
143
177
|
# DIVERGENT_VARS_ERROR is a SUBSET that is escalated to ERROR (rejected): these
|
|
144
178
|
# are silent MIS-ALIASES, not merely data-window divergences. They return a
|
|
145
179
|
# plausible value that is the WRONG quantity (last_bar_index -> current bar
|
|
146
|
-
# index
|
|
147
|
-
#
|
|
180
|
+
# index) and that value flows directly into trade logic, so the backtest would
|
|
181
|
+
# be silently wrong. A WARNING is not enough.
|
|
182
|
+
#
|
|
183
|
+
# NOTE: the bare ``time_close`` variable is NOT divergent — codegen lowers it to
|
|
184
|
+
# the engine's ``time_close()`` accessor, which returns the true bar-close
|
|
185
|
+
# timestamp (bar open + chart-timeframe duration via ``pine_time_close``),
|
|
186
|
+
# deterministic and TV-faithful. It was previously mis-listed here as a bar-open
|
|
187
|
+
# alias; that was stale. (The session-aware ``time_close(...)`` FUNCTION is a
|
|
188
|
+
# separate supported builtin handled in visit_call.)
|
|
148
189
|
DIVERGENT_VARS: dict[str, str] = {
|
|
149
190
|
"bar_index": "bar_index depends on the data window; PineForge and TradingView produce different values for the same script.",
|
|
150
191
|
"last_bar_index": "last_bar_index is aliased to the CURRENT bar index in PineForge codegen (not the index of the last bar); backtest would be silently wrong — rejected.",
|
|
151
192
|
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time.",
|
|
152
|
-
"time_close": "time_close is aliased to the bar OPEN timestamp in PineForge; it does not represent the bar close time; backtest would be silently wrong — rejected.",
|
|
153
193
|
}
|
|
154
194
|
|
|
155
195
|
# Subset of DIVERGENT_VARS escalated from WARNING to ERROR (see comment above).
|
|
156
|
-
DIVERGENT_VARS_ERROR: frozenset[str] = frozenset({"last_bar_index"
|
|
196
|
+
DIVERGENT_VARS_ERROR: frozenset[str] = frozenset({"last_bar_index"})
|
|
157
197
|
|
|
158
198
|
BARSTATE_APPROX_VARS: dict[str, str] = {
|
|
159
199
|
"barstate.islast": "barstate.islast is always false in PineForge batch backtests.",
|
|
@@ -215,10 +255,17 @@ NOT_YET_FUNC: dict[str, str] = {
|
|
|
215
255
|
}
|
|
216
256
|
|
|
217
257
|
# Bare (no-namespace) function names that codegen has no handler for.
|
|
218
|
-
# Without a handler, the generic emitter at visit_call.py:912 would
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
258
|
+
# Without a handler, the generic emitter at visit_call.py:912 would produce an
|
|
259
|
+
# undeclared C++ symbol. Reject loudly. (Currently empty — the bare color(...)
|
|
260
|
+
# cast moved to COSMETIC_BARE_FUNCS below.)
|
|
261
|
+
UNSUPPORTED_BARE_FUNCS: dict[str, str] = {}
|
|
262
|
+
|
|
263
|
+
# Bare (no-namespace) cosmetic casts with no backtest-logic effect. Warned
|
|
264
|
+
# (not rejected); codegen emits a benign default. ``color(x)`` is a Pine color
|
|
265
|
+
# cast — colors never affect trade logic, so codegen emits a default color
|
|
266
|
+
# (0 = na color) and the strategy keeps running.
|
|
267
|
+
COSMETIC_BARE_FUNCS: dict[str, str] = {
|
|
268
|
+
"color": "color(...) cast has no effect in PineForge backtests (visual only); it emits a default color.",
|
|
222
269
|
}
|
|
223
270
|
|
|
224
271
|
# Whole namespaces with NO codegen support. Any call into one of these
|
|
@@ -230,14 +277,19 @@ UNSUPPORTED_NAMESPACES: dict[str, str] = {
|
|
|
230
277
|
"volume_row": "volume_row.* is not supported in PineForge batch backtests; same reason as footprint.*.",
|
|
231
278
|
}
|
|
232
279
|
|
|
233
|
-
# Member-access references with no batch-mode equivalent.
|
|
234
|
-
#
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
280
|
+
# Member-access references with no batch-mode equivalent. Reject loudly.
|
|
281
|
+
# (Currently empty — the chart.* cosmetic reads moved to COSMETIC_MEMBERS.)
|
|
282
|
+
UNSUPPORTED_MEMBERS: dict[tuple[str, str], str] = {}
|
|
283
|
+
|
|
284
|
+
# Cosmetic / chart-only member reads with no backtest-logic effect. Warned
|
|
285
|
+
# (not rejected); codegen emits the benign default — chart.* falls through
|
|
286
|
+
# SKIP_NAMESPACES in visit_expr.py to ``0`` (and the analyzer types chart.* as
|
|
287
|
+
# COLOR, so the 0 lands in an int color slot and compiles cleanly).
|
|
288
|
+
COSMETIC_MEMBERS: dict[tuple[str, str], str] = {
|
|
289
|
+
("chart", "left_visible_bar_time"): "chart.left_visible_bar_time has no meaning in a batch backtest (no viewport); emits 0.",
|
|
290
|
+
("chart", "right_visible_bar_time"): "chart.right_visible_bar_time has no meaning in a batch backtest (no viewport); emits 0.",
|
|
291
|
+
("chart", "bg_color"): "chart.bg_color has no meaning in a batch backtest (no chart theme); emits a default color.",
|
|
292
|
+
("chart", "fg_color"): "chart.fg_color has no meaning in a batch backtest (no chart theme); emits a default color.",
|
|
241
293
|
}
|
|
242
294
|
|
|
243
295
|
# Constant-only namespaces whose members are drawing/visual style constants
|
|
@@ -396,6 +448,22 @@ class SupportChecker:
|
|
|
396
448
|
self._user_enums: set[str] = set()
|
|
397
449
|
self._user_funcs: set[str] = set()
|
|
398
450
|
self._user_methods: set[str] = set()
|
|
451
|
+
# Drawing lifecycle gap guard (spec L.3/U.7): only bare identifiers are
|
|
452
|
+
# promoted to Series<T> by analyzer/base.py::_visit_Subscript. A history
|
|
453
|
+
# read on a drawing handle nested in a UDT field (``p.a[1]``) or on a
|
|
454
|
+
# tuple element known to be a drawing handle (``la[1]`` after
|
|
455
|
+
# ``[la, lb] = makePair()``) would otherwise silently read the current
|
|
456
|
+
# bar. Track enough lightweight declaration information to reject those
|
|
457
|
+
# shapes before codegen.
|
|
458
|
+
self._udt_drawing_fields: dict[str, set[str]] = {}
|
|
459
|
+
self._var_udt_types: dict[str, str] = {}
|
|
460
|
+
# Names (vars and function params) declared as a scalar visual-container
|
|
461
|
+
# type (table/box/line/label/linefill). A method call on one of these
|
|
462
|
+
# (``panel.cell(...)``) is a visual sink whose args may carry visual
|
|
463
|
+
# constants, so it routes through ``_visit_children_const_ok``.
|
|
464
|
+
self._visual_container_vars: set[str] = set()
|
|
465
|
+
self._drawing_tuple_vars: set[str] = set()
|
|
466
|
+
self._func_tuple_drawing_returns: dict[str, list[bool]] = {}
|
|
399
467
|
# Track whether we are inside an if/ternary condition expression.
|
|
400
468
|
self._in_conditional_depth: int = 0
|
|
401
469
|
# Track whether we are inside an argument subtree that legitimately
|
|
@@ -410,6 +478,12 @@ class SupportChecker:
|
|
|
410
478
|
# bare ``time_close`` variable) must NOT be flagged as a divergent
|
|
411
479
|
# variable read. Populated as _visit_FuncCall descends into children.
|
|
412
480
|
self._callee_node_ids: set[int] = set()
|
|
481
|
+
# Declaration value of each scalar variable (``name = <expr>``), so the
|
|
482
|
+
# request.security symbol check can resolve an aliased symbol through
|
|
483
|
+
# its definition — e.g. ``haTicker = ticker.heikinashi(syminfo.tickerid)``
|
|
484
|
+
# then ``request.security(haTicker, ...)``, or ``reqSym = cond ? other :
|
|
485
|
+
# syminfo.tickerid``. First binding wins (closest to declaration).
|
|
486
|
+
self._scalar_defs: dict[str, ASTNode] = {}
|
|
413
487
|
|
|
414
488
|
# -- Public API --
|
|
415
489
|
|
|
@@ -431,12 +505,83 @@ class SupportChecker:
|
|
|
431
505
|
for stmt in ast.body:
|
|
432
506
|
if isinstance(stmt, TypeDecl):
|
|
433
507
|
self._user_types.add(stmt.name)
|
|
508
|
+
drawing_fields = {
|
|
509
|
+
field.name
|
|
510
|
+
for field in stmt.fields
|
|
511
|
+
if self._type_name_contains_drawing(field.type_name)
|
|
512
|
+
}
|
|
513
|
+
if drawing_fields:
|
|
514
|
+
self._udt_drawing_fields[stmt.name] = drawing_fields
|
|
434
515
|
elif isinstance(stmt, EnumDecl):
|
|
435
516
|
self._user_enums.add(stmt.name)
|
|
436
517
|
elif isinstance(stmt, FuncDef):
|
|
437
518
|
self._user_funcs.add(stmt.name)
|
|
519
|
+
tuple_returns = self._single_expr_tuple_drawing_mask(stmt)
|
|
520
|
+
if tuple_returns:
|
|
521
|
+
self._func_tuple_drawing_returns[stmt.name] = tuple_returns
|
|
522
|
+
self._collect_visual_container_params(stmt)
|
|
438
523
|
elif isinstance(stmt, MethodDef):
|
|
439
524
|
self._user_methods.add(stmt.name)
|
|
525
|
+
self._collect_visual_container_params(stmt)
|
|
526
|
+
|
|
527
|
+
def _collect_visual_container_params(self, fn) -> None:
|
|
528
|
+
"""Register a function's parameters that are declared with a scalar
|
|
529
|
+
visual-container type (``table panel``, ``line ln``) so method calls on
|
|
530
|
+
them inside the body (``panel.cell(...)``) are treated as visual sinks.
|
|
531
|
+
"""
|
|
532
|
+
hints = (getattr(fn, "annotations", None) or {}).get("param_type_hints") or []
|
|
533
|
+
for i, pname in enumerate(getattr(fn, "params", []) or []):
|
|
534
|
+
hint = hints[i] if i < len(hints) else None
|
|
535
|
+
if hint and str(hint).replace(" ", "") in _VISUAL_CONTAINER_TYPES:
|
|
536
|
+
self._visual_container_vars.add(pname)
|
|
537
|
+
|
|
538
|
+
@staticmethod
|
|
539
|
+
def _type_name_contains_drawing(type_name: str | None) -> bool:
|
|
540
|
+
if not type_name:
|
|
541
|
+
return False
|
|
542
|
+
compact = str(type_name).replace(" ", "")
|
|
543
|
+
if compact.endswith("[]"):
|
|
544
|
+
compact = f"array<{compact[:-2]}>"
|
|
545
|
+
if compact in _DRAWING_TYPE_NAMES:
|
|
546
|
+
return True
|
|
547
|
+
return any(f"<{drawing}>" in compact for drawing in _DRAWING_TYPE_NAMES)
|
|
548
|
+
|
|
549
|
+
@staticmethod
|
|
550
|
+
def _is_literal_zero_subscript(node: Subscript) -> bool:
|
|
551
|
+
return isinstance(node.index, NumberLiteral) and node.index.value == 0
|
|
552
|
+
|
|
553
|
+
def _expr_is_drawing_handle(self, expr: ASTNode | None) -> bool:
|
|
554
|
+
if isinstance(expr, FuncCall):
|
|
555
|
+
ns, name = _qualified_name(expr.callee)
|
|
556
|
+
if ns in _DRAWING_TYPE_NAMES and name in {"new", "copy"}:
|
|
557
|
+
return True
|
|
558
|
+
if ns == "linefill" and name in {"get_line1", "get_line2"}:
|
|
559
|
+
return True
|
|
560
|
+
if isinstance(expr, Identifier):
|
|
561
|
+
return expr.name in self._drawing_tuple_vars
|
|
562
|
+
if isinstance(expr, Ternary):
|
|
563
|
+
return self._expr_is_drawing_handle(expr.true_val) or self._expr_is_drawing_handle(expr.false_val)
|
|
564
|
+
return False
|
|
565
|
+
|
|
566
|
+
def _single_expr_tuple_drawing_mask(self, fn: FuncDef) -> list[bool] | None:
|
|
567
|
+
if not fn.is_single_expr or len(fn.body) != 1:
|
|
568
|
+
return None
|
|
569
|
+
stmt = fn.body[0]
|
|
570
|
+
expr = stmt.expr if isinstance(stmt, ExprStmt) else None
|
|
571
|
+
if not isinstance(expr, TupleLiteral):
|
|
572
|
+
return None
|
|
573
|
+
mask = [self._expr_is_drawing_handle(item) for item in expr.elements]
|
|
574
|
+
return mask if any(mask) else None
|
|
575
|
+
|
|
576
|
+
def _tuple_assign_drawing_mask(self, value: ASTNode | None) -> list[bool] | None:
|
|
577
|
+
if isinstance(value, TupleLiteral):
|
|
578
|
+
mask = [self._expr_is_drawing_handle(item) for item in value.elements]
|
|
579
|
+
return mask if any(mask) else None
|
|
580
|
+
if isinstance(value, FuncCall):
|
|
581
|
+
ns, name = _qualified_name(value.callee)
|
|
582
|
+
if ns is None and name in self._func_tuple_drawing_returns:
|
|
583
|
+
return self._func_tuple_drawing_returns[name]
|
|
584
|
+
return None
|
|
440
585
|
|
|
441
586
|
# -- Diagnostic emission --
|
|
442
587
|
|
|
@@ -624,6 +769,24 @@ class SupportChecker:
|
|
|
624
769
|
self._visit_children_const_ok(node)
|
|
625
770
|
|
|
626
771
|
def _visit_VarDecl(self, node: VarDecl) -> None:
|
|
772
|
+
if node.name and node.value is not None:
|
|
773
|
+
self._scalar_defs.setdefault(node.name, node.value)
|
|
774
|
+
if node.type_hint in self._user_types:
|
|
775
|
+
self._var_udt_types[node.name] = node.type_hint
|
|
776
|
+
elif isinstance(node.value, FuncCall):
|
|
777
|
+
ns, name = _qualified_name(node.value.callee)
|
|
778
|
+
if name == "new" and ns in self._user_types:
|
|
779
|
+
self._var_udt_types[node.name] = ns
|
|
780
|
+
# Track scalar visual-container vars (``var table dash = table.new(...)``)
|
|
781
|
+
# so a direct ``dash.cell(..., text.align_left)`` is treated as a visual
|
|
782
|
+
# sink (mirrors the table/box/line/label/linefill PARAM tracking).
|
|
783
|
+
decl_hint = str(node.type_hint).replace(" ", "") if node.type_hint else None
|
|
784
|
+
if decl_hint in _VISUAL_CONTAINER_TYPES:
|
|
785
|
+
self._visual_container_vars.add(node.name)
|
|
786
|
+
elif isinstance(node.value, FuncCall):
|
|
787
|
+
vns, vname = _qualified_name(node.value.callee)
|
|
788
|
+
if vname == "new" and vns in _VISUAL_CONTAINER_TYPES:
|
|
789
|
+
self._visual_container_vars.add(node.name)
|
|
627
790
|
if node.is_varip:
|
|
628
791
|
self._err(
|
|
629
792
|
node,
|
|
@@ -640,6 +803,11 @@ class SupportChecker:
|
|
|
640
803
|
self._visit_children(node)
|
|
641
804
|
|
|
642
805
|
def _visit_TupleAssign(self, node: TupleAssign) -> None:
|
|
806
|
+
drawing_mask = self._tuple_assign_drawing_mask(node.value)
|
|
807
|
+
if drawing_mask:
|
|
808
|
+
for idx, name in enumerate(node.names):
|
|
809
|
+
if idx < len(drawing_mask) and drawing_mask[idx]:
|
|
810
|
+
self._drawing_tuple_vars.add(name)
|
|
643
811
|
if isinstance(node.value, FuncCall):
|
|
644
812
|
ns, name = _qualified_name(node.value.callee)
|
|
645
813
|
if ns == "ta" and name == "stoch":
|
|
@@ -650,6 +818,40 @@ class SupportChecker:
|
|
|
650
818
|
)
|
|
651
819
|
self._visit_children(node)
|
|
652
820
|
|
|
821
|
+
def _visit_Subscript(self, node: Subscript) -> None:
|
|
822
|
+
# Best-effort defensive guard for the silent-wrong shapes called out in
|
|
823
|
+
# spec L.3/U.7. This is not a full expression type checker; it catches
|
|
824
|
+
# the known dangerous surface (UDT vars with drawing fields, and tuple
|
|
825
|
+
# destructured drawing handles) without trying to prove every nested or
|
|
826
|
+
# parameter-passed UDT expression. Literal [0] is current-bar in Pine and
|
|
827
|
+
# safe to leave to codegen's existing current-value behavior.
|
|
828
|
+
if self._is_literal_zero_subscript(node):
|
|
829
|
+
self._visit_children(node)
|
|
830
|
+
return
|
|
831
|
+
if isinstance(node.object, MemberAccess) and isinstance(node.object.object, Identifier):
|
|
832
|
+
var_name = node.object.object.name
|
|
833
|
+
udt_name = self._var_udt_types.get(var_name)
|
|
834
|
+
if udt_name is not None and node.object.member in self._udt_drawing_fields.get(udt_name, set()):
|
|
835
|
+
self._err(
|
|
836
|
+
node,
|
|
837
|
+
"History references on UDT drawing fields are not supported in PineForge.",
|
|
838
|
+
hint=(
|
|
839
|
+
f"{var_name}.{node.object.member}[...] would be read as the current-bar field, "
|
|
840
|
+
"not prior-bar history. Copy the field to a bare drawing variable first, "
|
|
841
|
+
"or remove the history reference."
|
|
842
|
+
),
|
|
843
|
+
)
|
|
844
|
+
elif isinstance(node.object, Identifier) and node.object.name in self._drawing_tuple_vars:
|
|
845
|
+
self._err(
|
|
846
|
+
node,
|
|
847
|
+
"History references on tuple-destructured drawing handles are not supported in PineForge.",
|
|
848
|
+
hint=(
|
|
849
|
+
f"{node.object.name}[...] is a drawing handle from tuple destructuring; "
|
|
850
|
+
"PineForge cannot historize that tuple element safely."
|
|
851
|
+
),
|
|
852
|
+
)
|
|
853
|
+
self._visit_children(node)
|
|
854
|
+
|
|
653
855
|
def _visit_FuncCall(self, node: FuncCall) -> None:
|
|
654
856
|
ns, name = _qualified_name(node.callee)
|
|
655
857
|
|
|
@@ -671,6 +873,25 @@ class SupportChecker:
|
|
|
671
873
|
self._visit_children(node)
|
|
672
874
|
return
|
|
673
875
|
|
|
876
|
+
# Heikin-Ashi: supported for the chart's OWN symbol only. The engine
|
|
877
|
+
# applies the HA candle transform inside the same-symbol security eval
|
|
878
|
+
# (request.security(ticker.heikinashi(syminfo.tickerid), ...)); a
|
|
879
|
+
# cross-symbol HA construction (an alternate symbol's candles) is not.
|
|
880
|
+
if full == "ticker.heikinashi":
|
|
881
|
+
arg0 = node.args[0] if node.args else node.kwargs.get("symbol")
|
|
882
|
+
if arg0 is None or not self._is_current_symbol_expr(arg0):
|
|
883
|
+
self._err(
|
|
884
|
+
node,
|
|
885
|
+
"ticker.heikinashi(...) is supported only for the chart's own symbol.",
|
|
886
|
+
hint=(
|
|
887
|
+
"Pass syminfo.tickerid: PineForge applies the Heikin-Ashi candle "
|
|
888
|
+
"transform to the chart symbol inside request.security, but cannot "
|
|
889
|
+
"load an alternate symbol's HA candles."
|
|
890
|
+
),
|
|
891
|
+
)
|
|
892
|
+
self._visit_children(node)
|
|
893
|
+
return
|
|
894
|
+
|
|
674
895
|
# Hard rejects by namespace (e.g. ticker.*).
|
|
675
896
|
if ns is not None and ns in HARD_REJECT_NAMESPACE:
|
|
676
897
|
self._err(
|
|
@@ -692,9 +913,15 @@ class SupportChecker:
|
|
|
692
913
|
self._visit_children(node)
|
|
693
914
|
return
|
|
694
915
|
|
|
695
|
-
# Bare
|
|
696
|
-
#
|
|
697
|
-
|
|
916
|
+
# Bare cosmetic casts (e.g. `color(na)` -> default color). No backtest
|
|
917
|
+
# effect; warn and let codegen emit a benign default color.
|
|
918
|
+
if ns is None and name in COSMETIC_BARE_FUNCS:
|
|
919
|
+
self._warn(node, COSMETIC_BARE_FUNCS[name])
|
|
920
|
+
self._visit_children(node)
|
|
921
|
+
return
|
|
922
|
+
|
|
923
|
+
# Bare-function rejections. Codegen would otherwise fall through to the
|
|
924
|
+
# generic emit at visit_call.py:912 and produce an undeclared C++ symbol.
|
|
698
925
|
if ns is None and self._reject_if_in(
|
|
699
926
|
UNSUPPORTED_BARE_FUNCS,
|
|
700
927
|
name,
|
|
@@ -865,6 +1092,16 @@ class SupportChecker:
|
|
|
865
1092
|
self._err(node, f"timeframe.{name}(...) is not implemented in PineForge runtime.")
|
|
866
1093
|
self._visit_children(node)
|
|
867
1094
|
return
|
|
1095
|
+
if ns == "color" and name in COSMETIC_COLOR_FUNC:
|
|
1096
|
+
# Cosmetic color builder (e.g. color.from_gradient): no backtest
|
|
1097
|
+
# effect. Warn and let codegen emit a default color (0).
|
|
1098
|
+
self._warn(
|
|
1099
|
+
node,
|
|
1100
|
+
f"color.{name}(...) has no effect in PineForge backtests "
|
|
1101
|
+
f"(visual only); it emits a default color.",
|
|
1102
|
+
)
|
|
1103
|
+
self._visit_children(node)
|
|
1104
|
+
return
|
|
868
1105
|
if ns == "color" and name not in SUPPORTED_COLOR_FUNC:
|
|
869
1106
|
self._err(node, f"color.{name}(...) is not implemented in PineForge runtime.")
|
|
870
1107
|
self._visit_children(node)
|
|
@@ -911,6 +1148,34 @@ class SupportChecker:
|
|
|
911
1148
|
self._visit_children(node)
|
|
912
1149
|
return
|
|
913
1150
|
|
|
1151
|
+
# Drawing-objects-as-data (line/box/label/linefill). Geometry methods
|
|
1152
|
+
# are REAL data; *_NOOP visual setters are accepted no-ops (warned);
|
|
1153
|
+
# any OTHER drawing method rejects loudly. Args carry constant-namespace
|
|
1154
|
+
# members (xloc.*, yloc.*, text.align_*, style consts), so children are
|
|
1155
|
+
# visited with const reads allowed. NOTE: user methods on a drawing var
|
|
1156
|
+
# (egoigor ``ln.slope()``) have ns="ln" (the receiver), NOT a DRAWING_NS,
|
|
1157
|
+
# so they are never gated here.
|
|
1158
|
+
if ns in _DRAWING_SUPPORTED_BY_NS:
|
|
1159
|
+
if name not in _DRAWING_SUPPORTED_BY_NS[ns]:
|
|
1160
|
+
self._err(node, f"{ns}.{name}(...) is not implemented in PineForge runtime.")
|
|
1161
|
+
self._visit_children(node)
|
|
1162
|
+
return
|
|
1163
|
+
if name in _DRAWING_NOOP_BY_NS[ns]:
|
|
1164
|
+
self._warn(
|
|
1165
|
+
node,
|
|
1166
|
+
f"{ns}.{name}(...) is a visual no-op in PineForge backtests "
|
|
1167
|
+
f"(drawing geometry is data; color/style/size are dropped).",
|
|
1168
|
+
)
|
|
1169
|
+
self._visit_children_const_ok(node)
|
|
1170
|
+
return
|
|
1171
|
+
if ns == "chart.point":
|
|
1172
|
+
if name not in SUPPORTED_CHART_POINT:
|
|
1173
|
+
self._err(node, f"chart.point.{name}(...) is not implemented in PineForge runtime.")
|
|
1174
|
+
self._visit_children(node)
|
|
1175
|
+
return
|
|
1176
|
+
self._visit_children_const_ok(node)
|
|
1177
|
+
return
|
|
1178
|
+
|
|
914
1179
|
# Drawing / charting / alert namespaces — codegen drops silently. Warn,
|
|
915
1180
|
# don't error: many strategies include these for the TradingView UI.
|
|
916
1181
|
# Their argument subtrees legitimately carry constant-namespace
|
|
@@ -931,6 +1196,17 @@ class SupportChecker:
|
|
|
931
1196
|
self._visit_children_const_ok(node)
|
|
932
1197
|
return
|
|
933
1198
|
|
|
1199
|
+
# Method call on a scalar visual-container receiver (``panel.cell(...)``
|
|
1200
|
+
# where ``panel`` is a ``table`` param, ``ln.set_xy1(...)`` on a ``line``
|
|
1201
|
+
# var). ``ns`` is the receiver variable/param name, not a namespace, so
|
|
1202
|
+
# the namespace-form drawing/table branches above did not fire. These are
|
|
1203
|
+
# visual sinks whose args legitimately carry visual constants
|
|
1204
|
+
# (``text.align_*``, ``size.*``), so visit children with const reads
|
|
1205
|
+
# allowed instead of tripping the free-expression const-namespace reject.
|
|
1206
|
+
if ns is not None and ns in self._visual_container_vars:
|
|
1207
|
+
self._visit_children_const_ok(node)
|
|
1208
|
+
return
|
|
1209
|
+
|
|
934
1210
|
self._visit_children(node)
|
|
935
1211
|
|
|
936
1212
|
def _visit_Identifier(self, node: Identifier) -> None:
|
|
@@ -1001,7 +1277,21 @@ class SupportChecker:
|
|
|
1001
1277
|
lambda k, v: f"{k}.{node.member}: {v}",
|
|
1002
1278
|
):
|
|
1003
1279
|
return
|
|
1004
|
-
#
|
|
1280
|
+
# Cosmetic / chart-only member reads (chart.fg_color/bg_color/visible
|
|
1281
|
+
# bar times). No backtest-logic effect; warn and let codegen emit the
|
|
1282
|
+
# benign default (chart.* -> 0 via SKIP_NAMESPACES in visit_expr.py).
|
|
1283
|
+
if (
|
|
1284
|
+
isinstance(node.object, Identifier)
|
|
1285
|
+
and (node.object.name, node.member) in COSMETIC_MEMBERS
|
|
1286
|
+
):
|
|
1287
|
+
self._warn(
|
|
1288
|
+
node,
|
|
1289
|
+
f"{node.object.name}.{node.member}: "
|
|
1290
|
+
f"{COSMETIC_MEMBERS[(node.object.name, node.member)]}",
|
|
1291
|
+
)
|
|
1292
|
+
self._visit_children(node)
|
|
1293
|
+
return
|
|
1294
|
+
# Specific unsupported (namespace, member) pairs.
|
|
1005
1295
|
if isinstance(node.object, Identifier) and self._reject_if_in(
|
|
1006
1296
|
UNSUPPORTED_MEMBERS,
|
|
1007
1297
|
(node.object.name, node.member),
|
|
@@ -1134,10 +1424,25 @@ class SupportChecker:
|
|
|
1134
1424
|
hint="Codegen only recognizes the barmerge.lookahead_* literal; other values are silently treated as lookahead_off.",
|
|
1135
1425
|
)
|
|
1136
1426
|
if self._is_barmerge_member(lookahead_node, "lookahead_on"):
|
|
1137
|
-
|
|
1427
|
+
# lookahead_on is supported by codegen + engine: base.py forwards
|
|
1428
|
+
# the flag into _security_eval_info, emit_top.py registers it via
|
|
1429
|
+
# register_security_eval(..., lookahead_on=true), and the engine
|
|
1430
|
+
# (engine_security.cpp) dispatches the partial HTF eval and gates
|
|
1431
|
+
# the per-bucket series slot on it. The completed-HTF value becomes
|
|
1432
|
+
# visible from the HTF bucket's first chart bar (TV's forward-look).
|
|
1433
|
+
# We emit a WARNING (not ERROR) because lookahead_on is inherently
|
|
1434
|
+
# data-sensitive: with a 0-offset HTF expression it exposes the
|
|
1435
|
+
# in-progress bucket's value (TV's documented repaint), whereas the
|
|
1436
|
+
# safe non-repainting idiom pairs it with a [1] offset
|
|
1437
|
+
# (e.g. close[1]) to read only completed prior buckets.
|
|
1438
|
+
self._warn(
|
|
1138
1439
|
lookahead_node,
|
|
1139
|
-
"request.security lookahead_on
|
|
1140
|
-
|
|
1440
|
+
"request.security lookahead_on changes HTF timing: the completed "
|
|
1441
|
+
"higher-timeframe value is exposed from the bucket's first chart "
|
|
1442
|
+
"bar. PineForge emits this (engine-supported) but results differ "
|
|
1443
|
+
"from lookahead_off and, with a 0-offset HTF expression, repaint "
|
|
1444
|
+
"future data.",
|
|
1445
|
+
hint="The safe non-repainting form pairs lookahead_on with a [1] offset (e.g. close[1]) to read only completed prior HTF bars.",
|
|
1141
1446
|
)
|
|
1142
1447
|
|
|
1143
1448
|
# Data-adjustment kwargs: codegen emits a numeric constant but the
|
|
@@ -1178,19 +1483,38 @@ class SupportChecker:
|
|
|
1178
1483
|
return False
|
|
1179
1484
|
return node.member in allowed
|
|
1180
1485
|
|
|
1181
|
-
def _is_current_symbol_expr(self, node: ASTNode) -> bool:
|
|
1486
|
+
def _is_current_symbol_expr(self, node: ASTNode, _seen: set[str] | None = None) -> bool:
|
|
1487
|
+
if _seen is None:
|
|
1488
|
+
_seen = set()
|
|
1182
1489
|
chain = _resolve_member_chain(node)
|
|
1183
1490
|
if chain in SECURITY_CURRENT_SYMBOL_NAMES:
|
|
1184
1491
|
return True
|
|
1185
|
-
# ticker.inherit(symbol, ...)
|
|
1186
|
-
#
|
|
1492
|
+
# ticker.inherit(symbol, ...) / ticker.standard(symbol) are passthrough,
|
|
1493
|
+
# and ticker.heikinashi(symbol) is the chart's OWN symbol with a causal
|
|
1494
|
+
# Heikin-Ashi candle transform (the engine applies it inside the security
|
|
1495
|
+
# eval via register_security_eval's heikinashi flag — no alternate symbol
|
|
1496
|
+
# is loaded). All allow a current-symbol first argument; a non-current
|
|
1497
|
+
# arg is genuine cross-symbol construction and still falls through.
|
|
1187
1498
|
if isinstance(node, FuncCall):
|
|
1188
1499
|
ns, fname = _qualified_name(node.callee)
|
|
1189
|
-
if ns == "ticker" and fname in ("inherit", "standard"):
|
|
1190
|
-
if node.args and self._is_current_symbol_expr(node.args[0]):
|
|
1500
|
+
if ns == "ticker" and fname in ("inherit", "standard", "heikinashi"):
|
|
1501
|
+
if node.args and self._is_current_symbol_expr(node.args[0], _seen):
|
|
1191
1502
|
return True
|
|
1192
|
-
if "symbol" in node.kwargs and self._is_current_symbol_expr(node.kwargs["symbol"]):
|
|
1503
|
+
if "symbol" in node.kwargs and self._is_current_symbol_expr(node.kwargs["symbol"], _seen):
|
|
1193
1504
|
return True
|
|
1505
|
+
# A ternary symbol (``cond ? other : syminfo.tickerid``) resolves to the
|
|
1506
|
+
# chart symbol whenever EITHER branch does. PineForge only ever loads the
|
|
1507
|
+
# chart symbol, so this is exact at any config where the chosen branch is
|
|
1508
|
+
# the chart symbol; an unconditional cross-symbol expression still errors.
|
|
1509
|
+
if isinstance(node, Ternary):
|
|
1510
|
+
return (self._is_current_symbol_expr(node.true_val, _seen)
|
|
1511
|
+
or self._is_current_symbol_expr(node.false_val, _seen))
|
|
1512
|
+
# Def-use: resolve a bare identifier through its declaration value so an
|
|
1513
|
+
# aliased symbol is accepted (``haTicker = ticker.heikinashi(...)`` then
|
|
1514
|
+
# ``request.security(haTicker, ...)``). Name-cycle-guarded.
|
|
1515
|
+
if isinstance(node, Identifier) and node.name in self._scalar_defs and node.name not in _seen:
|
|
1516
|
+
_seen.add(node.name)
|
|
1517
|
+
return self._is_current_symbol_expr(self._scalar_defs[node.name], _seen)
|
|
1194
1518
|
return False
|
|
1195
1519
|
|
|
1196
1520
|
# -- Pine timeframe-literal validation --
|
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codegen": "0.
|
|
2
|
+
"codegen": "0.8.1",
|
|
3
3
|
"pyodide": "314.0.0",
|
|
4
4
|
"python": "3.14.0",
|
|
5
5
|
"emscripten": "emscripten_5_0_3",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "85ac44383967dd8c9825a276661eab568b16ecb89fcb929a3b9b638b6a4f371c"
|
|
7
7
|
}
|
package/tables.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"CODEGEN_VERSION": "0.
|
|
2
|
+
"CODEGEN_VERSION": "0.8.1",
|
|
3
3
|
"TA_CLASS_MAP_KEYS": [
|
|
4
4
|
"sma",
|
|
5
5
|
"ema",
|
|
@@ -321,12 +321,8 @@
|
|
|
321
321
|
"plotshape"
|
|
322
322
|
],
|
|
323
323
|
"SKIP_NAMESPACES": [
|
|
324
|
-
"box",
|
|
325
324
|
"chart",
|
|
326
325
|
"display",
|
|
327
|
-
"label",
|
|
328
|
-
"line",
|
|
329
|
-
"linefill",
|
|
330
326
|
"polyline",
|
|
331
327
|
"position",
|
|
332
328
|
"size",
|
|
@@ -1438,8 +1434,6 @@
|
|
|
1438
1434
|
"request.seed": "External seed data feeds not available in PineForge.",
|
|
1439
1435
|
"request.quandl": "External Quandl data not available in PineForge.",
|
|
1440
1436
|
"request.currency_rate": "Currency conversion data not available in PineForge.",
|
|
1441
|
-
"color.from_gradient": "Charting helpers not available in PineForge backtests.",
|
|
1442
|
-
"ticker.heikinashi": "ticker.heikinashi() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
1443
1437
|
"ticker.renko": "ticker.renko() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
1444
1438
|
"ticker.kagi": "ticker.kagi() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
1445
1439
|
"ticker.linebreak": "ticker.linebreak() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
@@ -1454,8 +1448,7 @@
|
|
|
1454
1448
|
"DIVERGENT_VARS": {
|
|
1455
1449
|
"bar_index": "bar_index depends on the data window; PineForge and TradingView produce different values for the same script.",
|
|
1456
1450
|
"last_bar_index": "last_bar_index is aliased to the CURRENT bar index in PineForge codegen (not the index of the last bar); backtest would be silently wrong — rejected.",
|
|
1457
|
-
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time."
|
|
1458
|
-
"time_close": "time_close is aliased to the bar OPEN timestamp in PineForge; it does not represent the bar close time; backtest would be silently wrong — rejected."
|
|
1451
|
+
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time."
|
|
1459
1452
|
},
|
|
1460
1453
|
"BARSTATE_APPROX_VARS": {
|
|
1461
1454
|
"barstate.islast": "barstate.islast is always false in PineForge batch backtests.",
|
|
@@ -1541,9 +1534,7 @@
|
|
|
1541
1534
|
"trail_points",
|
|
1542
1535
|
"trail_price"
|
|
1543
1536
|
],
|
|
1544
|
-
"UNSUPPORTED_BARE_FUNCS": {
|
|
1545
|
-
"color": "Bare color(...) cast is not supported. Use color.new(c, alpha) or color.rgb(r, g, b, transp)."
|
|
1546
|
-
},
|
|
1537
|
+
"UNSUPPORTED_BARE_FUNCS": {},
|
|
1547
1538
|
"UNSUPPORTED_NAMESPACES": {
|
|
1548
1539
|
"footprint": "footprint.* (bid/ask volume rows) is not supported in PineForge batch backtests; it requires tick-level data the engine does not consume.",
|
|
1549
1540
|
"volume_row": "volume_row.* is not supported in PineForge batch backtests; same reason as footprint.*."
|
|
@@ -1562,12 +1553,7 @@
|
|
|
1562
1553
|
"barmerge": "is only valid as the gaps/lookahead argument of request.security(...); it has no runtime value as a free expression in PineForge.",
|
|
1563
1554
|
"alert": "is only valid as the freq argument of alert(...); it has no runtime value as a free expression in PineForge."
|
|
1564
1555
|
},
|
|
1565
|
-
"UNSUPPORTED_MEMBERS": {
|
|
1566
|
-
"chart.left_visible_bar_time": "chart.left_visible_bar_time has no meaning in a batch backtest (no viewport).",
|
|
1567
|
-
"chart.right_visible_bar_time": "chart.right_visible_bar_time has no meaning in a batch backtest (no viewport).",
|
|
1568
|
-
"chart.bg_color": "chart.bg_color has no meaning in a batch backtest (no chart theme).",
|
|
1569
|
-
"chart.fg_color": "chart.fg_color has no meaning in a batch backtest (no chart theme)."
|
|
1570
|
-
},
|
|
1556
|
+
"UNSUPPORTED_MEMBERS": {},
|
|
1571
1557
|
"UNSUPPORTED_NAMESPACE_VARS": {
|
|
1572
1558
|
"dividends": "dividends.* is not available in PineForge — fundamental dividend data is not loaded.",
|
|
1573
1559
|
"earnings": "earnings.* is not available in PineForge — fundamental earnings data is not loaded.",
|
|
Binary file
|