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,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