@pineforge/codegen-pyodide 0.8.1 → 0.10.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 +1348 -113
- package/pineforge_codegen/codegen/drawing.py +1 -1
- package/pineforge_codegen/codegen/emit_top.py +258 -21
- package/pineforge_codegen/codegen/input.py +17 -0
- package/pineforge_codegen/codegen/security.py +559 -36
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +56 -7
- package/pineforge_codegen/codegen/types.py +165 -30
- package/pineforge_codegen/codegen/visit_call.py +184 -53
- package/pineforge_codegen/codegen/visit_expr.py +149 -15
- package/pineforge_codegen/codegen/visit_stmt.py +85 -38
- 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.10.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,
|
|
@@ -506,18 +552,88 @@ class TypeInferer:
|
|
|
506
552
|
return True
|
|
507
553
|
return name in self._int64_reassign_targets()
|
|
508
554
|
|
|
555
|
+
def _na_reassign_cpp_type(self, name: str) -> str | None:
|
|
556
|
+
"""Declared scalar C++ type of a ``:=`` reassignment target ``name``, so a
|
|
557
|
+
bare-``na`` RHS (``x := na``) can be spelled ``na<T>()`` matching the
|
|
558
|
+
member/local type instead of the default ``na<double>()``.
|
|
559
|
+
|
|
560
|
+
Assigning a double quiet-NaN into an ``int``/``int64_t``/``bool`` member is
|
|
561
|
+
undefined behaviour (NaN->int is unspecified; on ARM64 it saturates to 0,
|
|
562
|
+
which is not the ``na<T>()`` sentinel) and defeats ``is_na<T>()``. Mirrors
|
|
563
|
+
the member-declaration type logic (``base._emit_class_members`` /
|
|
564
|
+
``_typed_na_init``): ``PINE_TYPE_TO_CPP`` plus the int->int64_t epoch
|
|
565
|
+
promotion. Returns ``None`` for collections / UDT / drawing handles and
|
|
566
|
+
for ``double`` (already the default lowering), so those paths are
|
|
567
|
+
unchanged.
|
|
568
|
+
"""
|
|
569
|
+
# Collections / UDT / drawing handles never take a scalar ``na<T>()``:
|
|
570
|
+
# leave them to the drawing-na / default lowering in _visit_rhs_value.
|
|
571
|
+
if (name in self._array_vars
|
|
572
|
+
or name in self._map_vars
|
|
573
|
+
or name in getattr(self, "_matrix_specs", {})
|
|
574
|
+
or name in self._udt_var_types):
|
|
575
|
+
return None
|
|
576
|
+
cpp_type: str | None = None
|
|
577
|
+
# 1. ``var`` member (class-scope OR function-local: both are recorded in
|
|
578
|
+
# ctx.var_members). This is the authoritative declaration source.
|
|
579
|
+
for vname, ptype, _init in self.ctx.var_members:
|
|
580
|
+
if vname == name:
|
|
581
|
+
cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
582
|
+
break
|
|
583
|
+
# 2. Function-local plain (non-``var``) scalar: its declared type was
|
|
584
|
+
# remembered at the VarDecl (``_type_for_decl``).
|
|
585
|
+
if cpp_type is None:
|
|
586
|
+
cpp_type = getattr(self, "_current_func_local_types", {}).get(name)
|
|
587
|
+
# 3. Function parameter.
|
|
588
|
+
if cpp_type is None:
|
|
589
|
+
cpp_type = getattr(self, "_current_func_param_types", {}).get(name)
|
|
590
|
+
# 4. Global-scope non-``var`` class member.
|
|
591
|
+
if cpp_type is None:
|
|
592
|
+
for gname, gptype in self.ctx.global_var_decls:
|
|
593
|
+
if gname == name:
|
|
594
|
+
cpp_type = PINE_TYPE_TO_CPP.get(gptype, "double")
|
|
595
|
+
break
|
|
596
|
+
if cpp_type is None:
|
|
597
|
+
return None
|
|
598
|
+
# int -> int64_t promotion for epoch-ms builtins, mirroring the member
|
|
599
|
+
# declaration so the na sentinel width matches the storage width.
|
|
600
|
+
if cpp_type == "int" and self._is_int64_builtin_init(name):
|
|
601
|
+
cpp_type = "int64_t"
|
|
602
|
+
# Only the retypeable scalar types are meaningful; ``double`` already
|
|
603
|
+
# lowers to ``na<double>()`` and everything else is left untouched.
|
|
604
|
+
if cpp_type in ("int", "int64_t", "bool", "std::string"):
|
|
605
|
+
return cpp_type
|
|
606
|
+
return None
|
|
607
|
+
|
|
509
608
|
# ------------------------------------------------------------------
|
|
510
609
|
# BUG C: user-defined-UDT lvalue aliasing
|
|
511
610
|
# ------------------------------------------------------------------
|
|
512
611
|
|
|
513
612
|
def _is_udt_lvalue(self, expr) -> str | None:
|
|
514
613
|
"""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;
|
|
614
|
+
names a class-scope ``var``/global UDT member, e.g. ``wyckoffSwingLow``,
|
|
615
|
+
or an element selected from ``array<UDT>``), return its UDT type name;
|
|
616
|
+
else ``None``.
|
|
517
617
|
|
|
518
618
|
Pine UDTs are reference types, so a local initialised from such an lvalue
|
|
519
619
|
and then mutated through must write back to the global. Drawing UDTs are
|
|
520
620
|
handled by the separate ``_uses_drawing`` path and are excluded here."""
|
|
621
|
+
if isinstance(expr, FuncCall):
|
|
622
|
+
callee = expr.callee
|
|
623
|
+
func_name, namespace = self._resolve_callee(callee)
|
|
624
|
+
receiver = None
|
|
625
|
+
if namespace == "array" and func_name in ("get", "first", "last") and expr.args:
|
|
626
|
+
receiver = expr.args[0]
|
|
627
|
+
elif (isinstance(callee, MemberAccess)
|
|
628
|
+
and func_name in ("get", "first", "last")):
|
|
629
|
+
receiver = callee.object
|
|
630
|
+
if receiver is not None:
|
|
631
|
+
spec = self._type_spec_from_expr(receiver)
|
|
632
|
+
elem = spec.element if spec is not None and spec.kind == "array" else None
|
|
633
|
+
if (elem is not None and elem.kind == "udt" and elem.name in self._udt_defs
|
|
634
|
+
and elem.name not in DRAWING_TYPE_TO_CPP):
|
|
635
|
+
return elem.name
|
|
636
|
+
return None
|
|
521
637
|
if not isinstance(expr, Identifier):
|
|
522
638
|
return None
|
|
523
639
|
udt_t = self._udt_var_types.get(expr.name)
|
|
@@ -791,6 +907,8 @@ class TypeInferer:
|
|
|
791
907
|
return "double"
|
|
792
908
|
if node.name in self._current_func_param_types:
|
|
793
909
|
return self._current_func_param_types[node.name]
|
|
910
|
+
if node.name in getattr(self, "_current_func_local_types", {}):
|
|
911
|
+
return self._current_func_local_types[node.name]
|
|
794
912
|
sym = self.ctx.symbols.resolve(node.name)
|
|
795
913
|
if sym is not None and getattr(sym, "type_spec", None) is not None:
|
|
796
914
|
return self._type_spec_to_cpp(sym.type_spec)
|
|
@@ -829,9 +947,11 @@ class TypeInferer:
|
|
|
829
947
|
if namespace == "str":
|
|
830
948
|
if func_name == "split":
|
|
831
949
|
return "std::vector<std::string>"
|
|
950
|
+
if func_name in ("contains", "startswith", "endswith"):
|
|
951
|
+
return "bool"
|
|
832
952
|
if func_name == "tonumber":
|
|
833
953
|
return "double"
|
|
834
|
-
if func_name
|
|
954
|
+
if func_name in ("length", "pos"):
|
|
835
955
|
return "int"
|
|
836
956
|
return "std::string"
|
|
837
957
|
if namespace == "ta" and func_name == "pivot_point_levels":
|
|
@@ -881,6 +1001,12 @@ class TypeInferer:
|
|
|
881
1001
|
# pine_str_tostring); bare reads must declare std::string.
|
|
882
1002
|
if ename == "format":
|
|
883
1003
|
return "std::string"
|
|
1004
|
+
if ename == "timeframe":
|
|
1005
|
+
if node.member in ("period", "main_period"):
|
|
1006
|
+
return "std::string"
|
|
1007
|
+
if node.member == "multiplier":
|
|
1008
|
+
return "int"
|
|
1009
|
+
return "bool"
|
|
884
1010
|
# syminfo.* type inference: look up in SYMINFO_MEMBER_MAP
|
|
885
1011
|
# and derive C++ type from the expression (na<T>() or function call).
|
|
886
1012
|
if ename == "syminfo":
|
|
@@ -888,6 +1014,9 @@ class TypeInferer:
|
|
|
888
1014
|
sym_key = f"syminfo.{node.member}"
|
|
889
1015
|
if sym_key in _pf_sigs.SYMINFO_VARIABLES:
|
|
890
1016
|
return PINE_TYPE_TO_CPP.get(_pf_sigs.SYMINFO_VARIABLES[sym_key], "double")
|
|
1017
|
+
spec = self._type_spec_from_expr(node)
|
|
1018
|
+
if spec is not None:
|
|
1019
|
+
return self._type_spec_to_cpp(spec)
|
|
891
1020
|
if isinstance(node, Ternary):
|
|
892
1021
|
tt = self._infer_type(node.true_val)
|
|
893
1022
|
ft = self._infer_type(node.false_val)
|
|
@@ -895,6 +1024,12 @@ class TypeInferer:
|
|
|
895
1024
|
return tt if tt.startswith("std::vector") else ft
|
|
896
1025
|
if tt == "std::string" or ft == "std::string":
|
|
897
1026
|
return "std::string"
|
|
1027
|
+
if tt == "double" or ft == "double":
|
|
1028
|
+
return "double"
|
|
1029
|
+
if tt == "int64_t" or ft == "int64_t":
|
|
1030
|
+
return "int64_t"
|
|
1031
|
+
if tt == "bool" and ft == "bool":
|
|
1032
|
+
return "bool"
|
|
898
1033
|
return tt
|
|
899
1034
|
# Block-as-expression cases: read the type of the last statement of
|
|
900
1035
|
# the first branch / case; matches Pine semantics for ``x = if...``.
|