pineforge-codegen 0.6.5__py3-none-any.whl
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.
- pineforge_codegen/__init__.py +53 -0
- pineforge_codegen/analyzer/__init__.py +60 -0
- pineforge_codegen/analyzer/base.py +1563 -0
- pineforge_codegen/analyzer/call_handlers.py +895 -0
- pineforge_codegen/analyzer/contracts.py +163 -0
- pineforge_codegen/analyzer/diagnostics.py +118 -0
- pineforge_codegen/analyzer/tables.py +204 -0
- pineforge_codegen/analyzer/types.py +250 -0
- pineforge_codegen/ast_nodes.py +293 -0
- pineforge_codegen/codegen/__init__.py +78 -0
- pineforge_codegen/codegen/base.py +1381 -0
- pineforge_codegen/codegen/emit_top.py +875 -0
- pineforge_codegen/codegen/helpers.py +163 -0
- pineforge_codegen/codegen/helpers_syminfo.py +134 -0
- pineforge_codegen/codegen/input.py +189 -0
- pineforge_codegen/codegen/security.py +1564 -0
- pineforge_codegen/codegen/ta.py +298 -0
- pineforge_codegen/codegen/tables.py +613 -0
- pineforge_codegen/codegen/types.py +573 -0
- pineforge_codegen/codegen/visit_call.py +1305 -0
- pineforge_codegen/codegen/visit_expr.py +701 -0
- pineforge_codegen/codegen/visit_stmt.py +729 -0
- pineforge_codegen/errors.py +98 -0
- pineforge_codegen/lexer.py +531 -0
- pineforge_codegen/parser.py +1198 -0
- pineforge_codegen/pragmas.py +117 -0
- pineforge_codegen/signatures.py +808 -0
- pineforge_codegen/support_checker.py +1111 -0
- pineforge_codegen/symbols.py +118 -0
- pineforge_codegen/tokens.py +406 -0
- pineforge_codegen/tv_input_choices.py +86 -0
- pineforge_codegen-0.6.5.dist-info/METADATA +462 -0
- pineforge_codegen-0.6.5.dist-info/RECORD +35 -0
- pineforge_codegen-0.6.5.dist-info/WHEEL +4 -0
- pineforge_codegen-0.6.5.dist-info/licenses/LICENSE +197 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""TA (technical analysis) call-site helpers for the codegen.
|
|
2
|
+
|
|
3
|
+
Holds the eval-free TA helpers: call-site lookup, ``.compute()`` arg
|
|
4
|
+
construction, the TA-hoisting machinery for if-bodies, and a small
|
|
5
|
+
``_is_compile_time_value`` predicate. The runtime-reset chain that
|
|
6
|
+
depends on Python's compile-time expression evaluator
|
|
7
|
+
(``_resolve_known`` / ``_runtime_ctor_arg_for_reset`` /
|
|
8
|
+
``_collect_ta_runtime_resets`` / ``_emit_ta_runtime_reset``) stays
|
|
9
|
+
on ``CodeGen`` in ``base.py`` for now — they sit at the bottom of
|
|
10
|
+
this file's docstring as a known follow-up.
|
|
11
|
+
|
|
12
|
+
Mixin contract — host class must provide:
|
|
13
|
+
|
|
14
|
+
- ``self._ta_site_map`` (``dict[int, TACallSite]``).
|
|
15
|
+
- ``self._active_ta_remap`` (``dict[str, str] | None``).
|
|
16
|
+
- ``self._hoist_var_counter`` (``int``, optional — auto-managed).
|
|
17
|
+
|
|
18
|
+
Sibling-mixin methods consumed via ``self``:
|
|
19
|
+
|
|
20
|
+
- ``self._visit_expr`` / ``self._visit_stmt`` (visitor mixins, currently
|
|
21
|
+
on ``base.py``; will move to ``visit_expr`` / ``visit_stmt`` mixins).
|
|
22
|
+
- ``self._build_security_expr`` (security mixin, currently on ``base.py``).
|
|
23
|
+
- ``self._get_target_name`` (``NamingHelper``).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
|
+
|
|
30
|
+
from ..ast_nodes import (
|
|
31
|
+
Assignment, BinOp, ExprStmt, FuncCall, Ternary, UnaryOp, VarDecl,
|
|
32
|
+
)
|
|
33
|
+
from .tables import TA_IMPLICIT_APPEND, TA_IMPLICIT_COMPUTE_FULL
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from ..analyzer import TACallSite
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TaSiteHelper:
|
|
40
|
+
"""TA call-site lookups, .compute() argument construction, and TA hoisting in if-bodies."""
|
|
41
|
+
|
|
42
|
+
# TA functions where an explicit source argument REPLACES the implicit
|
|
43
|
+
# bar-data default (vs. ATR / supertrend / DMI where bar OHLC must
|
|
44
|
+
# always be appended). Class-level so subclasses can override.
|
|
45
|
+
_TA_IMPLICIT_REPLACE = {"pivothigh", "pivotlow"}
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# Site / member lookup
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def _get_ta_site(self, node) -> "TACallSite | None":
|
|
52
|
+
"""Look up the TA call-site bound to ``node`` (by ``id(node)``)."""
|
|
53
|
+
if node is None:
|
|
54
|
+
return None
|
|
55
|
+
return self._ta_site_map.get(id(node))
|
|
56
|
+
|
|
57
|
+
def _ta_member_name(self, site: "TACallSite") -> str:
|
|
58
|
+
"""Resolve the C++ member name for a TA site, applying any active per-site remap.
|
|
59
|
+
|
|
60
|
+
Per-call-site function variants temporarily install a remap from
|
|
61
|
+
the canonical ``_ta_<name>_<n>`` member to a variant member; this
|
|
62
|
+
helper hides the lookup so call-sites stay readable."""
|
|
63
|
+
name = site.member_name
|
|
64
|
+
if self._active_ta_remap:
|
|
65
|
+
return self._active_ta_remap.get(name, name)
|
|
66
|
+
return name
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _ta_name_from_site(site: "TACallSite") -> str:
|
|
70
|
+
"""Extract the TA function name (e.g. ``"rsi"`` or ``"vwap_bands"``) from a TACallSite.
|
|
71
|
+
|
|
72
|
+
Member names follow the ``_ta_<name>_<n>`` convention where <name>
|
|
73
|
+
may itself contain underscores (e.g. ``_ta_vwap_bands_1``). We split
|
|
74
|
+
on ``_``, drop the leading empty string and ``"ta"`` prefix (parts 0
|
|
75
|
+
and 1 after split), then drop the trailing numeric counter (last part)
|
|
76
|
+
and rejoin the remaining components with ``_``."""
|
|
77
|
+
parts = site.member_name.split("_")
|
|
78
|
+
# parts = ['', 'ta', ...name_parts..., '<n>']
|
|
79
|
+
if len(parts) >= 4:
|
|
80
|
+
return "_".join(parts[2:-1])
|
|
81
|
+
if len(parts) >= 3:
|
|
82
|
+
return parts[2]
|
|
83
|
+
# Defensive: TA member names are internally generated as `_ta_<name>_<n>`
|
|
84
|
+
# (>= 3 parts after splitting on '_'). A shorter name is an internal
|
|
85
|
+
# codegen invariant violation, not reachable from Pine source. Returning
|
|
86
|
+
# "" here would flow an empty TA name into emission and produce invalid
|
|
87
|
+
# C++; raise loudly instead.
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"codegen: malformed TA member name {site.member_name!r} — expected "
|
|
90
|
+
f"'_ta_<name>_<n>' convention. Internal codegen bug."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
# TA hoisting in if-bodies (computations unconditional, result conditional)
|
|
95
|
+
# ------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def _if_body_has_ta(self, stmts: list) -> bool:
|
|
98
|
+
"""True if any statement in ``stmts`` references a TA call-site (recursively)."""
|
|
99
|
+
for s in stmts:
|
|
100
|
+
if isinstance(s, VarDecl) and s.value is not None:
|
|
101
|
+
if self._expr_contains_ta(s.value):
|
|
102
|
+
return True
|
|
103
|
+
if isinstance(s, Assignment) and hasattr(s, "value"):
|
|
104
|
+
if self._expr_contains_ta(s.value):
|
|
105
|
+
return True
|
|
106
|
+
if isinstance(s, ExprStmt):
|
|
107
|
+
if self._expr_contains_ta(s.expr):
|
|
108
|
+
return True
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def _is_result_assignment(self, stmt) -> bool:
|
|
112
|
+
"""True iff ``stmt`` is an assignment to the synthetic ``result`` variable.
|
|
113
|
+
|
|
114
|
+
``result`` is the function-body return target injected when a Pine
|
|
115
|
+
function body becomes a C++ function-call site; assignments to it
|
|
116
|
+
carry semantic weight in TA hoisting (they are the conditional-emit
|
|
117
|
+
targets)."""
|
|
118
|
+
if isinstance(stmt, Assignment):
|
|
119
|
+
target_name = self._get_target_name(stmt.target)
|
|
120
|
+
if target_name == "result":
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def _expr_contains_ta(self, expr) -> bool:
|
|
125
|
+
"""Recursive check: does any subnode of ``expr`` resolve to a TA site?"""
|
|
126
|
+
if expr is None:
|
|
127
|
+
return False
|
|
128
|
+
if self._get_ta_site(expr) is not None:
|
|
129
|
+
return True
|
|
130
|
+
if isinstance(expr, BinOp):
|
|
131
|
+
return self._expr_contains_ta(expr.left) or self._expr_contains_ta(expr.right)
|
|
132
|
+
if isinstance(expr, UnaryOp):
|
|
133
|
+
return self._expr_contains_ta(expr.operand)
|
|
134
|
+
if isinstance(expr, Ternary):
|
|
135
|
+
return (self._expr_contains_ta(expr.true_val)
|
|
136
|
+
or self._expr_contains_ta(expr.false_val))
|
|
137
|
+
if isinstance(expr, FuncCall):
|
|
138
|
+
return any(self._expr_contains_ta(a) for a in expr.args)
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def _hoist_if_body(self, stmts: list, cond: str, lines: list[str], pad: str, indent: int) -> None:
|
|
142
|
+
"""Emit an if-body with TA hoisting.
|
|
143
|
+
|
|
144
|
+
Pine evaluates TA on every bar regardless of branch; C++ TA
|
|
145
|
+
instances must compute() unconditionally to keep their state
|
|
146
|
+
in sync. We split each result-assignment whose RHS contains a
|
|
147
|
+
TA call into:
|
|
148
|
+
|
|
149
|
+
- an unconditional ``double _hoist_<n> = <rhs>;`` line,
|
|
150
|
+
- a conditional ``if (<cond>) { result = _hoist_<n>; }``.
|
|
151
|
+
|
|
152
|
+
Non-result statements are emitted unconditionally inside an
|
|
153
|
+
opening scope block; result assignments without a TA reference
|
|
154
|
+
stay fully conditional."""
|
|
155
|
+
lines.append(f"{pad}{{")
|
|
156
|
+
conditional_stmts: list = []
|
|
157
|
+
_hoist_counter = getattr(self, "_hoist_var_counter", 0)
|
|
158
|
+
|
|
159
|
+
for s in stmts:
|
|
160
|
+
if self._is_result_assignment(s):
|
|
161
|
+
rhs = s.value if hasattr(s, "value") else None
|
|
162
|
+
if rhs is not None and self._expr_contains_ta(rhs):
|
|
163
|
+
_hoist_counter += 1
|
|
164
|
+
tmp_var = f"_hoist_{_hoist_counter}"
|
|
165
|
+
compute_expr = self._visit_expr(rhs)
|
|
166
|
+
lines.append(f"{pad} double {tmp_var} = {compute_expr};")
|
|
167
|
+
conditional_stmts.append(("result", tmp_var))
|
|
168
|
+
else:
|
|
169
|
+
conditional_stmts.append(("stmt", s))
|
|
170
|
+
else:
|
|
171
|
+
self._visit_stmt(s, lines, indent + 1)
|
|
172
|
+
|
|
173
|
+
if conditional_stmts:
|
|
174
|
+
lines.append(f"{pad} if ({cond}) {{")
|
|
175
|
+
for item in conditional_stmts:
|
|
176
|
+
if item[0] == "result":
|
|
177
|
+
lines.append(f"{pad} result = {item[1]};")
|
|
178
|
+
else:
|
|
179
|
+
self._visit_stmt(item[1], lines, indent + 2)
|
|
180
|
+
lines.append(f"{pad} }}")
|
|
181
|
+
lines.append(f"{pad}}}")
|
|
182
|
+
self._hoist_var_counter = _hoist_counter
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
# .compute() arg-string construction
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def _ta_compute_args_for_site(self, site: "TACallSite") -> str:
|
|
189
|
+
"""Build the C++ argument string for ``<member>.compute(...)`` of a TA site.
|
|
190
|
+
|
|
191
|
+
Three layered cases:
|
|
192
|
+
|
|
193
|
+
- TA in ``TA_IMPLICIT_COMPUTE_FULL`` (atr / supertrend / dmi /
|
|
194
|
+
sar / pivothigh / pivotlow / wpr / volume indicators) gets
|
|
195
|
+
bar OHLC threaded in implicitly; explicit args either prefix
|
|
196
|
+
(most TA) or replace (pivothigh / pivotlow).
|
|
197
|
+
- TA with explicit ``compute_args`` from the analyzer renders
|
|
198
|
+
them and appends any implicit-suffix tokens (``vwma`` /
|
|
199
|
+
``kc`` / ``mfi`` / ``kcw`` / ``vwap``).
|
|
200
|
+
- TA with no explicit args still gets implicit suffix tokens
|
|
201
|
+
when applicable (e.g. ``vwma()`` -> ``volume`` only)."""
|
|
202
|
+
ta_name = self._ta_name_from_site(site)
|
|
203
|
+
|
|
204
|
+
if ta_name in TA_IMPLICIT_COMPUTE_FULL:
|
|
205
|
+
implicit = TA_IMPLICIT_COMPUTE_FULL[ta_name]
|
|
206
|
+
if site.compute_args:
|
|
207
|
+
explicit = ", ".join(self._visit_expr(a) for a in site.compute_args)
|
|
208
|
+
if ta_name in self._TA_IMPLICIT_REPLACE:
|
|
209
|
+
return explicit
|
|
210
|
+
return f"{explicit}, {implicit}" if explicit else implicit
|
|
211
|
+
return implicit
|
|
212
|
+
|
|
213
|
+
if site.compute_args:
|
|
214
|
+
explicit = ", ".join(self._visit_expr(a) for a in site.compute_args)
|
|
215
|
+
if ta_name in TA_IMPLICIT_APPEND:
|
|
216
|
+
return f"{explicit}, {TA_IMPLICIT_APPEND[ta_name]}"
|
|
217
|
+
return explicit
|
|
218
|
+
|
|
219
|
+
if ta_name in TA_IMPLICIT_APPEND:
|
|
220
|
+
return TA_IMPLICIT_APPEND[ta_name]
|
|
221
|
+
|
|
222
|
+
return ""
|
|
223
|
+
|
|
224
|
+
def _security_ta_compute_args_for_site(
|
|
225
|
+
self,
|
|
226
|
+
sec_id: int,
|
|
227
|
+
site: "TACallSite",
|
|
228
|
+
ta_results: dict[int, str],
|
|
229
|
+
security_mutable_names: set[str] | None = None,
|
|
230
|
+
helper_binding_stack: tuple[dict, ...] | None = None,
|
|
231
|
+
emitted_lines: list[str] | None = None,
|
|
232
|
+
) -> str:
|
|
233
|
+
"""Same as ``_ta_compute_args_for_site`` but inside an ``evaluate_security`` body.
|
|
234
|
+
|
|
235
|
+
``current_bar_.<field>`` references are rewritten to ``bar.<field>``
|
|
236
|
+
(the security context's local) and explicit args are funneled
|
|
237
|
+
through ``_build_security_expr`` so identifiers referencing
|
|
238
|
+
mutable globals get rebound to the security-context shadows."""
|
|
239
|
+
ta_name = self._ta_name_from_site(site)
|
|
240
|
+
|
|
241
|
+
if ta_name in TA_IMPLICIT_COMPUTE_FULL:
|
|
242
|
+
implicit = TA_IMPLICIT_COMPUTE_FULL[ta_name].replace("current_bar_.", "bar.")
|
|
243
|
+
if site.compute_args:
|
|
244
|
+
explicit = ", ".join(
|
|
245
|
+
self._build_security_expr(
|
|
246
|
+
sec_id,
|
|
247
|
+
a,
|
|
248
|
+
None,
|
|
249
|
+
ta_results,
|
|
250
|
+
security_mutable_names=security_mutable_names,
|
|
251
|
+
helper_binding_stack=helper_binding_stack,
|
|
252
|
+
emitted_lines=emitted_lines,
|
|
253
|
+
)
|
|
254
|
+
for a in site.compute_args
|
|
255
|
+
)
|
|
256
|
+
if ta_name in self._TA_IMPLICIT_REPLACE:
|
|
257
|
+
return explicit
|
|
258
|
+
return f"{explicit}, {implicit}" if explicit else implicit
|
|
259
|
+
return implicit
|
|
260
|
+
|
|
261
|
+
if site.compute_args:
|
|
262
|
+
explicit = ", ".join(
|
|
263
|
+
self._build_security_expr(
|
|
264
|
+
sec_id,
|
|
265
|
+
a,
|
|
266
|
+
None,
|
|
267
|
+
ta_results,
|
|
268
|
+
security_mutable_names=security_mutable_names,
|
|
269
|
+
helper_binding_stack=helper_binding_stack,
|
|
270
|
+
emitted_lines=emitted_lines,
|
|
271
|
+
)
|
|
272
|
+
for a in site.compute_args
|
|
273
|
+
)
|
|
274
|
+
if ta_name in TA_IMPLICIT_APPEND:
|
|
275
|
+
implicit = TA_IMPLICIT_APPEND[ta_name].replace("current_bar_.", "bar.")
|
|
276
|
+
return f"{explicit}, {implicit}" if explicit else implicit
|
|
277
|
+
return explicit
|
|
278
|
+
|
|
279
|
+
if ta_name in TA_IMPLICIT_APPEND:
|
|
280
|
+
return TA_IMPLICIT_APPEND[ta_name].replace("current_bar_.", "bar.")
|
|
281
|
+
|
|
282
|
+
return ""
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
# Compile-time-value predicate (paired with the runtime-reset chain
|
|
286
|
+
# that still lives on CodeGen because it uses Python's expression
|
|
287
|
+
# evaluator at codegen time).
|
|
288
|
+
# ------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def _is_compile_time_value(val: str) -> bool:
|
|
292
|
+
"""True if ``val`` is a literal that can be safely embedded in a TA ctor arg."""
|
|
293
|
+
try:
|
|
294
|
+
float(val)
|
|
295
|
+
return True
|
|
296
|
+
except ValueError:
|
|
297
|
+
pass
|
|
298
|
+
return val in ("true", "false", "0", "0.0", "na<double>()")
|