@pineforge/codegen-pyodide 0.8.0 → 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 +797 -45
- package/pineforge_codegen/analyzer/call_handlers.py +368 -68
- package/pineforge_codegen/analyzer/contracts.py +91 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +1613 -111
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +536 -45
- package/pineforge_codegen/codegen/input.py +31 -0
- package/pineforge_codegen/codegen/security.py +791 -39
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +115 -10
- package/pineforge_codegen/codegen/types.py +478 -46
- package/pineforge_codegen/codegen/visit_call.py +331 -75
- package/pineforge_codegen/codegen/visit_expr.py +116 -14
- package/pineforge_codegen/codegen/visit_stmt.py +208 -23
- package/pineforge_codegen/parser.py +129 -29
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +320 -19
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -10
- package/pineforge_codegen-0.8.0.tar.gz +0 -0
|
@@ -41,13 +41,33 @@ from ..ast_nodes import (
|
|
|
41
41
|
from ..symbols import PineType, TypeSpec
|
|
42
42
|
from .. import signatures as sigs
|
|
43
43
|
from .tables import (
|
|
44
|
+
ARRAY_DRAWING_NEW_CTORS,
|
|
44
45
|
ARRAY_METHODS,
|
|
45
46
|
BAR_BUILTINS,
|
|
46
47
|
BAR_FIELDS,
|
|
48
|
+
DRAWING_NS,
|
|
49
|
+
DRAWING_TYPE_TO_CPP,
|
|
47
50
|
PINE_TYPE_TO_CPP,
|
|
48
51
|
TA_RETURNS_BOOL,
|
|
49
52
|
)
|
|
50
53
|
|
|
54
|
+
# Collection (array / map / matrix) methods that MUTATE the receiver in place.
|
|
55
|
+
# Used by the BUG-2 collection-lvalue-alias path to decide whether a local
|
|
56
|
+
# bound to an existing collection lvalue must alias (mutated) or may value-copy
|
|
57
|
+
# (read-only). A method missing here only costs the alias optimization; a
|
|
58
|
+
# non-mutating method accidentally present would alias a read-only local, which
|
|
59
|
+
# is still correct (reads through a reference equal reads through a copy).
|
|
60
|
+
COLLECTION_MUTATING_METHODS = frozenset({
|
|
61
|
+
# array
|
|
62
|
+
"push", "unshift", "insert", "remove", "pop", "shift", "clear",
|
|
63
|
+
"set", "fill", "sort", "reverse", "concat",
|
|
64
|
+
# map
|
|
65
|
+
"put", "put_all",
|
|
66
|
+
# matrix
|
|
67
|
+
"add_row", "add_col", "remove_row", "remove_col", "reshape",
|
|
68
|
+
"swap_rows", "swap_columns",
|
|
69
|
+
})
|
|
70
|
+
|
|
51
71
|
|
|
52
72
|
class TypeInferer:
|
|
53
73
|
"""Type-spec / C++-type inference helpers shared across visitor mixins.
|
|
@@ -100,6 +120,12 @@ class TypeInferer:
|
|
|
100
120
|
return TypeSpec.map(key, val)
|
|
101
121
|
if name in self._udt_defs:
|
|
102
122
|
return TypeSpec.udt(name)
|
|
123
|
+
# Drawing-objects-as-data (spec §4.1 / P3): scalar ``line``/``box``/
|
|
124
|
+
# ``label``/``linefill``/``chart.point`` hints carry the handle identity
|
|
125
|
+
# via a udt TypeSpec. (``array<line>`` already resolves via the array
|
|
126
|
+
# fallback above.) Drawing names are NOT in _udt_defs.
|
|
127
|
+
if name in DRAWING_TYPE_TO_CPP:
|
|
128
|
+
return TypeSpec.udt(name)
|
|
103
129
|
return None
|
|
104
130
|
|
|
105
131
|
def _type_spec_to_cpp(self, spec: TypeSpec | None) -> str:
|
|
@@ -110,6 +136,11 @@ class TypeInferer:
|
|
|
110
136
|
return {"float": "double", "int": "int", "bool": "bool",
|
|
111
137
|
"string": "std::string", "color": "int"}.get(spec.name or "float", "double")
|
|
112
138
|
if spec.kind == "udt" and spec.name:
|
|
139
|
+
# Drawing handle structs (P1): map BEFORE the _udt_defs check so
|
|
140
|
+
# array<line> -> std::vector<Line> and scalar line -> Line instead
|
|
141
|
+
# of the old collapse to double / unknown-type-name.
|
|
142
|
+
if spec.name in DRAWING_TYPE_TO_CPP:
|
|
143
|
+
return DRAWING_TYPE_TO_CPP[spec.name]
|
|
113
144
|
return spec.name if spec.name in self._udt_defs else "double"
|
|
114
145
|
if spec.kind == "array":
|
|
115
146
|
return f"std::vector<{self._type_spec_to_cpp(spec.element)}>"
|
|
@@ -143,6 +174,10 @@ class TypeInferer:
|
|
|
143
174
|
UDTs would otherwise fall through to ``0`` which is type-incompatible.
|
|
144
175
|
"""
|
|
145
176
|
if spec is not None and spec.kind == "udt" and spec.name:
|
|
177
|
+
# Drawing handle default (P2): brace-init the C++ struct name
|
|
178
|
+
# (Line{} = na handle), NOT the lowercase Pine name (line{}).
|
|
179
|
+
if spec.name in DRAWING_TYPE_TO_CPP:
|
|
180
|
+
return f"{DRAWING_TYPE_TO_CPP[spec.name]}{{}}"
|
|
146
181
|
return f"{spec.name}{{}}"
|
|
147
182
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
148
183
|
if cpp_type.startswith("std::vector") or cpp_type.startswith("std::unordered_map"):
|
|
@@ -175,10 +210,22 @@ class TypeInferer:
|
|
|
175
210
|
if isinstance(node, StringLiteral):
|
|
176
211
|
return TypeSpec.primitive("string")
|
|
177
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]
|
|
178
216
|
if node.name in self._collection_types:
|
|
179
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]
|
|
180
221
|
if node.name in self._udt_var_types:
|
|
181
222
|
return TypeSpec.udt(self._udt_var_types[node.name])
|
|
223
|
+
# Drawing-typed method/function parameter (L.6d / U.5): a ``line ln``
|
|
224
|
+
# method receiver registers in _udt_param_udt so its body getters
|
|
225
|
+
# resolve to the drawing udt and dispatch through the §4.3 path.
|
|
226
|
+
_pu = getattr(self, "_udt_param_udt", None)
|
|
227
|
+
if _pu and node.name in _pu and _pu[node.name] in DRAWING_TYPE_TO_CPP:
|
|
228
|
+
return TypeSpec.udt(_pu[node.name])
|
|
182
229
|
sym = self.ctx.symbols.resolve(node.name)
|
|
183
230
|
if sym is not None and getattr(sym, "type_spec", None) is not None:
|
|
184
231
|
return sym.type_spec
|
|
@@ -190,12 +237,27 @@ class TypeInferer:
|
|
|
190
237
|
return None
|
|
191
238
|
if isinstance(node, FuncCall):
|
|
192
239
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
240
|
+
# ticker.* constructors (inherit/standard/heikinashi) return a symbol
|
|
241
|
+
# string; without this the member-type inference defaults to double
|
|
242
|
+
# and a ``haTicker = ticker.heikinashi(...)`` global mis-declares as
|
|
243
|
+
# double then assigns a std::string. (Analyzer agrees: ticker.* -> STRING.)
|
|
244
|
+
if namespace == "ticker":
|
|
245
|
+
return TypeSpec.primitive("string")
|
|
193
246
|
targs = self._template_args_from_call(node)
|
|
247
|
+
# Drawing-objects-as-data return typing (spec §4.5 DRAWING_RETURN_SPECS):
|
|
248
|
+
# *.new / *.copy -> handle of the self-type; linefill.get_line* -> line.
|
|
249
|
+
if namespace in DRAWING_NS:
|
|
250
|
+
if func_name in ("new", "copy"):
|
|
251
|
+
return TypeSpec.udt(namespace)
|
|
252
|
+
if namespace == "linefill" and func_name in ("get_line1", "get_line2"):
|
|
253
|
+
return TypeSpec.udt("line")
|
|
254
|
+
if self._is_chart_point_callee(node.callee):
|
|
255
|
+
return TypeSpec.udt("chart.point")
|
|
194
256
|
if namespace == "str" and func_name == "split":
|
|
195
257
|
return TypeSpec.array(TypeSpec.primitive("string"))
|
|
196
258
|
if namespace == "array" and func_name in (
|
|
197
259
|
"new", "new_float", "new_int", "new_bool", "new_string", "from",
|
|
198
|
-
):
|
|
260
|
+
) or (namespace == "array" and func_name in ARRAY_DRAWING_NEW_CTORS):
|
|
199
261
|
if func_name == "new_int":
|
|
200
262
|
return TypeSpec.array(TypeSpec.primitive("int"))
|
|
201
263
|
if func_name == "new_bool":
|
|
@@ -204,6 +266,10 @@ class TypeInferer:
|
|
|
204
266
|
return TypeSpec.array(TypeSpec.primitive("string"))
|
|
205
267
|
if func_name == "new_float":
|
|
206
268
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
269
|
+
if func_name in ARRAY_DRAWING_NEW_CTORS:
|
|
270
|
+
# array.new_line()/new_box()/new_label()/new_linefill() ->
|
|
271
|
+
# std::vector<Line/Box/Label/Linefill> (typed alias of new<T>).
|
|
272
|
+
return TypeSpec.array(TypeSpec.udt(ARRAY_DRAWING_NEW_CTORS[func_name]))
|
|
207
273
|
if targs:
|
|
208
274
|
return TypeSpec.array(self._type_spec_from_hint_name(targs[0]) or TypeSpec.udt(targs[0]))
|
|
209
275
|
if func_name == "from" and node.args:
|
|
@@ -249,12 +315,58 @@ class TypeInferer:
|
|
|
249
315
|
return recv_spec.element
|
|
250
316
|
if func_name == "eigenvalues":
|
|
251
317
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
318
|
+
# Drawing method-form: ``a.copy()`` -> same handle type;
|
|
319
|
+
# ``lf.get_line1()`` -> line. (L-N6 alias-vs-copy typing.)
|
|
320
|
+
if (recv_spec is not None and recv_spec.kind == "udt"
|
|
321
|
+
and recv_spec.name in DRAWING_TYPE_TO_CPP):
|
|
322
|
+
if func_name == "copy":
|
|
323
|
+
return recv_spec
|
|
324
|
+
if recv_spec.name == "linefill" and func_name in ("get_line1", "get_line2"):
|
|
325
|
+
return TypeSpec.udt("line")
|
|
252
326
|
return None
|
|
253
327
|
|
|
254
328
|
# ------------------------------------------------------------------
|
|
255
329
|
# Method lowering for collection types (used by visit_call paths)
|
|
256
330
|
# ------------------------------------------------------------------
|
|
257
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
|
+
|
|
258
370
|
def _array_method_expr(
|
|
259
371
|
self, array_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
|
|
260
372
|
) -> str:
|
|
@@ -263,34 +375,36 @@ class TypeInferer:
|
|
|
263
375
|
arr_cpp_type = self._type_spec_to_cpp(spec)
|
|
264
376
|
elem_cpp = self._type_spec_to_cpp(spec.element) if spec.element is not None else "double"
|
|
265
377
|
if method == "copy":
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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":
|
|
270
382
|
sep = args[0] if args else 'std::string(",")'
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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)
|
|
294
408
|
|
|
295
409
|
def _map_method_expr(
|
|
296
410
|
self, map_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
|
|
@@ -345,6 +459,22 @@ class TypeInferer:
|
|
|
345
459
|
if node.type_hint in self._udt_defs:
|
|
346
460
|
return node.type_hint
|
|
347
461
|
return PINE_TYPE_TO_CPP.get(node.type_hint, "double")
|
|
462
|
+
# Drawing handle local (L-N6): a hintless local whose RHS resolves to a
|
|
463
|
+
# drawing udt must declare as the handle struct, not the analyzer's
|
|
464
|
+
# scalar default. Covers ``ln = arr.get(i)``, alias ``b = a``, field read
|
|
465
|
+
# ``lvl.ln``, and ``c = a.copy()``. Also records _udt_var_types so later
|
|
466
|
+
# uses (``ln.set_x2(...)`` / ``ln.slope()``) resolve to the drawing udt.
|
|
467
|
+
if getattr(self, "_uses_drawing", False):
|
|
468
|
+
rhs_spec = self._type_spec_from_expr(node.value)
|
|
469
|
+
if (rhs_spec is not None and rhs_spec.kind == "udt"
|
|
470
|
+
and rhs_spec.name in DRAWING_TYPE_TO_CPP):
|
|
471
|
+
self._udt_var_types.setdefault(node.name, rhs_spec.name)
|
|
472
|
+
return DRAWING_TYPE_TO_CPP[rhs_spec.name]
|
|
473
|
+
# Scalar drawing getter local (get_text -> std::string, etc.).
|
|
474
|
+
if isinstance(node.value, FuncCall):
|
|
475
|
+
_dret = self._drawing_call_return_cpp(node.value)
|
|
476
|
+
if _dret is not None:
|
|
477
|
+
return _dret
|
|
348
478
|
sym = self.ctx.symbols.resolve(node.name)
|
|
349
479
|
if sym is not None:
|
|
350
480
|
inferred = self._infer_type(node.value)
|
|
@@ -369,25 +499,290 @@ class TypeInferer:
|
|
|
369
499
|
return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
370
500
|
return "double"
|
|
371
501
|
|
|
502
|
+
def _expr_is_int64_builtin(self, expr) -> bool:
|
|
503
|
+
"""True if ``expr`` is a top-level int64-returning Pine builtin: either a
|
|
504
|
+
call to one of ``INT64_BUILTINS`` (``time(...)``, ``timestamp(...)``, …)
|
|
505
|
+
or a bare ``Identifier`` spelled like ``time`` / ``time_close`` /
|
|
506
|
+
``timenow`` (which Pine exposes as a value, not a call)."""
|
|
507
|
+
from .tables import INT64_BUILTINS, INT64_BUILTIN_IDENTIFIERS
|
|
508
|
+
if expr is None:
|
|
509
|
+
return False
|
|
510
|
+
if isinstance(expr, FuncCall):
|
|
511
|
+
func_name, namespace = self._resolve_callee(expr.callee)
|
|
512
|
+
return namespace is None and func_name in INT64_BUILTINS
|
|
513
|
+
if isinstance(expr, Identifier):
|
|
514
|
+
return expr.name in INT64_BUILTIN_IDENTIFIERS
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
def _int64_reassign_targets(self) -> set[str]:
|
|
518
|
+
"""Names of vars that are reassigned (``:=``/``=``) anywhere in the AST
|
|
519
|
+
with an RHS that is a top-level int64-returning builtin. Cached on the
|
|
520
|
+
instance. Pine ``int`` collapses these to 32-bit, but the runtime stores
|
|
521
|
+
the epoch in 64 bits, so the member must be promoted to ``int64_t``."""
|
|
522
|
+
cached = getattr(self, "_int64_reassign_cache", None)
|
|
523
|
+
if cached is not None:
|
|
524
|
+
return cached
|
|
525
|
+
from ..ast_nodes import Assignment
|
|
526
|
+
targets: set[str] = set()
|
|
527
|
+
ast = getattr(self.ctx, "ast", None)
|
|
528
|
+
if ast is not None:
|
|
529
|
+
for node in self._walk_ast(ast):
|
|
530
|
+
if (isinstance(node, Assignment)
|
|
531
|
+
and isinstance(node.target, Identifier)
|
|
532
|
+
and self._expr_is_int64_builtin(node.value)):
|
|
533
|
+
targets.add(node.target.name)
|
|
534
|
+
self._int64_reassign_cache = targets
|
|
535
|
+
return targets
|
|
536
|
+
|
|
372
537
|
def _is_int64_builtin_init(self, name: str) -> bool:
|
|
373
|
-
"""True if ``name``'s
|
|
374
|
-
|
|
375
|
-
``timestamp``). The Pine type system
|
|
376
|
-
but the engine encodes the ``na`` sentinel
|
|
377
|
-
|
|
538
|
+
"""True if ``name``'s initializer OR any ``:=``/``=`` reassignment has an
|
|
539
|
+
RHS that is a top-level int64-returning builtin (``time``, ``time_close``,
|
|
540
|
+
``timenow``, ``timestamp``, ``time_tradingday``). The Pine type system
|
|
541
|
+
collapses these to ``int`` but the engine encodes the ``na`` sentinel
|
|
542
|
+
(and the full epoch-ms value, which overflows int32) in 64 bits, so
|
|
543
|
+
storing into ``int`` silently corrupts both the value and na detection.
|
|
544
|
+
A reassignment like ``var int entryTime = na`` then ``entryTime := time``
|
|
545
|
+
must promote even though the *initializer* alone is ``na``.
|
|
378
546
|
"""
|
|
379
|
-
from .tables import INT64_BUILTINS
|
|
380
547
|
expr = (
|
|
381
548
|
self.ctx.global_expr_map.get(name)
|
|
382
549
|
or self.ctx.var_member_init_exprs.get(name)
|
|
383
550
|
)
|
|
384
|
-
if expr
|
|
385
|
-
return
|
|
551
|
+
if self._expr_is_int64_builtin(expr):
|
|
552
|
+
return True
|
|
553
|
+
return name in self._int64_reassign_targets()
|
|
554
|
+
|
|
555
|
+
# ------------------------------------------------------------------
|
|
556
|
+
# BUG C: user-defined-UDT lvalue aliasing
|
|
557
|
+
# ------------------------------------------------------------------
|
|
558
|
+
|
|
559
|
+
def _is_udt_lvalue(self, expr) -> str | None:
|
|
560
|
+
"""If ``expr`` is a *user-defined* UDT lvalue (a bare ``Identifier`` that
|
|
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``.
|
|
564
|
+
|
|
565
|
+
Pine UDTs are reference types, so a local initialised from such an lvalue
|
|
566
|
+
and then mutated through must write back to the global. Drawing UDTs are
|
|
567
|
+
handled by the separate ``_uses_drawing`` path and are excluded here."""
|
|
386
568
|
if isinstance(expr, FuncCall):
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
584
|
+
if not isinstance(expr, Identifier):
|
|
585
|
+
return None
|
|
586
|
+
udt_t = self._udt_var_types.get(expr.name)
|
|
587
|
+
if udt_t is None or udt_t not in self._udt_defs:
|
|
588
|
+
return None
|
|
589
|
+
if udt_t in DRAWING_TYPE_TO_CPP:
|
|
590
|
+
return None
|
|
591
|
+
# Must be a known global/class-scope member (not a function param or a
|
|
592
|
+
# plain local snapshot) for write-through to be observable.
|
|
593
|
+
if expr.name in getattr(self, "_current_func_locals", set()):
|
|
594
|
+
# A function-local of UDT type that is itself a persistent ``var``
|
|
595
|
+
# member still write-through aliases; but a plain inline local does
|
|
596
|
+
# not represent shared state. Only treat ``var`` func-locals (in
|
|
597
|
+
# func_var_members) as aliasable shared state.
|
|
598
|
+
fname = getattr(self, "_active_func_name", None)
|
|
599
|
+
var_locals = {n for n, _, _ in self.ctx.func_var_members.get(fname, [])} if fname else set()
|
|
600
|
+
if expr.name not in var_locals:
|
|
601
|
+
return None
|
|
602
|
+
return udt_t
|
|
603
|
+
|
|
604
|
+
def _udt_lvalue_selection_type(self, expr) -> str | None:
|
|
605
|
+
"""UDT type if ``expr`` is a UDT lvalue OR a ternary/switch whose every
|
|
606
|
+
selectable branch is a UDT lvalue of the SAME user-defined UDT type.
|
|
607
|
+
Returns ``None`` otherwise (so plain ``UDT a = b`` value-snapshots, calls,
|
|
608
|
+
``.new(...)`` ctors, and mixed/non-lvalue selections never alias)."""
|
|
609
|
+
direct = self._is_udt_lvalue(expr)
|
|
610
|
+
if direct is not None:
|
|
611
|
+
return direct
|
|
612
|
+
branches: list = []
|
|
613
|
+
if isinstance(expr, Ternary):
|
|
614
|
+
branches = [expr.true_val, expr.false_val]
|
|
615
|
+
elif isinstance(expr, SwitchStmt):
|
|
616
|
+
for _case_expr, stmts in (expr.cases or []):
|
|
617
|
+
if not stmts:
|
|
618
|
+
return None
|
|
619
|
+
last = stmts[-1]
|
|
620
|
+
branches.append(last.expr if isinstance(last, ExprStmt) else last)
|
|
621
|
+
if expr.default_body:
|
|
622
|
+
last = expr.default_body[-1]
|
|
623
|
+
branches.append(last.expr if isinstance(last, ExprStmt) else last)
|
|
624
|
+
else:
|
|
625
|
+
return None
|
|
626
|
+
if not branches:
|
|
627
|
+
return None
|
|
628
|
+
types = {self._is_udt_lvalue(b) for b in branches}
|
|
629
|
+
if len(types) == 1 and None not in types:
|
|
630
|
+
return next(iter(types))
|
|
631
|
+
return None
|
|
632
|
+
|
|
633
|
+
def _udt_local_alias_kind(self, node: VarDecl) -> tuple[str, str] | None:
|
|
634
|
+
"""Decide whether a hintless/typed local UDT declaration must ALIAS the
|
|
635
|
+
global(s) it selects rather than value-copy (BUG C).
|
|
636
|
+
|
|
637
|
+
Returns ``("ref", udt_type)`` for a non-rebinding reference alias,
|
|
638
|
+
``("ptr", udt_type)`` for a pointer alias (the local is later reassigned
|
|
639
|
+
to a *different* UDT lvalue, which a C++ reference cannot do), or
|
|
640
|
+
``None`` to keep the existing value-copy semantics.
|
|
641
|
+
|
|
642
|
+
Conditions (all required):
|
|
643
|
+
* RHS is a UDT lvalue or a ternary/switch selecting same-typed UDT
|
|
644
|
+
lvalues (``_udt_lvalue_selection_type``).
|
|
645
|
+
* The local is MUTATED later in the enclosing function body
|
|
646
|
+
(``local.field := ...``) — a pure read-only snapshot needn't alias.
|
|
647
|
+
|
|
648
|
+
The mutation requirement is the safety guard: a local that is only read
|
|
649
|
+
keeps value semantics, and a local initialised from a non-lvalue (a
|
|
650
|
+
``.new()`` ctor, a function return, or a plain local copy) returns
|
|
651
|
+
``None`` here, preserving intentional independent-copy semantics."""
|
|
652
|
+
from ..ast_nodes import Assignment
|
|
653
|
+
body = getattr(self, "_current_func_body", None)
|
|
654
|
+
if body is None:
|
|
655
|
+
return None
|
|
656
|
+
udt_t = self._udt_lvalue_selection_type(node.value)
|
|
657
|
+
if udt_t is None:
|
|
658
|
+
return None
|
|
659
|
+
name = node.name
|
|
660
|
+
mutated = False
|
|
661
|
+
rebinds_to_other_lvalue = False
|
|
662
|
+
for stmt in self._walk_ast_list(body):
|
|
663
|
+
if not isinstance(stmt, Assignment):
|
|
664
|
+
continue
|
|
665
|
+
tgt = stmt.target
|
|
666
|
+
# Mutation through the local: ``p.field := ...``
|
|
667
|
+
if (isinstance(tgt, MemberAccess)
|
|
668
|
+
and isinstance(tgt.object, Identifier)
|
|
669
|
+
and tgt.object.name == name):
|
|
670
|
+
mutated = True
|
|
671
|
+
# Rebind of the local itself to another UDT lvalue: ``p := other``
|
|
672
|
+
elif isinstance(tgt, Identifier) and tgt.name == name:
|
|
673
|
+
if self._udt_lvalue_selection_type(stmt.value) is not None:
|
|
674
|
+
rebinds_to_other_lvalue = True
|
|
675
|
+
else:
|
|
676
|
+
# Reassigned to a non-lvalue (e.g. ``.new()`` / a copy):
|
|
677
|
+
# aliasing would be wrong; bail to value-copy.
|
|
678
|
+
return None
|
|
679
|
+
if not mutated:
|
|
680
|
+
return None
|
|
681
|
+
return ("ptr" if rebinds_to_other_lvalue else "ref"), udt_t
|
|
682
|
+
|
|
683
|
+
# ------------------------------------------------------------------
|
|
684
|
+
# BUG 2: collection (array / map / matrix) lvalue aliasing
|
|
685
|
+
# ------------------------------------------------------------------
|
|
686
|
+
|
|
687
|
+
def _collection_lvalue_spec(self, expr):
|
|
688
|
+
"""If ``expr`` is a bare ``Identifier`` naming an array/map/matrix
|
|
689
|
+
var/global member, return its ``TypeSpec``; else ``None``. Pine
|
|
690
|
+
collections are reference types, so a local bound to such an lvalue and
|
|
691
|
+
then mutated through must ALIAS it, not value-copy."""
|
|
692
|
+
if not isinstance(expr, Identifier):
|
|
693
|
+
return None
|
|
694
|
+
name = expr.name
|
|
695
|
+
if name in self._matrix_specs:
|
|
696
|
+
return self._matrix_specs[name]
|
|
697
|
+
spec = self._collection_types.get(name)
|
|
698
|
+
if spec is not None and spec.kind in ("array", "map", "matrix"):
|
|
699
|
+
return spec
|
|
700
|
+
if name in self._array_vars:
|
|
701
|
+
return self._array_spec_for_name(name)
|
|
702
|
+
if name in self._map_vars:
|
|
703
|
+
return self._map_spec_for_name(name)
|
|
704
|
+
return None
|
|
705
|
+
|
|
706
|
+
def _collection_lvalue_selection_spec(self, expr):
|
|
707
|
+
"""``TypeSpec`` if ``expr`` is a collection lvalue OR a ternary/switch
|
|
708
|
+
whose every selectable branch is a collection lvalue of the SAME C++
|
|
709
|
+
type; ``None`` otherwise (so ``array.new(...)`` ctors, copies, function
|
|
710
|
+
returns, and mixed selections keep value-copy semantics). Mirrors
|
|
711
|
+
``_udt_lvalue_selection_type`` for the BUG-2 collection-alias path."""
|
|
712
|
+
direct = self._collection_lvalue_spec(expr)
|
|
713
|
+
if direct is not None:
|
|
714
|
+
return direct
|
|
715
|
+
branches: list = []
|
|
716
|
+
if isinstance(expr, Ternary):
|
|
717
|
+
branches = [expr.true_val, expr.false_val]
|
|
718
|
+
elif isinstance(expr, SwitchStmt):
|
|
719
|
+
for _case_expr, stmts in (expr.cases or []):
|
|
720
|
+
if not stmts:
|
|
721
|
+
return None
|
|
722
|
+
last = stmts[-1]
|
|
723
|
+
branches.append(last.expr if isinstance(last, ExprStmt) else last)
|
|
724
|
+
if expr.default_body:
|
|
725
|
+
last = expr.default_body[-1]
|
|
726
|
+
branches.append(last.expr if isinstance(last, ExprStmt) else last)
|
|
727
|
+
else:
|
|
728
|
+
return None
|
|
729
|
+
if not branches:
|
|
730
|
+
return None
|
|
731
|
+
specs = [self._collection_lvalue_spec(b) for b in branches]
|
|
732
|
+
if any(s is None for s in specs):
|
|
733
|
+
return None
|
|
734
|
+
cpp_types = {self._type_spec_to_cpp(s) for s in specs}
|
|
735
|
+
if len(cpp_types) == 1:
|
|
736
|
+
return specs[0]
|
|
737
|
+
return None
|
|
738
|
+
|
|
739
|
+
def _collection_local_must_alias(self, node) -> bool:
|
|
740
|
+
"""True when the local ``node`` declares an alias of an existing
|
|
741
|
+
collection lvalue that is later MUTATED in the enclosing function body
|
|
742
|
+
(``local.push/unshift/insert/remove/set/clear/pop/...``). A purely-read
|
|
743
|
+
local needn't alias; a local REASSIGNED to a different value can't be a
|
|
744
|
+
C++ reference, so it bails to value-copy (returns ``False``)."""
|
|
745
|
+
from ..ast_nodes import Assignment
|
|
746
|
+
body = getattr(self, "_current_func_body", None)
|
|
747
|
+
if body is None:
|
|
748
|
+
return False
|
|
749
|
+
name = node.name
|
|
750
|
+
mutated = False
|
|
751
|
+
for stmt in self._walk_ast_list(body):
|
|
752
|
+
# Rebind of the local itself (``orderBlocks := other``) — a C++
|
|
753
|
+
# reference cannot rebind, so keep value-copy semantics.
|
|
754
|
+
if (isinstance(stmt, Assignment)
|
|
755
|
+
and isinstance(stmt.target, Identifier)
|
|
756
|
+
and stmt.target.name == name):
|
|
757
|
+
return False
|
|
758
|
+
if (isinstance(stmt, FuncCall)
|
|
759
|
+
and isinstance(stmt.callee, MemberAccess)
|
|
760
|
+
and isinstance(stmt.callee.object, Identifier)
|
|
761
|
+
and stmt.callee.object.name == name
|
|
762
|
+
and stmt.callee.member in COLLECTION_MUTATING_METHODS):
|
|
763
|
+
mutated = True
|
|
764
|
+
return mutated
|
|
765
|
+
|
|
766
|
+
def _walk_ast_list(self, stmts):
|
|
767
|
+
"""Yield every node within a list of statements (depth-first)."""
|
|
768
|
+
for s in stmts:
|
|
769
|
+
yield from self._walk_ast(s)
|
|
770
|
+
|
|
771
|
+
def _addr_of_udt_selection(self, expr, local_name: str):
|
|
772
|
+
"""Render the address-of form of a UDT lvalue selection for a pointer
|
|
773
|
+
alias (BUG C rebind case): ``other`` -> ``&(other)``;
|
|
774
|
+
``cond ? a : b`` -> ``(cond ? &(a) : &(b))``. The selectable branches are
|
|
775
|
+
guaranteed (by ``_udt_lvalue_selection_type``) to be UDT lvalues."""
|
|
776
|
+
if isinstance(expr, Identifier):
|
|
777
|
+
return f"&({self._safe_name(expr.name)})"
|
|
778
|
+
if isinstance(expr, Ternary):
|
|
779
|
+
cond = self._visit_expr(expr.condition)
|
|
780
|
+
t = self._addr_of_udt_selection(expr.true_val, local_name)
|
|
781
|
+
f = self._addr_of_udt_selection(expr.false_val, local_name)
|
|
782
|
+
return f"({cond} ? {t} : {f})"
|
|
783
|
+
# Switch selection: lower to nested ternaries over case equality. Rare in
|
|
784
|
+
# practice; fall back to address-of the whole lowered expression.
|
|
785
|
+
return f"&({self._visit_expr(expr)})"
|
|
391
786
|
|
|
392
787
|
def _infer_cpp_type_for_security_elem(self, node) -> str:
|
|
393
788
|
"""C++ type for one element of the ``request.security(..., expr, ...)`` payload.
|
|
@@ -459,6 +854,8 @@ class TypeInferer:
|
|
|
459
854
|
return "double"
|
|
460
855
|
if node.name in self._current_func_param_types:
|
|
461
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]
|
|
462
859
|
sym = self.ctx.symbols.resolve(node.name)
|
|
463
860
|
if sym is not None and getattr(sym, "type_spec", None) is not None:
|
|
464
861
|
return self._type_spec_to_cpp(sym.type_spec)
|
|
@@ -467,6 +864,12 @@ class TypeInferer:
|
|
|
467
864
|
return "double"
|
|
468
865
|
if isinstance(node, FuncCall):
|
|
469
866
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
867
|
+
# Drawing scalar getter return type (get_text -> std::string,
|
|
868
|
+
# get_x* -> int64_t, get_y*/get_price/get_top/get_bottom -> double).
|
|
869
|
+
if getattr(self, "_uses_drawing", False):
|
|
870
|
+
_dret = self._drawing_call_return_cpp(node)
|
|
871
|
+
if _dret is not None:
|
|
872
|
+
return _dret
|
|
470
873
|
if func_name in ("time", "time_close") and namespace is None and node.args:
|
|
471
874
|
return "int64_t"
|
|
472
875
|
if func_name == "timestamp" and namespace is None:
|
|
@@ -491,6 +894,12 @@ class TypeInferer:
|
|
|
491
894
|
if namespace == "str":
|
|
492
895
|
if func_name == "split":
|
|
493
896
|
return "std::vector<std::string>"
|
|
897
|
+
if func_name in ("contains", "startswith", "endswith"):
|
|
898
|
+
return "bool"
|
|
899
|
+
if func_name == "tonumber":
|
|
900
|
+
return "double"
|
|
901
|
+
if func_name in ("length", "pos"):
|
|
902
|
+
return "int"
|
|
494
903
|
return "std::string"
|
|
495
904
|
if namespace == "ta" and func_name == "pivot_point_levels":
|
|
496
905
|
return "std::vector<double>"
|
|
@@ -539,6 +948,12 @@ class TypeInferer:
|
|
|
539
948
|
# pine_str_tostring); bare reads must declare std::string.
|
|
540
949
|
if ename == "format":
|
|
541
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"
|
|
542
957
|
# syminfo.* type inference: look up in SYMINFO_MEMBER_MAP
|
|
543
958
|
# and derive C++ type from the expression (na<T>() or function call).
|
|
544
959
|
if ename == "syminfo":
|
|
@@ -546,6 +961,9 @@ class TypeInferer:
|
|
|
546
961
|
sym_key = f"syminfo.{node.member}"
|
|
547
962
|
if sym_key in _pf_sigs.SYMINFO_VARIABLES:
|
|
548
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)
|
|
549
967
|
if isinstance(node, Ternary):
|
|
550
968
|
tt = self._infer_type(node.true_val)
|
|
551
969
|
ft = self._infer_type(node.false_val)
|
|
@@ -553,6 +971,12 @@ class TypeInferer:
|
|
|
553
971
|
return tt if tt.startswith("std::vector") else ft
|
|
554
972
|
if tt == "std::string" or ft == "std::string":
|
|
555
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"
|
|
556
980
|
return tt
|
|
557
981
|
# Block-as-expression cases: read the type of the last statement of
|
|
558
982
|
# the first branch / case; matches Pine semantics for ``x = if...``.
|
|
@@ -575,16 +999,24 @@ class TypeInferer:
|
|
|
575
999
|
def _infer_tuple_types(self, func_node: FuncDef, count: int) -> list[str]:
|
|
576
1000
|
"""Infer the C++ type of each element returned by a tuple-returning function.
|
|
577
1001
|
|
|
578
|
-
Builds a lightweight local-type map from the function's
|
|
579
|
-
|
|
580
|
-
``[a, b, c]`` literal resolve precisely
|
|
581
|
-
``
|
|
1002
|
+
Builds a lightweight local-type map from the function's ``VarDecl``s
|
|
1003
|
+
(including ones nested inside if/for/switch blocks) so identifiers
|
|
1004
|
+
referenced inside the final ``[a, b, c]`` literal resolve precisely.
|
|
1005
|
+
An explicit type hint wins (``string tag = na`` -> ``std::string``,
|
|
1006
|
+
not the ``double`` implied by ``na``); otherwise the initializer
|
|
1007
|
+
expression is inferred. Falls back to ``_infer_type`` when no local
|
|
1008
|
+
declaration matches."""
|
|
582
1009
|
if not func_node.body:
|
|
583
1010
|
return ["double"] * count
|
|
584
1011
|
|
|
585
1012
|
local_types: dict[str, str] = {}
|
|
586
|
-
for stmt in func_node
|
|
587
|
-
if isinstance(stmt, VarDecl) and stmt.value is not None:
|
|
1013
|
+
for stmt in self._walk_ast(func_node):
|
|
1014
|
+
if isinstance(stmt, VarDecl) and stmt.value is not None and stmt.name:
|
|
1015
|
+
if stmt.type_hint:
|
|
1016
|
+
spec = self._type_spec_from_hint_name(stmt.type_hint)
|
|
1017
|
+
if spec is not None:
|
|
1018
|
+
local_types[stmt.name] = self._type_spec_to_cpp(spec)
|
|
1019
|
+
continue
|
|
588
1020
|
local_types[stmt.name] = self._infer_type(stmt.value)
|
|
589
1021
|
|
|
590
1022
|
last_stmt = func_node.body[-1]
|