@pineforge/codegen-pyodide 0.10.0 → 0.10.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/__init__.py +3 -0
- package/pineforge_codegen/analyzer/base.py +3999 -140
- package/pineforge_codegen/analyzer/call_handlers.py +662 -48
- package/pineforge_codegen/analyzer/contracts.py +104 -7
- package/pineforge_codegen/analyzer/types.py +951 -14
- package/pineforge_codegen/codegen/base.py +2140 -228
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +732 -177
- package/pineforge_codegen/codegen/security.py +1386 -132
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +224 -77
- package/pineforge_codegen/codegen/types.py +1154 -226
- package/pineforge_codegen/codegen/visit_call.py +1168 -99
- package/pineforge_codegen/codegen/visit_expr.py +283 -48
- package/pineforge_codegen/codegen/visit_stmt.py +1137 -202
- package/pineforge_codegen/finite_ta_length.py +798 -0
- package/pineforge_codegen/method_binding.py +163 -0
- package/pineforge_codegen/parser.py +48 -9
- package/pineforge_codegen/signatures.py +4 -4
- package/pineforge_codegen/support_checker.py +194 -19
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.10.0.tar.gz +0 -0
|
@@ -34,11 +34,11 @@ tables it needs come from ``codegen/tables.py``.
|
|
|
34
34
|
from __future__ import annotations
|
|
35
35
|
|
|
36
36
|
from ..ast_nodes import (
|
|
37
|
-
BinOp, BoolLiteral, ExprStmt, FuncCall, FuncDef, Identifier, IfStmt,
|
|
37
|
+
ASTNode, Assignment, BinOp, BoolLiteral, ExprStmt, FuncCall, FuncDef, Identifier, IfStmt,
|
|
38
38
|
MemberAccess, NaLiteral, NumberLiteral, StringLiteral, SwitchStmt,
|
|
39
|
-
Ternary, TupleLiteral, UnaryOp, VarDecl,
|
|
39
|
+
Subscript, Ternary, TupleLiteral, UnaryOp, VarDecl,
|
|
40
40
|
)
|
|
41
|
-
from ..symbols import PineType, TypeSpec
|
|
41
|
+
from ..symbols import PineType, TypeSpec, method_receiver_type_name
|
|
42
42
|
from .. import signatures as sigs
|
|
43
43
|
from .tables import (
|
|
44
44
|
ARRAY_DRAWING_NEW_CTORS,
|
|
@@ -47,6 +47,7 @@ from .tables import (
|
|
|
47
47
|
BAR_FIELDS,
|
|
48
48
|
DRAWING_NS,
|
|
49
49
|
DRAWING_TYPE_TO_CPP,
|
|
50
|
+
MATRIX_RETURNING_METHODS,
|
|
50
51
|
PINE_TYPE_TO_CPP,
|
|
51
52
|
TA_RETURNS_BOOL,
|
|
52
53
|
)
|
|
@@ -145,7 +146,7 @@ class TypeInferer:
|
|
|
145
146
|
if spec.kind == "array":
|
|
146
147
|
return f"std::vector<{self._type_spec_to_cpp(spec.element)}>"
|
|
147
148
|
if spec.kind == "map":
|
|
148
|
-
return f"
|
|
149
|
+
return f"PineMap<{self._type_spec_to_cpp(spec.key)}, {self._type_spec_to_cpp(spec.value)}>"
|
|
149
150
|
if spec.kind == "matrix":
|
|
150
151
|
elem = self._type_spec_to_cpp(spec.element)
|
|
151
152
|
if spec.element.kind == "primitive" and spec.element.name == "float":
|
|
@@ -153,17 +154,97 @@ class TypeInferer:
|
|
|
153
154
|
return f"PineGenericMatrix<{elem}>"
|
|
154
155
|
return "double"
|
|
155
156
|
|
|
157
|
+
def _udt_record_cpp_type(self, type_name: str) -> str:
|
|
158
|
+
"""Generated backing-record type for one user-defined object type."""
|
|
159
|
+
return getattr(self, "_udt_record_cpp_names", {}).get(
|
|
160
|
+
type_name, f"_PFUdtRecord_{type_name}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def _udt_arena_member_name(self, type_name: str) -> str:
|
|
164
|
+
"""Per-strategy arena member owning one user-defined object's records."""
|
|
165
|
+
return getattr(self, "_udt_arena_member_names", {}).get(
|
|
166
|
+
type_name, f"_pf_udt_{self._safe_name(type_name)}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def _udt_arena_cpp_type(self, type_name: str) -> str:
|
|
170
|
+
"""Generated arena specialization for one user-defined object type."""
|
|
171
|
+
return (
|
|
172
|
+
f"{getattr(self, '_udt_arena_template_cpp_name', '_PFUdtArena')}<"
|
|
173
|
+
f"{type_name}, "
|
|
174
|
+
f"{self._udt_record_cpp_type(type_name)}>"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _checkpoint_traits_cpp_type(self) -> str:
|
|
178
|
+
"""Collision-safe generated checkpoint trait template name."""
|
|
179
|
+
return getattr(
|
|
180
|
+
self, "_checkpoint_traits_cpp_name", "_PFCheckpointTraits"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _udt_direct_array_fields(self, type_name: str) -> tuple[str, ...]:
|
|
184
|
+
"""Direct array-valued fields whose UDT copy semantics are unsupported.
|
|
185
|
+
|
|
186
|
+
Maps, matrices, and nested UDTs are numeric/shared-ID handles, so an
|
|
187
|
+
outer record copy is correctly shallow. Arrays still lower directly to
|
|
188
|
+
``std::vector<T>``; copying such a record would silently deep-copy the
|
|
189
|
+
vector and diverge from Pine object identity. Keep this boundary narrow
|
|
190
|
+
and explicit until arrays themselves have a handle representation.
|
|
191
|
+
"""
|
|
192
|
+
return tuple(
|
|
193
|
+
field_name
|
|
194
|
+
for field_name, spec in self._udt_field_type_specs.get(
|
|
195
|
+
type_name, {}
|
|
196
|
+
).items()
|
|
197
|
+
if spec is not None and spec.kind == "array"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _reject_unsupported_udt_copy(self, type_name: str, node) -> None:
|
|
201
|
+
"""Fail closed before copying a record with direct vector storage."""
|
|
202
|
+
fields = self._udt_direct_array_fields(type_name)
|
|
203
|
+
if not fields:
|
|
204
|
+
return
|
|
205
|
+
rendered = ", ".join(fields)
|
|
206
|
+
self._codegen_error(
|
|
207
|
+
node,
|
|
208
|
+
f"{type_name}.copy() is unsupported because direct array field(s) "
|
|
209
|
+
f"{rendered} still use value storage",
|
|
210
|
+
hint=(
|
|
211
|
+
"Move the array outside the UDT and store a supported shared-ID "
|
|
212
|
+
"map/matrix or nested UDT handle until array fields gain handle "
|
|
213
|
+
"identity."
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
|
|
156
217
|
@staticmethod
|
|
157
|
-
def
|
|
218
|
+
def _is_nullable_collection_cpp_type(cpp_type: str | None) -> bool:
|
|
219
|
+
"""Whether ``cpp_type`` has a default-constructed Pine ``na`` ID.
|
|
220
|
+
|
|
221
|
+
Arrays still lower to ``std::vector`` and cannot distinguish a null ID
|
|
222
|
+
from a valid empty array. Keep them outside this predicate until their
|
|
223
|
+
runtime representation carries that distinction.
|
|
224
|
+
"""
|
|
225
|
+
return bool(
|
|
226
|
+
cpp_type
|
|
227
|
+
and (
|
|
228
|
+
cpp_type.startswith("PineMap<")
|
|
229
|
+
or cpp_type == "PineMatrix"
|
|
230
|
+
or cpp_type.startswith("PineGenericMatrix<")
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _default_for_type(self, cpp_type: str) -> str:
|
|
158
235
|
"""Default initialiser for a primitive C++ type (matches Pine ``na``)."""
|
|
159
236
|
if cpp_type == "std::string":
|
|
160
237
|
return 'std::string("")'
|
|
161
238
|
if cpp_type == "bool":
|
|
162
239
|
return "false"
|
|
163
|
-
if cpp_type
|
|
240
|
+
if cpp_type in ("int", "int64_t"):
|
|
164
241
|
return "0"
|
|
165
|
-
if cpp_type
|
|
242
|
+
if cpp_type in DRAWING_TYPE_TO_CPP.values():
|
|
243
|
+
return f"{cpp_type}{{}}"
|
|
244
|
+
if cpp_type.startswith("std::vector") or cpp_type.startswith("PineMap"):
|
|
166
245
|
return f"{cpp_type}()"
|
|
246
|
+
if self._is_nullable_collection_cpp_type(cpp_type):
|
|
247
|
+
return f"{cpp_type}{{}}"
|
|
167
248
|
return "0.0"
|
|
168
249
|
|
|
169
250
|
def _default_for_spec(self, spec: TypeSpec | None) -> str:
|
|
@@ -180,24 +261,217 @@ class TypeInferer:
|
|
|
180
261
|
return f"{DRAWING_TYPE_TO_CPP[spec.name]}{{}}"
|
|
181
262
|
return f"{spec.name}{{}}"
|
|
182
263
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
183
|
-
if cpp_type.startswith("std::vector") or cpp_type.startswith("
|
|
264
|
+
if cpp_type.startswith("std::vector") or cpp_type.startswith("PineMap"):
|
|
184
265
|
return f"{cpp_type}()"
|
|
266
|
+
if self._is_nullable_collection_cpp_type(cpp_type):
|
|
267
|
+
return f"{cpp_type}{{}}"
|
|
185
268
|
return self._default_for_type(cpp_type)
|
|
186
269
|
|
|
270
|
+
def _collection_spec_for_name(self, name: str) -> TypeSpec | None:
|
|
271
|
+
"""Resolve collection metadata with lexical precedence.
|
|
272
|
+
|
|
273
|
+
Source-ordered callable locals shadow loop bindings and parameters once
|
|
274
|
+
their declaration has executed; before that point, loop/parameter
|
|
275
|
+
bindings shadow top-level/on_bar state. The process-wide raw-name maps
|
|
276
|
+
are retained for top-level compatibility, but no callable may infer a
|
|
277
|
+
collection kind from a same-named sibling callable.
|
|
278
|
+
"""
|
|
279
|
+
local_specs = getattr(self, "_current_func_collection_specs", {})
|
|
280
|
+
if name in local_specs:
|
|
281
|
+
return local_specs[name]
|
|
282
|
+
# A scalar/UDT local still shadows a same-named top-level collection.
|
|
283
|
+
# Returning None is semantically different from "not found locally": it
|
|
284
|
+
# prevents legacy global kind registries from resurrecting the hidden
|
|
285
|
+
# binding during member dispatch or alias analysis.
|
|
286
|
+
if name in getattr(self, "_current_func_collection_shadows", set()):
|
|
287
|
+
return None
|
|
288
|
+
loop_specs = getattr(self, "_current_loop_var_specs", None)
|
|
289
|
+
if loop_specs and name in loop_specs:
|
|
290
|
+
return loop_specs[name]
|
|
291
|
+
if name in getattr(self, "_current_loop_vars", set()):
|
|
292
|
+
return None
|
|
293
|
+
param_specs = getattr(self, "_current_func_param_specs", {})
|
|
294
|
+
if name in param_specs:
|
|
295
|
+
param_spec = param_specs[name]
|
|
296
|
+
# Keep the established unresolved/untyped-parameter compatibility
|
|
297
|
+
# route: scalar TypeSpecs inferred only from a call site did not
|
|
298
|
+
# historically mask a same-named top-level collection registry.
|
|
299
|
+
# Declared scalar/UDT parameters do shadow it, while inferred or
|
|
300
|
+
# declared collection parameters always carry their exact kind.
|
|
301
|
+
if (param_spec.kind in {"array", "map", "matrix", "udt"}
|
|
302
|
+
or name in getattr(
|
|
303
|
+
self, "_current_func_declared_param_names", set()
|
|
304
|
+
)):
|
|
305
|
+
return param_spec
|
|
306
|
+
# During callable emission this is the copy-on-write lexical overlay;
|
|
307
|
+
# outside a callable it is the live top-level registry, including
|
|
308
|
+
# aliases discovered by codegen after construction.
|
|
309
|
+
return self._collection_types.get(name)
|
|
310
|
+
|
|
311
|
+
def _collection_name_is_lexically_shadowed(self, name: str) -> bool:
|
|
312
|
+
"""Whether ``name`` is bound in the active callable/block scope.
|
|
313
|
+
|
|
314
|
+
A ``None`` collection lookup alone cannot distinguish a real scalar or
|
|
315
|
+
UDT tombstone from an absent name. Callers that otherwise fall back to
|
|
316
|
+
the analyzer's popped symbol table use this predicate to avoid
|
|
317
|
+
resurrecting a hidden top-level collection. Untyped parameters retain
|
|
318
|
+
their established compatibility path and are intentionally not tested
|
|
319
|
+
here.
|
|
320
|
+
"""
|
|
321
|
+
if name in getattr(self, "_current_loop_vars", set()):
|
|
322
|
+
return True
|
|
323
|
+
if name in getattr(self, "_current_func_collection_specs", {}):
|
|
324
|
+
return True
|
|
325
|
+
return name in getattr(self, "_current_func_collection_shadows", set())
|
|
326
|
+
|
|
327
|
+
def _activate_callable_collection_binding(
|
|
328
|
+
self, name: str, spec: TypeSpec | None
|
|
329
|
+
) -> None:
|
|
330
|
+
"""Install one callable-local binding after its declaration RHS.
|
|
331
|
+
|
|
332
|
+
Every raw kind marker is removed first. A collection installs its exact
|
|
333
|
+
TypeSpec; a scalar/UDT leaves a tombstone in the lexical shadow set.
|
|
334
|
+
Function and block entry already established copy-on-write state, so
|
|
335
|
+
this mutation is restored at the appropriate lexical boundary.
|
|
336
|
+
"""
|
|
337
|
+
self._array_vars.discard(name)
|
|
338
|
+
self._map_vars.discard(name)
|
|
339
|
+
self._matrix_specs.pop(name, None)
|
|
340
|
+
self._current_func_collection_specs.pop(name, None)
|
|
341
|
+
self._collection_types.pop(name, None)
|
|
342
|
+
self._current_func_collection_shadows.add(name)
|
|
343
|
+
if spec is None or spec.kind not in {"array", "map", "matrix"}:
|
|
344
|
+
return
|
|
345
|
+
self._current_func_collection_specs[name] = spec
|
|
346
|
+
self._collection_types[name] = spec
|
|
347
|
+
if spec.kind == "array":
|
|
348
|
+
self._array_vars.add(name)
|
|
349
|
+
elif spec.kind == "map":
|
|
350
|
+
self._map_vars.add(name)
|
|
351
|
+
else:
|
|
352
|
+
self._matrix_specs[name] = spec
|
|
353
|
+
|
|
354
|
+
def _collection_receiver_expr(self, name: str) -> str:
|
|
355
|
+
"""C++ receiver for an active collection identifier."""
|
|
356
|
+
pending = getattr(self, "_pending_decl_outer_alias", {})
|
|
357
|
+
if name in pending:
|
|
358
|
+
return pending[name]
|
|
359
|
+
safe = self._safe_name(name)
|
|
360
|
+
owner = getattr(self, "_active_func_name", None)
|
|
361
|
+
exact = (
|
|
362
|
+
self._func_var_storage_name(owner, name)
|
|
363
|
+
if owner is not None
|
|
364
|
+
else name
|
|
365
|
+
)
|
|
366
|
+
# Once an exact callable-local collection declaration has become
|
|
367
|
+
# lexically active, its per-written-callsite variant must read the
|
|
368
|
+
# same cloned member that its declaration-site initializer wrote.
|
|
369
|
+
# Before the declaration, ``_current_func_collection_specs`` has no
|
|
370
|
+
# entry, so an inherited same-named global still resolves normally.
|
|
371
|
+
if (
|
|
372
|
+
name in getattr(self, "_current_func_collection_specs", {})
|
|
373
|
+
and safe in self._active_var_remap
|
|
374
|
+
):
|
|
375
|
+
return self._active_var_remap[safe]
|
|
376
|
+
# Preserve byte-identical output for every non-qualified callable. A
|
|
377
|
+
# qualified persistent array becomes visible through the raw lexical
|
|
378
|
+
# spelling only after its VarDecl installs this active remap.
|
|
379
|
+
if exact != name:
|
|
380
|
+
return self._active_var_remap.get(safe, safe)
|
|
381
|
+
return safe
|
|
382
|
+
|
|
187
383
|
def _array_spec_for_name(self, name: str) -> TypeSpec:
|
|
188
384
|
"""Spec for ``array<...>`` variable ``name`` (falls back to array<float>)."""
|
|
189
|
-
spec = self.
|
|
385
|
+
spec = self._collection_spec_for_name(name)
|
|
190
386
|
if spec is not None and spec.kind == "array":
|
|
191
387
|
return spec
|
|
192
388
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
193
389
|
|
|
194
390
|
def _map_spec_for_name(self, name: str) -> TypeSpec:
|
|
195
391
|
"""Spec for ``map<...>`` variable ``name`` (falls back to map<string, float>)."""
|
|
196
|
-
spec = self.
|
|
392
|
+
spec = self._collection_spec_for_name(name)
|
|
197
393
|
if spec is not None and spec.kind == "map":
|
|
198
394
|
return spec
|
|
199
395
|
return TypeSpec.map(TypeSpec.primitive("string"), TypeSpec.primitive("float"))
|
|
200
396
|
|
|
397
|
+
def _array_from_element_spec(self, node) -> TypeSpec | None:
|
|
398
|
+
"""Exact scalar element spec used only by ``array.from`` lowering.
|
|
399
|
+
|
|
400
|
+
This intentionally does not make BinOp TypeSpecs globally visible:
|
|
401
|
+
doing so changes unrelated float-band comparator output. Collection
|
|
402
|
+
construction needs the narrower fact so its vector type agrees with
|
|
403
|
+
the analyzer-captured declaration type, including lexical loop binders.
|
|
404
|
+
A primitive user-function result may have only the coarse ``PineType``
|
|
405
|
+
slot populated; recover that exact scalar here rather than silently
|
|
406
|
+
defaulting ``array.from(string_udf())`` to ``std::vector<double>``.
|
|
407
|
+
"""
|
|
408
|
+
if isinstance(node, BinOp):
|
|
409
|
+
left = self._array_from_element_spec(node.left)
|
|
410
|
+
right = self._array_from_element_spec(node.right)
|
|
411
|
+
if node.op in ("==", "!=", ">", "<", ">=", "<=", "and", "or"):
|
|
412
|
+
return TypeSpec.primitive("bool")
|
|
413
|
+
if (left is not None and right is not None
|
|
414
|
+
and left.kind == "primitive" and right.kind == "primitive"):
|
|
415
|
+
if left.name == "string" or right.name == "string":
|
|
416
|
+
return TypeSpec.primitive("string")
|
|
417
|
+
if node.op == "/" or left.name == "float" or right.name == "float":
|
|
418
|
+
return TypeSpec.primitive("float")
|
|
419
|
+
if left.name == "int" and right.name == "int":
|
|
420
|
+
return TypeSpec.primitive("int")
|
|
421
|
+
return None
|
|
422
|
+
spec = self._type_spec_from_expr(node)
|
|
423
|
+
if spec is not None:
|
|
424
|
+
return spec
|
|
425
|
+
if isinstance(node, FuncCall):
|
|
426
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
427
|
+
if namespace is None:
|
|
428
|
+
func_info = self._func_info_map.get(func_name)
|
|
429
|
+
pine_type = getattr(func_info, "return_type", None)
|
|
430
|
+
primitive_name = {
|
|
431
|
+
PineType.INT: "int",
|
|
432
|
+
PineType.FLOAT: "float",
|
|
433
|
+
PineType.BOOL: "bool",
|
|
434
|
+
PineType.STRING: "string",
|
|
435
|
+
PineType.COLOR: "color",
|
|
436
|
+
}.get(pine_type)
|
|
437
|
+
if primitive_name is not None:
|
|
438
|
+
return TypeSpec.primitive(primitive_name)
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
@staticmethod
|
|
442
|
+
def _selection_terminal_expr(body: list[ASTNode] | None) -> ASTNode | None:
|
|
443
|
+
"""Return one if/switch branch's value expression, if present."""
|
|
444
|
+
if not body:
|
|
445
|
+
return None
|
|
446
|
+
terminal = body[-1]
|
|
447
|
+
return terminal.expr if isinstance(terminal, ExprStmt) else terminal
|
|
448
|
+
|
|
449
|
+
@staticmethod
|
|
450
|
+
def _selection_node_is_na(node: ASTNode | None) -> bool:
|
|
451
|
+
"""Whether a selection arm is explicit or implicit Pine ``na``."""
|
|
452
|
+
return (
|
|
453
|
+
node is None
|
|
454
|
+
or isinstance(node, NaLiteral)
|
|
455
|
+
or (isinstance(node, Identifier) and node.name == "na")
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def _nullable_collection_selection_spec(
|
|
459
|
+
self,
|
|
460
|
+
branches: list[tuple[ASTNode | None, TypeSpec | None]],
|
|
461
|
+
) -> TypeSpec | None:
|
|
462
|
+
"""Unify compatible map/matrix selection arms around typed ``na``."""
|
|
463
|
+
concrete: list[TypeSpec] = []
|
|
464
|
+
for node, spec in branches:
|
|
465
|
+
if self._selection_node_is_na(node):
|
|
466
|
+
continue
|
|
467
|
+
if spec is None or spec.kind not in {"map", "matrix"}:
|
|
468
|
+
return None
|
|
469
|
+
concrete.append(spec)
|
|
470
|
+
if not concrete:
|
|
471
|
+
return None
|
|
472
|
+
first = concrete[0]
|
|
473
|
+
return first if all(spec == first for spec in concrete[1:]) else None
|
|
474
|
+
|
|
201
475
|
def _type_spec_from_expr(self, node) -> TypeSpec | None:
|
|
202
476
|
"""Best-effort TypeSpec inference for an expression node.
|
|
203
477
|
|
|
@@ -210,26 +484,139 @@ class TypeInferer:
|
|
|
210
484
|
if isinstance(node, StringLiteral):
|
|
211
485
|
return TypeSpec.primitive("string")
|
|
212
486
|
if isinstance(node, Identifier):
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
487
|
+
# An active lexical UDT/drawing binding must beat a same-spelled
|
|
488
|
+
# top-level primitive or collection. This is intentionally the
|
|
489
|
+
# exact lexical prefix of _identifier_udt_binding(), not its raw-
|
|
490
|
+
# name fallback: method receivers such as ``line ln`` otherwise
|
|
491
|
+
# inherit an unrelated global ``ln`` before dispatch is resolved.
|
|
492
|
+
lexical_drawing = getattr(self, "_lexical_drawing_types", {})
|
|
493
|
+
drawing_cpp = lexical_drawing.get(node.name)
|
|
494
|
+
if drawing_cpp is not None:
|
|
495
|
+
for pine_name, cpp_name in DRAWING_TYPE_TO_CPP.items():
|
|
496
|
+
if cpp_name == drawing_cpp:
|
|
497
|
+
return TypeSpec.udt(pine_name)
|
|
498
|
+
lexical_udts = getattr(self, "_lexical_udt_types", {})
|
|
499
|
+
lexical_udt = lexical_udts.get(node.name)
|
|
500
|
+
if lexical_udt in self._udt_defs:
|
|
501
|
+
return TypeSpec.udt(lexical_udt)
|
|
502
|
+
collection_spec = self._collection_spec_for_name(node.name)
|
|
503
|
+
# This resolver also carries exact primitive loop/parameter/global
|
|
504
|
+
# metadata despite its historical name. Preserve those scalar
|
|
505
|
+
# families here (notably bool for array.from); only arbitrary UDTs
|
|
506
|
+
# need the collision-safe lexical resolver below.
|
|
507
|
+
if (collection_spec is not None
|
|
508
|
+
and collection_spec.kind != "udt"):
|
|
509
|
+
return collection_spec
|
|
510
|
+
found_udt_binding, exact_udt = self._identifier_udt_binding(
|
|
511
|
+
node.name
|
|
512
|
+
)
|
|
513
|
+
if found_udt_binding:
|
|
514
|
+
return (
|
|
515
|
+
TypeSpec.udt(exact_udt)
|
|
516
|
+
if exact_udt is not None
|
|
517
|
+
else None
|
|
518
|
+
)
|
|
519
|
+
if self._collection_name_is_lexically_shadowed(node.name):
|
|
520
|
+
return None
|
|
229
521
|
sym = self.ctx.symbols.resolve(node.name)
|
|
230
|
-
if sym is not None
|
|
231
|
-
|
|
522
|
+
if sym is not None:
|
|
523
|
+
if getattr(sym, "type_spec", None) is not None:
|
|
524
|
+
return sym.type_spec
|
|
525
|
+
primitive_name = {
|
|
526
|
+
PineType.INT: "int",
|
|
527
|
+
PineType.FLOAT: "float",
|
|
528
|
+
PineType.BOOL: "bool",
|
|
529
|
+
PineType.STRING: "string",
|
|
530
|
+
PineType.COLOR: "color",
|
|
531
|
+
}.get(sym.pine_type)
|
|
532
|
+
if primitive_name is not None:
|
|
533
|
+
return TypeSpec.primitive(primitive_name)
|
|
534
|
+
return None
|
|
535
|
+
if isinstance(node, Subscript):
|
|
536
|
+
# History access preserves the scalar drawing-handle type. This
|
|
537
|
+
# is intentionally not generalized to arrays/maps: their
|
|
538
|
+
# subscripting rules are handled by the collection paths below.
|
|
539
|
+
receiver_spec = self._type_spec_from_expr(node.object)
|
|
540
|
+
if (receiver_spec is not None
|
|
541
|
+
and receiver_spec.kind == "udt"
|
|
542
|
+
and receiver_spec.name in DRAWING_TYPE_TO_CPP):
|
|
543
|
+
return receiver_spec
|
|
544
|
+
return None
|
|
545
|
+
if isinstance(node, Ternary):
|
|
546
|
+
true_spec = self._type_spec_from_expr(node.true_val)
|
|
547
|
+
false_spec = self._type_spec_from_expr(node.false_val)
|
|
548
|
+
collection_spec = self._nullable_collection_selection_spec([
|
|
549
|
+
(node.true_val, true_spec),
|
|
550
|
+
(node.false_val, false_spec),
|
|
551
|
+
])
|
|
552
|
+
if collection_spec is not None:
|
|
553
|
+
return collection_spec
|
|
554
|
+
if true_spec is not None and true_spec == false_spec:
|
|
555
|
+
return true_spec
|
|
556
|
+
if (true_spec is not None
|
|
557
|
+
and true_spec.kind == "udt"
|
|
558
|
+
and true_spec.name in DRAWING_TYPE_TO_CPP
|
|
559
|
+
and isinstance(node.false_val, NaLiteral)):
|
|
560
|
+
return true_spec
|
|
561
|
+
if (false_spec is not None
|
|
562
|
+
and false_spec.kind == "udt"
|
|
563
|
+
and false_spec.name in DRAWING_TYPE_TO_CPP
|
|
564
|
+
and isinstance(node.true_val, NaLiteral)):
|
|
565
|
+
return false_spec
|
|
232
566
|
return None
|
|
567
|
+
if isinstance(node, IfStmt):
|
|
568
|
+
true_node = self._selection_terminal_expr(node.body)
|
|
569
|
+
false_node = self._selection_terminal_expr(node.else_body)
|
|
570
|
+
true_spec = self._type_spec_from_expr(true_node)
|
|
571
|
+
false_spec = self._type_spec_from_expr(false_node)
|
|
572
|
+
collection_spec = self._nullable_collection_selection_spec([
|
|
573
|
+
(true_node, true_spec),
|
|
574
|
+
(false_node, false_spec),
|
|
575
|
+
])
|
|
576
|
+
if collection_spec is not None:
|
|
577
|
+
return collection_spec
|
|
578
|
+
true_is_na = (
|
|
579
|
+
isinstance(true_node, NaLiteral)
|
|
580
|
+
or (isinstance(true_node, Identifier)
|
|
581
|
+
and true_node.name == "na")
|
|
582
|
+
)
|
|
583
|
+
false_is_na = (
|
|
584
|
+
isinstance(false_node, NaLiteral)
|
|
585
|
+
or (isinstance(false_node, Identifier)
|
|
586
|
+
and false_node.name == "na")
|
|
587
|
+
)
|
|
588
|
+
if (true_spec is not None
|
|
589
|
+
and true_spec.kind == "udt"
|
|
590
|
+
and true_spec.name in DRAWING_TYPE_TO_CPP
|
|
591
|
+
and true_spec == false_spec):
|
|
592
|
+
return true_spec
|
|
593
|
+
if (true_spec is not None
|
|
594
|
+
and true_spec.kind == "udt"
|
|
595
|
+
and true_spec.name in DRAWING_TYPE_TO_CPP
|
|
596
|
+
and false_is_na):
|
|
597
|
+
return true_spec
|
|
598
|
+
if (false_spec is not None
|
|
599
|
+
and false_spec.kind == "udt"
|
|
600
|
+
and false_spec.name in DRAWING_TYPE_TO_CPP
|
|
601
|
+
and true_is_na):
|
|
602
|
+
return false_spec
|
|
603
|
+
return None
|
|
604
|
+
if isinstance(node, SwitchStmt):
|
|
605
|
+
branches: list[tuple[ASTNode | None, TypeSpec | None]] = []
|
|
606
|
+
for _case_expr, case_body in node.cases:
|
|
607
|
+
terminal = self._selection_terminal_expr(case_body)
|
|
608
|
+
branches.append((
|
|
609
|
+
terminal,
|
|
610
|
+
self._type_spec_from_expr(terminal),
|
|
611
|
+
))
|
|
612
|
+
default_terminal = self._selection_terminal_expr(
|
|
613
|
+
node.default_body
|
|
614
|
+
)
|
|
615
|
+
branches.append((
|
|
616
|
+
default_terminal,
|
|
617
|
+
self._type_spec_from_expr(default_terminal),
|
|
618
|
+
))
|
|
619
|
+
return self._nullable_collection_selection_spec(branches)
|
|
233
620
|
if isinstance(node, MemberAccess):
|
|
234
621
|
owner = self._type_spec_from_expr(node.object)
|
|
235
622
|
if owner is not None and owner.kind == "udt" and owner.name:
|
|
@@ -237,6 +624,73 @@ class TypeInferer:
|
|
|
237
624
|
return None
|
|
238
625
|
if isinstance(node, FuncCall):
|
|
239
626
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
627
|
+
if namespace is None:
|
|
628
|
+
func_info = self._func_info_map.get(func_name)
|
|
629
|
+
return_spec = getattr(func_info, "return_type_spec", None)
|
|
630
|
+
if return_spec is not None:
|
|
631
|
+
return return_spec
|
|
632
|
+
udt_return = getattr(func_info, "udt_return_type", None)
|
|
633
|
+
if (udt_return in DRAWING_TYPE_TO_CPP
|
|
634
|
+
or udt_return in self._udt_defs):
|
|
635
|
+
return TypeSpec.udt(udt_return)
|
|
636
|
+
if isinstance(node.callee, MemberAccess):
|
|
637
|
+
receiver_spec = self._type_spec_from_expr(
|
|
638
|
+
node.callee.object
|
|
639
|
+
)
|
|
640
|
+
receiver_name = method_receiver_type_name(receiver_spec)
|
|
641
|
+
method_info = (
|
|
642
|
+
getattr(self, "_func_info_map", {}).get(
|
|
643
|
+
f"{receiver_name}.{node.callee.member}"
|
|
644
|
+
)
|
|
645
|
+
if receiver_name is not None
|
|
646
|
+
else None
|
|
647
|
+
)
|
|
648
|
+
if (
|
|
649
|
+
method_info is not None
|
|
650
|
+
and getattr(method_info, "is_udt_method", False)
|
|
651
|
+
):
|
|
652
|
+
return_spec = getattr(
|
|
653
|
+
method_info, "return_type_spec", None
|
|
654
|
+
)
|
|
655
|
+
if return_spec is not None:
|
|
656
|
+
return return_spec
|
|
657
|
+
udt_return = getattr(
|
|
658
|
+
method_info, "udt_return_type", None
|
|
659
|
+
)
|
|
660
|
+
if (
|
|
661
|
+
udt_return in DRAWING_TYPE_TO_CPP
|
|
662
|
+
or udt_return in self._udt_defs
|
|
663
|
+
):
|
|
664
|
+
return TypeSpec.udt(udt_return)
|
|
665
|
+
call_site_idx = self._callable_target_callsite_idx(
|
|
666
|
+
method_info,
|
|
667
|
+
node,
|
|
668
|
+
)
|
|
669
|
+
return_pine_type = (
|
|
670
|
+
self._callsite_callable_return_pine_type(
|
|
671
|
+
method_info,
|
|
672
|
+
call_site_idx,
|
|
673
|
+
)
|
|
674
|
+
)
|
|
675
|
+
primitive_name = {
|
|
676
|
+
PineType.INT: "int",
|
|
677
|
+
PineType.FLOAT: "float",
|
|
678
|
+
PineType.BOOL: "bool",
|
|
679
|
+
PineType.STRING: "string",
|
|
680
|
+
PineType.COLOR: "color",
|
|
681
|
+
}.get(return_pine_type)
|
|
682
|
+
return (
|
|
683
|
+
TypeSpec.primitive(primitive_name)
|
|
684
|
+
if primitive_name is not None
|
|
685
|
+
else None
|
|
686
|
+
)
|
|
687
|
+
if (
|
|
688
|
+
receiver_spec is not None
|
|
689
|
+
and receiver_spec.kind == "udt"
|
|
690
|
+
and receiver_spec.name in self._udt_defs
|
|
691
|
+
and node.callee.member == "copy"
|
|
692
|
+
):
|
|
693
|
+
return receiver_spec
|
|
240
694
|
# ticker.* constructors (inherit/standard/heikinashi) return a symbol
|
|
241
695
|
# string; without this the member-type inference defaults to double
|
|
242
696
|
# and a ``haTicker = ticker.heikinashi(...)`` global mis-declares as
|
|
@@ -255,6 +709,13 @@ class TypeInferer:
|
|
|
255
709
|
return TypeSpec.udt("chart.point")
|
|
256
710
|
if namespace == "str" and func_name == "split":
|
|
257
711
|
return TypeSpec.array(TypeSpec.primitive("string"))
|
|
712
|
+
if namespace == "matrix" and func_name == "new":
|
|
713
|
+
elem = (
|
|
714
|
+
self._type_spec_from_hint_name(targs[0])
|
|
715
|
+
if targs
|
|
716
|
+
else TypeSpec.primitive("float")
|
|
717
|
+
)
|
|
718
|
+
return TypeSpec.matrix(elem or TypeSpec.primitive("float"))
|
|
258
719
|
if namespace == "array" and func_name in (
|
|
259
720
|
"new", "new_float", "new_int", "new_bool", "new_string", "from",
|
|
260
721
|
) or (namespace == "array" and func_name in ARRAY_DRAWING_NEW_CTORS):
|
|
@@ -273,41 +734,93 @@ class TypeInferer:
|
|
|
273
734
|
if targs:
|
|
274
735
|
return TypeSpec.array(self._type_spec_from_hint_name(targs[0]) or TypeSpec.udt(targs[0]))
|
|
275
736
|
if func_name == "from" and node.args:
|
|
276
|
-
return TypeSpec.array(
|
|
737
|
+
return TypeSpec.array(
|
|
738
|
+
self._array_from_element_spec(node.args[0])
|
|
739
|
+
or TypeSpec.primitive("float")
|
|
740
|
+
)
|
|
277
741
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
278
742
|
# Functional-form array element/copy accessors: the receiver is
|
|
279
743
|
# the first argument (``array.copy(arr)``), mirroring the
|
|
280
744
|
# method-form handling below (``arr.copy()``).
|
|
281
|
-
if (namespace == "array"
|
|
745
|
+
if (namespace == "array"
|
|
282
746
|
and func_name in ("copy", "slice", "get", "first", "last",
|
|
283
747
|
"pop", "shift", "remove")):
|
|
284
|
-
|
|
748
|
+
receiver_node = node.args[0] if node.args else node.kwargs.get("id")
|
|
749
|
+
arg_spec = self._type_spec_from_expr(receiver_node)
|
|
285
750
|
if arg_spec is not None and arg_spec.kind == "array":
|
|
286
751
|
if func_name in ("copy", "slice"):
|
|
287
752
|
return arg_spec
|
|
288
753
|
return arg_spec.element
|
|
754
|
+
if namespace == "matrix" and func_name in MATRIX_RETURNING_METHODS:
|
|
755
|
+
receiver_node = node.args[0] if node.args else node.kwargs.get("id")
|
|
756
|
+
receiver_spec = self._type_spec_from_expr(receiver_node)
|
|
757
|
+
if receiver_spec is not None and receiver_spec.kind == "matrix":
|
|
758
|
+
return receiver_spec
|
|
289
759
|
if namespace == "map" and func_name == "new":
|
|
290
760
|
key = self._type_spec_from_hint_name(targs[0]) if len(targs) > 0 else TypeSpec.primitive("string")
|
|
291
761
|
val = self._type_spec_from_hint_name(targs[1]) if len(targs) > 1 else TypeSpec.primitive("float")
|
|
292
762
|
return TypeSpec.map(key or TypeSpec.primitive("string"), val or TypeSpec.primitive("float"))
|
|
293
|
-
if namespace
|
|
763
|
+
if namespace == "map" and func_name in {
|
|
764
|
+
"put", "get", "remove", "contains", "size", "keys",
|
|
765
|
+
"values", "copy", "put_all", "clear",
|
|
766
|
+
} and node.args:
|
|
767
|
+
receiver_spec = self._type_spec_from_expr(node.args[0])
|
|
768
|
+
if receiver_spec is not None and receiver_spec.kind == "map":
|
|
769
|
+
if func_name in ("put", "get", "remove"):
|
|
770
|
+
return receiver_spec.value
|
|
771
|
+
if func_name == "keys":
|
|
772
|
+
return TypeSpec.array(
|
|
773
|
+
receiver_spec.key or TypeSpec.primitive("string")
|
|
774
|
+
)
|
|
775
|
+
if func_name == "values":
|
|
776
|
+
return TypeSpec.array(
|
|
777
|
+
receiver_spec.value or TypeSpec.primitive("float")
|
|
778
|
+
)
|
|
779
|
+
if func_name == "copy":
|
|
780
|
+
return receiver_spec
|
|
781
|
+
if func_name == "contains":
|
|
782
|
+
return TypeSpec.primitive("bool")
|
|
783
|
+
if func_name == "size":
|
|
784
|
+
return TypeSpec.primitive("int")
|
|
785
|
+
if namespace in self._udt_defs and func_name in {"new", "copy"}:
|
|
294
786
|
return TypeSpec.udt(namespace)
|
|
295
787
|
if isinstance(node.callee, MemberAccess):
|
|
296
788
|
recv_spec = self._type_spec_from_expr(node.callee.object)
|
|
789
|
+
member_name = node.callee.member
|
|
297
790
|
if recv_spec is not None and recv_spec.kind == "array":
|
|
298
|
-
if
|
|
791
|
+
if member_name in (
|
|
792
|
+
"get", "first", "last", "pop", "shift", "remove",
|
|
793
|
+
):
|
|
299
794
|
return recv_spec.element
|
|
300
|
-
if
|
|
795
|
+
if member_name in ("copy", "slice"):
|
|
301
796
|
return recv_spec
|
|
302
797
|
if recv_spec is not None and recv_spec.kind == "map":
|
|
303
|
-
if
|
|
798
|
+
if member_name in ("put", "get", "remove"):
|
|
304
799
|
return recv_spec.value
|
|
305
|
-
if
|
|
800
|
+
if member_name == "keys":
|
|
306
801
|
return TypeSpec.array(recv_spec.key or TypeSpec.primitive("string"))
|
|
307
|
-
if
|
|
802
|
+
if member_name == "values":
|
|
308
803
|
return TypeSpec.array(recv_spec.value or TypeSpec.primitive("float"))
|
|
804
|
+
if member_name == "copy":
|
|
805
|
+
return recv_spec
|
|
806
|
+
if member_name == "contains":
|
|
807
|
+
return TypeSpec.primitive("bool")
|
|
808
|
+
if member_name == "size":
|
|
809
|
+
return TypeSpec.primitive("int")
|
|
810
|
+
receiver_name = method_receiver_type_name(recv_spec)
|
|
811
|
+
if receiver_name is not None:
|
|
812
|
+
method_info = getattr(
|
|
813
|
+
self, "_func_info_map", {}
|
|
814
|
+
).get(
|
|
815
|
+
f"{receiver_name}.{member_name}"
|
|
816
|
+
)
|
|
817
|
+
return_spec = getattr(
|
|
818
|
+
method_info, "return_type_spec", None
|
|
819
|
+
)
|
|
820
|
+
if return_spec is not None:
|
|
821
|
+
return return_spec
|
|
309
822
|
if recv_spec is not None and recv_spec.kind == "matrix":
|
|
310
|
-
if func_name in
|
|
823
|
+
if func_name in MATRIX_RETURNING_METHODS:
|
|
311
824
|
return recv_spec
|
|
312
825
|
if func_name in ("row", "col"):
|
|
313
826
|
return TypeSpec.array(recv_spec.element)
|
|
@@ -325,6 +838,76 @@ class TypeInferer:
|
|
|
325
838
|
return TypeSpec.udt("line")
|
|
326
839
|
return None
|
|
327
840
|
|
|
841
|
+
def _identifier_udt_binding(
|
|
842
|
+
self, name: str
|
|
843
|
+
) -> tuple[bool, str | None]:
|
|
844
|
+
"""Return ``(binding_known, exact_udt_or_none)`` for ``name``.
|
|
845
|
+
|
|
846
|
+
``ctx.udt_var_types`` is a legacy raw-name registry: a declaration in
|
|
847
|
+
an unrelated callable can overwrite the type of a same-named global.
|
|
848
|
+
Every correctness-sensitive consumer must prefer the active lexical
|
|
849
|
+
binding (including scalar tombstones), then exact parameter/local and
|
|
850
|
+
direct-program metadata, before consulting that registry.
|
|
851
|
+
"""
|
|
852
|
+
known_udts = set(self._udt_defs) | set(DRAWING_TYPE_TO_CPP)
|
|
853
|
+
|
|
854
|
+
lexical_drawing = getattr(self, "_lexical_drawing_types", {})
|
|
855
|
+
if name in lexical_drawing and lexical_drawing[name] is not None:
|
|
856
|
+
cpp_type = lexical_drawing[name]
|
|
857
|
+
for pine_name, candidate_cpp in DRAWING_TYPE_TO_CPP.items():
|
|
858
|
+
if candidate_cpp == cpp_type:
|
|
859
|
+
return True, pine_name
|
|
860
|
+
|
|
861
|
+
lexical = getattr(self, "_lexical_udt_types", {})
|
|
862
|
+
if name in lexical:
|
|
863
|
+
candidate = lexical[name]
|
|
864
|
+
return True, candidate if candidate in known_udts else None
|
|
865
|
+
|
|
866
|
+
param_specs = getattr(self, "_current_func_param_specs", {})
|
|
867
|
+
param_spec = param_specs.get(name) or param_specs.get(
|
|
868
|
+
self._safe_name(name)
|
|
869
|
+
)
|
|
870
|
+
if (param_spec is not None
|
|
871
|
+
and param_spec.kind == "udt"
|
|
872
|
+
and param_spec.name in known_udts):
|
|
873
|
+
return True, param_spec.name
|
|
874
|
+
param_types = getattr(self, "_current_func_param_types", {})
|
|
875
|
+
if name in param_types or self._safe_name(name) in param_types:
|
|
876
|
+
return True, None
|
|
877
|
+
|
|
878
|
+
local_types = getattr(self, "_current_func_local_types", {})
|
|
879
|
+
if name in local_types:
|
|
880
|
+
local_cpp = local_types[name]
|
|
881
|
+
candidate = local_cpp.removesuffix("&").removesuffix("*")
|
|
882
|
+
return True, candidate if candidate in known_udts else None
|
|
883
|
+
|
|
884
|
+
param_udts = getattr(self, "_udt_param_udt", {})
|
|
885
|
+
candidate = param_udts.get(name) or param_udts.get(
|
|
886
|
+
self._safe_name(name)
|
|
887
|
+
)
|
|
888
|
+
if candidate in known_udts:
|
|
889
|
+
return True, candidate
|
|
890
|
+
|
|
891
|
+
global_types = getattr(self, "_global_udt_types", {})
|
|
892
|
+
if name in global_types:
|
|
893
|
+
candidate = global_types[name]
|
|
894
|
+
return True, candidate if candidate in known_udts else None
|
|
895
|
+
|
|
896
|
+
member_resolver = getattr(self, "_member_udt_type", None)
|
|
897
|
+
candidate = (
|
|
898
|
+
member_resolver(name)
|
|
899
|
+
if callable(member_resolver)
|
|
900
|
+
else self._udt_var_types.get(name)
|
|
901
|
+
)
|
|
902
|
+
if candidate is not None:
|
|
903
|
+
return True, candidate if candidate in known_udts else None
|
|
904
|
+
return False, None
|
|
905
|
+
|
|
906
|
+
def _identifier_udt_type(self, name: str) -> str | None:
|
|
907
|
+
"""Exact UDT type for ``name``; scalar tombstones return ``None``."""
|
|
908
|
+
_found, udt_type = self._identifier_udt_binding(name)
|
|
909
|
+
return udt_type
|
|
910
|
+
|
|
328
911
|
# ------------------------------------------------------------------
|
|
329
912
|
# Method lowering for collection types (used by visit_call paths)
|
|
330
913
|
# ------------------------------------------------------------------
|
|
@@ -402,42 +985,77 @@ class TypeInferer:
|
|
|
402
985
|
f"codegen: unhandled array method '{method}' — analyzer should have "
|
|
403
986
|
f"rejected. Add it to ARRAY_METHODS."
|
|
404
987
|
)
|
|
405
|
-
|
|
988
|
+
# Pine evaluates call arguments before entering the array
|
|
989
|
+
# calculation, even when an empty receiver makes the result ``na``.
|
|
990
|
+
# The empty guards in these methods must therefore consume a
|
|
991
|
+
# one-evaluation binding rather than leaving the original argument
|
|
992
|
+
# expression after the guard. Build the binding into
|
|
993
|
+
# ``lower_receiver`` so a temporary receiver is still evaluated
|
|
994
|
+
# first by ``_array_receiver_once_expr``.
|
|
995
|
+
eager_scalar_arg_methods = {
|
|
996
|
+
"stdev": (0,),
|
|
997
|
+
"variance": (0,),
|
|
998
|
+
"percentile_linear_interpolation": (0,),
|
|
999
|
+
"percentile_nearest_rank": (0,),
|
|
1000
|
+
}
|
|
1001
|
+
bound_args = list(args)
|
|
1002
|
+
arg_bindings: list[tuple[str, str]] = []
|
|
1003
|
+
occupied = "\n".join((array_expr, *args))
|
|
1004
|
+
counter = getattr(self, "_array_arg_counter", 0)
|
|
1005
|
+
for arg_index in eager_scalar_arg_methods.get(method, ()):
|
|
1006
|
+
if arg_index >= len(args):
|
|
1007
|
+
continue
|
|
1008
|
+
while True:
|
|
1009
|
+
token = f"__pf_array_arg_{counter}"
|
|
1010
|
+
counter += 1
|
|
1011
|
+
if token not in occupied:
|
|
1012
|
+
break
|
|
1013
|
+
bound_args[arg_index] = token
|
|
1014
|
+
arg_bindings.append((token, args[arg_index]))
|
|
1015
|
+
self._array_arg_counter = counter
|
|
1016
|
+
|
|
1017
|
+
def lower_receiver(recv: str) -> str:
|
|
1018
|
+
lowered = ARRAY_METHODS[method](recv, bound_args)
|
|
1019
|
+
for token, original in reversed(arg_bindings):
|
|
1020
|
+
lowered = (
|
|
1021
|
+
f"[&](){{ auto {token}=({original}); "
|
|
1022
|
+
f"return {lowered}; }}()"
|
|
1023
|
+
)
|
|
1024
|
+
return lowered
|
|
406
1025
|
|
|
407
1026
|
return self._array_receiver_once_expr(array_expr, args, lower_receiver)
|
|
408
1027
|
|
|
409
1028
|
def _map_method_expr(
|
|
410
1029
|
self, map_expr: str, method: str, args: list[str], spec: TypeSpec | None = None,
|
|
411
1030
|
) -> str:
|
|
412
|
-
"""Lower
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
)
|
|
1031
|
+
"""Lower a Pine map operation to the ordered, alias-preserving runtime.
|
|
1032
|
+
|
|
1033
|
+
``PineMap`` owns all missing-value, insertion-order and explicit-copy
|
|
1034
|
+
semantics. Keeping this lowering as a direct method delegation also
|
|
1035
|
+
guarantees that each receiver and argument expression occurs exactly
|
|
1036
|
+
once; the typed-parameter lane may still wrap arguments to impose
|
|
1037
|
+
Pine's source evaluation order.
|
|
1038
|
+
"""
|
|
421
1039
|
if method == "put":
|
|
422
|
-
return f"
|
|
1040
|
+
return f"{map_expr}.put({args[0]}, {args[1]})"
|
|
423
1041
|
if method == "get":
|
|
424
|
-
return f"
|
|
1042
|
+
return f"{map_expr}.get({args[0]})"
|
|
425
1043
|
if method == "remove":
|
|
426
|
-
return f"
|
|
1044
|
+
return f"{map_expr}.remove({args[0]})"
|
|
427
1045
|
if method == "contains":
|
|
428
|
-
return f"
|
|
1046
|
+
return f"{map_expr}.contains({args[0]})"
|
|
429
1047
|
if method == "size":
|
|
430
|
-
return f"
|
|
1048
|
+
return f"{map_expr}.size()"
|
|
431
1049
|
if method == "clear":
|
|
432
1050
|
return f"{map_expr}.clear()"
|
|
433
1051
|
if method == "keys":
|
|
434
|
-
return f"
|
|
1052
|
+
return f"{map_expr}.keys()"
|
|
435
1053
|
if method == "values":
|
|
436
|
-
return f"
|
|
1054
|
+
return f"{map_expr}.values()"
|
|
437
1055
|
if method == "copy":
|
|
438
|
-
return f"{
|
|
1056
|
+
return f"{map_expr}.copy()"
|
|
439
1057
|
if method == "put_all":
|
|
440
|
-
return f"{map_expr}.
|
|
1058
|
+
return f"{map_expr}.put_all({args[0]})"
|
|
441
1059
|
# Defensive: support_checker rejects any map.* method not in SUPPORTED_MAP
|
|
442
1060
|
# (derived from MAP_METHODS, which mirrors this if-chain). Reaching here
|
|
443
1061
|
# means the checker was bypassed or the tables drifted.
|
|
@@ -452,13 +1070,39 @@ class TypeInferer:
|
|
|
452
1070
|
|
|
453
1071
|
def _type_for_decl(self, node: VarDecl) -> str:
|
|
454
1072
|
"""Determine the C++ type for a ``VarDecl``: explicit hint, then symbol, then RHS inference."""
|
|
1073
|
+
def promote_wide_int(cpp_type: str) -> str:
|
|
1074
|
+
if cpp_type != "int":
|
|
1075
|
+
return cpp_type
|
|
1076
|
+
owner = self._func_info_map.get(
|
|
1077
|
+
getattr(self, "_active_func_name", "") or ""
|
|
1078
|
+
)
|
|
1079
|
+
if self._expr_returns_wide_int(
|
|
1080
|
+
node.value,
|
|
1081
|
+
owner,
|
|
1082
|
+
set(),
|
|
1083
|
+
getattr(self, "_active_call_site_idx", None),
|
|
1084
|
+
) or self._is_int64_builtin_init(node.name):
|
|
1085
|
+
return "int64_t"
|
|
1086
|
+
return cpp_type
|
|
1087
|
+
|
|
455
1088
|
if node.type_hint:
|
|
456
1089
|
spec = self._type_spec_from_hint_name(node.type_hint)
|
|
457
1090
|
if spec is not None:
|
|
458
|
-
return self._type_spec_to_cpp(spec)
|
|
1091
|
+
return promote_wide_int(self._type_spec_to_cpp(spec))
|
|
459
1092
|
if node.type_hint in self._udt_defs:
|
|
460
1093
|
return node.type_hint
|
|
461
|
-
return
|
|
1094
|
+
return promote_wide_int(
|
|
1095
|
+
PINE_TYPE_TO_CPP.get(node.type_hint, "double")
|
|
1096
|
+
)
|
|
1097
|
+
# The analyzer records exact callable-local collection bindings before
|
|
1098
|
+
# its lexical scopes are popped. In particular, an inferred local
|
|
1099
|
+
# such as ``selected = cond ? na : global_map`` has a map TypeSpec even
|
|
1100
|
+
# though generic C++ expression inference sees ``na`` as ``double``.
|
|
1101
|
+
# Consume nullable map/matrix forms here. Arrays still lack a runtime
|
|
1102
|
+
# null-ID representation and retain their established inference path.
|
|
1103
|
+
captured = self._callable_collection_bindings.get(id(node))
|
|
1104
|
+
if captured is not None and captured.kind in {"map", "matrix"}:
|
|
1105
|
+
return self._type_spec_to_cpp(captured)
|
|
462
1106
|
# Drawing handle local (L-N6): a hintless local whose RHS resolves to a
|
|
463
1107
|
# drawing udt must declare as the handle struct, not the analyzer's
|
|
464
1108
|
# scalar default. Covers ``ln = arr.get(i)``, alias ``b = a``, field read
|
|
@@ -475,6 +1119,12 @@ class TypeInferer:
|
|
|
475
1119
|
_dret = self._drawing_call_return_cpp(node.value)
|
|
476
1120
|
if _dret is not None:
|
|
477
1121
|
return _dret
|
|
1122
|
+
# Analyzer scopes are popped before codegen, so ctx.symbols.resolve may
|
|
1123
|
+
# find a same-named global instead of this active callable's plain
|
|
1124
|
+
# local. The local binding is known from the emitted body inventory;
|
|
1125
|
+
# infer it from its own RHS rather than borrowing the global's PineType.
|
|
1126
|
+
if node.name in getattr(self, "_current_func_locals", set()):
|
|
1127
|
+
return promote_wide_int(self._infer_type(node.value))
|
|
478
1128
|
sym = self.ctx.symbols.resolve(node.name)
|
|
479
1129
|
if sym is not None:
|
|
480
1130
|
inferred = self._infer_type(node.value)
|
|
@@ -482,8 +1132,8 @@ class TypeInferer:
|
|
|
482
1132
|
return inferred
|
|
483
1133
|
cpp_type = PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
484
1134
|
if cpp_type != "double" or sym.pine_type != PineType.UNKNOWN:
|
|
485
|
-
return cpp_type
|
|
486
|
-
return self._infer_type(node.value)
|
|
1135
|
+
return promote_wide_int(cpp_type)
|
|
1136
|
+
return promote_wide_int(self._infer_type(node.value))
|
|
487
1137
|
|
|
488
1138
|
def _series_type_for(self, name: str) -> str:
|
|
489
1139
|
"""C++ element type for a series variable's history buffer."""
|
|
@@ -499,6 +1149,323 @@ class TypeInferer:
|
|
|
499
1149
|
return PINE_TYPE_TO_CPP.get(sym.pine_type, "double")
|
|
500
1150
|
return "double"
|
|
501
1151
|
|
|
1152
|
+
def _series_param_element_cpp_type(
|
|
1153
|
+
self,
|
|
1154
|
+
func_info,
|
|
1155
|
+
index: int,
|
|
1156
|
+
call_site_idx: int | None = None,
|
|
1157
|
+
) -> str:
|
|
1158
|
+
"""Canonical element type for one history-referenced UDF parameter.
|
|
1159
|
+
|
|
1160
|
+
A Pine parameter used as ``src[n]`` is a typed time series, not an
|
|
1161
|
+
implicitly-float series. The old emitter hard-coded every such
|
|
1162
|
+
parameter as ``Series<double>``. That happened to work for price
|
|
1163
|
+
sources, but rejected real ``series int``/``series bool`` bindings and
|
|
1164
|
+
let synthetic-expression bridges conceal the same mismatch by
|
|
1165
|
+
converting their values to double.
|
|
1166
|
+
|
|
1167
|
+
Pine ``int`` also carries epoch-millisecond builtins such as ``time``.
|
|
1168
|
+
Use the codegen's existing wide integer representation at this
|
|
1169
|
+
callable boundary so one declared ``int`` parameter can safely accept
|
|
1170
|
+
both ``bar_index`` and timestamp values without narrowing. Scalar
|
|
1171
|
+
integer values are widened when a synthetic bridge is required.
|
|
1172
|
+
"""
|
|
1173
|
+
declared_specs = list(
|
|
1174
|
+
getattr(self.ctx, "func_declared_param_type_specs", {}).get(
|
|
1175
|
+
func_info.name, ()
|
|
1176
|
+
)
|
|
1177
|
+
)
|
|
1178
|
+
declared_spec = (
|
|
1179
|
+
declared_specs[index] if index < len(declared_specs) else None
|
|
1180
|
+
)
|
|
1181
|
+
# A declared type is authoritative across every written call. Only a
|
|
1182
|
+
# truly untyped slot may consult the per-callsite specialization map.
|
|
1183
|
+
spec = declared_spec
|
|
1184
|
+
if spec is not None and spec.kind == "primitive":
|
|
1185
|
+
if spec.name == "int":
|
|
1186
|
+
return "int64_t"
|
|
1187
|
+
if spec.name == "bool":
|
|
1188
|
+
return "bool"
|
|
1189
|
+
if spec.name == "color":
|
|
1190
|
+
return "int"
|
|
1191
|
+
if spec.name == "float":
|
|
1192
|
+
return "double"
|
|
1193
|
+
|
|
1194
|
+
if call_site_idx is not None:
|
|
1195
|
+
variant = getattr(
|
|
1196
|
+
self.ctx, "func_callsite_param_types", {}
|
|
1197
|
+
).get((func_info.name, call_site_idx))
|
|
1198
|
+
pine_type = (
|
|
1199
|
+
variant[index]
|
|
1200
|
+
if variant is not None and index < len(variant)
|
|
1201
|
+
else PineType.UNKNOWN
|
|
1202
|
+
)
|
|
1203
|
+
if pine_type == PineType.INT:
|
|
1204
|
+
return "int64_t"
|
|
1205
|
+
if pine_type == PineType.BOOL:
|
|
1206
|
+
return "bool"
|
|
1207
|
+
if pine_type == PineType.COLOR:
|
|
1208
|
+
return "int"
|
|
1209
|
+
if pine_type == PineType.FLOAT:
|
|
1210
|
+
return "double"
|
|
1211
|
+
|
|
1212
|
+
specs = list(getattr(func_info, "param_type_specs", ()) or ())
|
|
1213
|
+
spec = specs[index] if index < len(specs) else None
|
|
1214
|
+
if spec is not None and spec.kind == "primitive":
|
|
1215
|
+
if spec.name == "int":
|
|
1216
|
+
return "int64_t"
|
|
1217
|
+
if spec.name == "bool":
|
|
1218
|
+
return "bool"
|
|
1219
|
+
if spec.name == "color":
|
|
1220
|
+
return "int"
|
|
1221
|
+
if spec.name == "float":
|
|
1222
|
+
return "double"
|
|
1223
|
+
param_types = list(getattr(func_info, "param_types", ()) or ())
|
|
1224
|
+
pine_type = param_types[index] if index < len(param_types) else None
|
|
1225
|
+
if pine_type == PineType.INT:
|
|
1226
|
+
return "int64_t"
|
|
1227
|
+
if pine_type == PineType.BOOL:
|
|
1228
|
+
return "bool"
|
|
1229
|
+
if pine_type == PineType.COLOR:
|
|
1230
|
+
return "int"
|
|
1231
|
+
return "double"
|
|
1232
|
+
|
|
1233
|
+
def _callsite_callable_return_pine_type(
|
|
1234
|
+
self, func_info, call_site_idx: int | None
|
|
1235
|
+
):
|
|
1236
|
+
if call_site_idx is not None:
|
|
1237
|
+
variant = getattr(
|
|
1238
|
+
self.ctx, "func_callsite_return_types", {}
|
|
1239
|
+
).get((func_info.name, call_site_idx))
|
|
1240
|
+
if variant is not None and variant != PineType.UNKNOWN:
|
|
1241
|
+
return variant
|
|
1242
|
+
return getattr(func_info, "return_type", PineType.UNKNOWN)
|
|
1243
|
+
|
|
1244
|
+
def _func_int_return_uses_wide_history(
|
|
1245
|
+
self,
|
|
1246
|
+
func_info,
|
|
1247
|
+
seen: set[str] | None = None,
|
|
1248
|
+
call_site_idx: int | None = None,
|
|
1249
|
+
) -> bool:
|
|
1250
|
+
"""Whether an integer callable must preserve the ``int64_t`` family.
|
|
1251
|
+
|
|
1252
|
+
History-referenced ``int`` parameters are one source of wide values,
|
|
1253
|
+
but not the only one. A parameterless helper can capture ``time[1]``
|
|
1254
|
+
directly, and an otherwise-pure wrapper can return that helper. The
|
|
1255
|
+
analyzer's Pine-level return type is intentionally just ``INT`` in all
|
|
1256
|
+
of those cases, so follow the terminal expression/call chain here to
|
|
1257
|
+
prevent the C++ callable boundary from narrowing epoch milliseconds.
|
|
1258
|
+
"""
|
|
1259
|
+
if self._callsite_callable_return_pine_type(
|
|
1260
|
+
func_info, call_site_idx
|
|
1261
|
+
) != PineType.INT:
|
|
1262
|
+
return False
|
|
1263
|
+
node = getattr(func_info, "node", None)
|
|
1264
|
+
if node is None:
|
|
1265
|
+
return False
|
|
1266
|
+
|
|
1267
|
+
if seen is None:
|
|
1268
|
+
seen = set()
|
|
1269
|
+
func_name = getattr(func_info, "name", "")
|
|
1270
|
+
if func_name in seen:
|
|
1271
|
+
return False
|
|
1272
|
+
seen = {*seen, func_name}
|
|
1273
|
+
|
|
1274
|
+
series_names = self.ctx.func_series_vars.get(func_info.name, set())
|
|
1275
|
+
if any(
|
|
1276
|
+
param in series_names
|
|
1277
|
+
and self._series_param_element_cpp_type(
|
|
1278
|
+
func_info, index, call_site_idx
|
|
1279
|
+
)
|
|
1280
|
+
== "int64_t"
|
|
1281
|
+
for index, param in enumerate(node.params)
|
|
1282
|
+
):
|
|
1283
|
+
return True
|
|
1284
|
+
|
|
1285
|
+
if not node.body:
|
|
1286
|
+
return False
|
|
1287
|
+
terminal = node.body[-1]
|
|
1288
|
+
if isinstance(terminal, ExprStmt):
|
|
1289
|
+
terminal = terminal.expr
|
|
1290
|
+
return self._expr_returns_wide_int(
|
|
1291
|
+
terminal, func_info, seen, call_site_idx
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
def _expr_returns_wide_int(
|
|
1295
|
+
self,
|
|
1296
|
+
expr,
|
|
1297
|
+
owner_info,
|
|
1298
|
+
seen: set[str],
|
|
1299
|
+
call_site_idx: int | None = None,
|
|
1300
|
+
) -> bool:
|
|
1301
|
+
"""Trace wide integer provenance through a callable return expression."""
|
|
1302
|
+
if expr is None:
|
|
1303
|
+
return False
|
|
1304
|
+
if isinstance(expr, Subscript):
|
|
1305
|
+
return self._expr_returns_wide_int(
|
|
1306
|
+
expr.object, owner_info, seen, call_site_idx
|
|
1307
|
+
)
|
|
1308
|
+
if isinstance(expr, Identifier):
|
|
1309
|
+
# A terminal local can be a narrow-looking alias for ``time[1]``.
|
|
1310
|
+
# Resolve its declaration inside this callable without consulting
|
|
1311
|
+
# the already-popped analyzer scope.
|
|
1312
|
+
node = getattr(owner_info, "node", None)
|
|
1313
|
+
if node is not None:
|
|
1314
|
+
if expr.name in node.params:
|
|
1315
|
+
return False
|
|
1316
|
+
local_key = (
|
|
1317
|
+
f"{getattr(owner_info, 'name', '')}:local:{expr.name}"
|
|
1318
|
+
)
|
|
1319
|
+
if local_key in seen:
|
|
1320
|
+
return False
|
|
1321
|
+
local_seen = {*seen, local_key}
|
|
1322
|
+
for child in self._walk_ast_list(node.body):
|
|
1323
|
+
if (
|
|
1324
|
+
isinstance(child, VarDecl)
|
|
1325
|
+
and child.name == expr.name
|
|
1326
|
+
and child.value is not None
|
|
1327
|
+
):
|
|
1328
|
+
if self._expr_returns_wide_int(
|
|
1329
|
+
child.value,
|
|
1330
|
+
owner_info,
|
|
1331
|
+
local_seen,
|
|
1332
|
+
call_site_idx,
|
|
1333
|
+
):
|
|
1334
|
+
return True
|
|
1335
|
+
if (
|
|
1336
|
+
isinstance(child, Assignment)
|
|
1337
|
+
and isinstance(child.target, Identifier)
|
|
1338
|
+
and child.target.name == expr.name
|
|
1339
|
+
):
|
|
1340
|
+
if self._expr_returns_wide_int(
|
|
1341
|
+
child.value,
|
|
1342
|
+
owner_info,
|
|
1343
|
+
local_seen,
|
|
1344
|
+
call_site_idx,
|
|
1345
|
+
):
|
|
1346
|
+
return True
|
|
1347
|
+
return self._expr_is_int64_builtin(expr)
|
|
1348
|
+
if self._expr_is_int64_builtin(expr):
|
|
1349
|
+
return True
|
|
1350
|
+
if isinstance(expr, FuncCall):
|
|
1351
|
+
func_name, namespace = self._resolve_callee(expr.callee)
|
|
1352
|
+
if namespace is None and func_name in {
|
|
1353
|
+
"int",
|
|
1354
|
+
"float",
|
|
1355
|
+
"nz",
|
|
1356
|
+
"fixnan",
|
|
1357
|
+
}:
|
|
1358
|
+
# Casts and Pine's missing-value wrappers preserve the wide
|
|
1359
|
+
# integer provenance needed by an int destination. ``nz`` may
|
|
1360
|
+
# obtain it from either the source or replacement argument.
|
|
1361
|
+
return any(
|
|
1362
|
+
self._expr_returns_wide_int(
|
|
1363
|
+
arg, owner_info, seen, call_site_idx
|
|
1364
|
+
)
|
|
1365
|
+
for arg in expr.args
|
|
1366
|
+
)
|
|
1367
|
+
if namespace == "math" and func_name in {"abs", "max", "min"}:
|
|
1368
|
+
return any(
|
|
1369
|
+
self._expr_returns_wide_int(
|
|
1370
|
+
arg, owner_info, seen, call_site_idx
|
|
1371
|
+
)
|
|
1372
|
+
for arg in expr.args
|
|
1373
|
+
)
|
|
1374
|
+
|
|
1375
|
+
callee_info = None
|
|
1376
|
+
if namespace is None and func_name:
|
|
1377
|
+
callee_info = self._func_info_map.get(func_name)
|
|
1378
|
+
elif isinstance(expr.callee, MemberAccess):
|
|
1379
|
+
receiver = expr.callee.object
|
|
1380
|
+
owner_node = getattr(owner_info, "node", None)
|
|
1381
|
+
if (
|
|
1382
|
+
getattr(owner_info, "is_udt_method", False)
|
|
1383
|
+
and owner_node is not None
|
|
1384
|
+
and owner_node.params
|
|
1385
|
+
and isinstance(receiver, Identifier)
|
|
1386
|
+
and receiver.name == owner_node.params[0]
|
|
1387
|
+
):
|
|
1388
|
+
receiver_specs = list(
|
|
1389
|
+
getattr(owner_info, "param_type_specs", ()) or ()
|
|
1390
|
+
)
|
|
1391
|
+
receiver_name = method_receiver_type_name(
|
|
1392
|
+
receiver_specs[0] if receiver_specs else None
|
|
1393
|
+
) or getattr(owner_info, "udt_type_name", None)
|
|
1394
|
+
if receiver_name is not None:
|
|
1395
|
+
callee_info = self._func_info_map.get(
|
|
1396
|
+
f"{receiver_name}.{expr.callee.member}"
|
|
1397
|
+
)
|
|
1398
|
+
if callee_info is None:
|
|
1399
|
+
receiver_spec = self._type_spec_from_expr(receiver)
|
|
1400
|
+
receiver_name = method_receiver_type_name(
|
|
1401
|
+
receiver_spec
|
|
1402
|
+
)
|
|
1403
|
+
if receiver_name is not None:
|
|
1404
|
+
callee_info = self._func_info_map.get(
|
|
1405
|
+
f"{receiver_name}.{expr.callee.member}"
|
|
1406
|
+
)
|
|
1407
|
+
return (
|
|
1408
|
+
callee_info is not None
|
|
1409
|
+
and self._func_int_return_uses_wide_history(
|
|
1410
|
+
callee_info,
|
|
1411
|
+
seen,
|
|
1412
|
+
call_site_idx=call_site_idx,
|
|
1413
|
+
)
|
|
1414
|
+
)
|
|
1415
|
+
if isinstance(expr, BinOp):
|
|
1416
|
+
return (
|
|
1417
|
+
self._expr_returns_wide_int(
|
|
1418
|
+
expr.left, owner_info, seen, call_site_idx
|
|
1419
|
+
)
|
|
1420
|
+
or self._expr_returns_wide_int(
|
|
1421
|
+
expr.right, owner_info, seen, call_site_idx
|
|
1422
|
+
)
|
|
1423
|
+
)
|
|
1424
|
+
if isinstance(expr, UnaryOp):
|
|
1425
|
+
return self._expr_returns_wide_int(
|
|
1426
|
+
expr.operand, owner_info, seen, call_site_idx
|
|
1427
|
+
)
|
|
1428
|
+
if isinstance(expr, Ternary):
|
|
1429
|
+
return (
|
|
1430
|
+
self._expr_returns_wide_int(
|
|
1431
|
+
expr.true_val, owner_info, seen, call_site_idx
|
|
1432
|
+
)
|
|
1433
|
+
or self._expr_returns_wide_int(
|
|
1434
|
+
expr.false_val, owner_info, seen, call_site_idx
|
|
1435
|
+
)
|
|
1436
|
+
)
|
|
1437
|
+
if isinstance(expr, IfStmt):
|
|
1438
|
+
branches = [expr.body, expr.else_body]
|
|
1439
|
+
return any(
|
|
1440
|
+
branch
|
|
1441
|
+
and self._expr_returns_wide_int(
|
|
1442
|
+
branch[-1].expr
|
|
1443
|
+
if isinstance(branch[-1], ExprStmt)
|
|
1444
|
+
else branch[-1],
|
|
1445
|
+
owner_info,
|
|
1446
|
+
seen,
|
|
1447
|
+
call_site_idx,
|
|
1448
|
+
)
|
|
1449
|
+
for branch in branches
|
|
1450
|
+
)
|
|
1451
|
+
if isinstance(expr, SwitchStmt):
|
|
1452
|
+
branches = [body for _, body in expr.cases]
|
|
1453
|
+
if expr.default_body:
|
|
1454
|
+
branches.append(expr.default_body)
|
|
1455
|
+
return any(
|
|
1456
|
+
branch
|
|
1457
|
+
and self._expr_returns_wide_int(
|
|
1458
|
+
branch[-1].expr
|
|
1459
|
+
if isinstance(branch[-1], ExprStmt)
|
|
1460
|
+
else branch[-1],
|
|
1461
|
+
owner_info,
|
|
1462
|
+
seen,
|
|
1463
|
+
call_site_idx,
|
|
1464
|
+
)
|
|
1465
|
+
for branch in branches
|
|
1466
|
+
)
|
|
1467
|
+
return False
|
|
1468
|
+
|
|
502
1469
|
def _expr_is_int64_builtin(self, expr) -> bool:
|
|
503
1470
|
"""True if ``expr`` is a top-level int64-returning Pine builtin: either a
|
|
504
1471
|
call to one of ``INT64_BUILTINS`` (``time(...)``, ``timestamp(...)``, …)
|
|
@@ -514,6 +1481,28 @@ class TypeInferer:
|
|
|
514
1481
|
return expr.name in INT64_BUILTIN_IDENTIFIERS
|
|
515
1482
|
return False
|
|
516
1483
|
|
|
1484
|
+
def _fixnan_site_cpp_type(self, site) -> str:
|
|
1485
|
+
"""Storage type for one fixnan previous-value slot.
|
|
1486
|
+
|
|
1487
|
+
``PineType.INT`` does not distinguish ordinary counters from epoch
|
|
1488
|
+
milliseconds, but fixnan stores the exact prior value. Inspect the
|
|
1489
|
+
authored argument's wide provenance so the state member cannot narrow
|
|
1490
|
+
``time[1]`` before an otherwise-int64 local receives it.
|
|
1491
|
+
"""
|
|
1492
|
+
cpp_type = PINE_TYPE_TO_CPP.get(site.pine_type, "double")
|
|
1493
|
+
if site.pine_type != PineType.INT:
|
|
1494
|
+
return cpp_type
|
|
1495
|
+
node = getattr(site, "node", None)
|
|
1496
|
+
args = getattr(node, "args", ()) if node is not None else ()
|
|
1497
|
+
owner_info = self._func_info_map.get(
|
|
1498
|
+
getattr(site, "owner_func", None) or ""
|
|
1499
|
+
)
|
|
1500
|
+
if args and self._expr_returns_wide_int(
|
|
1501
|
+
args[0], owner_info, set(), None
|
|
1502
|
+
):
|
|
1503
|
+
return "int64_t"
|
|
1504
|
+
return cpp_type
|
|
1505
|
+
|
|
517
1506
|
def _int64_reassign_targets(self) -> set[str]:
|
|
518
1507
|
"""Names of vars that are reassigned (``:=``/``=``) anywhere in the AST
|
|
519
1508
|
with an RHS that is a top-level int64-returning builtin. Cached on the
|
|
@@ -522,14 +1511,31 @@ class TypeInferer:
|
|
|
522
1511
|
cached = getattr(self, "_int64_reassign_cache", None)
|
|
523
1512
|
if cached is not None:
|
|
524
1513
|
return cached
|
|
525
|
-
from ..ast_nodes import Assignment
|
|
526
1514
|
targets: set[str] = set()
|
|
1515
|
+
# Callable locals need their lexical owner so a reassignment through a
|
|
1516
|
+
# wrapper/method returning wide history is recognized as well as a
|
|
1517
|
+
# direct ``time[1]`` RHS.
|
|
1518
|
+
for info in getattr(self.ctx, "func_infos", ()):
|
|
1519
|
+
node = getattr(info, "node", None)
|
|
1520
|
+
if node is None:
|
|
1521
|
+
continue
|
|
1522
|
+
for child in self._walk_ast_list(node.body):
|
|
1523
|
+
if (
|
|
1524
|
+
isinstance(child, Assignment)
|
|
1525
|
+
and isinstance(child.target, Identifier)
|
|
1526
|
+
and self._expr_returns_wide_int(
|
|
1527
|
+
child.value, info, set(), None
|
|
1528
|
+
)
|
|
1529
|
+
):
|
|
1530
|
+
targets.add(child.target.name)
|
|
527
1531
|
ast = getattr(self.ctx, "ast", None)
|
|
528
1532
|
if ast is not None:
|
|
529
1533
|
for node in self._walk_ast(ast):
|
|
530
1534
|
if (isinstance(node, Assignment)
|
|
531
1535
|
and isinstance(node.target, Identifier)
|
|
532
|
-
and self.
|
|
1536
|
+
and self._expr_returns_wide_int(
|
|
1537
|
+
node.value, None, set(), None
|
|
1538
|
+
)):
|
|
533
1539
|
targets.add(node.target.name)
|
|
534
1540
|
self._int64_reassign_cache = targets
|
|
535
1541
|
return targets
|
|
@@ -566,12 +1572,15 @@ class TypeInferer:
|
|
|
566
1572
|
for ``double`` (already the default lowering), so those paths are
|
|
567
1573
|
unchanged.
|
|
568
1574
|
"""
|
|
569
|
-
#
|
|
570
|
-
#
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
1575
|
+
# Maps and matrices use their default-constructed null ID for Pine
|
|
1576
|
+
# ``na``. Arrays still lack a nullable runtime representation.
|
|
1577
|
+
collection_spec = self._collection_spec_for_name(name)
|
|
1578
|
+
if (collection_spec is not None
|
|
1579
|
+
and collection_spec.kind in {"map", "matrix"}):
|
|
1580
|
+
return self._type_spec_to_cpp(collection_spec)
|
|
1581
|
+
if ((collection_spec is not None
|
|
1582
|
+
and collection_spec.kind == "array")
|
|
1583
|
+
or self._identifier_udt_type(name) is not None):
|
|
575
1584
|
return None
|
|
576
1585
|
cpp_type: str | None = None
|
|
577
1586
|
# 1. ``var`` member (class-scope OR function-local: both are recorded in
|
|
@@ -605,134 +1614,6 @@ class TypeInferer:
|
|
|
605
1614
|
return cpp_type
|
|
606
1615
|
return None
|
|
607
1616
|
|
|
608
|
-
# ------------------------------------------------------------------
|
|
609
|
-
# BUG C: user-defined-UDT lvalue aliasing
|
|
610
|
-
# ------------------------------------------------------------------
|
|
611
|
-
|
|
612
|
-
def _is_udt_lvalue(self, expr) -> str | None:
|
|
613
|
-
"""If ``expr`` is a *user-defined* UDT lvalue (a bare ``Identifier`` that
|
|
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``.
|
|
617
|
-
|
|
618
|
-
Pine UDTs are reference types, so a local initialised from such an lvalue
|
|
619
|
-
and then mutated through must write back to the global. Drawing UDTs are
|
|
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
|
|
637
|
-
if not isinstance(expr, Identifier):
|
|
638
|
-
return None
|
|
639
|
-
udt_t = self._udt_var_types.get(expr.name)
|
|
640
|
-
if udt_t is None or udt_t not in self._udt_defs:
|
|
641
|
-
return None
|
|
642
|
-
if udt_t in DRAWING_TYPE_TO_CPP:
|
|
643
|
-
return None
|
|
644
|
-
# Must be a known global/class-scope member (not a function param or a
|
|
645
|
-
# plain local snapshot) for write-through to be observable.
|
|
646
|
-
if expr.name in getattr(self, "_current_func_locals", set()):
|
|
647
|
-
# A function-local of UDT type that is itself a persistent ``var``
|
|
648
|
-
# member still write-through aliases; but a plain inline local does
|
|
649
|
-
# not represent shared state. Only treat ``var`` func-locals (in
|
|
650
|
-
# func_var_members) as aliasable shared state.
|
|
651
|
-
fname = getattr(self, "_active_func_name", None)
|
|
652
|
-
var_locals = {n for n, _, _ in self.ctx.func_var_members.get(fname, [])} if fname else set()
|
|
653
|
-
if expr.name not in var_locals:
|
|
654
|
-
return None
|
|
655
|
-
return udt_t
|
|
656
|
-
|
|
657
|
-
def _udt_lvalue_selection_type(self, expr) -> str | None:
|
|
658
|
-
"""UDT type if ``expr`` is a UDT lvalue OR a ternary/switch whose every
|
|
659
|
-
selectable branch is a UDT lvalue of the SAME user-defined UDT type.
|
|
660
|
-
Returns ``None`` otherwise (so plain ``UDT a = b`` value-snapshots, calls,
|
|
661
|
-
``.new(...)`` ctors, and mixed/non-lvalue selections never alias)."""
|
|
662
|
-
direct = self._is_udt_lvalue(expr)
|
|
663
|
-
if direct is not None:
|
|
664
|
-
return direct
|
|
665
|
-
branches: list = []
|
|
666
|
-
if isinstance(expr, Ternary):
|
|
667
|
-
branches = [expr.true_val, expr.false_val]
|
|
668
|
-
elif isinstance(expr, SwitchStmt):
|
|
669
|
-
for _case_expr, stmts in (expr.cases or []):
|
|
670
|
-
if not stmts:
|
|
671
|
-
return None
|
|
672
|
-
last = stmts[-1]
|
|
673
|
-
branches.append(last.expr if isinstance(last, ExprStmt) else last)
|
|
674
|
-
if expr.default_body:
|
|
675
|
-
last = expr.default_body[-1]
|
|
676
|
-
branches.append(last.expr if isinstance(last, ExprStmt) else last)
|
|
677
|
-
else:
|
|
678
|
-
return None
|
|
679
|
-
if not branches:
|
|
680
|
-
return None
|
|
681
|
-
types = {self._is_udt_lvalue(b) for b in branches}
|
|
682
|
-
if len(types) == 1 and None not in types:
|
|
683
|
-
return next(iter(types))
|
|
684
|
-
return None
|
|
685
|
-
|
|
686
|
-
def _udt_local_alias_kind(self, node: VarDecl) -> tuple[str, str] | None:
|
|
687
|
-
"""Decide whether a hintless/typed local UDT declaration must ALIAS the
|
|
688
|
-
global(s) it selects rather than value-copy (BUG C).
|
|
689
|
-
|
|
690
|
-
Returns ``("ref", udt_type)`` for a non-rebinding reference alias,
|
|
691
|
-
``("ptr", udt_type)`` for a pointer alias (the local is later reassigned
|
|
692
|
-
to a *different* UDT lvalue, which a C++ reference cannot do), or
|
|
693
|
-
``None`` to keep the existing value-copy semantics.
|
|
694
|
-
|
|
695
|
-
Conditions (all required):
|
|
696
|
-
* RHS is a UDT lvalue or a ternary/switch selecting same-typed UDT
|
|
697
|
-
lvalues (``_udt_lvalue_selection_type``).
|
|
698
|
-
* The local is MUTATED later in the enclosing function body
|
|
699
|
-
(``local.field := ...``) — a pure read-only snapshot needn't alias.
|
|
700
|
-
|
|
701
|
-
The mutation requirement is the safety guard: a local that is only read
|
|
702
|
-
keeps value semantics, and a local initialised from a non-lvalue (a
|
|
703
|
-
``.new()`` ctor, a function return, or a plain local copy) returns
|
|
704
|
-
``None`` here, preserving intentional independent-copy semantics."""
|
|
705
|
-
from ..ast_nodes import Assignment
|
|
706
|
-
body = getattr(self, "_current_func_body", None)
|
|
707
|
-
if body is None:
|
|
708
|
-
return None
|
|
709
|
-
udt_t = self._udt_lvalue_selection_type(node.value)
|
|
710
|
-
if udt_t is None:
|
|
711
|
-
return None
|
|
712
|
-
name = node.name
|
|
713
|
-
mutated = False
|
|
714
|
-
rebinds_to_other_lvalue = False
|
|
715
|
-
for stmt in self._walk_ast_list(body):
|
|
716
|
-
if not isinstance(stmt, Assignment):
|
|
717
|
-
continue
|
|
718
|
-
tgt = stmt.target
|
|
719
|
-
# Mutation through the local: ``p.field := ...``
|
|
720
|
-
if (isinstance(tgt, MemberAccess)
|
|
721
|
-
and isinstance(tgt.object, Identifier)
|
|
722
|
-
and tgt.object.name == name):
|
|
723
|
-
mutated = True
|
|
724
|
-
# Rebind of the local itself to another UDT lvalue: ``p := other``
|
|
725
|
-
elif isinstance(tgt, Identifier) and tgt.name == name:
|
|
726
|
-
if self._udt_lvalue_selection_type(stmt.value) is not None:
|
|
727
|
-
rebinds_to_other_lvalue = True
|
|
728
|
-
else:
|
|
729
|
-
# Reassigned to a non-lvalue (e.g. ``.new()`` / a copy):
|
|
730
|
-
# aliasing would be wrong; bail to value-copy.
|
|
731
|
-
return None
|
|
732
|
-
if not mutated:
|
|
733
|
-
return None
|
|
734
|
-
return ("ptr" if rebinds_to_other_lvalue else "ref"), udt_t
|
|
735
|
-
|
|
736
1617
|
# ------------------------------------------------------------------
|
|
737
1618
|
# BUG 2: collection (array / map / matrix) lvalue aliasing
|
|
738
1619
|
# ------------------------------------------------------------------
|
|
@@ -745,15 +1626,9 @@ class TypeInferer:
|
|
|
745
1626
|
if not isinstance(expr, Identifier):
|
|
746
1627
|
return None
|
|
747
1628
|
name = expr.name
|
|
748
|
-
|
|
749
|
-
return self._matrix_specs[name]
|
|
750
|
-
spec = self._collection_types.get(name)
|
|
1629
|
+
spec = self._collection_spec_for_name(name)
|
|
751
1630
|
if spec is not None and spec.kind in ("array", "map", "matrix"):
|
|
752
1631
|
return spec
|
|
753
|
-
if name in self._array_vars:
|
|
754
|
-
return self._array_spec_for_name(name)
|
|
755
|
-
if name in self._map_vars:
|
|
756
|
-
return self._map_spec_for_name(name)
|
|
757
1632
|
return None
|
|
758
1633
|
|
|
759
1634
|
def _collection_lvalue_selection_spec(self, expr):
|
|
@@ -761,7 +1636,7 @@ class TypeInferer:
|
|
|
761
1636
|
whose every selectable branch is a collection lvalue of the SAME C++
|
|
762
1637
|
type; ``None`` otherwise (so ``array.new(...)`` ctors, copies, function
|
|
763
1638
|
returns, and mixed selections keep value-copy semantics). Mirrors
|
|
764
|
-
|
|
1639
|
+
the former UDT alias-selection path."""
|
|
765
1640
|
direct = self._collection_lvalue_spec(expr)
|
|
766
1641
|
if direct is not None:
|
|
767
1642
|
return direct
|
|
@@ -821,22 +1696,6 @@ class TypeInferer:
|
|
|
821
1696
|
for s in stmts:
|
|
822
1697
|
yield from self._walk_ast(s)
|
|
823
1698
|
|
|
824
|
-
def _addr_of_udt_selection(self, expr, local_name: str):
|
|
825
|
-
"""Render the address-of form of a UDT lvalue selection for a pointer
|
|
826
|
-
alias (BUG C rebind case): ``other`` -> ``&(other)``;
|
|
827
|
-
``cond ? a : b`` -> ``(cond ? &(a) : &(b))``. The selectable branches are
|
|
828
|
-
guaranteed (by ``_udt_lvalue_selection_type``) to be UDT lvalues."""
|
|
829
|
-
if isinstance(expr, Identifier):
|
|
830
|
-
return f"&({self._safe_name(expr.name)})"
|
|
831
|
-
if isinstance(expr, Ternary):
|
|
832
|
-
cond = self._visit_expr(expr.condition)
|
|
833
|
-
t = self._addr_of_udt_selection(expr.true_val, local_name)
|
|
834
|
-
f = self._addr_of_udt_selection(expr.false_val, local_name)
|
|
835
|
-
return f"({cond} ? {t} : {f})"
|
|
836
|
-
# Switch selection: lower to nested ternaries over case equality. Rare in
|
|
837
|
-
# practice; fall back to address-of the whole lowered expression.
|
|
838
|
-
return f"&({self._visit_expr(expr)})"
|
|
839
|
-
|
|
840
1699
|
def _infer_cpp_type_for_security_elem(self, node) -> str:
|
|
841
1700
|
"""C++ type for one element of the ``request.security(..., expr, ...)`` payload.
|
|
842
1701
|
|
|
@@ -882,6 +1741,10 @@ class TypeInferer:
|
|
|
882
1741
|
and ternaries / if / switch expressions. Returns the string
|
|
883
1742
|
``"double"`` as the safe fallback when no narrower type can be
|
|
884
1743
|
determined."""
|
|
1744
|
+
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
1745
|
+
# A callable-result history read keeps the callable's scalar
|
|
1746
|
+
# family. Collection subscripts follow separate element-type paths.
|
|
1747
|
+
return self._infer_type(node.object)
|
|
885
1748
|
if isinstance(node, NumberLiteral):
|
|
886
1749
|
return "double" if isinstance(node.value, float) else "int"
|
|
887
1750
|
if isinstance(node, BoolLiteral):
|
|
@@ -895,7 +1758,8 @@ class TypeInferer:
|
|
|
895
1758
|
return "int64_t"
|
|
896
1759
|
if node.name in BAR_FIELDS or node.name in BAR_BUILTINS:
|
|
897
1760
|
return "double"
|
|
898
|
-
if node.name in self._known_vars
|
|
1761
|
+
if (node.name in self._known_vars
|
|
1762
|
+
and not self._known_var_is_lexically_shadowed(node.name)):
|
|
899
1763
|
val = self._known_vars[node.name]
|
|
900
1764
|
if isinstance(val, bool):
|
|
901
1765
|
return "bool"
|
|
@@ -905,10 +1769,16 @@ class TypeInferer:
|
|
|
905
1769
|
return "int"
|
|
906
1770
|
if isinstance(val, float):
|
|
907
1771
|
return "double"
|
|
1772
|
+
if node.name in getattr(
|
|
1773
|
+
self, "_current_func_series_param_types", {}
|
|
1774
|
+
):
|
|
1775
|
+
return self._current_func_series_param_types[node.name]
|
|
908
1776
|
if node.name in self._current_func_param_types:
|
|
909
1777
|
return self._current_func_param_types[node.name]
|
|
910
1778
|
if node.name in getattr(self, "_current_func_local_types", {}):
|
|
911
1779
|
return self._current_func_local_types[node.name]
|
|
1780
|
+
if node.name in getattr(self, "_current_loop_vars", set()):
|
|
1781
|
+
return "double"
|
|
912
1782
|
sym = self.ctx.symbols.resolve(node.name)
|
|
913
1783
|
if sym is not None and getattr(sym, "type_spec", None) is not None:
|
|
914
1784
|
return self._type_spec_to_cpp(sym.type_spec)
|
|
@@ -917,6 +1787,18 @@ class TypeInferer:
|
|
|
917
1787
|
return "double"
|
|
918
1788
|
if isinstance(node, FuncCall):
|
|
919
1789
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
1790
|
+
# Nested trade-accessor calls bypass the flat namespace signature
|
|
1791
|
+
# table. Their textual metadata accessors return std::string from
|
|
1792
|
+
# the runtime, so hintless locals must not use the double fallback.
|
|
1793
|
+
if (isinstance(node.callee, MemberAccess)
|
|
1794
|
+
and isinstance(node.callee.object, MemberAccess)
|
|
1795
|
+
and isinstance(node.callee.object.object, Identifier)
|
|
1796
|
+
and node.callee.object.object.name == "strategy"
|
|
1797
|
+
and node.callee.object.member in ("closedtrades", "opentrades")
|
|
1798
|
+
and func_name in (
|
|
1799
|
+
"entry_id", "exit_id", "entry_comment", "exit_comment",
|
|
1800
|
+
)):
|
|
1801
|
+
return "std::string"
|
|
920
1802
|
# Drawing scalar getter return type (get_text -> std::string,
|
|
921
1803
|
# get_x* -> int64_t, get_y*/get_price/get_top/get_bottom -> double).
|
|
922
1804
|
if getattr(self, "_uses_drawing", False):
|
|
@@ -965,17 +1847,48 @@ class TypeInferer:
|
|
|
965
1847
|
recv_spec = self._type_spec_from_expr(node.callee.object)
|
|
966
1848
|
if recv_spec is not None and recv_spec.kind == "array" and member_name == "join":
|
|
967
1849
|
return "std::string"
|
|
968
|
-
|
|
969
|
-
|
|
1850
|
+
receiver_name = method_receiver_type_name(recv_spec)
|
|
1851
|
+
if receiver_name is not None:
|
|
1852
|
+
fi_u = self._func_info_map.get(
|
|
1853
|
+
f"{receiver_name}.{member_name}"
|
|
1854
|
+
)
|
|
970
1855
|
if fi_u is not None:
|
|
971
|
-
|
|
1856
|
+
call_site_idx = self._callable_target_callsite_idx(
|
|
1857
|
+
fi_u, node
|
|
1858
|
+
)
|
|
1859
|
+
if self._func_int_return_uses_wide_history(
|
|
1860
|
+
fi_u, call_site_idx=call_site_idx
|
|
1861
|
+
):
|
|
1862
|
+
return "int64_t"
|
|
1863
|
+
if getattr(fi_u, "return_type_spec", None) is not None:
|
|
1864
|
+
return self._type_spec_to_cpp(fi_u.return_type_spec)
|
|
1865
|
+
return PINE_TYPE_TO_CPP.get(
|
|
1866
|
+
self._callsite_callable_return_pine_type(
|
|
1867
|
+
fi_u, call_site_idx
|
|
1868
|
+
),
|
|
1869
|
+
"double",
|
|
1870
|
+
)
|
|
1871
|
+
if namespace is None and func_name in self._func_info_map:
|
|
1872
|
+
fi_u = self._func_info_map[func_name]
|
|
1873
|
+
call_site_idx = self._callable_target_callsite_idx(fi_u, node)
|
|
1874
|
+
if self._func_int_return_uses_wide_history(
|
|
1875
|
+
fi_u, call_site_idx=call_site_idx
|
|
1876
|
+
):
|
|
1877
|
+
return "int64_t"
|
|
972
1878
|
spec = self._type_spec_from_expr(node)
|
|
973
1879
|
if spec is not None:
|
|
974
1880
|
return self._type_spec_to_cpp(spec)
|
|
975
1881
|
if namespace in self._udt_defs and func_name == "new":
|
|
976
1882
|
return namespace
|
|
977
1883
|
if namespace is None and func_name in self._func_info_map:
|
|
978
|
-
|
|
1884
|
+
fi_u = self._func_info_map[func_name]
|
|
1885
|
+
return PINE_TYPE_TO_CPP.get(
|
|
1886
|
+
self._callsite_callable_return_pine_type(
|
|
1887
|
+
fi_u,
|
|
1888
|
+
self._callable_target_callsite_idx(fi_u, node),
|
|
1889
|
+
),
|
|
1890
|
+
"double",
|
|
1891
|
+
)
|
|
979
1892
|
site = self._get_ta_site(node)
|
|
980
1893
|
if site is not None:
|
|
981
1894
|
ta_name = self._ta_name_from_site(site)
|
|
@@ -1018,6 +1931,11 @@ class TypeInferer:
|
|
|
1018
1931
|
if spec is not None:
|
|
1019
1932
|
return self._type_spec_to_cpp(spec)
|
|
1020
1933
|
if isinstance(node, Ternary):
|
|
1934
|
+
ternary_spec = self._type_spec_from_expr(node)
|
|
1935
|
+
if (ternary_spec is not None
|
|
1936
|
+
and ternary_spec.kind == "udt"
|
|
1937
|
+
and ternary_spec.name in DRAWING_TYPE_TO_CPP):
|
|
1938
|
+
return self._type_spec_to_cpp(ternary_spec)
|
|
1021
1939
|
tt = self._infer_type(node.true_val)
|
|
1022
1940
|
ft = self._infer_type(node.false_val)
|
|
1023
1941
|
if tt.startswith("std::vector") or ft.startswith("std::vector"):
|
|
@@ -1034,6 +1952,11 @@ class TypeInferer:
|
|
|
1034
1952
|
# Block-as-expression cases: read the type of the last statement of
|
|
1035
1953
|
# the first branch / case; matches Pine semantics for ``x = if...``.
|
|
1036
1954
|
if isinstance(node, IfStmt):
|
|
1955
|
+
block_spec = self._type_spec_from_expr(node)
|
|
1956
|
+
if (block_spec is not None
|
|
1957
|
+
and block_spec.kind == "udt"
|
|
1958
|
+
and block_spec.name in DRAWING_TYPE_TO_CPP):
|
|
1959
|
+
return self._type_spec_to_cpp(block_spec)
|
|
1037
1960
|
if node.body:
|
|
1038
1961
|
last = node.body[-1]
|
|
1039
1962
|
if isinstance(last, ExprStmt):
|
|
@@ -1065,6 +1988,11 @@ class TypeInferer:
|
|
|
1065
1988
|
local_types: dict[str, str] = {}
|
|
1066
1989
|
for stmt in self._walk_ast(func_node):
|
|
1067
1990
|
if isinstance(stmt, VarDecl) and stmt.value is not None and stmt.name:
|
|
1991
|
+
captured = self._callable_collection_bindings.get(id(stmt))
|
|
1992
|
+
if (captured is not None
|
|
1993
|
+
and captured.kind in {"array", "map", "matrix"}):
|
|
1994
|
+
local_types[stmt.name] = self._type_spec_to_cpp(captured)
|
|
1995
|
+
continue
|
|
1068
1996
|
if stmt.type_hint:
|
|
1069
1997
|
spec = self._type_spec_from_hint_name(stmt.type_hint)
|
|
1070
1998
|
if spec is not None:
|