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.
Files changed (35) hide show
  1. pineforge_codegen/__init__.py +53 -0
  2. pineforge_codegen/analyzer/__init__.py +60 -0
  3. pineforge_codegen/analyzer/base.py +1563 -0
  4. pineforge_codegen/analyzer/call_handlers.py +895 -0
  5. pineforge_codegen/analyzer/contracts.py +163 -0
  6. pineforge_codegen/analyzer/diagnostics.py +118 -0
  7. pineforge_codegen/analyzer/tables.py +204 -0
  8. pineforge_codegen/analyzer/types.py +250 -0
  9. pineforge_codegen/ast_nodes.py +293 -0
  10. pineforge_codegen/codegen/__init__.py +78 -0
  11. pineforge_codegen/codegen/base.py +1381 -0
  12. pineforge_codegen/codegen/emit_top.py +875 -0
  13. pineforge_codegen/codegen/helpers.py +163 -0
  14. pineforge_codegen/codegen/helpers_syminfo.py +134 -0
  15. pineforge_codegen/codegen/input.py +189 -0
  16. pineforge_codegen/codegen/security.py +1564 -0
  17. pineforge_codegen/codegen/ta.py +298 -0
  18. pineforge_codegen/codegen/tables.py +613 -0
  19. pineforge_codegen/codegen/types.py +573 -0
  20. pineforge_codegen/codegen/visit_call.py +1305 -0
  21. pineforge_codegen/codegen/visit_expr.py +701 -0
  22. pineforge_codegen/codegen/visit_stmt.py +729 -0
  23. pineforge_codegen/errors.py +98 -0
  24. pineforge_codegen/lexer.py +531 -0
  25. pineforge_codegen/parser.py +1198 -0
  26. pineforge_codegen/pragmas.py +117 -0
  27. pineforge_codegen/signatures.py +808 -0
  28. pineforge_codegen/support_checker.py +1111 -0
  29. pineforge_codegen/symbols.py +118 -0
  30. pineforge_codegen/tokens.py +406 -0
  31. pineforge_codegen/tv_input_choices.py +86 -0
  32. pineforge_codegen-0.6.5.dist-info/METADATA +462 -0
  33. pineforge_codegen-0.6.5.dist-info/RECORD +35 -0
  34. pineforge_codegen-0.6.5.dist-info/WHEEL +4 -0
  35. 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>()")