@pineforge/codegen-pyodide 0.7.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/README.md +25 -0
- package/index.mjs +20 -0
- package/package.json +32 -0
- package/pineforge_codegen/__init__.py +53 -0
- package/pineforge_codegen/analyzer/__init__.py +60 -0
- package/pineforge_codegen/analyzer/base.py +1566 -0
- package/pineforge_codegen/analyzer/call_handlers.py +895 -0
- package/pineforge_codegen/analyzer/contracts.py +163 -0
- package/pineforge_codegen/analyzer/diagnostics.py +118 -0
- package/pineforge_codegen/analyzer/tables.py +204 -0
- package/pineforge_codegen/analyzer/types.py +261 -0
- package/pineforge_codegen/ast_nodes.py +293 -0
- package/pineforge_codegen/codegen/__init__.py +78 -0
- package/pineforge_codegen/codegen/base.py +1381 -0
- package/pineforge_codegen/codegen/emit_top.py +875 -0
- package/pineforge_codegen/codegen/helpers.py +163 -0
- package/pineforge_codegen/codegen/helpers_syminfo.py +132 -0
- package/pineforge_codegen/codegen/input.py +189 -0
- package/pineforge_codegen/codegen/security.py +1564 -0
- package/pineforge_codegen/codegen/ta.py +298 -0
- package/pineforge_codegen/codegen/tables.py +683 -0
- package/pineforge_codegen/codegen/types.py +592 -0
- package/pineforge_codegen/codegen/visit_call.py +1387 -0
- package/pineforge_codegen/codegen/visit_expr.py +729 -0
- package/pineforge_codegen/codegen/visit_stmt.py +766 -0
- package/pineforge_codegen/errors.py +98 -0
- package/pineforge_codegen/lexer.py +531 -0
- package/pineforge_codegen/parser.py +1198 -0
- package/pineforge_codegen/pragmas.py +117 -0
- package/pineforge_codegen/signatures.py +808 -0
- package/pineforge_codegen/support_checker.py +1223 -0
- package/pineforge_codegen/symbols.py +118 -0
- package/pineforge_codegen/tokens.py +406 -0
- package/pineforge_codegen/tv_input_choices.py +86 -0
- package/pineforge_codegen-0.7.0.tar.gz +0 -0
- package/release.json +7 -0
- package/tables.json +1653 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Pure-utility codegen helpers.
|
|
2
|
+
|
|
3
|
+
Mixin holding stateless / near-stateless name-mangling and AST-walk
|
|
4
|
+
helpers used everywhere in the codegen. Lives here so the heavier
|
|
5
|
+
visitor / emitter mixins can depend on it without owning its
|
|
6
|
+
implementation. Keep this module free of imports from any other
|
|
7
|
+
``codegen/*`` submodule so it stays at the bottom of the dependency
|
|
8
|
+
graph.
|
|
9
|
+
|
|
10
|
+
Mixin contract: ``NamingHelper`` reads at most one piece of host state,
|
|
11
|
+
``self._all_member_names`` (used by ``_func_safe_name``). The class
|
|
12
|
+
mixing this in (``CodeGen``) sets that attribute in its constructor.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from ..ast_nodes import (
|
|
18
|
+
Identifier, MemberAccess,
|
|
19
|
+
VarDecl, Assignment, TupleAssign, ForStmt, ForInStmt,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# C++ reserved names that conflict with PineScript identifiers when used
|
|
24
|
+
# verbatim as variable names. Carried forward from the historic codegen.py
|
|
25
|
+
# table; intentionally narrower than the full C++ keyword set because Pine
|
|
26
|
+
# already reserves keywords like ``if``/``else``/``for``/``return`` so they
|
|
27
|
+
# can never reach codegen as identifier strings. Lives here (not base.py)
|
|
28
|
+
# so the mixin can read it without forcing an import cycle on ``base``.
|
|
29
|
+
CPP_RESERVED = {
|
|
30
|
+
"exp", "log", "abs", "max", "min", "and", "or", "not",
|
|
31
|
+
"int", "float", "bool", "string", "short", "long", "new", "delete",
|
|
32
|
+
"class", "struct", "return", "void", "auto", "const", "static",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NamingHelper:
|
|
37
|
+
"""Identifier escaping, callee resolution, and a generic AST walker.
|
|
38
|
+
|
|
39
|
+
Mixed into ``CodeGen``; not meant to be instantiated standalone.
|
|
40
|
+
Methods that need shared state (``_all_member_names``) document the
|
|
41
|
+
contract explicitly so substitution is safe."""
|
|
42
|
+
|
|
43
|
+
# Set by CodeGen.__init__; declared here only as documentation of the
|
|
44
|
+
# mixin contract. Stays a plain set; the host class owns the value.
|
|
45
|
+
_all_member_names: set[str]
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _cpp_string_escape(s: str) -> str:
|
|
49
|
+
"""Escape a Python string for embedding inside a C++ string literal."""
|
|
50
|
+
return (
|
|
51
|
+
s.replace("\\", "\\\\")
|
|
52
|
+
.replace('"', '\\"')
|
|
53
|
+
.replace("\n", "\\n")
|
|
54
|
+
.replace("\r", "\\r")
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _safe_name(self, name: str) -> str:
|
|
58
|
+
"""Rename identifiers that collide with C++ reserved words."""
|
|
59
|
+
if name in CPP_RESERVED:
|
|
60
|
+
return f"_{name}_"
|
|
61
|
+
return name
|
|
62
|
+
|
|
63
|
+
def _func_safe_name(self, name: str) -> str:
|
|
64
|
+
"""Prefix function names that collide with class members (series vars or var members)."""
|
|
65
|
+
safe = self._safe_name(name)
|
|
66
|
+
if safe in self._all_member_names:
|
|
67
|
+
return f"_fn_{safe}"
|
|
68
|
+
return safe
|
|
69
|
+
|
|
70
|
+
def _resolve_callee(self, callee) -> tuple[str | None, str | None]:
|
|
71
|
+
"""Extract ``(func_name, namespace)`` from a callee expression.
|
|
72
|
+
|
|
73
|
+
- ``foo()`` -> ``("foo", None)``
|
|
74
|
+
- ``ns.foo()`` -> ``("foo", "ns")``
|
|
75
|
+
- ``strategy.risk.max_orders(...)`` -> ``("max_orders", "strategy")``
|
|
76
|
+
(only the outermost root namespace is reported; nested chains
|
|
77
|
+
collapse to the leftmost identifier — historical behavior).
|
|
78
|
+
- anything else -> ``(None, None)``"""
|
|
79
|
+
if isinstance(callee, Identifier):
|
|
80
|
+
return callee.name, None
|
|
81
|
+
if isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier):
|
|
82
|
+
return callee.member, callee.object.name
|
|
83
|
+
if isinstance(callee, MemberAccess) and isinstance(callee.object, MemberAccess):
|
|
84
|
+
if isinstance(callee.object.object, Identifier):
|
|
85
|
+
return callee.member, callee.object.object.name
|
|
86
|
+
return None, None
|
|
87
|
+
|
|
88
|
+
def _get_target_name(self, target) -> str | None:
|
|
89
|
+
"""Return the bare name of an assignment target, or ``None`` for non-trivial LHS."""
|
|
90
|
+
if isinstance(target, Identifier):
|
|
91
|
+
return target.name
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def _walk_ast(self, node):
|
|
95
|
+
"""Yield every node in the subtree rooted at ``node`` (including ``node``).
|
|
96
|
+
|
|
97
|
+
Walks every attribute that historically holds an AST child or list
|
|
98
|
+
of children: ``body``/``else_body``/``cases``/``default_body``,
|
|
99
|
+
the standard binary/unary/ternary slots, ``args``, ``kwargs``,
|
|
100
|
+
and ``TypeDecl.fields[*].default``. Order is depth-first; the
|
|
101
|
+
visit order itself is not part of the public contract — only the
|
|
102
|
+
set of yielded nodes."""
|
|
103
|
+
if node is None:
|
|
104
|
+
return
|
|
105
|
+
yield node
|
|
106
|
+
for attr in ("body", "else_body"):
|
|
107
|
+
children = getattr(node, attr, None)
|
|
108
|
+
if isinstance(children, list):
|
|
109
|
+
for child in children:
|
|
110
|
+
yield from self._walk_ast(child)
|
|
111
|
+
if hasattr(node, "cases") and isinstance(node.cases, list):
|
|
112
|
+
for expr, stmts in node.cases:
|
|
113
|
+
if expr is not None:
|
|
114
|
+
yield from self._walk_ast(expr)
|
|
115
|
+
for child in stmts:
|
|
116
|
+
yield from self._walk_ast(child)
|
|
117
|
+
if hasattr(node, "default_body") and isinstance(node.default_body, list):
|
|
118
|
+
for child in node.default_body:
|
|
119
|
+
yield from self._walk_ast(child)
|
|
120
|
+
for attr in ("value", "target", "condition", "true_val", "false_val",
|
|
121
|
+
"left", "right", "object", "operand", "callee", "index",
|
|
122
|
+
"expr"):
|
|
123
|
+
child = getattr(node, attr, None)
|
|
124
|
+
if child is not None:
|
|
125
|
+
yield from self._walk_ast(child)
|
|
126
|
+
args = getattr(node, "args", None)
|
|
127
|
+
if isinstance(args, list):
|
|
128
|
+
for a in args:
|
|
129
|
+
yield from self._walk_ast(a)
|
|
130
|
+
kwargs = getattr(node, "kwargs", None)
|
|
131
|
+
if isinstance(kwargs, dict):
|
|
132
|
+
for v in kwargs.values():
|
|
133
|
+
yield from self._walk_ast(v)
|
|
134
|
+
fields = getattr(node, "fields", None)
|
|
135
|
+
if isinstance(fields, list):
|
|
136
|
+
for f in fields:
|
|
137
|
+
if hasattr(f, "default") and f.default is not None:
|
|
138
|
+
yield from self._walk_ast(f.default)
|
|
139
|
+
|
|
140
|
+
def _collect_binding_names(self, stmts) -> set[str]:
|
|
141
|
+
"""Return every name bound by a statement in ``stmts`` (recursively):
|
|
142
|
+
``var``/plain declarations, assignment targets, tuple-assign names, and
|
|
143
|
+
``for`` loop variables. Used to teach the unknown-identifier guard
|
|
144
|
+
about ordinary function-local scalars, which are emitted inline and are
|
|
145
|
+
otherwise tracked nowhere (``func_var_members`` only carries vars that
|
|
146
|
+
become persistent struct members)."""
|
|
147
|
+
names: set[str] = set()
|
|
148
|
+
for stmt in stmts or []:
|
|
149
|
+
for n in self._walk_ast(stmt):
|
|
150
|
+
if isinstance(n, VarDecl) and n.name:
|
|
151
|
+
names.add(n.name)
|
|
152
|
+
elif isinstance(n, TupleAssign):
|
|
153
|
+
names.update(x for x in n.names if x)
|
|
154
|
+
elif isinstance(n, Assignment) and isinstance(n.target, Identifier):
|
|
155
|
+
names.add(n.target.name)
|
|
156
|
+
elif isinstance(n, ForStmt) and n.var:
|
|
157
|
+
names.add(n.var)
|
|
158
|
+
elif isinstance(n, ForInStmt):
|
|
159
|
+
if n.var:
|
|
160
|
+
names.add(n.var)
|
|
161
|
+
if n.vars:
|
|
162
|
+
names.update(x for x in n.vars if x)
|
|
163
|
+
return names
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Syminfo derivation helpers for PineForge codegen.
|
|
2
|
+
|
|
3
|
+
Emits C++ inline helper functions for syminfo fields that can be derived
|
|
4
|
+
at codegen/runtime from the existing SymInfo struct without requiring
|
|
5
|
+
engine changes:
|
|
6
|
+
|
|
7
|
+
- ``_pf_derive_main_tickerid(tickerid)`` — strip futures suffix from tickerid
|
|
8
|
+
e.g., ``"CME_MINI:ES1!"`` → ``"CME_MINI:ES"``, ``"NASDAQ:AAPL"`` → ``"NASDAQ:AAPL"``
|
|
9
|
+
- ``_pf_derive_country(tickerid)`` — lookup country by exchange prefix
|
|
10
|
+
e.g., ``"NASDAQ:AAPL"`` → ``"US"``, ``"LSE:BP"`` → ``"GB"`` (ISO 3166-1)
|
|
11
|
+
|
|
12
|
+
These are emitted as ``static inline`` free functions before the
|
|
13
|
+
``GeneratedStrategy`` class definition. They depend only on ``<string>``
|
|
14
|
+
and ``<regex>`` (both already pulled in by the standard includes block).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Prefix → country lookup table used for ``syminfo.country`` derivation.
|
|
18
|
+
# Mirrors Pine v6 semantics best-effort; not an exhaustive list. All values
|
|
19
|
+
# MUST be ISO 3166-1 alpha-2 codes (Pine returns ISO codes: LSE → "GB", not
|
|
20
|
+
# "UK"). Prefixes with no single ISO country (pan-European EURONEXT, global
|
|
21
|
+
# crypto venues BINANCE/KRAKEN/BYBIT/OKX/BITMEX/DERIBIT) are intentionally
|
|
22
|
+
# absent — the helper returns na<std::string>() for them, matching TV's na
|
|
23
|
+
# for symbols without a listing country.
|
|
24
|
+
PREFIX_TO_COUNTRY: dict[str, str] = {
|
|
25
|
+
"NASDAQ": "US",
|
|
26
|
+
"NYSE": "US",
|
|
27
|
+
"NYMEX": "US",
|
|
28
|
+
"AMEX": "US",
|
|
29
|
+
"ARCA": "US",
|
|
30
|
+
"CBOE": "US",
|
|
31
|
+
"CME": "US",
|
|
32
|
+
"CME_MINI": "US",
|
|
33
|
+
"CBOT": "US",
|
|
34
|
+
"COMEX": "US",
|
|
35
|
+
"OTC": "US",
|
|
36
|
+
"LSE": "GB",
|
|
37
|
+
"AQUIS": "GB",
|
|
38
|
+
"TSE": "JP",
|
|
39
|
+
"OSE": "JP",
|
|
40
|
+
"HKEX": "HK",
|
|
41
|
+
"SGX": "SG",
|
|
42
|
+
"ASX": "AU",
|
|
43
|
+
"XETRA": "DE",
|
|
44
|
+
"BSE": "IN",
|
|
45
|
+
"NSE": "IN",
|
|
46
|
+
"COINBASE": "US",
|
|
47
|
+
"UPBIT": "KR",
|
|
48
|
+
"KRX": "KR",
|
|
49
|
+
"KOSPI": "KR",
|
|
50
|
+
"SSE": "CN",
|
|
51
|
+
"SZSE": "CN",
|
|
52
|
+
"JSE": "ZA",
|
|
53
|
+
"BMF": "BR",
|
|
54
|
+
"BMFBOVESPA": "BR",
|
|
55
|
+
"B3": "BR",
|
|
56
|
+
"MOEX": "RU",
|
|
57
|
+
"TSX": "CA",
|
|
58
|
+
"VENTURE": "CA",
|
|
59
|
+
"SIX": "CH",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# C++ code generation
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
# The futures-suffix regex: strips one or more digits at the end, optionally
|
|
67
|
+
# followed by a ``!``. Examples:
|
|
68
|
+
# CME_MINI:ES1! → CME_MINI:ES
|
|
69
|
+
# NYMEX:CL2! → NYMEX:CL
|
|
70
|
+
# NASDAQ:AAPL → NASDAQ:AAPL (no suffix, unchanged)
|
|
71
|
+
_MAIN_TICKERID_CPP = r"""
|
|
72
|
+
static inline std::string _pf_derive_prefix(const std::string& tickerid) {
|
|
73
|
+
std::size_t colon = tickerid.find(':');
|
|
74
|
+
return (colon == std::string::npos) ? tickerid : tickerid.substr(0, colon);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static inline std::string _pf_derive_main_tickerid(const std::string& tickerid) {
|
|
78
|
+
// Strip trailing digits (optionally followed by '!') from the symbol part.
|
|
79
|
+
// e.g. "CME_MINI:ES1!" -> "CME_MINI:ES", "NYMEX:CL2!" -> "NYMEX:CL"
|
|
80
|
+
std::string result = tickerid;
|
|
81
|
+
std::size_t colon = result.find(':');
|
|
82
|
+
std::size_t start = (colon == std::string::npos) ? 0 : colon + 1;
|
|
83
|
+
// Find end of base symbol (strip trailing digits + optional '!')
|
|
84
|
+
std::size_t end = result.size();
|
|
85
|
+
if (end > start && result[end - 1] == '!') {
|
|
86
|
+
--end;
|
|
87
|
+
}
|
|
88
|
+
while (end > start && std::isdigit((unsigned char)result[end - 1])) {
|
|
89
|
+
--end;
|
|
90
|
+
}
|
|
91
|
+
return result.substr(0, end);
|
|
92
|
+
}
|
|
93
|
+
""".strip()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_country_lookup_cpp() -> str:
|
|
97
|
+
"""Build the C++ prefix-to-country lookup table from PREFIX_TO_COUNTRY."""
|
|
98
|
+
entries = []
|
|
99
|
+
for prefix, country in sorted(PREFIX_TO_COUNTRY.items()):
|
|
100
|
+
entries.append(f' {{"{prefix}", "{country}"}}')
|
|
101
|
+
table = ",\n".join(entries)
|
|
102
|
+
return (
|
|
103
|
+
"static inline std::string _pf_derive_country(const std::string& tickerid) {\n"
|
|
104
|
+
" // Lookup country by exchange prefix (text before ':').\n"
|
|
105
|
+
" std::size_t colon = tickerid.find(':');\n"
|
|
106
|
+
" std::string prefix = (colon == std::string::npos)\n"
|
|
107
|
+
" ? tickerid : tickerid.substr(0, colon);\n"
|
|
108
|
+
" static const std::unordered_map<std::string, std::string> _tbl = {\n"
|
|
109
|
+
f"{table}\n"
|
|
110
|
+
" };\n"
|
|
111
|
+
" auto it = _tbl.find(prefix);\n"
|
|
112
|
+
' return (it != _tbl.end()) ? it->second : na<std::string>();\n'
|
|
113
|
+
"}\n"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def emit_syminfo_helpers() -> list[str]:
|
|
118
|
+
"""Return list of C++ lines for the syminfo derivation helper functions.
|
|
119
|
+
|
|
120
|
+
Call this from ``_emit_includes`` (after the ``using namespace pineforge;``
|
|
121
|
+
line) to inject the helpers before the ``GeneratedStrategy`` class.
|
|
122
|
+
"""
|
|
123
|
+
lines: list[str] = []
|
|
124
|
+
lines.append("// --- syminfo derivation helpers (PineForge G2) ---")
|
|
125
|
+
for cpp_line in _MAIN_TICKERID_CPP.splitlines():
|
|
126
|
+
lines.append(cpp_line)
|
|
127
|
+
lines.append("")
|
|
128
|
+
for cpp_line in _build_country_lookup_cpp().splitlines():
|
|
129
|
+
lines.append(cpp_line)
|
|
130
|
+
lines.append("// --- end syminfo derivation helpers ---")
|
|
131
|
+
lines.append("")
|
|
132
|
+
return lines
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Pine ``input.*`` call helpers for the codegen.
|
|
2
|
+
|
|
3
|
+
Mixin holding the family of ``_is_input_*`` / ``_get_input_*`` /
|
|
4
|
+
``_input_type_to_getter`` / ``_enforce_enum_declared_before_input_enum``
|
|
5
|
+
methods. They share a small piece of state (``self._enum_defs``) and a
|
|
6
|
+
common dependency on the analyzer's signature registry.
|
|
7
|
+
|
|
8
|
+
Mixin contract — host class must provide:
|
|
9
|
+
|
|
10
|
+
- ``self._enum_defs`` (``dict[str, list[str]]``).
|
|
11
|
+
- ``self._resolve_callee`` (``NamingHelper``).
|
|
12
|
+
- ``self._visit_expr`` (visitor mixin, currently on ``base.py``;
|
|
13
|
+
used to render the title-arg fallback expression).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from ..ast_nodes import FuncCall, Identifier, MemberAccess, StringLiteral
|
|
19
|
+
from .. import signatures as sigs
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InputHelper:
|
|
23
|
+
"""``input.*`` call analysis helpers — defaults, titles, getter dispatch, enum guard."""
|
|
24
|
+
|
|
25
|
+
def _is_input_call(self, node: FuncCall) -> bool:
|
|
26
|
+
"""True if ``node`` is an ``input(...)`` or ``input.<type>(...)`` call."""
|
|
27
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
28
|
+
return self._is_input_call_by_name(func_name, namespace)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def _is_input_call_by_name(func_name: str | None, namespace: str | None) -> bool:
|
|
32
|
+
"""Stateless variant: classify a ``(func_name, namespace)`` pair as input call.
|
|
33
|
+
|
|
34
|
+
Used during prescan when only the resolved callee tuple is
|
|
35
|
+
available (no FuncCall node)."""
|
|
36
|
+
if func_name == "input" and namespace is None:
|
|
37
|
+
return True
|
|
38
|
+
if namespace == "input":
|
|
39
|
+
return True
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
def _get_input_default(self, node: FuncCall):
|
|
43
|
+
"""Pull the default-value AST node out of an ``input(...)`` / ``input.<t>(...)`` call.
|
|
44
|
+
|
|
45
|
+
Honors keyword arguments by merging them in declaration order via
|
|
46
|
+
the signatures registry; falls back to the first positional
|
|
47
|
+
argument or the ``defval=`` kwarg when the registry has no entry."""
|
|
48
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
49
|
+
param_names = None
|
|
50
|
+
if namespace == "input" and func_name in sigs.INPUT_FUNCTIONS:
|
|
51
|
+
param_names = sigs.get_param_names("input", func_name)
|
|
52
|
+
elif func_name == "input" and namespace is None:
|
|
53
|
+
param_names = sigs.get_param_names(None, "input")
|
|
54
|
+
if param_names:
|
|
55
|
+
merged = list(node.args)
|
|
56
|
+
for i, pname in enumerate(param_names):
|
|
57
|
+
if pname in node.kwargs:
|
|
58
|
+
while len(merged) <= i:
|
|
59
|
+
merged.append(None)
|
|
60
|
+
if i >= len(merged) or merged[i] is None:
|
|
61
|
+
merged[i] = node.kwargs[pname]
|
|
62
|
+
while merged and merged[-1] is None:
|
|
63
|
+
merged.pop()
|
|
64
|
+
if merged and merged[0] is not None:
|
|
65
|
+
return merged[0]
|
|
66
|
+
return None
|
|
67
|
+
if node.args:
|
|
68
|
+
return node.args[0]
|
|
69
|
+
if "defval" in node.kwargs:
|
|
70
|
+
return node.kwargs["defval"]
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
def _get_input_title(self, node: FuncCall, var_name: str | None = None) -> str:
|
|
74
|
+
"""Pull the title string from an ``input(...)`` call.
|
|
75
|
+
|
|
76
|
+
Reads positional arg #2 or the ``title=`` kwarg via the
|
|
77
|
+
signatures registry. When the title isn't a ``StringLiteral``
|
|
78
|
+
(e.g. computed at runtime), the rendered C++ expression is
|
|
79
|
+
returned. Falls back to ``var_name`` when no title is provided
|
|
80
|
+
so generated UI labels still have a stable identity."""
|
|
81
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
82
|
+
param_names = None
|
|
83
|
+
if namespace == "input" and func_name in sigs.INPUT_FUNCTIONS:
|
|
84
|
+
param_names = sigs.get_param_names("input", func_name)
|
|
85
|
+
elif func_name == "input" and namespace is None:
|
|
86
|
+
param_names = sigs.get_param_names(None, "input")
|
|
87
|
+
if param_names:
|
|
88
|
+
merged = list(node.args)
|
|
89
|
+
for i, pname in enumerate(param_names):
|
|
90
|
+
if pname in node.kwargs:
|
|
91
|
+
while len(merged) <= i:
|
|
92
|
+
merged.append(None)
|
|
93
|
+
if i >= len(merged) or merged[i] is None:
|
|
94
|
+
merged[i] = node.kwargs[pname]
|
|
95
|
+
if len(merged) > 1 and merged[1] is not None:
|
|
96
|
+
title_node = merged[1]
|
|
97
|
+
if isinstance(title_node, StringLiteral):
|
|
98
|
+
return title_node.value
|
|
99
|
+
return self._visit_expr(title_node)
|
|
100
|
+
return var_name if var_name else ""
|
|
101
|
+
|
|
102
|
+
# Native price series accepted as an input.source defval. The analyzer
|
|
103
|
+
# hard-rejects anything else (see support_checker), so a defval reaching
|
|
104
|
+
# codegen is one of these or a user-series identifier.
|
|
105
|
+
_NATIVE_SOURCE_SERIES = frozenset(
|
|
106
|
+
{"open", "high", "low", "close", "volume",
|
|
107
|
+
"hl2", "hlc3", "ohlc4", "hlcc4"}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _input_type_to_getter(func_name: str | None, namespace: str | None) -> str:
|
|
112
|
+
"""Map an ``input.<type>`` call to its C++ runtime getter name.
|
|
113
|
+
|
|
114
|
+
``input.float`` / ``input.price`` collapse to ``get_input_double``;
|
|
115
|
+
``input.int`` / ``input.enum`` map to ``get_input_int`` (C++ int32);
|
|
116
|
+
``input.color`` maps to ``get_input_int64`` (a packed ARGB color
|
|
117
|
+
``0xAARRGGBB`` overflows signed int32); ``input.time`` maps to
|
|
118
|
+
``get_input_int64`` because Pine v6 ``input.time`` returns a series
|
|
119
|
+
int Unix timestamp in MILLISECONDS, which overflows int32 for any
|
|
120
|
+
modern date. ``input.source`` is rendered separately by
|
|
121
|
+
``_render_input_value`` (it returns a Series<double>&, not a scalar)
|
|
122
|
+
and never routes through this table. Bare ``input(...)`` and any
|
|
123
|
+
unrecognised variant default to double."""
|
|
124
|
+
if func_name in ("int",):
|
|
125
|
+
return "get_input_int"
|
|
126
|
+
if func_name in ("float", "source", "price"):
|
|
127
|
+
return "get_input_double"
|
|
128
|
+
if func_name in ("bool",):
|
|
129
|
+
return "get_input_bool"
|
|
130
|
+
if func_name in ("string", "timeframe", "session", "symbol", "text_area"):
|
|
131
|
+
return "get_input_string"
|
|
132
|
+
if func_name == "color":
|
|
133
|
+
return "get_input_int64"
|
|
134
|
+
if func_name == "time":
|
|
135
|
+
return "get_input_int64"
|
|
136
|
+
if func_name == "enum":
|
|
137
|
+
return "get_input_int"
|
|
138
|
+
return "get_input_double"
|
|
139
|
+
|
|
140
|
+
def _source_defval_to_base_series(self, default) -> str:
|
|
141
|
+
"""Map an input.source defval (close/high/hl2/…) to its engine base
|
|
142
|
+
source series member (``_src_close_`` …). Falls back to
|
|
143
|
+
``_src_close_`` for a non-native defval — unreachable once the
|
|
144
|
+
analyzer guard is in place, but keeps codegen total."""
|
|
145
|
+
name = default.name if isinstance(default, Identifier) else None
|
|
146
|
+
if name in self._NATIVE_SOURCE_SERIES:
|
|
147
|
+
return f"_src_{name}_"
|
|
148
|
+
return "_src_close_"
|
|
149
|
+
|
|
150
|
+
def _render_input_value(self, node: FuncCall, func_name: str | None,
|
|
151
|
+
namespace: str | None, title: str) -> str:
|
|
152
|
+
"""Render the C++ value expression for an ``input.*`` call site.
|
|
153
|
+
|
|
154
|
+
``input.source`` resolves to ``get_input_source("title", _src_<field>_)[0]``
|
|
155
|
+
— the engine returns the (optionally operator-overridden) native
|
|
156
|
+
series and we read its current value; a subscripted source var is
|
|
157
|
+
already tracked as a series var by the analyzer so ``src[1]`` lowers
|
|
158
|
+
to a Series subscript. Every other input type routes through the
|
|
159
|
+
scalar getter table."""
|
|
160
|
+
if namespace == "input" and func_name == "source":
|
|
161
|
+
default = self._get_input_default(node)
|
|
162
|
+
base = self._source_defval_to_base_series(default)
|
|
163
|
+
return f'get_input_source("{title}", {base})[0]'
|
|
164
|
+
default = self._get_input_default(node)
|
|
165
|
+
default_cpp = self._visit_expr(default) if default is not None else "0"
|
|
166
|
+
getter = self._input_type_to_getter(func_name, namespace)
|
|
167
|
+
return f'{getter}("{title}", {default_cpp})'
|
|
168
|
+
|
|
169
|
+
def _enforce_enum_declared_before_input_enum(self, node: FuncCall) -> None:
|
|
170
|
+
"""Mirror the analyzer: ``input.enum(Enum.member)`` requires Enum to be defined above the call.
|
|
171
|
+
|
|
172
|
+
Codegen runs after the analyzer so the constraint should already
|
|
173
|
+
be enforced; we duplicate the check defensively because
|
|
174
|
+
violations would otherwise lead to a silent ``KeyError`` during
|
|
175
|
+
member resolution."""
|
|
176
|
+
dv = self._get_input_default(node)
|
|
177
|
+
if not isinstance(dv, MemberAccess) or not isinstance(dv.object, Identifier):
|
|
178
|
+
return
|
|
179
|
+
ename = dv.object.name
|
|
180
|
+
if ename not in self._enum_defs:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"enum '{ename}' must be declared above input.enum() "
|
|
183
|
+
"(internal: Analyzer should reject this first)"
|
|
184
|
+
)
|
|
185
|
+
if dv.member not in self._enum_defs[ename]:
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"{ename}.{dv.member} is not a member of enum {ename} "
|
|
188
|
+
"(internal: Analyzer should reject this first)"
|
|
189
|
+
)
|