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,573 @@
|
|
|
1
|
+
"""Type inference + collection-type lowering for the codegen.
|
|
2
|
+
|
|
3
|
+
The historic ``codegen.py`` had ~15 methods scattered across the file
|
|
4
|
+
that all answered one question: "given this Pine expression / hint /
|
|
5
|
+
declaration, what C++ type should we emit?" This mixin collects them
|
|
6
|
+
in one place. ``CodeGen`` mixes ``TypeInferer`` in alongside
|
|
7
|
+
``NamingHelper`` and the future visitor mixins.
|
|
8
|
+
|
|
9
|
+
Mixin contract — host class must provide the following attributes:
|
|
10
|
+
|
|
11
|
+
- ``self.ctx`` (``AnalyzerContext``): symbol table source.
|
|
12
|
+
- ``self._udt_defs`` (``dict``): UDT name -> field info.
|
|
13
|
+
- ``self._udt_var_types`` (``dict[str, str]``): variable name -> UDT name.
|
|
14
|
+
- ``self._udt_field_type_specs`` (``dict[str, dict[str, TypeSpec]]``).
|
|
15
|
+
- ``self._collection_types`` (``dict[str, TypeSpec]``).
|
|
16
|
+
- ``self._matrix_specs`` (``dict[str, TypeSpec]``).
|
|
17
|
+
- ``self._known_vars`` (``dict[str, Any]``): compile-time-known values.
|
|
18
|
+
- ``self._enum_defs`` (``dict[str, list[str]]``).
|
|
19
|
+
- ``self._current_func_param_types`` (``dict[str, str]``).
|
|
20
|
+
- ``self._func_info_map`` (``dict[str, FuncInfo]``).
|
|
21
|
+
|
|
22
|
+
And the following methods (expected to come from sibling mixins):
|
|
23
|
+
|
|
24
|
+
- ``self._resolve_callee`` (``NamingHelper``).
|
|
25
|
+
- ``self._codegen_error`` (``CodeGen.base``).
|
|
26
|
+
- ``self._get_ta_site`` / ``self._ta_name_from_site`` (TA helper, currently
|
|
27
|
+
on ``CodeGen.base`` — will move into a ``TaSiteHelper`` mixin in a
|
|
28
|
+
later refactor step).
|
|
29
|
+
|
|
30
|
+
The mixin avoids importing from ``base.py`` to stay free of cycles; all
|
|
31
|
+
tables it needs come from ``codegen/tables.py``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from ..ast_nodes import (
|
|
37
|
+
BinOp, BoolLiteral, ExprStmt, FuncCall, FuncDef, Identifier, IfStmt,
|
|
38
|
+
MemberAccess, NaLiteral, NumberLiteral, StringLiteral, SwitchStmt,
|
|
39
|
+
Ternary, TupleLiteral, UnaryOp, VarDecl,
|
|
40
|
+
)
|
|
41
|
+
from ..symbols import PineType, TypeSpec
|
|
42
|
+
from .. import signatures as sigs
|
|
43
|
+
from .tables import (
|
|
44
|
+
ARRAY_METHODS,
|
|
45
|
+
BAR_BUILTINS,
|
|
46
|
+
BAR_FIELDS,
|
|
47
|
+
PINE_TYPE_TO_CPP,
|
|
48
|
+
TA_RETURNS_BOOL,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TypeInferer:
|
|
53
|
+
"""Type-spec / C++-type inference helpers shared across visitor mixins.
|
|
54
|
+
|
|
55
|
+
Mixed into ``CodeGen``; not intended to be instantiated standalone."""
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# Hint-name + spec helpers
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def _template_args_from_call(self, node: FuncCall) -> list[str]:
|
|
62
|
+
"""Pull ``<T>`` template-style hints off a ``FuncCall`` callee.
|
|
63
|
+
|
|
64
|
+
The parser stores ``<T>`` annotations on the ``callee`` node; this
|
|
65
|
+
helper normalizes them by stripping whitespace so call-sites can
|
|
66
|
+
compare strings directly."""
|
|
67
|
+
callee = node.callee
|
|
68
|
+
ann = getattr(callee, "annotations", None) or {}
|
|
69
|
+
return [str(x).replace(" ", "") for x in (ann.get("template_args") or [])]
|
|
70
|
+
|
|
71
|
+
def _type_spec_from_hint_name(self, name: str | None) -> TypeSpec | None:
|
|
72
|
+
"""Parse a Pine type-hint string (e.g. ``array<float>``) into a TypeSpec."""
|
|
73
|
+
if not name:
|
|
74
|
+
return None
|
|
75
|
+
name = name.strip().replace(" ", "")
|
|
76
|
+
primitives = {"float", "int", "bool", "string", "color"}
|
|
77
|
+
if name in primitives:
|
|
78
|
+
return TypeSpec.primitive(name)
|
|
79
|
+
if name.startswith("array<") and name.endswith(">"):
|
|
80
|
+
inner = name[len("array<"):-1]
|
|
81
|
+
return TypeSpec.array(self._type_spec_from_hint_name(inner) or TypeSpec.udt(inner))
|
|
82
|
+
if name.startswith("matrix<") and name.endswith(">"):
|
|
83
|
+
inner = name[len("matrix<"):-1]
|
|
84
|
+
return TypeSpec.matrix(self._type_spec_from_hint_name(inner) or TypeSpec.udt(inner))
|
|
85
|
+
if name.startswith("map<") and name.endswith(">"):
|
|
86
|
+
inner = name[len("map<"):-1]
|
|
87
|
+
depth = 0
|
|
88
|
+
split = None
|
|
89
|
+
for i, ch in enumerate(inner):
|
|
90
|
+
if ch == "<":
|
|
91
|
+
depth += 1
|
|
92
|
+
elif ch == ">":
|
|
93
|
+
depth -= 1
|
|
94
|
+
elif ch == "," and depth == 0:
|
|
95
|
+
split = i
|
|
96
|
+
break
|
|
97
|
+
if split is not None:
|
|
98
|
+
key = self._type_spec_from_hint_name(inner[:split]) or TypeSpec.udt(inner[:split])
|
|
99
|
+
val = self._type_spec_from_hint_name(inner[split + 1:]) or TypeSpec.udt(inner[split + 1:])
|
|
100
|
+
return TypeSpec.map(key, val)
|
|
101
|
+
if name in self._udt_defs:
|
|
102
|
+
return TypeSpec.udt(name)
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def _type_spec_to_cpp(self, spec: TypeSpec | None) -> str:
|
|
106
|
+
"""Render a TypeSpec as the equivalent C++ declaration string."""
|
|
107
|
+
if spec is None:
|
|
108
|
+
return "double"
|
|
109
|
+
if spec.kind == "primitive":
|
|
110
|
+
return {"float": "double", "int": "int", "bool": "bool",
|
|
111
|
+
"string": "std::string", "color": "int"}.get(spec.name or "float", "double")
|
|
112
|
+
if spec.kind == "udt" and spec.name:
|
|
113
|
+
return spec.name if spec.name in self._udt_defs else "double"
|
|
114
|
+
if spec.kind == "array":
|
|
115
|
+
return f"std::vector<{self._type_spec_to_cpp(spec.element)}>"
|
|
116
|
+
if spec.kind == "map":
|
|
117
|
+
return f"std::unordered_map<{self._type_spec_to_cpp(spec.key)}, {self._type_spec_to_cpp(spec.value)}>"
|
|
118
|
+
if spec.kind == "matrix":
|
|
119
|
+
elem = self._type_spec_to_cpp(spec.element)
|
|
120
|
+
if spec.element.kind == "primitive" and spec.element.name == "float":
|
|
121
|
+
return "PineMatrix"
|
|
122
|
+
return f"PineGenericMatrix<{elem}>"
|
|
123
|
+
return "double"
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _default_for_type(cpp_type: str) -> str:
|
|
127
|
+
"""Default initialiser for a primitive C++ type (matches Pine ``na``)."""
|
|
128
|
+
if cpp_type == "std::string":
|
|
129
|
+
return 'std::string("")'
|
|
130
|
+
if cpp_type == "bool":
|
|
131
|
+
return "false"
|
|
132
|
+
if cpp_type == "int":
|
|
133
|
+
return "0"
|
|
134
|
+
if cpp_type.startswith("std::vector") or cpp_type.startswith("std::unordered_map"):
|
|
135
|
+
return f"{cpp_type}()"
|
|
136
|
+
return "0.0"
|
|
137
|
+
|
|
138
|
+
def _default_for_spec(self, spec: TypeSpec | None) -> str:
|
|
139
|
+
"""Default initialiser for a TypeSpec; vector/map specs get ``T()``.
|
|
140
|
+
|
|
141
|
+
UDT specs always brace-init (``T{}``) regardless of whether the UDT was
|
|
142
|
+
declared in the current translation unit — imported / forward-declared
|
|
143
|
+
UDTs would otherwise fall through to ``0`` which is type-incompatible.
|
|
144
|
+
"""
|
|
145
|
+
if spec is not None and spec.kind == "udt" and spec.name:
|
|
146
|
+
return f"{spec.name}{{}}"
|
|
147
|
+
cpp_type = self._type_spec_to_cpp(spec)
|
|
148
|
+
if cpp_type.startswith("std::vector") or cpp_type.startswith("std::unordered_map"):
|
|
149
|
+
return f"{cpp_type}()"
|
|
150
|
+
return self._default_for_type(cpp_type)
|
|
151
|
+
|
|
152
|
+
def _array_spec_for_name(self, name: str) -> TypeSpec:
|
|
153
|
+
"""Spec for ``array<...>`` variable ``name`` (falls back to array<float>)."""
|
|
154
|
+
spec = self._collection_types.get(name)
|
|
155
|
+
if spec is not None and spec.kind == "array":
|
|
156
|
+
return spec
|
|
157
|
+
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
158
|
+
|
|
159
|
+
def _map_spec_for_name(self, name: str) -> TypeSpec:
|
|
160
|
+
"""Spec for ``map<...>`` variable ``name`` (falls back to map<string, float>)."""
|
|
161
|
+
spec = self._collection_types.get(name)
|
|
162
|
+
if spec is not None and spec.kind == "map":
|
|
163
|
+
return spec
|
|
164
|
+
return TypeSpec.map(TypeSpec.primitive("string"), TypeSpec.primitive("float"))
|
|
165
|
+
|
|
166
|
+
def _type_spec_from_expr(self, node) -> TypeSpec | None:
|
|
167
|
+
"""Best-effort TypeSpec inference for an expression node.
|
|
168
|
+
|
|
169
|
+
Returns ``None`` when the node's type cannot be narrowed beyond
|
|
170
|
+
the runtime default (most callers fall back to ``double``)."""
|
|
171
|
+
if isinstance(node, NumberLiteral):
|
|
172
|
+
return TypeSpec.primitive("float" if isinstance(node.value, float) else "int")
|
|
173
|
+
if isinstance(node, BoolLiteral):
|
|
174
|
+
return TypeSpec.primitive("bool")
|
|
175
|
+
if isinstance(node, StringLiteral):
|
|
176
|
+
return TypeSpec.primitive("string")
|
|
177
|
+
if isinstance(node, Identifier):
|
|
178
|
+
if node.name in self._collection_types:
|
|
179
|
+
return self._collection_types[node.name]
|
|
180
|
+
if node.name in self._udt_var_types:
|
|
181
|
+
return TypeSpec.udt(self._udt_var_types[node.name])
|
|
182
|
+
sym = self.ctx.symbols.resolve(node.name)
|
|
183
|
+
if sym is not None and getattr(sym, "type_spec", None) is not None:
|
|
184
|
+
return sym.type_spec
|
|
185
|
+
return None
|
|
186
|
+
if isinstance(node, MemberAccess):
|
|
187
|
+
owner = self._type_spec_from_expr(node.object)
|
|
188
|
+
if owner is not None and owner.kind == "udt" and owner.name:
|
|
189
|
+
return (self._udt_field_type_specs.get(owner.name) or {}).get(node.member)
|
|
190
|
+
return None
|
|
191
|
+
if isinstance(node, FuncCall):
|
|
192
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
193
|
+
targs = self._template_args_from_call(node)
|
|
194
|
+
if namespace == "str" and func_name == "split":
|
|
195
|
+
return TypeSpec.array(TypeSpec.primitive("string"))
|
|
196
|
+
if namespace == "array" and func_name in (
|
|
197
|
+
"new", "new_float", "new_int", "new_bool", "new_string", "from",
|
|
198
|
+
):
|
|
199
|
+
if func_name == "new_int":
|
|
200
|
+
return TypeSpec.array(TypeSpec.primitive("int"))
|
|
201
|
+
if func_name == "new_bool":
|
|
202
|
+
return TypeSpec.array(TypeSpec.primitive("bool"))
|
|
203
|
+
if func_name == "new_string":
|
|
204
|
+
return TypeSpec.array(TypeSpec.primitive("string"))
|
|
205
|
+
if func_name == "new_float":
|
|
206
|
+
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
207
|
+
if targs:
|
|
208
|
+
return TypeSpec.array(self._type_spec_from_hint_name(targs[0]) or TypeSpec.udt(targs[0]))
|
|
209
|
+
if func_name == "from" and node.args:
|
|
210
|
+
return TypeSpec.array(self._type_spec_from_expr(node.args[0]) or TypeSpec.primitive("float"))
|
|
211
|
+
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
212
|
+
if namespace == "map" and func_name == "new":
|
|
213
|
+
key = self._type_spec_from_hint_name(targs[0]) if len(targs) > 0 else TypeSpec.primitive("string")
|
|
214
|
+
val = self._type_spec_from_hint_name(targs[1]) if len(targs) > 1 else TypeSpec.primitive("float")
|
|
215
|
+
return TypeSpec.map(key or TypeSpec.primitive("string"), val or TypeSpec.primitive("float"))
|
|
216
|
+
if namespace in self._udt_defs and func_name == "new":
|
|
217
|
+
return TypeSpec.udt(namespace)
|
|
218
|
+
if isinstance(node.callee, MemberAccess):
|
|
219
|
+
recv_spec = self._type_spec_from_expr(node.callee.object)
|
|
220
|
+
if recv_spec is not None and recv_spec.kind == "array":
|
|
221
|
+
if func_name in ("get", "first", "last", "pop", "shift", "remove"):
|
|
222
|
+
return recv_spec.element
|
|
223
|
+
if func_name in ("copy", "slice"):
|
|
224
|
+
return recv_spec
|
|
225
|
+
if recv_spec is not None and recv_spec.kind == "map":
|
|
226
|
+
if func_name in ("get", "remove"):
|
|
227
|
+
return recv_spec.value
|
|
228
|
+
if func_name == "keys":
|
|
229
|
+
return TypeSpec.array(recv_spec.key or TypeSpec.primitive("string"))
|
|
230
|
+
if func_name == "values":
|
|
231
|
+
return TypeSpec.array(recv_spec.value or TypeSpec.primitive("float"))
|
|
232
|
+
if recv_spec is not None and recv_spec.kind == "matrix":
|
|
233
|
+
if func_name in ("copy", "submatrix", "transpose", "concat"):
|
|
234
|
+
return recv_spec
|
|
235
|
+
if func_name in ("row", "col"):
|
|
236
|
+
return TypeSpec.array(recv_spec.element)
|
|
237
|
+
if func_name == "get":
|
|
238
|
+
return recv_spec.element
|
|
239
|
+
if func_name == "eigenvalues":
|
|
240
|
+
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
# Method lowering for collection types (used by visit_call paths)
|
|
245
|
+
# ------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
def _array_method_expr(
|
|
248
|
+
self, array_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
|
|
249
|
+
) -> str:
|
|
250
|
+
"""Lower ``arr.method(...)`` to its C++ form, validating numeric requirements."""
|
|
251
|
+
spec = spec or TypeSpec.array(TypeSpec.primitive("float"))
|
|
252
|
+
arr_cpp_type = self._type_spec_to_cpp(spec)
|
|
253
|
+
elem_cpp = self._type_spec_to_cpp(spec.element) if spec.element is not None else "double"
|
|
254
|
+
if method == "copy":
|
|
255
|
+
return f"{arr_cpp_type}({array_expr})"
|
|
256
|
+
if method == "slice":
|
|
257
|
+
return f"{arr_cpp_type}({array_expr}.begin()+(int)({args[0]}),{array_expr}.begin()+(int)({args[1]}))"
|
|
258
|
+
if method == "join":
|
|
259
|
+
sep = args[0] if args else 'std::string(",")'
|
|
260
|
+
if elem_cpp == "std::string":
|
|
261
|
+
return f"[&](){{ std::string r; for(size_t i=0;i<{array_expr}.size();i++){{ if(i>0)r+={sep}; r+={array_expr}[i]; }} return r; }}()"
|
|
262
|
+
numeric_only = {
|
|
263
|
+
"sum", "avg", "min", "max", "range", "stdev", "variance", "median",
|
|
264
|
+
"mode", "percentile_linear_interpolation", "percentile_nearest_rank",
|
|
265
|
+
"percentrank", "abs", "standardize", "covariance", "binary_search",
|
|
266
|
+
"binary_search_leftmost", "binary_search_rightmost", "sort_indices",
|
|
267
|
+
}
|
|
268
|
+
if method in numeric_only and elem_cpp not in ("double", "int"):
|
|
269
|
+
self._codegen_error(
|
|
270
|
+
None,
|
|
271
|
+
f"array.{method} requires a numeric array",
|
|
272
|
+
hint="Use numeric arrays for aggregate/statistical array functions.",
|
|
273
|
+
)
|
|
274
|
+
if method in ARRAY_METHODS:
|
|
275
|
+
return ARRAY_METHODS[method](array_expr, args)
|
|
276
|
+
# Defensive: support_checker rejects any array.* method not in
|
|
277
|
+
# SUPPORTED_ARRAY (derived from ARRAY_METHODS). Reaching here means the
|
|
278
|
+
# checker was bypassed or the tables drifted.
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"codegen: unhandled array method '{method}' — analyzer should have "
|
|
281
|
+
f"rejected. Add it to ARRAY_METHODS."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _map_method_expr(
|
|
285
|
+
self, map_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
|
|
286
|
+
) -> str:
|
|
287
|
+
"""Lower ``map.method(...)`` to its C++ form using the receiver's spec for default-key/value typing."""
|
|
288
|
+
spec = spec or TypeSpec.map(TypeSpec.primitive("string"), TypeSpec.primitive("float"))
|
|
289
|
+
key_cpp = self._type_spec_to_cpp(spec.key)
|
|
290
|
+
value_cpp = self._type_spec_to_cpp(spec.value)
|
|
291
|
+
map_cpp = self._type_spec_to_cpp(spec)
|
|
292
|
+
default_value = (
|
|
293
|
+
'std::string("")' if value_cpp == "std::string"
|
|
294
|
+
else ("false" if value_cpp == "bool" else self._default_for_type(value_cpp))
|
|
295
|
+
)
|
|
296
|
+
if method == "put":
|
|
297
|
+
return f"({map_expr}[{args[0]}] = {args[1]})"
|
|
298
|
+
if method == "get":
|
|
299
|
+
return f"({map_expr}.count({args[0]}) ? {map_expr}[{args[0]}] : {default_value})"
|
|
300
|
+
if method == "remove":
|
|
301
|
+
return f"[&](){{ auto it={map_expr}.find({args[0]}); if(it!={map_expr}.end()){{ auto v=it->second; {map_expr}.erase(it); return v; }} return {default_value}; }}()"
|
|
302
|
+
if method == "contains":
|
|
303
|
+
return f"({map_expr}.count({args[0]}) > 0)"
|
|
304
|
+
if method == "size":
|
|
305
|
+
return f"(double){map_expr}.size()"
|
|
306
|
+
if method == "clear":
|
|
307
|
+
return f"{map_expr}.clear()"
|
|
308
|
+
if method == "keys":
|
|
309
|
+
return f"[&](){{ std::vector<{key_cpp}> v; for(auto& p:{map_expr}) v.push_back(p.first); return v; }}()"
|
|
310
|
+
if method == "values":
|
|
311
|
+
return f"[&](){{ std::vector<{value_cpp}> v; for(auto& p:{map_expr}) v.push_back(p.second); return v; }}()"
|
|
312
|
+
if method == "copy":
|
|
313
|
+
return f"{map_cpp}({map_expr})"
|
|
314
|
+
if method == "put_all":
|
|
315
|
+
return f"{map_expr}.insert({args[0]}.begin(), {args[0]}.end())"
|
|
316
|
+
# Defensive: support_checker rejects any map.* method not in SUPPORTED_MAP
|
|
317
|
+
# (derived from MAP_METHODS, which mirrors this if-chain). Reaching here
|
|
318
|
+
# means the checker was bypassed or the tables drifted.
|
|
319
|
+
raise ValueError(
|
|
320
|
+
f"codegen: unhandled map method '{method}' — analyzer should have "
|
|
321
|
+
f"rejected. Add it to MAP_METHODS and the if-chain above."
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# ------------------------------------------------------------------
|
|
325
|
+
# Whole-expression / declaration-level inference
|
|
326
|
+
# ------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
def _type_for_decl(self, node: VarDecl) -> str:
|
|
329
|
+
"""Determine the C++ type for a ``VarDecl``: explicit hint, then symbol, then RHS inference."""
|
|
330
|
+
if node.type_hint:
|
|
331
|
+
spec = self._type_spec_from_hint_name(node.type_hint)
|
|
332
|
+
if spec is not None:
|
|
333
|
+
return self._type_spec_to_cpp(spec)
|
|
334
|
+
if node.type_hint in self._udt_defs:
|
|
335
|
+
return node.type_hint
|
|
336
|
+
return PINE_TYPE_TO_CPP.get(node.type_hint, "double")
|
|
337
|
+
sym = self.ctx.symbols.resolve(node.name)
|
|
338
|
+
if sym is not None:
|
|
339
|
+
inferred = self._infer_type(node.value)
|
|
340
|
+
if inferred == "std::vector<double>":
|
|
341
|
+
return inferred
|
|
342
|
+
cpp_type = PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
343
|
+
if cpp_type != "double" or sym.pine_type != PineType.UNKNOWN:
|
|
344
|
+
return cpp_type
|
|
345
|
+
return self._infer_type(node.value)
|
|
346
|
+
|
|
347
|
+
def _series_type_for(self, name: str) -> str:
|
|
348
|
+
"""C++ element type for a series variable's history buffer."""
|
|
349
|
+
if self._is_int64_builtin_init(name):
|
|
350
|
+
return "int64_t"
|
|
351
|
+
sym = self.ctx.symbols.resolve(name)
|
|
352
|
+
if sym is not None:
|
|
353
|
+
return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
354
|
+
return "double"
|
|
355
|
+
|
|
356
|
+
def _is_int64_builtin_init(self, name: str) -> bool:
|
|
357
|
+
"""True if ``name``'s defining expression is a top-level call to a
|
|
358
|
+
Pine builtin that returns ``int64_t`` (``time``, ``time_close``,
|
|
359
|
+
``timestamp``). The Pine type system collapses these to ``int``
|
|
360
|
+
but the engine encodes the ``na`` sentinel in the upper 32 bits,
|
|
361
|
+
so storing into ``Series<int>`` would silently corrupt na detection.
|
|
362
|
+
"""
|
|
363
|
+
from .tables import INT64_BUILTINS
|
|
364
|
+
expr = (
|
|
365
|
+
self.ctx.global_expr_map.get(name)
|
|
366
|
+
or self.ctx.var_member_init_exprs.get(name)
|
|
367
|
+
)
|
|
368
|
+
if expr is None:
|
|
369
|
+
return False
|
|
370
|
+
if isinstance(expr, FuncCall):
|
|
371
|
+
func_name, namespace = self._resolve_callee(expr.callee)
|
|
372
|
+
if namespace is None and func_name in INT64_BUILTINS:
|
|
373
|
+
return True
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
def _infer_cpp_type_for_security_elem(self, node) -> str:
|
|
377
|
+
"""C++ type for one element of the ``request.security(..., expr, ...)`` payload.
|
|
378
|
+
|
|
379
|
+
Special-cases the few payload shapes that resolve to vectors
|
|
380
|
+
(e.g. ``ta.pivot_point_levels``, the historical pivot-point
|
|
381
|
+
local arrays) before falling back to generic spec inference.
|
|
382
|
+
|
|
383
|
+
The trailing ``_infer_type`` call lets boolean-producing
|
|
384
|
+
expressions (``close > open``, ``a and b``) and arithmetic
|
|
385
|
+
expressions land on the right C++ scalar type when used as the
|
|
386
|
+
``request.security_lower_tf`` payload — without it the
|
|
387
|
+
per-sub-bar accumulator vector would always default to
|
|
388
|
+
``std::vector<double>`` regardless of the source expression."""
|
|
389
|
+
if isinstance(node, FuncCall):
|
|
390
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
391
|
+
if namespace == "ta" and func_name == "pivot_point_levels":
|
|
392
|
+
return "std::vector<double>"
|
|
393
|
+
spec = self._type_spec_from_expr(node)
|
|
394
|
+
if spec is not None:
|
|
395
|
+
return self._type_spec_to_cpp(spec)
|
|
396
|
+
if isinstance(node, Identifier):
|
|
397
|
+
if node.name in (
|
|
398
|
+
"localPivots", "securityPivotPointsArray", "pivotPointsArray",
|
|
399
|
+
):
|
|
400
|
+
return "std::vector<double>"
|
|
401
|
+
sym = self.ctx.symbols.resolve(node.name)
|
|
402
|
+
if sym is not None and sym.pine_type != PineType.UNKNOWN:
|
|
403
|
+
return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
404
|
+
inferred = self._infer_type(node)
|
|
405
|
+
if inferred in ("bool", "int", "double", "std::string"):
|
|
406
|
+
return inferred
|
|
407
|
+
if inferred.startswith("std::vector"):
|
|
408
|
+
return "std::vector<double>"
|
|
409
|
+
return "double"
|
|
410
|
+
|
|
411
|
+
def _infer_type(self, node) -> str:
|
|
412
|
+
"""Infer the C++ type for an expression node — workhorse used everywhere.
|
|
413
|
+
|
|
414
|
+
Falls through a layered set of checks: literals first, then
|
|
415
|
+
identifiers (bar fields, known-constants, function params,
|
|
416
|
+
symbol-table lookup), then function calls (built-in dispatch,
|
|
417
|
+
UDT methods, TA sites, intrinsic signatures), then operators
|
|
418
|
+
and ternaries / if / switch expressions. Returns the string
|
|
419
|
+
``"double"`` as the safe fallback when no narrower type can be
|
|
420
|
+
determined."""
|
|
421
|
+
if isinstance(node, NumberLiteral):
|
|
422
|
+
return "double" if isinstance(node.value, float) else "int"
|
|
423
|
+
if isinstance(node, BoolLiteral):
|
|
424
|
+
return "bool"
|
|
425
|
+
if isinstance(node, StringLiteral):
|
|
426
|
+
return "std::string"
|
|
427
|
+
if isinstance(node, NaLiteral):
|
|
428
|
+
return "double"
|
|
429
|
+
if isinstance(node, Identifier):
|
|
430
|
+
if node.name in ("time", "time_close", "timenow"):
|
|
431
|
+
return "int64_t"
|
|
432
|
+
if node.name in BAR_FIELDS or node.name in BAR_BUILTINS:
|
|
433
|
+
return "double"
|
|
434
|
+
if node.name in self._known_vars:
|
|
435
|
+
val = self._known_vars[node.name]
|
|
436
|
+
if isinstance(val, bool):
|
|
437
|
+
return "bool"
|
|
438
|
+
if isinstance(val, str):
|
|
439
|
+
return "std::string"
|
|
440
|
+
if isinstance(val, int):
|
|
441
|
+
return "int"
|
|
442
|
+
if isinstance(val, float):
|
|
443
|
+
return "double"
|
|
444
|
+
if node.name in self._current_func_param_types:
|
|
445
|
+
return self._current_func_param_types[node.name]
|
|
446
|
+
sym = self.ctx.symbols.resolve(node.name)
|
|
447
|
+
if sym is not None and getattr(sym, "type_spec", None) is not None:
|
|
448
|
+
return self._type_spec_to_cpp(sym.type_spec)
|
|
449
|
+
if sym is not None and sym.pine_type != PineType.UNKNOWN:
|
|
450
|
+
return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
451
|
+
return "double"
|
|
452
|
+
if isinstance(node, FuncCall):
|
|
453
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
454
|
+
if func_name in ("time", "time_close") and namespace is None and node.args:
|
|
455
|
+
return "int64_t"
|
|
456
|
+
if func_name == "timestamp" and namespace is None:
|
|
457
|
+
return "int64_t"
|
|
458
|
+
if func_name == "na":
|
|
459
|
+
return "bool"
|
|
460
|
+
if namespace == "input" or (namespace is None and func_name == "input"):
|
|
461
|
+
if func_name in ("string", "timeframe", "session", "symbol", "text_area"):
|
|
462
|
+
return "std::string"
|
|
463
|
+
if func_name == "bool":
|
|
464
|
+
return "bool"
|
|
465
|
+
if func_name in ("int", "color", "time"):
|
|
466
|
+
return "int"
|
|
467
|
+
return "double"
|
|
468
|
+
if namespace == "str":
|
|
469
|
+
if func_name == "split":
|
|
470
|
+
return "std::vector<std::string>"
|
|
471
|
+
return "std::string"
|
|
472
|
+
if namespace == "ta" and func_name == "pivot_point_levels":
|
|
473
|
+
return "std::vector<double>"
|
|
474
|
+
if isinstance(node.callee, MemberAccess):
|
|
475
|
+
member_name = func_name or node.callee.member
|
|
476
|
+
recv_spec = self._type_spec_from_expr(node.callee.object)
|
|
477
|
+
if recv_spec is not None and recv_spec.kind == "array" and member_name == "join":
|
|
478
|
+
return "std::string"
|
|
479
|
+
if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name:
|
|
480
|
+
fi_u = self._func_info_map.get(f"{recv_spec.name}.{member_name}")
|
|
481
|
+
if fi_u is not None:
|
|
482
|
+
return PINE_TYPE_TO_CPP.get(fi_u.return_type, "double")
|
|
483
|
+
spec = self._type_spec_from_expr(node)
|
|
484
|
+
if spec is not None:
|
|
485
|
+
return self._type_spec_to_cpp(spec)
|
|
486
|
+
if namespace in self._udt_defs and func_name == "new":
|
|
487
|
+
return namespace
|
|
488
|
+
if namespace is None and func_name in self._func_info_map:
|
|
489
|
+
return PINE_TYPE_TO_CPP.get(self._func_info_map[func_name].return_type, "double")
|
|
490
|
+
site = self._get_ta_site(node)
|
|
491
|
+
if site is not None:
|
|
492
|
+
ta_name = self._ta_name_from_site(site)
|
|
493
|
+
return "bool" if ta_name in TA_RETURNS_BOOL else "double"
|
|
494
|
+
if func_name and sigs.is_intrinsic_function(namespace, func_name):
|
|
495
|
+
ret = sigs.get_return_type(namespace, func_name, len(node.args))
|
|
496
|
+
return PINE_TYPE_TO_CPP.get(ret, "double")
|
|
497
|
+
if isinstance(node, BinOp):
|
|
498
|
+
if node.op in ("==", "!=", ">", "<", ">=", "<=", "and", "or"):
|
|
499
|
+
return "bool"
|
|
500
|
+
lt = self._infer_type(node.left)
|
|
501
|
+
rt = self._infer_type(node.right)
|
|
502
|
+
if lt == "std::string" or rt == "std::string":
|
|
503
|
+
return "std::string"
|
|
504
|
+
return "double"
|
|
505
|
+
if isinstance(node, UnaryOp) and node.op == "not":
|
|
506
|
+
return "bool"
|
|
507
|
+
if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
|
|
508
|
+
ename = node.object.name
|
|
509
|
+
if ename in self._enum_defs and node.member in self._enum_defs[ename]:
|
|
510
|
+
return "int"
|
|
511
|
+
# syminfo.* type inference: look up in SYMINFO_MEMBER_MAP
|
|
512
|
+
# and derive C++ type from the expression (na<T>() or function call).
|
|
513
|
+
if ename == "syminfo":
|
|
514
|
+
from .. import signatures as _pf_sigs
|
|
515
|
+
sym_key = f"syminfo.{node.member}"
|
|
516
|
+
if sym_key in _pf_sigs.SYMINFO_VARIABLES:
|
|
517
|
+
return PINE_TYPE_TO_CPP.get(_pf_sigs.SYMINFO_VARIABLES[sym_key], "double")
|
|
518
|
+
if isinstance(node, Ternary):
|
|
519
|
+
tt = self._infer_type(node.true_val)
|
|
520
|
+
ft = self._infer_type(node.false_val)
|
|
521
|
+
if tt.startswith("std::vector") or ft.startswith("std::vector"):
|
|
522
|
+
return tt if tt.startswith("std::vector") else ft
|
|
523
|
+
if tt == "std::string" or ft == "std::string":
|
|
524
|
+
return "std::string"
|
|
525
|
+
return tt
|
|
526
|
+
# Block-as-expression cases: read the type of the last statement of
|
|
527
|
+
# the first branch / case; matches Pine semantics for ``x = if...``.
|
|
528
|
+
if isinstance(node, IfStmt):
|
|
529
|
+
if node.body:
|
|
530
|
+
last = node.body[-1]
|
|
531
|
+
if isinstance(last, ExprStmt):
|
|
532
|
+
return self._infer_type(last.expr)
|
|
533
|
+
return "double"
|
|
534
|
+
if isinstance(node, SwitchStmt):
|
|
535
|
+
if node.cases:
|
|
536
|
+
_, case_body = node.cases[0]
|
|
537
|
+
if case_body:
|
|
538
|
+
last = case_body[-1]
|
|
539
|
+
if isinstance(last, ExprStmt):
|
|
540
|
+
return self._infer_type(last.expr)
|
|
541
|
+
return "double"
|
|
542
|
+
return "double"
|
|
543
|
+
|
|
544
|
+
def _infer_tuple_types(self, func_node: FuncDef, count: int) -> list[str]:
|
|
545
|
+
"""Infer the C++ type of each element returned by a tuple-returning function.
|
|
546
|
+
|
|
547
|
+
Builds a lightweight local-type map from the function's
|
|
548
|
+
``VarDecl``s so identifiers referenced inside the final
|
|
549
|
+
``[a, b, c]`` literal resolve precisely; falls back to
|
|
550
|
+
``_infer_type`` when no local declaration matches."""
|
|
551
|
+
if not func_node.body:
|
|
552
|
+
return ["double"] * count
|
|
553
|
+
|
|
554
|
+
local_types: dict[str, str] = {}
|
|
555
|
+
for stmt in func_node.body:
|
|
556
|
+
if isinstance(stmt, VarDecl) and stmt.value is not None:
|
|
557
|
+
local_types[stmt.name] = self._infer_type(stmt.value)
|
|
558
|
+
|
|
559
|
+
last_stmt = func_node.body[-1]
|
|
560
|
+
expr = None
|
|
561
|
+
if isinstance(last_stmt, ExprStmt) and isinstance(last_stmt.expr, TupleLiteral):
|
|
562
|
+
expr = last_stmt.expr
|
|
563
|
+
elif isinstance(last_stmt, TupleLiteral):
|
|
564
|
+
expr = last_stmt
|
|
565
|
+
if expr is not None:
|
|
566
|
+
result: list[str] = []
|
|
567
|
+
for e in expr.elements:
|
|
568
|
+
if isinstance(e, Identifier) and e.name in local_types:
|
|
569
|
+
result.append(local_types[e.name])
|
|
570
|
+
else:
|
|
571
|
+
result.append(self._infer_type(e))
|
|
572
|
+
return result
|
|
573
|
+
return ["double"] * count
|