@pineforge/codegen-pyodide 0.8.1 → 0.9.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/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +350 -36
- package/pineforge_codegen/analyzer/call_handlers.py +244 -133
- package/pineforge_codegen/analyzer/contracts.py +29 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/codegen/base.py +1147 -110
- package/pineforge_codegen/codegen/drawing.py +1 -1
- package/pineforge_codegen/codegen/emit_top.py +253 -21
- package/pineforge_codegen/codegen/input.py +17 -0
- package/pineforge_codegen/codegen/security.py +506 -35
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +56 -7
- package/pineforge_codegen/codegen/types.py +112 -30
- package/pineforge_codegen/codegen/visit_call.py +184 -53
- package/pineforge_codegen/codegen/visit_expr.py +62 -14
- package/pineforge_codegen/codegen/visit_stmt.py +74 -36
- package/pineforge_codegen/parser.py +73 -6
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +39 -2
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +6 -3
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
|
@@ -28,7 +28,9 @@ from __future__ import annotations
|
|
|
28
28
|
from typing import TYPE_CHECKING
|
|
29
29
|
|
|
30
30
|
from ..ast_nodes import (
|
|
31
|
-
Assignment, BinOp,
|
|
31
|
+
Assignment, BinOp, BoolLiteral, ColorLiteral, ExprStmt, FuncCall,
|
|
32
|
+
Identifier, MemberAccess, NaLiteral, NumberLiteral, StringLiteral,
|
|
33
|
+
Subscript, Ternary, TupleLiteral, UnaryOp, VarDecl,
|
|
32
34
|
)
|
|
33
35
|
from .tables import TA_IMPLICIT_APPEND, TA_IMPLICIT_COMPUTE_FULL
|
|
34
36
|
|
|
@@ -221,6 +223,90 @@ class TaSiteHelper:
|
|
|
221
223
|
|
|
222
224
|
return ""
|
|
223
225
|
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
# Precalculation safety
|
|
228
|
+
# ------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
_PRECALC_BAR_IDENTIFIERS = {
|
|
231
|
+
"open", "high", "low", "close", "volume",
|
|
232
|
+
"hl2", "hlc3", "ohlc4", "hlcc4",
|
|
233
|
+
"time", "time_close", "bar_index",
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
def _is_precalc_replayed_source_var(self, name: str) -> bool:
|
|
237
|
+
"""True for top-level ``x = input.source(...)`` variables replayed in
|
|
238
|
+
``precalculate()``.
|
|
239
|
+
|
|
240
|
+
The precompute loop explicitly advances native source series and then
|
|
241
|
+
replays those source-input assignments before computing static TA
|
|
242
|
+
sites. Other user aliases, even when they are statically derived from
|
|
243
|
+
bar data (``src = close`` / ``ha_close = close``), are not replayed
|
|
244
|
+
there and must therefore use the normal per-bar TA path."""
|
|
245
|
+
ast = getattr(self.ctx, "ast", None)
|
|
246
|
+
for stmt in getattr(ast, "body", ()):
|
|
247
|
+
if (
|
|
248
|
+
isinstance(stmt, VarDecl)
|
|
249
|
+
and stmt.name == name
|
|
250
|
+
and isinstance(stmt.value, FuncCall)
|
|
251
|
+
and self._is_source_input(stmt.value)
|
|
252
|
+
):
|
|
253
|
+
return True
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def _expr_safe_for_ta_precalc(self, expr) -> bool:
|
|
257
|
+
if expr is None:
|
|
258
|
+
return True
|
|
259
|
+
if isinstance(expr, (NumberLiteral, StringLiteral, BoolLiteral, NaLiteral, ColorLiteral)):
|
|
260
|
+
return True
|
|
261
|
+
if isinstance(expr, Identifier):
|
|
262
|
+
if expr.name in self._PRECALC_BAR_IDENTIFIERS:
|
|
263
|
+
return True
|
|
264
|
+
if self._is_precalc_replayed_source_var(expr.name):
|
|
265
|
+
return True
|
|
266
|
+
if expr.name in getattr(self.ctx, "series_vars", set()):
|
|
267
|
+
return False
|
|
268
|
+
return expr.name in getattr(self, "_static_vars", set())
|
|
269
|
+
if isinstance(expr, MemberAccess):
|
|
270
|
+
if isinstance(expr.object, Identifier) and (
|
|
271
|
+
expr.object.name.startswith("input") or expr.object.name in getattr(self, "_enum_defs", {})
|
|
272
|
+
):
|
|
273
|
+
return True
|
|
274
|
+
return self._expr_safe_for_ta_precalc(expr.object)
|
|
275
|
+
if isinstance(expr, BinOp):
|
|
276
|
+
return self._expr_safe_for_ta_precalc(expr.left) and self._expr_safe_for_ta_precalc(expr.right)
|
|
277
|
+
if isinstance(expr, UnaryOp):
|
|
278
|
+
return self._expr_safe_for_ta_precalc(expr.operand)
|
|
279
|
+
if isinstance(expr, Ternary):
|
|
280
|
+
return (
|
|
281
|
+
self._expr_safe_for_ta_precalc(expr.condition)
|
|
282
|
+
and self._expr_safe_for_ta_precalc(expr.true_val)
|
|
283
|
+
and self._expr_safe_for_ta_precalc(expr.false_val)
|
|
284
|
+
)
|
|
285
|
+
if isinstance(expr, Subscript):
|
|
286
|
+
return self._expr_safe_for_ta_precalc(expr.object) and self._expr_safe_for_ta_precalc(expr.index)
|
|
287
|
+
if isinstance(expr, TupleLiteral):
|
|
288
|
+
return all(self._expr_safe_for_ta_precalc(elem) for elem in expr.elements)
|
|
289
|
+
if isinstance(expr, FuncCall):
|
|
290
|
+
if isinstance(expr.callee, MemberAccess) and isinstance(expr.callee.object, Identifier):
|
|
291
|
+
if expr.callee.object.name in ("math", "str", "color"):
|
|
292
|
+
return all(self._expr_safe_for_ta_precalc(arg) for arg in expr.args)
|
|
293
|
+
return False
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
def _ta_site_uses_precalc(self, site: "TACallSite") -> bool:
|
|
297
|
+
"""Whether a static TA site can safely read from ``_precalc_*``.
|
|
298
|
+
|
|
299
|
+
Static-ness from the analyzer means the expression can be represented
|
|
300
|
+
from bar data and constants, but the standalone precompute loop only
|
|
301
|
+
replays a narrow subset of per-bar assignments. A user alias such as
|
|
302
|
+
``ha_close = close`` is static in that analyzer sense, yet its Series is
|
|
303
|
+
empty during precompute, so ``ta.stdev(ha_close, 20)`` precalculates as
|
|
304
|
+
all-``na``. Opting that site out preserves correctness; it simply uses
|
|
305
|
+
the ordinary stateful TA object during ``on_bar``."""
|
|
306
|
+
if not getattr(site, "is_static", False):
|
|
307
|
+
return False
|
|
308
|
+
return all(self._expr_safe_for_ta_precalc(arg) for arg in site.compute_args)
|
|
309
|
+
|
|
224
310
|
def _security_ta_compute_args_for_site(
|
|
225
311
|
self,
|
|
226
312
|
sec_id: int,
|
|
@@ -55,7 +55,7 @@ def tz_time_field_lambda(field_expr: str, ts_arg: str, tz_arg: str) -> str:
|
|
|
55
55
|
"""
|
|
56
56
|
return (
|
|
57
57
|
"[&]() -> int { "
|
|
58
|
-
f"std::string _tz = ({tz_arg}); "
|
|
58
|
+
f"std::string _tz = pineforge::normalize_timezone_for_posix(({tz_arg})); "
|
|
59
59
|
f"time_t _secs = (time_t)(({ts_arg}) / 1000); "
|
|
60
60
|
"struct tm tm_buf; "
|
|
61
61
|
"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") { "
|
|
@@ -76,11 +76,11 @@ def tz_time_field_lambda(field_expr: str, ts_arg: str, tz_arg: str) -> str:
|
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
BAR_BUILTINS = {
|
|
79
|
-
"bar_index": "
|
|
79
|
+
"bar_index": "pine_bar_index()",
|
|
80
80
|
"time": "current_bar_.timestamp",
|
|
81
81
|
"time_close": "time_close()",
|
|
82
82
|
"timenow": "current_bar_.timestamp",
|
|
83
|
-
"last_bar_index": "
|
|
83
|
+
"last_bar_index": "pine_last_bar_index()",
|
|
84
84
|
"last_bar_time": "last_bar_time_",
|
|
85
85
|
# time_tradingday: Unix-ms of the session-open of the trading day that
|
|
86
86
|
# contains the current bar. Backed by pine_time_tradingday() in the engine.
|
|
@@ -96,10 +96,12 @@ BAR_BUILTINS = {
|
|
|
96
96
|
# ``_bar_hour()`` helpers. For UTC-exchange data (the crypto corpus,
|
|
97
97
|
# SymInfo's constructor default) the lambda takes the gmtime_r fast
|
|
98
98
|
# path and is value-identical to the old emission.
|
|
99
|
+
# Route through the engine's cached pine_hour/pine_minute/... (session_time.hpp)
|
|
100
|
+
# instead of an inline per-call setenv+tzset lambda — the lambda's TZ churn
|
|
101
|
+
# caused a macOS tzset()->notifyd IPC storm (KI-35). The engine helpers are
|
|
102
|
+
# value-identical (same tm fields + Pine offsets) but tzset-churn-free.
|
|
99
103
|
**{
|
|
100
|
-
name:
|
|
101
|
-
TIME_FIELD_EXPRS[name], "current_bar_.timestamp", "syminfo_.timezone"
|
|
102
|
-
)
|
|
104
|
+
name: f"pine_{name}(current_bar_.timestamp, syminfo_.timezone)"
|
|
103
105
|
for name in ("hour", "minute", "second", "dayofmonth", "dayofweek",
|
|
104
106
|
"month", "year", "weekofyear")
|
|
105
107
|
},
|
|
@@ -113,7 +115,27 @@ BAR_SERIES_PUSH = {
|
|
|
113
115
|
"ohlc4": "((current_bar_.open + current_bar_.high + current_bar_.low + current_bar_.close) / 4.0)",
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
#
|
|
118
|
+
# Bar identifiers that refer to the *security* (HTF) bar inside
|
|
119
|
+
# ``request.security()``. ``time`` is the HTF bar-open timestamp.
|
|
120
|
+
SECURITY_BAR_FIELD_EXPRS = {
|
|
121
|
+
"open": "bar.open",
|
|
122
|
+
"high": "bar.high",
|
|
123
|
+
"low": "bar.low",
|
|
124
|
+
"close": "bar.close",
|
|
125
|
+
"volume": "bar.volume",
|
|
126
|
+
"time": "bar.timestamp",
|
|
127
|
+
}
|
|
128
|
+
SECURITY_BAR_FIELD_TYPES = {
|
|
129
|
+
"open": "double",
|
|
130
|
+
"high": "double",
|
|
131
|
+
"low": "double",
|
|
132
|
+
"close": "double",
|
|
133
|
+
"volume": "double",
|
|
134
|
+
"time": "int64_t",
|
|
135
|
+
}
|
|
136
|
+
SECURITY_BAR_FIELDS = frozenset(SECURITY_BAR_FIELD_EXPRS)
|
|
137
|
+
|
|
138
|
+
# Backwards-compatible name for consumers that only need the OHLCV subset.
|
|
117
139
|
SECURITY_OHLC_BAR_FIELDS = frozenset({"open", "high", "low", "close", "volume"})
|
|
118
140
|
|
|
119
141
|
# Generated C++ runtime function names referenced by the codegen as string
|
|
@@ -634,6 +656,33 @@ MATRIX_METHODS = {
|
|
|
634
656
|
# Math / String dispatch
|
|
635
657
|
# ---------------------------------------------------------------------------
|
|
636
658
|
|
|
659
|
+
def _math_minmax_na_expr(func_name: str, args: list[str]) -> str:
|
|
660
|
+
"""Emit Pine-compatible math.min/math.max with na propagation.
|
|
661
|
+
|
|
662
|
+
``std::min``/``std::max`` do not propagate NaN consistently because their
|
|
663
|
+
comparison is specified in terms of ``operator<``. Pine math.min/max return
|
|
664
|
+
``na`` when any operand is ``na``, so generated clamp-style expressions
|
|
665
|
+
like ``math.max(-1, math.min(1, na))`` must stay ``na``.
|
|
666
|
+
"""
|
|
667
|
+
if not args:
|
|
668
|
+
return "na<double>()"
|
|
669
|
+
if len(args) == 1:
|
|
670
|
+
return f"(double)({args[0]})"
|
|
671
|
+
op = "std::max" if func_name == "max" else "std::min"
|
|
672
|
+
decls = " ".join(
|
|
673
|
+
f"double _v{i} = (double)({arg});" for i, arg in enumerate(args)
|
|
674
|
+
)
|
|
675
|
+
guard = " || ".join(f"is_na(_v{i})" for i in range(len(args)))
|
|
676
|
+
updates = " ".join(
|
|
677
|
+
f"_out = {op}(_out, _v{i});" for i in range(1, len(args))
|
|
678
|
+
)
|
|
679
|
+
return (
|
|
680
|
+
f"([&]() -> double {{ {decls} "
|
|
681
|
+
f"if ({guard}) return na<double>(); "
|
|
682
|
+
f"double _out = _v0; {updates} return _out; }}())"
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
637
686
|
MATH_FUNC_MAP = {
|
|
638
687
|
"abs": "std::abs", "max": "std::max", "min": "std::min",
|
|
639
688
|
"ceil": "std::ceil", "floor": "std::floor", "round": "std::round",
|
|
@@ -210,8 +210,14 @@ class TypeInferer:
|
|
|
210
210
|
if isinstance(node, StringLiteral):
|
|
211
211
|
return TypeSpec.primitive("string")
|
|
212
212
|
if isinstance(node, Identifier):
|
|
213
|
+
loop_specs = getattr(self, "_current_loop_var_specs", None)
|
|
214
|
+
if loop_specs and node.name in loop_specs:
|
|
215
|
+
return loop_specs[node.name]
|
|
213
216
|
if node.name in self._collection_types:
|
|
214
217
|
return self._collection_types[node.name]
|
|
218
|
+
param_specs = getattr(self, "_current_func_param_specs", {})
|
|
219
|
+
if node.name in param_specs:
|
|
220
|
+
return param_specs[node.name]
|
|
215
221
|
if node.name in self._udt_var_types:
|
|
216
222
|
return TypeSpec.udt(self._udt_var_types[node.name])
|
|
217
223
|
# Drawing-typed method/function parameter (L.6d / U.5): a ``line ln``
|
|
@@ -323,6 +329,44 @@ class TypeInferer:
|
|
|
323
329
|
# Method lowering for collection types (used by visit_call paths)
|
|
324
330
|
# ------------------------------------------------------------------
|
|
325
331
|
|
|
332
|
+
def _array_receiver_once_expr(
|
|
333
|
+
self, array_expr: str, args: list[str], lower_receiver,
|
|
334
|
+
) -> str:
|
|
335
|
+
"""Lower an array method without duplicating its receiver evaluation.
|
|
336
|
+
|
|
337
|
+
``ARRAY_METHODS`` is intentionally a compact table of expression
|
|
338
|
+
templates. Many templates need the receiver more than once (for
|
|
339
|
+
example ``begin()`` + ``end()``). Substituting a temporary-producing
|
|
340
|
+
receiver directly into those slots creates distinct objects, so the
|
|
341
|
+
resulting iterator range is invalid. Render first with a fresh token;
|
|
342
|
+
when the template uses it repeatedly and the real receiver is not a
|
|
343
|
+
plain identifier lvalue, bind it to one lambda-local forwarding
|
|
344
|
+
reference and render every use through that binding. The lambda's
|
|
345
|
+
deduced ``auto`` return copies scalar results before a temporary
|
|
346
|
+
receiver dies; mutations still reach lvalue arrays through the
|
|
347
|
+
forwarding reference. Single-use and plain-identifier lowerings
|
|
348
|
+
remain byte-for-byte unchanged.
|
|
349
|
+
|
|
350
|
+
This is receiver-only by design: Pine argument evaluation and the
|
|
351
|
+
separate empty-array semantics are outside this fix.
|
|
352
|
+
"""
|
|
353
|
+
counter = getattr(self, "_array_receiver_counter", 0)
|
|
354
|
+
occupied = "\n".join((array_expr, *args))
|
|
355
|
+
while True:
|
|
356
|
+
receiver = f"__pf_array_receiver_{counter}"
|
|
357
|
+
counter += 1
|
|
358
|
+
if receiver not in occupied:
|
|
359
|
+
break
|
|
360
|
+
self._array_receiver_counter = counter
|
|
361
|
+
|
|
362
|
+
lowered = lower_receiver(receiver)
|
|
363
|
+
if lowered.count(receiver) <= 1 or array_expr.isidentifier():
|
|
364
|
+
return lower_receiver(array_expr)
|
|
365
|
+
return (
|
|
366
|
+
f"[&]() {{ auto&& {receiver} = ({array_expr}); "
|
|
367
|
+
f"return {lowered}; }}()"
|
|
368
|
+
)
|
|
369
|
+
|
|
326
370
|
def _array_method_expr(
|
|
327
371
|
self, array_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
|
|
328
372
|
) -> str:
|
|
@@ -331,34 +375,36 @@ class TypeInferer:
|
|
|
331
375
|
arr_cpp_type = self._type_spec_to_cpp(spec)
|
|
332
376
|
elem_cpp = self._type_spec_to_cpp(spec.element) if spec.element is not None else "double"
|
|
333
377
|
if method == "copy":
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
378
|
+
lower_receiver = lambda recv: f"{arr_cpp_type}({recv})"
|
|
379
|
+
elif method == "slice":
|
|
380
|
+
lower_receiver = lambda recv: f"{arr_cpp_type}({recv}.begin()+(int)({args[0]}),{recv}.begin()+(int)({args[1]}))"
|
|
381
|
+
elif method == "join" and elem_cpp == "std::string":
|
|
338
382
|
sep = args[0] if args else 'std::string(",")'
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
383
|
+
lower_receiver = lambda recv: f"[&](){{ std::string r; for(size_t i=0;i<{recv}.size();i++){{ if(i>0)r+={sep}; r+={recv}[i]; }} return r; }}()"
|
|
384
|
+
else:
|
|
385
|
+
numeric_only = {
|
|
386
|
+
"sum", "avg", "min", "max", "range", "stdev", "variance", "median",
|
|
387
|
+
"mode", "percentile_linear_interpolation", "percentile_nearest_rank",
|
|
388
|
+
"percentrank", "abs", "standardize", "covariance", "binary_search",
|
|
389
|
+
"binary_search_leftmost", "binary_search_rightmost", "sort_indices",
|
|
390
|
+
}
|
|
391
|
+
if method in numeric_only and elem_cpp not in ("double", "int"):
|
|
392
|
+
self._codegen_error(
|
|
393
|
+
None,
|
|
394
|
+
f"array.{method} requires a numeric array",
|
|
395
|
+
hint="Use numeric arrays for aggregate/statistical array functions.",
|
|
396
|
+
)
|
|
397
|
+
if method not in ARRAY_METHODS:
|
|
398
|
+
# Defensive: support_checker rejects any array.* method not in
|
|
399
|
+
# SUPPORTED_ARRAY (derived from ARRAY_METHODS). Reaching here means the
|
|
400
|
+
# checker was bypassed or the tables drifted.
|
|
401
|
+
raise ValueError(
|
|
402
|
+
f"codegen: unhandled array method '{method}' — analyzer should have "
|
|
403
|
+
f"rejected. Add it to ARRAY_METHODS."
|
|
404
|
+
)
|
|
405
|
+
lower_receiver = lambda recv: ARRAY_METHODS[method](recv, args)
|
|
406
|
+
|
|
407
|
+
return self._array_receiver_once_expr(array_expr, args, lower_receiver)
|
|
362
408
|
|
|
363
409
|
def _map_method_expr(
|
|
364
410
|
self, map_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
|
|
@@ -512,12 +558,29 @@ class TypeInferer:
|
|
|
512
558
|
|
|
513
559
|
def _is_udt_lvalue(self, expr) -> str | None:
|
|
514
560
|
"""If ``expr`` is a *user-defined* UDT lvalue (a bare ``Identifier`` that
|
|
515
|
-
names a class-scope ``var``/global UDT member, e.g. ``wyckoffSwingLow
|
|
516
|
-
return its UDT type name;
|
|
561
|
+
names a class-scope ``var``/global UDT member, e.g. ``wyckoffSwingLow``,
|
|
562
|
+
or an element selected from ``array<UDT>``), return its UDT type name;
|
|
563
|
+
else ``None``.
|
|
517
564
|
|
|
518
565
|
Pine UDTs are reference types, so a local initialised from such an lvalue
|
|
519
566
|
and then mutated through must write back to the global. Drawing UDTs are
|
|
520
567
|
handled by the separate ``_uses_drawing`` path and are excluded here."""
|
|
568
|
+
if isinstance(expr, FuncCall):
|
|
569
|
+
callee = expr.callee
|
|
570
|
+
func_name, namespace = self._resolve_callee(callee)
|
|
571
|
+
receiver = None
|
|
572
|
+
if namespace == "array" and func_name in ("get", "first", "last") and expr.args:
|
|
573
|
+
receiver = expr.args[0]
|
|
574
|
+
elif (isinstance(callee, MemberAccess)
|
|
575
|
+
and func_name in ("get", "first", "last")):
|
|
576
|
+
receiver = callee.object
|
|
577
|
+
if receiver is not None:
|
|
578
|
+
spec = self._type_spec_from_expr(receiver)
|
|
579
|
+
elem = spec.element if spec is not None and spec.kind == "array" else None
|
|
580
|
+
if (elem is not None and elem.kind == "udt" and elem.name in self._udt_defs
|
|
581
|
+
and elem.name not in DRAWING_TYPE_TO_CPP):
|
|
582
|
+
return elem.name
|
|
583
|
+
return None
|
|
521
584
|
if not isinstance(expr, Identifier):
|
|
522
585
|
return None
|
|
523
586
|
udt_t = self._udt_var_types.get(expr.name)
|
|
@@ -791,6 +854,8 @@ class TypeInferer:
|
|
|
791
854
|
return "double"
|
|
792
855
|
if node.name in self._current_func_param_types:
|
|
793
856
|
return self._current_func_param_types[node.name]
|
|
857
|
+
if node.name in getattr(self, "_current_func_local_types", {}):
|
|
858
|
+
return self._current_func_local_types[node.name]
|
|
794
859
|
sym = self.ctx.symbols.resolve(node.name)
|
|
795
860
|
if sym is not None and getattr(sym, "type_spec", None) is not None:
|
|
796
861
|
return self._type_spec_to_cpp(sym.type_spec)
|
|
@@ -829,9 +894,11 @@ class TypeInferer:
|
|
|
829
894
|
if namespace == "str":
|
|
830
895
|
if func_name == "split":
|
|
831
896
|
return "std::vector<std::string>"
|
|
897
|
+
if func_name in ("contains", "startswith", "endswith"):
|
|
898
|
+
return "bool"
|
|
832
899
|
if func_name == "tonumber":
|
|
833
900
|
return "double"
|
|
834
|
-
if func_name
|
|
901
|
+
if func_name in ("length", "pos"):
|
|
835
902
|
return "int"
|
|
836
903
|
return "std::string"
|
|
837
904
|
if namespace == "ta" and func_name == "pivot_point_levels":
|
|
@@ -881,6 +948,12 @@ class TypeInferer:
|
|
|
881
948
|
# pine_str_tostring); bare reads must declare std::string.
|
|
882
949
|
if ename == "format":
|
|
883
950
|
return "std::string"
|
|
951
|
+
if ename == "timeframe":
|
|
952
|
+
if node.member in ("period", "main_period"):
|
|
953
|
+
return "std::string"
|
|
954
|
+
if node.member == "multiplier":
|
|
955
|
+
return "int"
|
|
956
|
+
return "bool"
|
|
884
957
|
# syminfo.* type inference: look up in SYMINFO_MEMBER_MAP
|
|
885
958
|
# and derive C++ type from the expression (na<T>() or function call).
|
|
886
959
|
if ename == "syminfo":
|
|
@@ -888,6 +961,9 @@ class TypeInferer:
|
|
|
888
961
|
sym_key = f"syminfo.{node.member}"
|
|
889
962
|
if sym_key in _pf_sigs.SYMINFO_VARIABLES:
|
|
890
963
|
return PINE_TYPE_TO_CPP.get(_pf_sigs.SYMINFO_VARIABLES[sym_key], "double")
|
|
964
|
+
spec = self._type_spec_from_expr(node)
|
|
965
|
+
if spec is not None:
|
|
966
|
+
return self._type_spec_to_cpp(spec)
|
|
891
967
|
if isinstance(node, Ternary):
|
|
892
968
|
tt = self._infer_type(node.true_val)
|
|
893
969
|
ft = self._infer_type(node.false_val)
|
|
@@ -895,6 +971,12 @@ class TypeInferer:
|
|
|
895
971
|
return tt if tt.startswith("std::vector") else ft
|
|
896
972
|
if tt == "std::string" or ft == "std::string":
|
|
897
973
|
return "std::string"
|
|
974
|
+
if tt == "double" or ft == "double":
|
|
975
|
+
return "double"
|
|
976
|
+
if tt == "int64_t" or ft == "int64_t":
|
|
977
|
+
return "int64_t"
|
|
978
|
+
if tt == "bool" and ft == "bool":
|
|
979
|
+
return "bool"
|
|
898
980
|
return tt
|
|
899
981
|
# Block-as-expression cases: read the type of the last statement of
|
|
900
982
|
# the first branch / case; matches Pine semantics for ``x = if...``.
|