@pineforge/codegen-pyodide 0.8.0 → 0.8.1
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 +453 -15
- package/pineforge_codegen/analyzer/call_handlers.py +200 -11
- package/pineforge_codegen/analyzer/contracts.py +62 -0
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +509 -44
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +286 -27
- package/pineforge_codegen/codegen/input.py +14 -0
- package/pineforge_codegen/codegen/security.py +294 -13
- package/pineforge_codegen/codegen/tables.py +59 -3
- package/pineforge_codegen/codegen/types.py +369 -19
- package/pineforge_codegen/codegen/visit_call.py +147 -22
- package/pineforge_codegen/codegen/visit_expr.py +54 -0
- package/pineforge_codegen/codegen/visit_stmt.py +161 -14
- package/pineforge_codegen/parser.py +56 -23
- package/pineforge_codegen/support_checker.py +281 -17
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +2 -8
- 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"):
|
|
@@ -179,6 +214,12 @@ class TypeInferer:
|
|
|
179
214
|
return self._collection_types[node.name]
|
|
180
215
|
if node.name in self._udt_var_types:
|
|
181
216
|
return TypeSpec.udt(self._udt_var_types[node.name])
|
|
217
|
+
# Drawing-typed method/function parameter (L.6d / U.5): a ``line ln``
|
|
218
|
+
# method receiver registers in _udt_param_udt so its body getters
|
|
219
|
+
# resolve to the drawing udt and dispatch through the §4.3 path.
|
|
220
|
+
_pu = getattr(self, "_udt_param_udt", None)
|
|
221
|
+
if _pu and node.name in _pu and _pu[node.name] in DRAWING_TYPE_TO_CPP:
|
|
222
|
+
return TypeSpec.udt(_pu[node.name])
|
|
182
223
|
sym = self.ctx.symbols.resolve(node.name)
|
|
183
224
|
if sym is not None and getattr(sym, "type_spec", None) is not None:
|
|
184
225
|
return sym.type_spec
|
|
@@ -190,12 +231,27 @@ class TypeInferer:
|
|
|
190
231
|
return None
|
|
191
232
|
if isinstance(node, FuncCall):
|
|
192
233
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
234
|
+
# ticker.* constructors (inherit/standard/heikinashi) return a symbol
|
|
235
|
+
# string; without this the member-type inference defaults to double
|
|
236
|
+
# and a ``haTicker = ticker.heikinashi(...)`` global mis-declares as
|
|
237
|
+
# double then assigns a std::string. (Analyzer agrees: ticker.* -> STRING.)
|
|
238
|
+
if namespace == "ticker":
|
|
239
|
+
return TypeSpec.primitive("string")
|
|
193
240
|
targs = self._template_args_from_call(node)
|
|
241
|
+
# Drawing-objects-as-data return typing (spec §4.5 DRAWING_RETURN_SPECS):
|
|
242
|
+
# *.new / *.copy -> handle of the self-type; linefill.get_line* -> line.
|
|
243
|
+
if namespace in DRAWING_NS:
|
|
244
|
+
if func_name in ("new", "copy"):
|
|
245
|
+
return TypeSpec.udt(namespace)
|
|
246
|
+
if namespace == "linefill" and func_name in ("get_line1", "get_line2"):
|
|
247
|
+
return TypeSpec.udt("line")
|
|
248
|
+
if self._is_chart_point_callee(node.callee):
|
|
249
|
+
return TypeSpec.udt("chart.point")
|
|
194
250
|
if namespace == "str" and func_name == "split":
|
|
195
251
|
return TypeSpec.array(TypeSpec.primitive("string"))
|
|
196
252
|
if namespace == "array" and func_name in (
|
|
197
253
|
"new", "new_float", "new_int", "new_bool", "new_string", "from",
|
|
198
|
-
):
|
|
254
|
+
) or (namespace == "array" and func_name in ARRAY_DRAWING_NEW_CTORS):
|
|
199
255
|
if func_name == "new_int":
|
|
200
256
|
return TypeSpec.array(TypeSpec.primitive("int"))
|
|
201
257
|
if func_name == "new_bool":
|
|
@@ -204,6 +260,10 @@ class TypeInferer:
|
|
|
204
260
|
return TypeSpec.array(TypeSpec.primitive("string"))
|
|
205
261
|
if func_name == "new_float":
|
|
206
262
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
263
|
+
if func_name in ARRAY_DRAWING_NEW_CTORS:
|
|
264
|
+
# array.new_line()/new_box()/new_label()/new_linefill() ->
|
|
265
|
+
# std::vector<Line/Box/Label/Linefill> (typed alias of new<T>).
|
|
266
|
+
return TypeSpec.array(TypeSpec.udt(ARRAY_DRAWING_NEW_CTORS[func_name]))
|
|
207
267
|
if targs:
|
|
208
268
|
return TypeSpec.array(self._type_spec_from_hint_name(targs[0]) or TypeSpec.udt(targs[0]))
|
|
209
269
|
if func_name == "from" and node.args:
|
|
@@ -249,6 +309,14 @@ class TypeInferer:
|
|
|
249
309
|
return recv_spec.element
|
|
250
310
|
if func_name == "eigenvalues":
|
|
251
311
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
312
|
+
# Drawing method-form: ``a.copy()`` -> same handle type;
|
|
313
|
+
# ``lf.get_line1()`` -> line. (L-N6 alias-vs-copy typing.)
|
|
314
|
+
if (recv_spec is not None and recv_spec.kind == "udt"
|
|
315
|
+
and recv_spec.name in DRAWING_TYPE_TO_CPP):
|
|
316
|
+
if func_name == "copy":
|
|
317
|
+
return recv_spec
|
|
318
|
+
if recv_spec.name == "linefill" and func_name in ("get_line1", "get_line2"):
|
|
319
|
+
return TypeSpec.udt("line")
|
|
252
320
|
return None
|
|
253
321
|
|
|
254
322
|
# ------------------------------------------------------------------
|
|
@@ -345,6 +413,22 @@ class TypeInferer:
|
|
|
345
413
|
if node.type_hint in self._udt_defs:
|
|
346
414
|
return node.type_hint
|
|
347
415
|
return PINE_TYPE_TO_CPP.get(node.type_hint, "double")
|
|
416
|
+
# Drawing handle local (L-N6): a hintless local whose RHS resolves to a
|
|
417
|
+
# drawing udt must declare as the handle struct, not the analyzer's
|
|
418
|
+
# scalar default. Covers ``ln = arr.get(i)``, alias ``b = a``, field read
|
|
419
|
+
# ``lvl.ln``, and ``c = a.copy()``. Also records _udt_var_types so later
|
|
420
|
+
# uses (``ln.set_x2(...)`` / ``ln.slope()``) resolve to the drawing udt.
|
|
421
|
+
if getattr(self, "_uses_drawing", False):
|
|
422
|
+
rhs_spec = self._type_spec_from_expr(node.value)
|
|
423
|
+
if (rhs_spec is not None and rhs_spec.kind == "udt"
|
|
424
|
+
and rhs_spec.name in DRAWING_TYPE_TO_CPP):
|
|
425
|
+
self._udt_var_types.setdefault(node.name, rhs_spec.name)
|
|
426
|
+
return DRAWING_TYPE_TO_CPP[rhs_spec.name]
|
|
427
|
+
# Scalar drawing getter local (get_text -> std::string, etc.).
|
|
428
|
+
if isinstance(node.value, FuncCall):
|
|
429
|
+
_dret = self._drawing_call_return_cpp(node.value)
|
|
430
|
+
if _dret is not None:
|
|
431
|
+
return _dret
|
|
348
432
|
sym = self.ctx.symbols.resolve(node.name)
|
|
349
433
|
if sym is not None:
|
|
350
434
|
inferred = self._infer_type(node.value)
|
|
@@ -369,25 +453,273 @@ class TypeInferer:
|
|
|
369
453
|
return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
370
454
|
return "double"
|
|
371
455
|
|
|
456
|
+
def _expr_is_int64_builtin(self, expr) -> bool:
|
|
457
|
+
"""True if ``expr`` is a top-level int64-returning Pine builtin: either a
|
|
458
|
+
call to one of ``INT64_BUILTINS`` (``time(...)``, ``timestamp(...)``, …)
|
|
459
|
+
or a bare ``Identifier`` spelled like ``time`` / ``time_close`` /
|
|
460
|
+
``timenow`` (which Pine exposes as a value, not a call)."""
|
|
461
|
+
from .tables import INT64_BUILTINS, INT64_BUILTIN_IDENTIFIERS
|
|
462
|
+
if expr is None:
|
|
463
|
+
return False
|
|
464
|
+
if isinstance(expr, FuncCall):
|
|
465
|
+
func_name, namespace = self._resolve_callee(expr.callee)
|
|
466
|
+
return namespace is None and func_name in INT64_BUILTINS
|
|
467
|
+
if isinstance(expr, Identifier):
|
|
468
|
+
return expr.name in INT64_BUILTIN_IDENTIFIERS
|
|
469
|
+
return False
|
|
470
|
+
|
|
471
|
+
def _int64_reassign_targets(self) -> set[str]:
|
|
472
|
+
"""Names of vars that are reassigned (``:=``/``=``) anywhere in the AST
|
|
473
|
+
with an RHS that is a top-level int64-returning builtin. Cached on the
|
|
474
|
+
instance. Pine ``int`` collapses these to 32-bit, but the runtime stores
|
|
475
|
+
the epoch in 64 bits, so the member must be promoted to ``int64_t``."""
|
|
476
|
+
cached = getattr(self, "_int64_reassign_cache", None)
|
|
477
|
+
if cached is not None:
|
|
478
|
+
return cached
|
|
479
|
+
from ..ast_nodes import Assignment
|
|
480
|
+
targets: set[str] = set()
|
|
481
|
+
ast = getattr(self.ctx, "ast", None)
|
|
482
|
+
if ast is not None:
|
|
483
|
+
for node in self._walk_ast(ast):
|
|
484
|
+
if (isinstance(node, Assignment)
|
|
485
|
+
and isinstance(node.target, Identifier)
|
|
486
|
+
and self._expr_is_int64_builtin(node.value)):
|
|
487
|
+
targets.add(node.target.name)
|
|
488
|
+
self._int64_reassign_cache = targets
|
|
489
|
+
return targets
|
|
490
|
+
|
|
372
491
|
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
|
-
|
|
492
|
+
"""True if ``name``'s initializer OR any ``:=``/``=`` reassignment has an
|
|
493
|
+
RHS that is a top-level int64-returning builtin (``time``, ``time_close``,
|
|
494
|
+
``timenow``, ``timestamp``, ``time_tradingday``). The Pine type system
|
|
495
|
+
collapses these to ``int`` but the engine encodes the ``na`` sentinel
|
|
496
|
+
(and the full epoch-ms value, which overflows int32) in 64 bits, so
|
|
497
|
+
storing into ``int`` silently corrupts both the value and na detection.
|
|
498
|
+
A reassignment like ``var int entryTime = na`` then ``entryTime := time``
|
|
499
|
+
must promote even though the *initializer* alone is ``na``.
|
|
378
500
|
"""
|
|
379
|
-
from .tables import INT64_BUILTINS
|
|
380
501
|
expr = (
|
|
381
502
|
self.ctx.global_expr_map.get(name)
|
|
382
503
|
or self.ctx.var_member_init_exprs.get(name)
|
|
383
504
|
)
|
|
384
|
-
if expr
|
|
505
|
+
if self._expr_is_int64_builtin(expr):
|
|
506
|
+
return True
|
|
507
|
+
return name in self._int64_reassign_targets()
|
|
508
|
+
|
|
509
|
+
# ------------------------------------------------------------------
|
|
510
|
+
# BUG C: user-defined-UDT lvalue aliasing
|
|
511
|
+
# ------------------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
def _is_udt_lvalue(self, expr) -> str | None:
|
|
514
|
+
"""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; else ``None``.
|
|
517
|
+
|
|
518
|
+
Pine UDTs are reference types, so a local initialised from such an lvalue
|
|
519
|
+
and then mutated through must write back to the global. Drawing UDTs are
|
|
520
|
+
handled by the separate ``_uses_drawing`` path and are excluded here."""
|
|
521
|
+
if not isinstance(expr, Identifier):
|
|
522
|
+
return None
|
|
523
|
+
udt_t = self._udt_var_types.get(expr.name)
|
|
524
|
+
if udt_t is None or udt_t not in self._udt_defs:
|
|
525
|
+
return None
|
|
526
|
+
if udt_t in DRAWING_TYPE_TO_CPP:
|
|
527
|
+
return None
|
|
528
|
+
# Must be a known global/class-scope member (not a function param or a
|
|
529
|
+
# plain local snapshot) for write-through to be observable.
|
|
530
|
+
if expr.name in getattr(self, "_current_func_locals", set()):
|
|
531
|
+
# A function-local of UDT type that is itself a persistent ``var``
|
|
532
|
+
# member still write-through aliases; but a plain inline local does
|
|
533
|
+
# not represent shared state. Only treat ``var`` func-locals (in
|
|
534
|
+
# func_var_members) as aliasable shared state.
|
|
535
|
+
fname = getattr(self, "_active_func_name", None)
|
|
536
|
+
var_locals = {n for n, _, _ in self.ctx.func_var_members.get(fname, [])} if fname else set()
|
|
537
|
+
if expr.name not in var_locals:
|
|
538
|
+
return None
|
|
539
|
+
return udt_t
|
|
540
|
+
|
|
541
|
+
def _udt_lvalue_selection_type(self, expr) -> str | None:
|
|
542
|
+
"""UDT type if ``expr`` is a UDT lvalue OR a ternary/switch whose every
|
|
543
|
+
selectable branch is a UDT lvalue of the SAME user-defined UDT type.
|
|
544
|
+
Returns ``None`` otherwise (so plain ``UDT a = b`` value-snapshots, calls,
|
|
545
|
+
``.new(...)`` ctors, and mixed/non-lvalue selections never alias)."""
|
|
546
|
+
direct = self._is_udt_lvalue(expr)
|
|
547
|
+
if direct is not None:
|
|
548
|
+
return direct
|
|
549
|
+
branches: list = []
|
|
550
|
+
if isinstance(expr, Ternary):
|
|
551
|
+
branches = [expr.true_val, expr.false_val]
|
|
552
|
+
elif isinstance(expr, SwitchStmt):
|
|
553
|
+
for _case_expr, stmts in (expr.cases or []):
|
|
554
|
+
if not stmts:
|
|
555
|
+
return None
|
|
556
|
+
last = stmts[-1]
|
|
557
|
+
branches.append(last.expr if isinstance(last, ExprStmt) else last)
|
|
558
|
+
if expr.default_body:
|
|
559
|
+
last = expr.default_body[-1]
|
|
560
|
+
branches.append(last.expr if isinstance(last, ExprStmt) else last)
|
|
561
|
+
else:
|
|
562
|
+
return None
|
|
563
|
+
if not branches:
|
|
564
|
+
return None
|
|
565
|
+
types = {self._is_udt_lvalue(b) for b in branches}
|
|
566
|
+
if len(types) == 1 and None not in types:
|
|
567
|
+
return next(iter(types))
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
def _udt_local_alias_kind(self, node: VarDecl) -> tuple[str, str] | None:
|
|
571
|
+
"""Decide whether a hintless/typed local UDT declaration must ALIAS the
|
|
572
|
+
global(s) it selects rather than value-copy (BUG C).
|
|
573
|
+
|
|
574
|
+
Returns ``("ref", udt_type)`` for a non-rebinding reference alias,
|
|
575
|
+
``("ptr", udt_type)`` for a pointer alias (the local is later reassigned
|
|
576
|
+
to a *different* UDT lvalue, which a C++ reference cannot do), or
|
|
577
|
+
``None`` to keep the existing value-copy semantics.
|
|
578
|
+
|
|
579
|
+
Conditions (all required):
|
|
580
|
+
* RHS is a UDT lvalue or a ternary/switch selecting same-typed UDT
|
|
581
|
+
lvalues (``_udt_lvalue_selection_type``).
|
|
582
|
+
* The local is MUTATED later in the enclosing function body
|
|
583
|
+
(``local.field := ...``) — a pure read-only snapshot needn't alias.
|
|
584
|
+
|
|
585
|
+
The mutation requirement is the safety guard: a local that is only read
|
|
586
|
+
keeps value semantics, and a local initialised from a non-lvalue (a
|
|
587
|
+
``.new()`` ctor, a function return, or a plain local copy) returns
|
|
588
|
+
``None`` here, preserving intentional independent-copy semantics."""
|
|
589
|
+
from ..ast_nodes import Assignment
|
|
590
|
+
body = getattr(self, "_current_func_body", None)
|
|
591
|
+
if body is None:
|
|
592
|
+
return None
|
|
593
|
+
udt_t = self._udt_lvalue_selection_type(node.value)
|
|
594
|
+
if udt_t is None:
|
|
595
|
+
return None
|
|
596
|
+
name = node.name
|
|
597
|
+
mutated = False
|
|
598
|
+
rebinds_to_other_lvalue = False
|
|
599
|
+
for stmt in self._walk_ast_list(body):
|
|
600
|
+
if not isinstance(stmt, Assignment):
|
|
601
|
+
continue
|
|
602
|
+
tgt = stmt.target
|
|
603
|
+
# Mutation through the local: ``p.field := ...``
|
|
604
|
+
if (isinstance(tgt, MemberAccess)
|
|
605
|
+
and isinstance(tgt.object, Identifier)
|
|
606
|
+
and tgt.object.name == name):
|
|
607
|
+
mutated = True
|
|
608
|
+
# Rebind of the local itself to another UDT lvalue: ``p := other``
|
|
609
|
+
elif isinstance(tgt, Identifier) and tgt.name == name:
|
|
610
|
+
if self._udt_lvalue_selection_type(stmt.value) is not None:
|
|
611
|
+
rebinds_to_other_lvalue = True
|
|
612
|
+
else:
|
|
613
|
+
# Reassigned to a non-lvalue (e.g. ``.new()`` / a copy):
|
|
614
|
+
# aliasing would be wrong; bail to value-copy.
|
|
615
|
+
return None
|
|
616
|
+
if not mutated:
|
|
617
|
+
return None
|
|
618
|
+
return ("ptr" if rebinds_to_other_lvalue else "ref"), udt_t
|
|
619
|
+
|
|
620
|
+
# ------------------------------------------------------------------
|
|
621
|
+
# BUG 2: collection (array / map / matrix) lvalue aliasing
|
|
622
|
+
# ------------------------------------------------------------------
|
|
623
|
+
|
|
624
|
+
def _collection_lvalue_spec(self, expr):
|
|
625
|
+
"""If ``expr`` is a bare ``Identifier`` naming an array/map/matrix
|
|
626
|
+
var/global member, return its ``TypeSpec``; else ``None``. Pine
|
|
627
|
+
collections are reference types, so a local bound to such an lvalue and
|
|
628
|
+
then mutated through must ALIAS it, not value-copy."""
|
|
629
|
+
if not isinstance(expr, Identifier):
|
|
630
|
+
return None
|
|
631
|
+
name = expr.name
|
|
632
|
+
if name in self._matrix_specs:
|
|
633
|
+
return self._matrix_specs[name]
|
|
634
|
+
spec = self._collection_types.get(name)
|
|
635
|
+
if spec is not None and spec.kind in ("array", "map", "matrix"):
|
|
636
|
+
return spec
|
|
637
|
+
if name in self._array_vars:
|
|
638
|
+
return self._array_spec_for_name(name)
|
|
639
|
+
if name in self._map_vars:
|
|
640
|
+
return self._map_spec_for_name(name)
|
|
641
|
+
return None
|
|
642
|
+
|
|
643
|
+
def _collection_lvalue_selection_spec(self, expr):
|
|
644
|
+
"""``TypeSpec`` if ``expr`` is a collection lvalue OR a ternary/switch
|
|
645
|
+
whose every selectable branch is a collection lvalue of the SAME C++
|
|
646
|
+
type; ``None`` otherwise (so ``array.new(...)`` ctors, copies, function
|
|
647
|
+
returns, and mixed selections keep value-copy semantics). Mirrors
|
|
648
|
+
``_udt_lvalue_selection_type`` for the BUG-2 collection-alias path."""
|
|
649
|
+
direct = self._collection_lvalue_spec(expr)
|
|
650
|
+
if direct is not None:
|
|
651
|
+
return direct
|
|
652
|
+
branches: list = []
|
|
653
|
+
if isinstance(expr, Ternary):
|
|
654
|
+
branches = [expr.true_val, expr.false_val]
|
|
655
|
+
elif isinstance(expr, SwitchStmt):
|
|
656
|
+
for _case_expr, stmts in (expr.cases or []):
|
|
657
|
+
if not stmts:
|
|
658
|
+
return None
|
|
659
|
+
last = stmts[-1]
|
|
660
|
+
branches.append(last.expr if isinstance(last, ExprStmt) else last)
|
|
661
|
+
if expr.default_body:
|
|
662
|
+
last = expr.default_body[-1]
|
|
663
|
+
branches.append(last.expr if isinstance(last, ExprStmt) else last)
|
|
664
|
+
else:
|
|
665
|
+
return None
|
|
666
|
+
if not branches:
|
|
667
|
+
return None
|
|
668
|
+
specs = [self._collection_lvalue_spec(b) for b in branches]
|
|
669
|
+
if any(s is None for s in specs):
|
|
670
|
+
return None
|
|
671
|
+
cpp_types = {self._type_spec_to_cpp(s) for s in specs}
|
|
672
|
+
if len(cpp_types) == 1:
|
|
673
|
+
return specs[0]
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
def _collection_local_must_alias(self, node) -> bool:
|
|
677
|
+
"""True when the local ``node`` declares an alias of an existing
|
|
678
|
+
collection lvalue that is later MUTATED in the enclosing function body
|
|
679
|
+
(``local.push/unshift/insert/remove/set/clear/pop/...``). A purely-read
|
|
680
|
+
local needn't alias; a local REASSIGNED to a different value can't be a
|
|
681
|
+
C++ reference, so it bails to value-copy (returns ``False``)."""
|
|
682
|
+
from ..ast_nodes import Assignment
|
|
683
|
+
body = getattr(self, "_current_func_body", None)
|
|
684
|
+
if body is None:
|
|
385
685
|
return False
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
686
|
+
name = node.name
|
|
687
|
+
mutated = False
|
|
688
|
+
for stmt in self._walk_ast_list(body):
|
|
689
|
+
# Rebind of the local itself (``orderBlocks := other``) — a C++
|
|
690
|
+
# reference cannot rebind, so keep value-copy semantics.
|
|
691
|
+
if (isinstance(stmt, Assignment)
|
|
692
|
+
and isinstance(stmt.target, Identifier)
|
|
693
|
+
and stmt.target.name == name):
|
|
694
|
+
return False
|
|
695
|
+
if (isinstance(stmt, FuncCall)
|
|
696
|
+
and isinstance(stmt.callee, MemberAccess)
|
|
697
|
+
and isinstance(stmt.callee.object, Identifier)
|
|
698
|
+
and stmt.callee.object.name == name
|
|
699
|
+
and stmt.callee.member in COLLECTION_MUTATING_METHODS):
|
|
700
|
+
mutated = True
|
|
701
|
+
return mutated
|
|
702
|
+
|
|
703
|
+
def _walk_ast_list(self, stmts):
|
|
704
|
+
"""Yield every node within a list of statements (depth-first)."""
|
|
705
|
+
for s in stmts:
|
|
706
|
+
yield from self._walk_ast(s)
|
|
707
|
+
|
|
708
|
+
def _addr_of_udt_selection(self, expr, local_name: str):
|
|
709
|
+
"""Render the address-of form of a UDT lvalue selection for a pointer
|
|
710
|
+
alias (BUG C rebind case): ``other`` -> ``&(other)``;
|
|
711
|
+
``cond ? a : b`` -> ``(cond ? &(a) : &(b))``. The selectable branches are
|
|
712
|
+
guaranteed (by ``_udt_lvalue_selection_type``) to be UDT lvalues."""
|
|
713
|
+
if isinstance(expr, Identifier):
|
|
714
|
+
return f"&({self._safe_name(expr.name)})"
|
|
715
|
+
if isinstance(expr, Ternary):
|
|
716
|
+
cond = self._visit_expr(expr.condition)
|
|
717
|
+
t = self._addr_of_udt_selection(expr.true_val, local_name)
|
|
718
|
+
f = self._addr_of_udt_selection(expr.false_val, local_name)
|
|
719
|
+
return f"({cond} ? {t} : {f})"
|
|
720
|
+
# Switch selection: lower to nested ternaries over case equality. Rare in
|
|
721
|
+
# practice; fall back to address-of the whole lowered expression.
|
|
722
|
+
return f"&({self._visit_expr(expr)})"
|
|
391
723
|
|
|
392
724
|
def _infer_cpp_type_for_security_elem(self, node) -> str:
|
|
393
725
|
"""C++ type for one element of the ``request.security(..., expr, ...)`` payload.
|
|
@@ -467,6 +799,12 @@ class TypeInferer:
|
|
|
467
799
|
return "double"
|
|
468
800
|
if isinstance(node, FuncCall):
|
|
469
801
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
802
|
+
# Drawing scalar getter return type (get_text -> std::string,
|
|
803
|
+
# get_x* -> int64_t, get_y*/get_price/get_top/get_bottom -> double).
|
|
804
|
+
if getattr(self, "_uses_drawing", False):
|
|
805
|
+
_dret = self._drawing_call_return_cpp(node)
|
|
806
|
+
if _dret is not None:
|
|
807
|
+
return _dret
|
|
470
808
|
if func_name in ("time", "time_close") and namespace is None and node.args:
|
|
471
809
|
return "int64_t"
|
|
472
810
|
if func_name == "timestamp" and namespace is None:
|
|
@@ -491,6 +829,10 @@ class TypeInferer:
|
|
|
491
829
|
if namespace == "str":
|
|
492
830
|
if func_name == "split":
|
|
493
831
|
return "std::vector<std::string>"
|
|
832
|
+
if func_name == "tonumber":
|
|
833
|
+
return "double"
|
|
834
|
+
if func_name == "length":
|
|
835
|
+
return "int"
|
|
494
836
|
return "std::string"
|
|
495
837
|
if namespace == "ta" and func_name == "pivot_point_levels":
|
|
496
838
|
return "std::vector<double>"
|
|
@@ -575,16 +917,24 @@ class TypeInferer:
|
|
|
575
917
|
def _infer_tuple_types(self, func_node: FuncDef, count: int) -> list[str]:
|
|
576
918
|
"""Infer the C++ type of each element returned by a tuple-returning function.
|
|
577
919
|
|
|
578
|
-
Builds a lightweight local-type map from the function's
|
|
579
|
-
|
|
580
|
-
``[a, b, c]`` literal resolve precisely
|
|
581
|
-
``
|
|
920
|
+
Builds a lightweight local-type map from the function's ``VarDecl``s
|
|
921
|
+
(including ones nested inside if/for/switch blocks) so identifiers
|
|
922
|
+
referenced inside the final ``[a, b, c]`` literal resolve precisely.
|
|
923
|
+
An explicit type hint wins (``string tag = na`` -> ``std::string``,
|
|
924
|
+
not the ``double`` implied by ``na``); otherwise the initializer
|
|
925
|
+
expression is inferred. Falls back to ``_infer_type`` when no local
|
|
926
|
+
declaration matches."""
|
|
582
927
|
if not func_node.body:
|
|
583
928
|
return ["double"] * count
|
|
584
929
|
|
|
585
930
|
local_types: dict[str, str] = {}
|
|
586
|
-
for stmt in func_node
|
|
587
|
-
if isinstance(stmt, VarDecl) and stmt.value is not None:
|
|
931
|
+
for stmt in self._walk_ast(func_node):
|
|
932
|
+
if isinstance(stmt, VarDecl) and stmt.value is not None and stmt.name:
|
|
933
|
+
if stmt.type_hint:
|
|
934
|
+
spec = self._type_spec_from_hint_name(stmt.type_hint)
|
|
935
|
+
if spec is not None:
|
|
936
|
+
local_types[stmt.name] = self._type_spec_to_cpp(spec)
|
|
937
|
+
continue
|
|
588
938
|
local_types[stmt.name] = self._infer_type(stmt.value)
|
|
589
939
|
|
|
590
940
|
last_stmt = func_node.body[-1]
|