@pineforge/codegen-pyodide 0.10.0 → 0.10.2
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 +2280 -228
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +747 -177
- package/pineforge_codegen/codegen/security.py +1546 -132
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +328 -80
- package/pineforge_codegen/codegen/types.py +1160 -227
- package/pineforge_codegen/codegen/visit_call.py +1191 -104
- 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 +262 -23
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.2.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.10.0.tar.gz +0 -0
|
@@ -133,14 +133,21 @@ all tables it needs come from ``codegen/tables.py``, AST classes from
|
|
|
133
133
|
from __future__ import annotations
|
|
134
134
|
|
|
135
135
|
from ..ast_nodes import (
|
|
136
|
+
ASTNode,
|
|
136
137
|
FuncCall,
|
|
137
138
|
Identifier,
|
|
138
139
|
MemberAccess,
|
|
139
140
|
NaLiteral,
|
|
140
141
|
TupleLiteral,
|
|
141
142
|
StringLiteral,
|
|
143
|
+
VarDecl,
|
|
144
|
+
)
|
|
145
|
+
from ..symbols import TypeSpec, method_receiver_type_name
|
|
146
|
+
from ..method_binding import (
|
|
147
|
+
MethodBindError,
|
|
148
|
+
bind_method_call,
|
|
149
|
+
signature_from_callable,
|
|
142
150
|
)
|
|
143
|
-
from ..symbols import TypeSpec
|
|
144
151
|
from .. import signatures as sigs
|
|
145
152
|
from .drawing import ALL_DRAWING_METHODS
|
|
146
153
|
from .tables import (
|
|
@@ -148,9 +155,11 @@ from .tables import (
|
|
|
148
155
|
ARRAY_METHODS,
|
|
149
156
|
BAR_FIELDS,
|
|
150
157
|
BAR_SERIES_PUSH,
|
|
158
|
+
CHECKED_ARRAY_METHOD_KWARGS,
|
|
151
159
|
DRAWING_NS,
|
|
152
160
|
DRAWING_TYPE_TO_CPP,
|
|
153
161
|
MAP_METHODS,
|
|
162
|
+
MAP_METHOD_KWARGS,
|
|
154
163
|
MATH_FUNC_MAP,
|
|
155
164
|
MATRIX_METHODS,
|
|
156
165
|
MATRIX_METHOD_KWARGS,
|
|
@@ -163,7 +172,6 @@ from .tables import (
|
|
|
163
172
|
TIME_FIELD_EXPRS,
|
|
164
173
|
_math_minmax_na_expr,
|
|
165
174
|
_merge_kwargs,
|
|
166
|
-
_merge_kwargs_with_defaults,
|
|
167
175
|
tz_time_field_lambda,
|
|
168
176
|
)
|
|
169
177
|
|
|
@@ -243,6 +251,149 @@ class CallVisitor:
|
|
|
243
251
|
return f"{base}_cs{self._active_call_site_idx}"
|
|
244
252
|
return base
|
|
245
253
|
|
|
254
|
+
def _typed_user_method_info(self, receiver, member: str):
|
|
255
|
+
"""Resolve an exact typed user method before any builtin method."""
|
|
256
|
+
receiver_spec = self._type_spec_from_expr(receiver)
|
|
257
|
+
receiver_name = method_receiver_type_name(receiver_spec)
|
|
258
|
+
if receiver_name is None:
|
|
259
|
+
return receiver_spec, None
|
|
260
|
+
method_info = self._func_info_map.get(f"{receiver_name}.{member}")
|
|
261
|
+
if (
|
|
262
|
+
method_info is None
|
|
263
|
+
or not getattr(method_info, "is_udt_method", False)
|
|
264
|
+
):
|
|
265
|
+
return receiver_spec, None
|
|
266
|
+
return receiver_spec, method_info
|
|
267
|
+
|
|
268
|
+
def _bind_typed_method_args(self, method_info, node: FuncCall):
|
|
269
|
+
"""Bind typed method arguments through the analyzer-shared rules."""
|
|
270
|
+
|
|
271
|
+
param_names = (
|
|
272
|
+
list(method_info.node.params[1:])
|
|
273
|
+
if method_info.node is not None
|
|
274
|
+
else []
|
|
275
|
+
)
|
|
276
|
+
param_defaults = list(
|
|
277
|
+
getattr(method_info, "param_defaults", ()) or ()
|
|
278
|
+
)[1:]
|
|
279
|
+
hints = list(
|
|
280
|
+
(getattr(method_info.node, "annotations", None) or {}).get(
|
|
281
|
+
"param_type_hints", ()
|
|
282
|
+
)
|
|
283
|
+
if method_info.node is not None
|
|
284
|
+
else ()
|
|
285
|
+
)[1:]
|
|
286
|
+
signature = signature_from_callable(
|
|
287
|
+
method_info.name,
|
|
288
|
+
param_names,
|
|
289
|
+
param_defaults,
|
|
290
|
+
hints,
|
|
291
|
+
)
|
|
292
|
+
try:
|
|
293
|
+
return bind_method_call(signature, node)
|
|
294
|
+
except MethodBindError as exc:
|
|
295
|
+
self._codegen_error(node, str(exc))
|
|
296
|
+
raise AssertionError("unreachable") from exc
|
|
297
|
+
|
|
298
|
+
def _emit_udt_new_expr(
|
|
299
|
+
self,
|
|
300
|
+
type_name: str,
|
|
301
|
+
field_initializers: list[str],
|
|
302
|
+
) -> str:
|
|
303
|
+
"""Allocate one authored UDT record and return its numeric object ID."""
|
|
304
|
+
record_type = self._udt_record_cpp_type(type_name)
|
|
305
|
+
arena = self._udt_arena_member_name(type_name)
|
|
306
|
+
return f"{arena}.create({record_type}{{{', '.join(field_initializers)}}})"
|
|
307
|
+
|
|
308
|
+
def _emit_udt_copy_expr(self, type_name: str, source_cpp: str) -> str:
|
|
309
|
+
"""Allocate a detached outer record while shallow-copying nested IDs."""
|
|
310
|
+
return f"{self._udt_arena_member_name(type_name)}.copy({source_cpp})"
|
|
311
|
+
|
|
312
|
+
def _emit_typed_user_method_call(
|
|
313
|
+
self,
|
|
314
|
+
node: FuncCall,
|
|
315
|
+
receiver_spec: TypeSpec,
|
|
316
|
+
method_info,
|
|
317
|
+
) -> str:
|
|
318
|
+
"""Lower a typed extension method with ordinary callable semantics."""
|
|
319
|
+
callee = node.callee
|
|
320
|
+
assert isinstance(callee, MemberAccess)
|
|
321
|
+
receiver_node = callee.object
|
|
322
|
+
fn_cpp = self._udt_method_call_emit_name(method_info, node)
|
|
323
|
+
|
|
324
|
+
binding = self._bind_typed_method_args(method_info, node)
|
|
325
|
+
rest_nodes = list(binding.args_by_param)
|
|
326
|
+
receiver_cpp = self._visit_typed_method_param(
|
|
327
|
+
method_info,
|
|
328
|
+
node,
|
|
329
|
+
receiver_node,
|
|
330
|
+
0,
|
|
331
|
+
)
|
|
332
|
+
rest_cpp = [
|
|
333
|
+
self._visit_typed_method_param(
|
|
334
|
+
method_info,
|
|
335
|
+
node,
|
|
336
|
+
arg,
|
|
337
|
+
index + 1,
|
|
338
|
+
)
|
|
339
|
+
for index, arg in enumerate(rest_nodes)
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
receiver_root = receiver_node
|
|
343
|
+
while isinstance(receiver_root, MemberAccess):
|
|
344
|
+
receiver_root = receiver_root.object
|
|
345
|
+
receiver_passes_by_reference = (
|
|
346
|
+
receiver_spec.kind in {"array", "matrix"}
|
|
347
|
+
or (
|
|
348
|
+
receiver_spec.kind == "udt"
|
|
349
|
+
and receiver_spec.name not in self._udt_defs
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
return self._ordered_user_call_expr(
|
|
353
|
+
fn_cpp,
|
|
354
|
+
[receiver_node, *rest_nodes],
|
|
355
|
+
[receiver_cpp, *rest_cpp],
|
|
356
|
+
source_order_nodes=[
|
|
357
|
+
receiver_node,
|
|
358
|
+
*binding.evaluation_order,
|
|
359
|
+
],
|
|
360
|
+
force_stage=(
|
|
361
|
+
receiver_passes_by_reference
|
|
362
|
+
and not isinstance(receiver_root, Identifier)
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def _callable_target_callsite_idx(self, fi, node: FuncCall) -> int | None:
|
|
367
|
+
"""Return the primitive profile selected by this emitted call path."""
|
|
368
|
+
cs_info = self.ctx.func_call_cs_map.get(id(node))
|
|
369
|
+
count = self.ctx.func_call_site_counts.get(fi.name, 0)
|
|
370
|
+
if self._active_call_site_idx is not None and (
|
|
371
|
+
cs_info is not None or count > 1
|
|
372
|
+
):
|
|
373
|
+
return self._active_call_site_idx
|
|
374
|
+
if cs_info is not None and cs_info[0] == fi.name:
|
|
375
|
+
return cs_info[1]
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _series_bridge_value_expr(expr_cpp: str, cpp_type: str) -> str:
|
|
380
|
+
"""Convert one scalar to a Series element without erasing Pine na.
|
|
381
|
+
|
|
382
|
+
Numeric C++ casts do not translate sentinels: INT64_MIN converted to
|
|
383
|
+
double is finite, while NaN converted to an integer is undefined. A
|
|
384
|
+
history bridge must re-materialize ``na<T>`` in the destination family
|
|
385
|
+
before pushing the value.
|
|
386
|
+
"""
|
|
387
|
+
return (
|
|
388
|
+
"([&]() { auto _pf_series_raw = ("
|
|
389
|
+
+ expr_cpp
|
|
390
|
+
+ "); return is_na(_pf_series_raw) ? na<"
|
|
391
|
+
+ cpp_type
|
|
392
|
+
+ ">() : static_cast<"
|
|
393
|
+
+ cpp_type
|
|
394
|
+
+ ">(_pf_series_raw); }())"
|
|
395
|
+
)
|
|
396
|
+
|
|
246
397
|
def _array_init_value_expr(self, elem_spec: TypeSpec | None, value_node) -> str:
|
|
247
398
|
if isinstance(value_node, NaLiteral):
|
|
248
399
|
if elem_spec is not None and elem_spec.kind == "udt":
|
|
@@ -281,27 +432,660 @@ class CallVisitor:
|
|
|
281
432
|
for idx, arg in enumerate(arg_nodes)
|
|
282
433
|
]
|
|
283
434
|
|
|
435
|
+
def _array_method_arg_nodes(self, method: str, node: FuncCall) -> list:
|
|
436
|
+
"""Merge checked-array method kwargs into Pine signature order."""
|
|
437
|
+
param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
|
|
438
|
+
if param_names is None:
|
|
439
|
+
return list(node.args)
|
|
440
|
+
return _merge_kwargs(node.args, node.kwargs, param_names, lambda arg: arg)
|
|
441
|
+
|
|
442
|
+
def _array_function_arg_nodes(self, method: str, node: FuncCall) -> list:
|
|
443
|
+
"""Merge ``array.method(id=..., ...)`` arguments in signature order."""
|
|
444
|
+
if method == "copy":
|
|
445
|
+
if len(node.args) == 1 and not node.kwargs:
|
|
446
|
+
return list(node.args)
|
|
447
|
+
if not node.args and set(node.kwargs) == {"id"}:
|
|
448
|
+
return [node.kwargs["id"]]
|
|
449
|
+
self._codegen_error(
|
|
450
|
+
node,
|
|
451
|
+
"array.copy: expected exactly one receiver 'id'",
|
|
452
|
+
hint="Use array.copy(source) or array.copy(id=source).",
|
|
453
|
+
)
|
|
454
|
+
param_names = CHECKED_ARRAY_METHOD_KWARGS.get(method)
|
|
455
|
+
if param_names is None:
|
|
456
|
+
return list(node.args)
|
|
457
|
+
return _merge_kwargs(
|
|
458
|
+
node.args, node.kwargs, ["id", *param_names], lambda arg: arg
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def _map_identifier_is_visible_binding(self, node: FuncCall) -> bool:
|
|
462
|
+
"""Whether ``map`` is a lexical value at this exact source position.
|
|
463
|
+
|
|
464
|
+
Global collection registries are intentionally pre-populated before
|
|
465
|
+
code emission, so raw membership in ``_var_names`` / collection maps
|
|
466
|
+
cannot answer this question: a later ``map`` declaration must not
|
|
467
|
+
capture an earlier built-in ``map.*`` call. Callable/block overlays,
|
|
468
|
+
by contrast, are activated in source order and therefore take first
|
|
469
|
+
refusal. For a direct top-level binding, compare the containing
|
|
470
|
+
top-level statement indexes; the binding becomes visible only after
|
|
471
|
+
its declaration statement, never inside its own RHS.
|
|
472
|
+
"""
|
|
473
|
+
name = "map"
|
|
474
|
+
if (
|
|
475
|
+
name in getattr(self, "_current_func_param_types", {})
|
|
476
|
+
or name in getattr(self, "_current_func_collection_specs", {})
|
|
477
|
+
or name in getattr(self, "_current_func_collection_shadows", set())
|
|
478
|
+
or name in getattr(self, "_current_func_local_types", {})
|
|
479
|
+
or name in getattr(self, "_current_loop_vars", set())
|
|
480
|
+
or getattr(self, "_block_map_binding_visible", False)
|
|
481
|
+
):
|
|
482
|
+
return True
|
|
483
|
+
|
|
484
|
+
top_index_by_node = getattr(self, "_top_level_index_by_ast_node", None)
|
|
485
|
+
top_index_by_span = getattr(self, "_top_level_index_by_source_span", None)
|
|
486
|
+
map_decl_index = getattr(self, "_top_level_map_decl_index", None)
|
|
487
|
+
if top_index_by_node is None or top_index_by_span is None:
|
|
488
|
+
top_index_by_node = {}
|
|
489
|
+
top_index_by_span = {}
|
|
490
|
+
ambiguous_spans = set()
|
|
491
|
+
map_decl_index = None
|
|
492
|
+
for index, statement in enumerate(self.ctx.ast.body):
|
|
493
|
+
for candidate in self._walk_ast(statement):
|
|
494
|
+
top_index_by_node[id(candidate)] = index
|
|
495
|
+
loc = getattr(candidate, "loc", None)
|
|
496
|
+
if loc is not None:
|
|
497
|
+
span = (
|
|
498
|
+
type(candidate), loc.file, loc.line, loc.col, loc.end_col,
|
|
499
|
+
)
|
|
500
|
+
previous = top_index_by_span.get(span)
|
|
501
|
+
if previous is None:
|
|
502
|
+
top_index_by_span[span] = index
|
|
503
|
+
elif previous != index:
|
|
504
|
+
ambiguous_spans.add(span)
|
|
505
|
+
if (
|
|
506
|
+
map_decl_index is None
|
|
507
|
+
and isinstance(statement, VarDecl)
|
|
508
|
+
and statement.name == name
|
|
509
|
+
):
|
|
510
|
+
map_decl_index = index
|
|
511
|
+
for span in ambiguous_spans:
|
|
512
|
+
top_index_by_span.pop(span, None)
|
|
513
|
+
self._top_level_index_by_ast_node = top_index_by_node
|
|
514
|
+
self._top_level_index_by_source_span = top_index_by_span
|
|
515
|
+
self._top_level_map_decl_index = map_decl_index
|
|
516
|
+
|
|
517
|
+
if map_decl_index is None:
|
|
518
|
+
return False
|
|
519
|
+
call_index = top_index_by_node.get(id(node))
|
|
520
|
+
if call_index is None:
|
|
521
|
+
# Setup-time rewrites (notably request.security timeframe input
|
|
522
|
+
# substitution) may clone an expression. Those clones retain the
|
|
523
|
+
# original source span, so recover their containing top-level
|
|
524
|
+
# statement from the unmodified AST instead of treating every
|
|
525
|
+
# synthetic node as if all globals were already visible.
|
|
526
|
+
loc = getattr(node, "loc", None)
|
|
527
|
+
if loc is not None:
|
|
528
|
+
call_index = top_index_by_span.get(
|
|
529
|
+
(type(node), loc.file, loc.line, loc.col, loc.end_col)
|
|
530
|
+
)
|
|
531
|
+
if call_index is None:
|
|
532
|
+
# With no identity or source ancestry there is no evidence that a
|
|
533
|
+
# later global binding was visible. Fail closed to the built-in
|
|
534
|
+
# namespace; synthetic lexical receivers must preserve provenance.
|
|
535
|
+
return False
|
|
536
|
+
return map_decl_index < call_index
|
|
537
|
+
|
|
538
|
+
def _map_call_arg_nodes(
|
|
539
|
+
self,
|
|
540
|
+
method: str,
|
|
541
|
+
node: FuncCall,
|
|
542
|
+
*,
|
|
543
|
+
functional: bool,
|
|
544
|
+
allow_keywords: bool,
|
|
545
|
+
) -> list:
|
|
546
|
+
"""Validate and bind one established map-call form.
|
|
547
|
+
|
|
548
|
+
Map lowerings index directly into their argument arrays. Before this
|
|
549
|
+
guard, malformed calls therefore either reached a raw ``IndexError``
|
|
550
|
+
or, for keyword-only ``map.*`` functional calls, silently lowered to
|
|
551
|
+
``0``. Keep routing policy separate from signature validation: only
|
|
552
|
+
the typed-map UDF-parameter method lane currently accepts canonical
|
|
553
|
+
keywords; all other lanes retain their established positional-only
|
|
554
|
+
semantics and fail closed when keywords are supplied.
|
|
555
|
+
"""
|
|
556
|
+
if method == "new":
|
|
557
|
+
param_names: list[str] = []
|
|
558
|
+
else:
|
|
559
|
+
method_params = MAP_METHOD_KWARGS.get(method)
|
|
560
|
+
if method_params is None:
|
|
561
|
+
return list(node.args)
|
|
562
|
+
param_names = (["id", *method_params] if functional
|
|
563
|
+
else list(method_params))
|
|
564
|
+
|
|
565
|
+
signature = f"map.{method}({', '.join(param_names)})"
|
|
566
|
+
unknown = sorted(set(node.kwargs) - set(param_names))
|
|
567
|
+
if unknown:
|
|
568
|
+
name = unknown[0]
|
|
569
|
+
hint = f"Expected {signature}."
|
|
570
|
+
if method == "put_all" and name == "from":
|
|
571
|
+
hint = f"Use 'id2=' for the source map. Expected {signature}."
|
|
572
|
+
self._codegen_error(
|
|
573
|
+
node,
|
|
574
|
+
f"map.{method}: unknown keyword argument '{name}'",
|
|
575
|
+
hint=hint,
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
if len(node.args) > len(param_names):
|
|
579
|
+
self._codegen_error(
|
|
580
|
+
node,
|
|
581
|
+
(
|
|
582
|
+
f"map.{method}: too many positional arguments "
|
|
583
|
+
f"(expected {len(param_names)}, got {len(node.args)})"
|
|
584
|
+
),
|
|
585
|
+
hint=f"Expected {signature}.",
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
for name in param_names[:len(node.args)]:
|
|
589
|
+
if name in node.kwargs:
|
|
590
|
+
self._codegen_error(
|
|
591
|
+
node,
|
|
592
|
+
(
|
|
593
|
+
f"map.{method}: argument '{name}' passed both "
|
|
594
|
+
"positionally and by keyword"
|
|
595
|
+
),
|
|
596
|
+
hint=f"Expected {signature}; bind each argument once.",
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
bound: list = [None] * len(param_names)
|
|
600
|
+
for index, arg in enumerate(node.args):
|
|
601
|
+
bound[index] = arg
|
|
602
|
+
for name, value in node.kwargs.items():
|
|
603
|
+
bound[param_names.index(name)] = value
|
|
604
|
+
|
|
605
|
+
missing = [
|
|
606
|
+
name for name, value in zip(param_names, bound) if value is None
|
|
607
|
+
]
|
|
608
|
+
if missing:
|
|
609
|
+
self._codegen_error(
|
|
610
|
+
node,
|
|
611
|
+
f"map.{method}: missing required argument '{missing[0]}'",
|
|
612
|
+
hint=f"Expected {signature}.",
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
if node.kwargs and not allow_keywords:
|
|
616
|
+
form = "functional" if functional else "receiver-method"
|
|
617
|
+
self._codegen_error(
|
|
618
|
+
node,
|
|
619
|
+
(
|
|
620
|
+
f"map.{method}: keyword arguments are not supported "
|
|
621
|
+
f"for this {form} call form"
|
|
622
|
+
),
|
|
623
|
+
hint=f"Use the established positional form: {signature}.",
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
return bound
|
|
627
|
+
|
|
628
|
+
def _map_param_method_expr(
|
|
629
|
+
self, map_expr: str, method: str, arg_nodes: list, spec: TypeSpec,
|
|
630
|
+
) -> str:
|
|
631
|
+
"""Lower one map operation with receiver-first, ordered single use.
|
|
632
|
+
|
|
633
|
+
C++17 sequences a member-call receiver before its arguments but does
|
|
634
|
+
not order sibling arguments. Pine requires receiver, key, then value.
|
|
635
|
+
Nested immediately-invoked lambdas bind the receiver first and every
|
|
636
|
+
argument in source/signature order, while retaining lvalue aliases and
|
|
637
|
+
extending temporary receiver lifetime through the operation.
|
|
638
|
+
"""
|
|
639
|
+
args = [self._visit_expr(arg) for arg in arg_nodes]
|
|
640
|
+
occupied = "\n".join((map_expr, *args))
|
|
641
|
+
counter = getattr(self, "_map_param_arg_counter", 0)
|
|
642
|
+
bindings: list[tuple[str, str]] = []
|
|
643
|
+
for arg in args:
|
|
644
|
+
while True:
|
|
645
|
+
token = f"__pf_map_param_arg_{counter}"
|
|
646
|
+
counter += 1
|
|
647
|
+
if token not in occupied:
|
|
648
|
+
break
|
|
649
|
+
bindings.append((token, arg))
|
|
650
|
+
self._map_param_arg_counter = counter
|
|
651
|
+
|
|
652
|
+
receiver_counter = getattr(self, "_map_receiver_counter", 0)
|
|
653
|
+
while True:
|
|
654
|
+
receiver_token = f"__pf_map_receiver_{receiver_counter}"
|
|
655
|
+
receiver_counter += 1
|
|
656
|
+
if receiver_token not in occupied:
|
|
657
|
+
break
|
|
658
|
+
self._map_receiver_counter = receiver_counter
|
|
659
|
+
|
|
660
|
+
lowered = self._map_method_expr(
|
|
661
|
+
receiver_token, method, [token for token, _arg in bindings], spec
|
|
662
|
+
)
|
|
663
|
+
for token, arg in reversed(bindings):
|
|
664
|
+
lowered = (
|
|
665
|
+
f"[&](auto&& {token})->decltype(auto){{ "
|
|
666
|
+
f"return {lowered}; }}(({arg}))"
|
|
667
|
+
)
|
|
668
|
+
return (
|
|
669
|
+
f"[&](auto&& {receiver_token})->decltype(auto){{ "
|
|
670
|
+
f"return {lowered}; }}(({map_expr}))"
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
def _expr_contains_map_operation(
|
|
674
|
+
self,
|
|
675
|
+
node,
|
|
676
|
+
visiting_callables: frozenset[str] = frozenset(),
|
|
677
|
+
lexical_specs: dict[str, TypeSpec | None] | None = None,
|
|
678
|
+
) -> bool:
|
|
679
|
+
"""Whether an expression directly or transitively operates on a map.
|
|
680
|
+
|
|
681
|
+
C++17 does not order sibling function arguments. We keep ordinary
|
|
682
|
+
calls byte-identical and only stage calls whose argument tree can
|
|
683
|
+
observe map identity/state (including a user helper whose body performs
|
|
684
|
+
such an operation).
|
|
685
|
+
"""
|
|
686
|
+
if not isinstance(node, ASTNode):
|
|
687
|
+
return False
|
|
688
|
+
lexical_specs = lexical_specs or {}
|
|
689
|
+
if isinstance(node, FuncCall):
|
|
690
|
+
callee = node.callee
|
|
691
|
+
if isinstance(callee, MemberAccess):
|
|
692
|
+
if (
|
|
693
|
+
isinstance(callee.object, Identifier)
|
|
694
|
+
and callee.object.name == "map"
|
|
695
|
+
and callee.member in (set(MAP_METHODS) | {"new"})
|
|
696
|
+
):
|
|
697
|
+
return True
|
|
698
|
+
receiver_spec = self._map_effect_type_spec(
|
|
699
|
+
callee.object, lexical_specs
|
|
700
|
+
)
|
|
701
|
+
if (
|
|
702
|
+
receiver_spec is not None
|
|
703
|
+
and receiver_spec.kind == "map"
|
|
704
|
+
and callee.member in MAP_METHODS
|
|
705
|
+
):
|
|
706
|
+
return True
|
|
707
|
+
|
|
708
|
+
info_key, func_info = self._map_effect_callable_info(
|
|
709
|
+
node, lexical_specs
|
|
710
|
+
)
|
|
711
|
+
if (
|
|
712
|
+
func_info is not None
|
|
713
|
+
and getattr(func_info, "node", None) is not None
|
|
714
|
+
and info_key not in visiting_callables
|
|
715
|
+
):
|
|
716
|
+
next_visiting = visiting_callables | {info_key}
|
|
717
|
+
child_specs = self._map_effect_callable_specs(
|
|
718
|
+
func_info,
|
|
719
|
+
node,
|
|
720
|
+
lexical_specs,
|
|
721
|
+
)
|
|
722
|
+
if any(
|
|
723
|
+
self._expr_contains_map_operation(
|
|
724
|
+
child,
|
|
725
|
+
next_visiting,
|
|
726
|
+
child_specs,
|
|
727
|
+
)
|
|
728
|
+
for child in func_info.node.body
|
|
729
|
+
):
|
|
730
|
+
return True
|
|
731
|
+
|
|
732
|
+
for value in vars(node).values():
|
|
733
|
+
if isinstance(value, ASTNode):
|
|
734
|
+
if self._expr_contains_map_operation(
|
|
735
|
+
value, visiting_callables, lexical_specs
|
|
736
|
+
):
|
|
737
|
+
return True
|
|
738
|
+
elif isinstance(value, list):
|
|
739
|
+
if any(
|
|
740
|
+
self._expr_contains_map_operation(
|
|
741
|
+
item, visiting_callables, lexical_specs
|
|
742
|
+
)
|
|
743
|
+
for item in value
|
|
744
|
+
if isinstance(item, ASTNode)
|
|
745
|
+
):
|
|
746
|
+
return True
|
|
747
|
+
elif isinstance(value, dict):
|
|
748
|
+
if any(
|
|
749
|
+
self._expr_contains_map_operation(
|
|
750
|
+
item, visiting_callables, lexical_specs
|
|
751
|
+
)
|
|
752
|
+
for item in value.values()
|
|
753
|
+
if isinstance(item, ASTNode)
|
|
754
|
+
):
|
|
755
|
+
return True
|
|
756
|
+
return False
|
|
757
|
+
|
|
758
|
+
def _map_effect_type_spec(
|
|
759
|
+
self,
|
|
760
|
+
node,
|
|
761
|
+
lexical_specs: dict[str, TypeSpec | None],
|
|
762
|
+
) -> TypeSpec | None:
|
|
763
|
+
"""Resolve an expression type in the callable being inspected.
|
|
764
|
+
|
|
765
|
+
The ordinary codegen resolver describes the function currently being
|
|
766
|
+
emitted, not a transitive helper whose AST is being audited for effects.
|
|
767
|
+
Respect that helper's parameter/local bindings first, then fall back to
|
|
768
|
+
the established expression inference for globals and constructors.
|
|
769
|
+
"""
|
|
770
|
+
if isinstance(node, Identifier) and node.name in lexical_specs:
|
|
771
|
+
return lexical_specs[node.name]
|
|
772
|
+
if isinstance(node, MemberAccess):
|
|
773
|
+
owner = self._map_effect_type_spec(node.object, lexical_specs)
|
|
774
|
+
if owner is not None and owner.kind == "udt" and owner.name:
|
|
775
|
+
return (self._udt_field_type_specs.get(owner.name) or {}).get(
|
|
776
|
+
node.member
|
|
777
|
+
)
|
|
778
|
+
if isinstance(node, FuncCall):
|
|
779
|
+
_key, info = self._map_effect_callable_info(node, lexical_specs)
|
|
780
|
+
return_spec = getattr(info, "return_type_spec", None)
|
|
781
|
+
if return_spec is not None:
|
|
782
|
+
return return_spec
|
|
783
|
+
return self._type_spec_from_expr(node)
|
|
784
|
+
|
|
785
|
+
def _map_effect_callable_info(
|
|
786
|
+
self,
|
|
787
|
+
node: FuncCall,
|
|
788
|
+
lexical_specs: dict[str, TypeSpec | None],
|
|
789
|
+
):
|
|
790
|
+
"""Resolve a plain UDF or UDT method for transitive effect analysis."""
|
|
791
|
+
callee = node.callee
|
|
792
|
+
if isinstance(callee, Identifier):
|
|
793
|
+
key = callee.name
|
|
794
|
+
return key, self._func_info_map.get(key)
|
|
795
|
+
if isinstance(callee, MemberAccess):
|
|
796
|
+
receiver_spec = self._map_effect_type_spec(
|
|
797
|
+
callee.object, lexical_specs
|
|
798
|
+
)
|
|
799
|
+
receiver_name = method_receiver_type_name(receiver_spec)
|
|
800
|
+
if receiver_name is not None:
|
|
801
|
+
key = f"{receiver_name}.{callee.member}"
|
|
802
|
+
return key, self._func_info_map.get(key)
|
|
803
|
+
return "", None
|
|
804
|
+
|
|
805
|
+
def _map_effect_callable_specs(
|
|
806
|
+
self,
|
|
807
|
+
func_info,
|
|
808
|
+
call: FuncCall,
|
|
809
|
+
caller_specs: dict[str, TypeSpec | None],
|
|
810
|
+
) -> dict[str, TypeSpec | None]:
|
|
811
|
+
"""Build the inspected callee's lexical TypeSpec environment.
|
|
812
|
+
|
|
813
|
+
Declared and already-inferred parameter specs are authoritative. Any
|
|
814
|
+
unresolved slot is filled from this exact call's argument in the
|
|
815
|
+
caller's lexical environment, which also lets an untyped map flow
|
|
816
|
+
through multiple helper layers before analyzer call-site inference has
|
|
817
|
+
materialized every nested parameter.
|
|
818
|
+
"""
|
|
819
|
+
func_node = func_info.node
|
|
820
|
+
params = list(func_node.params) if func_node is not None else []
|
|
821
|
+
declared_specs = list(getattr(func_info, "param_type_specs", ()) or ())
|
|
822
|
+
result: dict[str, TypeSpec | None] = {
|
|
823
|
+
param: declared_specs[index] if index < len(declared_specs) else None
|
|
824
|
+
for index, param in enumerate(params)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
is_method = bool(getattr(func_info, "is_udt_method", False))
|
|
828
|
+
if is_method and isinstance(call.callee, MemberAccess):
|
|
829
|
+
binding = self._bind_typed_method_args(func_info, call)
|
|
830
|
+
positional = [call.callee.object, *binding.args_by_param]
|
|
831
|
+
else:
|
|
832
|
+
positional = list(call.args)
|
|
833
|
+
for index, actual in enumerate(positional):
|
|
834
|
+
if index >= len(params) or result[params[index]] is not None:
|
|
835
|
+
continue
|
|
836
|
+
result[params[index]] = self._map_effect_type_spec(
|
|
837
|
+
actual, caller_specs
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
if not is_method:
|
|
841
|
+
for name, actual in call.kwargs.items():
|
|
842
|
+
if name not in params:
|
|
843
|
+
continue
|
|
844
|
+
if result[name] is None:
|
|
845
|
+
result[name] = self._map_effect_type_spec(
|
|
846
|
+
actual, caller_specs
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
# Direct callable locals already carry analyzer-owned lexical
|
|
850
|
+
# provenance. They override same-named parameters/globals exactly as
|
|
851
|
+
# they do during real function emission.
|
|
852
|
+
result.update(self._func_collection_types.get(func_info.name, {}))
|
|
853
|
+
return result
|
|
854
|
+
|
|
855
|
+
def _ordered_user_call_expr(
|
|
856
|
+
self,
|
|
857
|
+
call_head: str,
|
|
858
|
+
arg_nodes: list,
|
|
859
|
+
arg_exprs: list[str],
|
|
860
|
+
*,
|
|
861
|
+
source_order_nodes: list | None = None,
|
|
862
|
+
force_stage: bool = False,
|
|
863
|
+
) -> str:
|
|
864
|
+
"""Emit a user call with deterministic argument evaluation when needed.
|
|
865
|
+
|
|
866
|
+
``arg_nodes``/``arg_exprs`` are in destination parameter order.
|
|
867
|
+
``source_order_nodes`` records the caller's written order, allowing
|
|
868
|
+
named arguments to be evaluated as written while still occupying their
|
|
869
|
+
declared parameter slots.
|
|
870
|
+
"""
|
|
871
|
+
raw = f"{call_head}({', '.join(arg_exprs)})"
|
|
872
|
+
has_map_effect = any(
|
|
873
|
+
self._expr_contains_map_operation(node) for node in arg_nodes
|
|
874
|
+
)
|
|
875
|
+
if not force_stage and (len(arg_exprs) < 2 or not has_map_effect):
|
|
876
|
+
return raw
|
|
877
|
+
|
|
878
|
+
occupied = "\n".join((call_head, *arg_exprs))
|
|
879
|
+
counter = getattr(self, "_ordered_call_arg_counter", 0)
|
|
880
|
+
tokens: list[str] = []
|
|
881
|
+
for _ in arg_exprs:
|
|
882
|
+
while True:
|
|
883
|
+
token = f"__pf_call_arg_{counter}"
|
|
884
|
+
counter += 1
|
|
885
|
+
if token not in occupied:
|
|
886
|
+
break
|
|
887
|
+
tokens.append(token)
|
|
888
|
+
self._ordered_call_arg_counter = counter
|
|
889
|
+
|
|
890
|
+
remaining = list(range(len(arg_nodes)))
|
|
891
|
+
evaluation_order: list[int] = []
|
|
892
|
+
for source_node in source_order_nodes or arg_nodes:
|
|
893
|
+
for index in remaining:
|
|
894
|
+
if arg_nodes[index] is source_node:
|
|
895
|
+
evaluation_order.append(index)
|
|
896
|
+
remaining.remove(index)
|
|
897
|
+
break
|
|
898
|
+
evaluation_order.extend(remaining)
|
|
899
|
+
|
|
900
|
+
lowered = f"{call_head}({', '.join(tokens)})"
|
|
901
|
+
for index in reversed(evaluation_order):
|
|
902
|
+
lowered = (
|
|
903
|
+
f"[&](auto&& {tokens[index]})->decltype(auto){{ "
|
|
904
|
+
f"return {lowered}; }}(({arg_exprs[index]}))"
|
|
905
|
+
)
|
|
906
|
+
return lowered
|
|
907
|
+
|
|
908
|
+
def _visit_typed_method_param(
|
|
909
|
+
self,
|
|
910
|
+
func_info,
|
|
911
|
+
call_node: FuncCall,
|
|
912
|
+
arg_node,
|
|
913
|
+
param_index: int,
|
|
914
|
+
) -> str:
|
|
915
|
+
"""Lower one typed-method parameter with history-Series awareness."""
|
|
916
|
+
param_name = (
|
|
917
|
+
func_info.node.params[param_index]
|
|
918
|
+
if func_info.node is not None
|
|
919
|
+
and param_index < len(func_info.node.params)
|
|
920
|
+
else None
|
|
921
|
+
)
|
|
922
|
+
param_specs = list(
|
|
923
|
+
getattr(func_info, "param_type_specs", ()) or ()
|
|
924
|
+
)
|
|
925
|
+
param_spec = (
|
|
926
|
+
param_specs[param_index]
|
|
927
|
+
if param_index < len(param_specs)
|
|
928
|
+
else None
|
|
929
|
+
)
|
|
930
|
+
if param_spec is not None and param_spec.kind in {"map", "matrix"}:
|
|
931
|
+
# Both UDT-method dispatch paths funnel through this helper after
|
|
932
|
+
# positional/keyword/default merging. Lower a bare ``na`` (and
|
|
933
|
+
# nullable selections containing it) against the declared exact
|
|
934
|
+
# parameter type instead of the legacy numeric ``na<double>()``.
|
|
935
|
+
return self._visit_rhs_value(
|
|
936
|
+
arg_node,
|
|
937
|
+
target_cpp_type=self._type_spec_to_cpp(param_spec),
|
|
938
|
+
)
|
|
939
|
+
method_series = self.ctx.func_series_vars.get(func_info.name, set())
|
|
940
|
+
if param_name not in method_series:
|
|
941
|
+
return self._visit_expr(arg_node)
|
|
942
|
+
expected_cpp_type = self._series_param_element_cpp_type(
|
|
943
|
+
func_info,
|
|
944
|
+
param_index,
|
|
945
|
+
self._callable_target_callsite_idx(func_info, call_node),
|
|
946
|
+
)
|
|
947
|
+
if isinstance(arg_node, Identifier):
|
|
948
|
+
arg_name = arg_node.name
|
|
949
|
+
if (
|
|
950
|
+
(arg_name in BAR_FIELDS or arg_name in BAR_SERIES_PUSH)
|
|
951
|
+
and expected_cpp_type == "double"
|
|
952
|
+
):
|
|
953
|
+
return f"_s_{arg_name}"
|
|
954
|
+
safe = self._safe_name(arg_name)
|
|
955
|
+
is_current_series_param = (
|
|
956
|
+
arg_name in self._current_func_series_params
|
|
957
|
+
)
|
|
958
|
+
if (
|
|
959
|
+
is_current_series_param
|
|
960
|
+
and self._current_func_series_param_types.get(arg_name)
|
|
961
|
+
== expected_cpp_type
|
|
962
|
+
):
|
|
963
|
+
return safe
|
|
964
|
+
if self._active_var_remap and safe in self._active_var_remap:
|
|
965
|
+
safe = self._active_var_remap[safe]
|
|
966
|
+
if (
|
|
967
|
+
not is_current_series_param
|
|
968
|
+
and self._binding_is_series(arg_name, safe)
|
|
969
|
+
and self._series_type_for(arg_name) == expected_cpp_type
|
|
970
|
+
):
|
|
971
|
+
return safe
|
|
972
|
+
expr_cpp = self._visit_expr(arg_node)
|
|
973
|
+
cpp_type = expected_cpp_type
|
|
974
|
+
bridge_cpp = self._series_bridge_value_expr(expr_cpp, cpp_type)
|
|
975
|
+
member = self._inline_history_member(
|
|
976
|
+
"series_arg", call_node, arg_idx=param_index
|
|
977
|
+
)
|
|
978
|
+
return (
|
|
979
|
+
f"([&]() -> const Series<{cpp_type}>& {{ "
|
|
980
|
+
f"{cpp_type} _sv = {bridge_cpp}; "
|
|
981
|
+
f"if (history_advances_new_bar()) {member}.push(_sv); "
|
|
982
|
+
f"else {member}.update(_sv); "
|
|
983
|
+
f"return {member}; }}())"
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
def _visit_udt_method_series_arg(
|
|
987
|
+
self,
|
|
988
|
+
func_info,
|
|
989
|
+
call_node: FuncCall,
|
|
990
|
+
arg_node,
|
|
991
|
+
rest_index: int,
|
|
992
|
+
) -> str:
|
|
993
|
+
"""Backward-compatible wrapper for non-receiver method arguments."""
|
|
994
|
+
return self._visit_typed_method_param(
|
|
995
|
+
func_info,
|
|
996
|
+
call_node,
|
|
997
|
+
arg_node,
|
|
998
|
+
rest_index + 1,
|
|
999
|
+
)
|
|
1000
|
+
|
|
284
1001
|
def _visit_func_call(self, node: FuncCall) -> str:
|
|
285
1002
|
callee = node.callee
|
|
286
1003
|
if isinstance(callee, MemberAccess):
|
|
1004
|
+
recv_spec, method_info = self._typed_user_method_info(
|
|
1005
|
+
callee.object,
|
|
1006
|
+
callee.member,
|
|
1007
|
+
)
|
|
1008
|
+
if method_info is not None and recv_spec is not None:
|
|
1009
|
+
# Pine extension methods are exact type-directed overloads.
|
|
1010
|
+
# They must win before collection/drawing builtins; otherwise
|
|
1011
|
+
# a user ``array<int>.push`` silently executes push_back.
|
|
1012
|
+
return self._emit_typed_user_method_call(
|
|
1013
|
+
node,
|
|
1014
|
+
recv_spec,
|
|
1015
|
+
method_info,
|
|
1016
|
+
)
|
|
287
1017
|
recv_spec = self._type_spec_from_expr(callee.object)
|
|
1018
|
+
if (
|
|
1019
|
+
recv_spec is not None
|
|
1020
|
+
and recv_spec.kind == "udt"
|
|
1021
|
+
and recv_spec.name in self._udt_defs
|
|
1022
|
+
and callee.member == "copy"
|
|
1023
|
+
):
|
|
1024
|
+
if node.args or node.kwargs:
|
|
1025
|
+
self._codegen_error(
|
|
1026
|
+
node,
|
|
1027
|
+
f"{recv_spec.name}.copy() expects no arguments",
|
|
1028
|
+
hint="Call copy() on the UDT object without arguments.",
|
|
1029
|
+
)
|
|
1030
|
+
self._reject_unsupported_udt_copy(recv_spec.name, node)
|
|
1031
|
+
return self._emit_udt_copy_expr(
|
|
1032
|
+
recv_spec.name,
|
|
1033
|
+
self._visit_expr(callee.object),
|
|
1034
|
+
)
|
|
1035
|
+
if (
|
|
1036
|
+
recv_spec is not None
|
|
1037
|
+
and recv_spec.kind == "map"
|
|
1038
|
+
and not isinstance(callee.object, (Identifier, MemberAccess))
|
|
1039
|
+
and callee.member in MAP_METHODS
|
|
1040
|
+
):
|
|
1041
|
+
# Returned, constructed and selected map IDs are ordinary
|
|
1042
|
+
# receivers too. Parenthesize the expression so ternaries and
|
|
1043
|
+
# other compound producers remain one C++ receiver expression.
|
|
1044
|
+
arg_nodes = self._map_call_arg_nodes(
|
|
1045
|
+
callee.member,
|
|
1046
|
+
node,
|
|
1047
|
+
functional=False,
|
|
1048
|
+
allow_keywords=False,
|
|
1049
|
+
)
|
|
1050
|
+
receiver = f"({self._visit_expr(callee.object)})"
|
|
1051
|
+
return self._map_param_method_expr(
|
|
1052
|
+
receiver,
|
|
1053
|
+
callee.member,
|
|
1054
|
+
arg_nodes,
|
|
1055
|
+
recv_spec,
|
|
1056
|
+
)
|
|
288
1057
|
if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name:
|
|
289
1058
|
mk = f"{recv_spec.name}.{callee.member}"
|
|
290
1059
|
fi_u = self._func_info_map.get(mk)
|
|
291
1060
|
if fi_u is not None and getattr(fi_u, "is_udt_method", False):
|
|
292
1061
|
fn_cpp = self._udt_method_call_emit_name(fi_u, node)
|
|
293
1062
|
recv_e = self._visit_expr(callee.object)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
rest_nodes =
|
|
300
|
-
|
|
301
|
-
|
|
1063
|
+
receiver_root = callee.object
|
|
1064
|
+
while isinstance(receiver_root, MemberAccess):
|
|
1065
|
+
receiver_root = receiver_root.object
|
|
1066
|
+
stage_receiver = not isinstance(receiver_root, Identifier)
|
|
1067
|
+
binding = self._bind_typed_method_args(fi_u, node)
|
|
1068
|
+
rest_nodes = list(binding.args_by_param)
|
|
1069
|
+
rest = [
|
|
1070
|
+
self._visit_udt_method_series_arg(
|
|
1071
|
+
fi_u, node, arg, index
|
|
1072
|
+
)
|
|
1073
|
+
for index, arg in enumerate(rest_nodes)
|
|
1074
|
+
]
|
|
1075
|
+
return self._ordered_user_call_expr(
|
|
1076
|
+
fn_cpp,
|
|
1077
|
+
[callee.object, *rest_nodes],
|
|
1078
|
+
[recv_e, *rest],
|
|
1079
|
+
source_order_nodes=[
|
|
1080
|
+
callee.object,
|
|
1081
|
+
*binding.evaluation_order,
|
|
1082
|
+
],
|
|
1083
|
+
# Generated UDT methods accept ``T&``. A constructor or
|
|
1084
|
+
# function-return receiver is a C++ rvalue; bind it to a
|
|
1085
|
+
# named forwarding-reference lambda parameter first so
|
|
1086
|
+
# the method sees a valid lvalue for the full call.
|
|
1087
|
+
force_stage=stage_receiver,
|
|
302
1088
|
)
|
|
303
|
-
rest = [self._visit_expr(a) for a in rest_nodes]
|
|
304
|
-
return f"{fn_cpp}({', '.join([recv_e] + rest)})"
|
|
305
1089
|
|
|
306
1090
|
# Drawing method dispatch (spec §4.3 / L.1). A KNOWN drawing method on a
|
|
307
1091
|
# receiver that resolves to a drawing udt — gated on the METHOD NAME
|
|
@@ -320,6 +1104,21 @@ class CallVisitor:
|
|
|
320
1104
|
list(node.args), node,
|
|
321
1105
|
)
|
|
322
1106
|
|
|
1107
|
+
# Array method on an arbitrary expression receiver, e.g.
|
|
1108
|
+
# ``make_array().get(-1)``, ``m.row(0).last()``, or
|
|
1109
|
+
# ``(cond ? a : b).get(-1)``. Identifier and member receivers have
|
|
1110
|
+
# dedicated paths below; other receiver shapes used to fall through to
|
|
1111
|
+
# ``None(...)`` despite carrying a known array spec.
|
|
1112
|
+
if (isinstance(callee, MemberAccess)
|
|
1113
|
+
and not isinstance(callee.object, (Identifier, MemberAccess))
|
|
1114
|
+
and callee.member in ARRAY_METHODS):
|
|
1115
|
+
recv_spec = self._type_spec_from_expr(callee.object)
|
|
1116
|
+
if recv_spec is not None and recv_spec.kind == "array":
|
|
1117
|
+
recv = self._visit_expr(callee.object)
|
|
1118
|
+
arg_nodes = self._array_method_arg_nodes(callee.member, node)
|
|
1119
|
+
args = self._array_method_args(callee.member, arg_nodes, recv_spec)
|
|
1120
|
+
return self._array_method_expr(recv, callee.member, args, recv_spec)
|
|
1121
|
+
|
|
323
1122
|
# chart.point.now/new/from_index/from_time/copy — REAL data (a ChartPoint
|
|
324
1123
|
# aggregate). Routed here BEFORE the obj.field.method receiver logic,
|
|
325
1124
|
# which would otherwise mis-treat ``chart.point`` as a receiver object
|
|
@@ -333,20 +1132,49 @@ class CallVisitor:
|
|
|
333
1132
|
obj = callee.object
|
|
334
1133
|
if isinstance(obj, MemberAccess):
|
|
335
1134
|
root = obj.object
|
|
336
|
-
|
|
1135
|
+
root_is_builtin_namespace = (
|
|
1136
|
+
isinstance(root, Identifier)
|
|
1137
|
+
and root.name in (
|
|
337
1138
|
"strategy", "ta", "math", "input", "str", "timeframe", "syminfo",
|
|
338
1139
|
"barstate", "color", "request", "runtime", "array", "matrix", "map",
|
|
339
|
-
|
|
1140
|
+
)
|
|
1141
|
+
)
|
|
1142
|
+
if (
|
|
1143
|
+
root_is_builtin_namespace
|
|
1144
|
+
and root.name == "map"
|
|
1145
|
+
and self._map_identifier_is_visible_binding(node)
|
|
1146
|
+
):
|
|
1147
|
+
root_is_builtin_namespace = False
|
|
1148
|
+
if not root_is_builtin_namespace:
|
|
340
1149
|
recv_spec = self._type_spec_from_expr(obj)
|
|
341
1150
|
recv = self._visit_expr(obj)
|
|
342
1151
|
meth = callee.member
|
|
1152
|
+
if (
|
|
1153
|
+
recv_spec is not None
|
|
1154
|
+
and recv_spec.kind == "map"
|
|
1155
|
+
and meth in MAP_METHODS
|
|
1156
|
+
):
|
|
1157
|
+
arg_nodes = self._map_call_arg_nodes(
|
|
1158
|
+
meth,
|
|
1159
|
+
node,
|
|
1160
|
+
functional=False,
|
|
1161
|
+
allow_keywords=False,
|
|
1162
|
+
)
|
|
1163
|
+
return self._map_param_method_expr(
|
|
1164
|
+
recv,
|
|
1165
|
+
meth,
|
|
1166
|
+
arg_nodes,
|
|
1167
|
+
recv_spec,
|
|
1168
|
+
)
|
|
343
1169
|
raw_args = [self._visit_expr(a) for a in node.args]
|
|
344
1170
|
if recv_spec is not None and recv_spec.kind == "array" and meth in ARRAY_METHODS:
|
|
1171
|
+
arg_nodes = self._array_method_arg_nodes(meth, node)
|
|
345
1172
|
return self._array_method_expr(
|
|
346
|
-
recv,
|
|
1173
|
+
recv,
|
|
1174
|
+
meth,
|
|
1175
|
+
self._array_method_args(meth, arg_nodes, recv_spec),
|
|
1176
|
+
recv_spec,
|
|
347
1177
|
)
|
|
348
|
-
if recv_spec is not None and recv_spec.kind == "map" and meth in MAP_METHODS:
|
|
349
|
-
return self._map_method_expr(recv, meth, raw_args, recv_spec)
|
|
350
1178
|
args = ", ".join(raw_args)
|
|
351
1179
|
if meth == "delete":
|
|
352
1180
|
meth = "_delete_"
|
|
@@ -354,27 +1182,101 @@ class CallVisitor:
|
|
|
354
1182
|
# obj.method() where obj is a user var/param — not namespace::method
|
|
355
1183
|
if isinstance(obj, Identifier):
|
|
356
1184
|
oname = obj.name
|
|
357
|
-
|
|
1185
|
+
receiver_is_visible = (
|
|
1186
|
+
oname != "map"
|
|
1187
|
+
or self._map_identifier_is_visible_binding(node)
|
|
1188
|
+
)
|
|
1189
|
+
if receiver_is_visible and (
|
|
358
1190
|
oname in self._var_names
|
|
359
1191
|
or oname in self._current_func_param_types
|
|
360
1192
|
or oname in self._current_loop_vars
|
|
361
1193
|
or oname in self._global_member_vars
|
|
362
1194
|
):
|
|
363
1195
|
meth_raw = callee.member
|
|
364
|
-
#
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1196
|
+
# Function parameters have lexical precedence over the
|
|
1197
|
+
# global collection registries. Route a declared/inferred
|
|
1198
|
+
# ``array<T>`` parameter before consulting same-named
|
|
1199
|
+
# global map/array/matrix entries, and retain its element
|
|
1200
|
+
# TypeSpec for checked reads and mutations.
|
|
1201
|
+
param_spec = getattr(
|
|
1202
|
+
self, "_current_func_param_specs", {}
|
|
1203
|
+
).get(oname)
|
|
1204
|
+
active_local_shadow = (
|
|
1205
|
+
oname in getattr(
|
|
1206
|
+
self, "_current_func_collection_specs", {}
|
|
1207
|
+
)
|
|
1208
|
+
or oname in getattr(
|
|
1209
|
+
self, "_current_func_collection_shadows", set()
|
|
1210
|
+
)
|
|
1211
|
+
)
|
|
1212
|
+
if (
|
|
1213
|
+
param_spec is not None
|
|
1214
|
+
and param_spec.kind == "array"
|
|
1215
|
+
and not active_local_shadow
|
|
1216
|
+
and meth_raw in ARRAY_METHODS
|
|
1217
|
+
):
|
|
1218
|
+
arr = self._collection_receiver_expr(oname)
|
|
1219
|
+
arg_nodes = self._array_method_arg_nodes(meth_raw, node)
|
|
371
1220
|
margs = self._array_method_args(
|
|
372
|
-
meth_raw,
|
|
1221
|
+
meth_raw, arg_nodes, param_spec
|
|
1222
|
+
)
|
|
1223
|
+
return self._array_method_expr(
|
|
1224
|
+
arr, meth_raw, margs, param_spec
|
|
1225
|
+
)
|
|
1226
|
+
if (
|
|
1227
|
+
param_spec is not None
|
|
1228
|
+
and param_spec.kind == "map"
|
|
1229
|
+
and not active_local_shadow
|
|
1230
|
+
and meth_raw in MAP_METHODS
|
|
1231
|
+
):
|
|
1232
|
+
m = self._collection_receiver_expr(oname)
|
|
1233
|
+
arg_nodes = self._map_call_arg_nodes(
|
|
1234
|
+
meth_raw,
|
|
1235
|
+
node,
|
|
1236
|
+
functional=False,
|
|
1237
|
+
allow_keywords=True,
|
|
373
1238
|
)
|
|
374
|
-
return self.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
1239
|
+
return self._map_param_method_expr(
|
|
1240
|
+
m, meth_raw, arg_nodes, param_spec
|
|
1241
|
+
)
|
|
1242
|
+
recv_spec = self._collection_spec_for_name(oname)
|
|
1243
|
+
# Resolve the lexical TypeSpec once and route by its exact
|
|
1244
|
+
# kind. Raw membership sets can contain a same-named
|
|
1245
|
+
# binding from another scope; overlapping methods such as
|
|
1246
|
+
# ``get`` must never use those as a tie-breaker.
|
|
1247
|
+
if (
|
|
1248
|
+
recv_spec is not None
|
|
1249
|
+
and recv_spec.kind == "map"
|
|
1250
|
+
and meth_raw in MAP_METHODS
|
|
1251
|
+
):
|
|
1252
|
+
m = self._collection_receiver_expr(oname)
|
|
1253
|
+
arg_nodes = self._map_call_arg_nodes(
|
|
1254
|
+
meth_raw,
|
|
1255
|
+
node,
|
|
1256
|
+
functional=False,
|
|
1257
|
+
allow_keywords=False,
|
|
1258
|
+
)
|
|
1259
|
+
return self._map_param_method_expr(
|
|
1260
|
+
m, meth_raw, arg_nodes, recv_spec
|
|
1261
|
+
)
|
|
1262
|
+
if (
|
|
1263
|
+
recv_spec is not None
|
|
1264
|
+
and recv_spec.kind == "array"
|
|
1265
|
+
and meth_raw in ARRAY_METHODS
|
|
1266
|
+
):
|
|
1267
|
+
arr = self._collection_receiver_expr(oname)
|
|
1268
|
+
arg_nodes = self._array_method_arg_nodes(meth_raw, node)
|
|
1269
|
+
margs = self._array_method_args(
|
|
1270
|
+
meth_raw, arg_nodes, recv_spec
|
|
1271
|
+
)
|
|
1272
|
+
return self._array_method_expr(arr, meth_raw, margs, recv_spec)
|
|
1273
|
+
if (
|
|
1274
|
+
recv_spec is not None
|
|
1275
|
+
and recv_spec.kind == "matrix"
|
|
1276
|
+
and meth_raw in MATRIX_METHODS
|
|
1277
|
+
):
|
|
1278
|
+
arr = self._collection_receiver_expr(oname)
|
|
1279
|
+
self._check_matrix_method_allowed(meth_raw, recv_spec, node)
|
|
378
1280
|
param_names = MATRIX_METHOD_KWARGS.get(meth_raw)
|
|
379
1281
|
if param_names and node.kwargs:
|
|
380
1282
|
margs = _merge_kwargs(
|
|
@@ -391,27 +1293,35 @@ class CallVisitor:
|
|
|
391
1293
|
f"matrix.{meth_raw}: wrong number of arguments",
|
|
392
1294
|
hint="Check Pine v6 matrix method signature (positional vs keyword).",
|
|
393
1295
|
)
|
|
394
|
-
|
|
395
|
-
udt_t =
|
|
396
|
-
|
|
397
|
-
|
|
1296
|
+
recv_spec = self._type_spec_from_expr(obj)
|
|
1297
|
+
udt_t = (
|
|
1298
|
+
recv_spec.name
|
|
1299
|
+
if recv_spec is not None and recv_spec.kind == "udt"
|
|
1300
|
+
else None
|
|
1301
|
+
)
|
|
398
1302
|
if udt_t is not None:
|
|
399
1303
|
mk = f"{udt_t}.{meth_raw}"
|
|
400
1304
|
fi_u = self._func_info_map.get(mk)
|
|
401
1305
|
if fi_u is not None and getattr(fi_u, "is_udt_method", False):
|
|
402
1306
|
fn_cpp = self._udt_method_call_emit_name(fi_u, node)
|
|
403
1307
|
recv_e = self._visit_expr(obj)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
1308
|
+
binding = self._bind_typed_method_args(fi_u, node)
|
|
1309
|
+
rest_nodes = list(binding.args_by_param)
|
|
1310
|
+
rest = [
|
|
1311
|
+
self._visit_udt_method_series_arg(
|
|
1312
|
+
fi_u, node, arg, index
|
|
1313
|
+
)
|
|
1314
|
+
for index, arg in enumerate(rest_nodes)
|
|
1315
|
+
]
|
|
1316
|
+
return self._ordered_user_call_expr(
|
|
1317
|
+
fn_cpp,
|
|
1318
|
+
[obj, *rest_nodes],
|
|
1319
|
+
[recv_e, *rest],
|
|
1320
|
+
source_order_nodes=[
|
|
1321
|
+
obj,
|
|
1322
|
+
*binding.evaluation_order,
|
|
1323
|
+
],
|
|
412
1324
|
)
|
|
413
|
-
rest = [self._visit_expr(a) for a in rest_nodes]
|
|
414
|
-
return f"{fn_cpp}({', '.join([recv_e] + rest)})"
|
|
415
1325
|
args = ", ".join(self._visit_expr(a) for a in node.args)
|
|
416
1326
|
recv = self._visit_expr(obj)
|
|
417
1327
|
meth = meth_raw
|
|
@@ -466,6 +1376,8 @@ class CallVisitor:
|
|
|
466
1376
|
uses_precalc = self._ta_site_uses_precalc(site)
|
|
467
1377
|
if getattr(self, "_precalc_loop_active", False) and uses_precalc:
|
|
468
1378
|
return f"_precalc_{ta_mem}[i]"
|
|
1379
|
+
if self._is_lazy_saturated_roc3_site(site):
|
|
1380
|
+
return self._lazy_saturated_roc3_expr(site)
|
|
469
1381
|
if uses_precalc:
|
|
470
1382
|
return (
|
|
471
1383
|
f"(_use_precalc ? _precalc_{ta_mem}[bar_index_] : "
|
|
@@ -497,26 +1409,64 @@ class CallVisitor:
|
|
|
497
1409
|
return self._visit_str_call(func_name, node)
|
|
498
1410
|
|
|
499
1411
|
# Map method syntax: m.put(key, val) where namespace is the map variable name
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
1412
|
+
namespace_spec = (
|
|
1413
|
+
self._collection_spec_for_name(namespace)
|
|
1414
|
+
if namespace is not None
|
|
1415
|
+
else None
|
|
1416
|
+
)
|
|
1417
|
+
if (
|
|
1418
|
+
namespace == "map"
|
|
1419
|
+
and not self._map_identifier_is_visible_binding(node)
|
|
1420
|
+
):
|
|
1421
|
+
# A later global named ``map`` is already present in the flat
|
|
1422
|
+
# collection registry, but is not yet a lexical receiver here.
|
|
1423
|
+
namespace_spec = None
|
|
1424
|
+
if (
|
|
1425
|
+
namespace_spec is not None
|
|
1426
|
+
and namespace_spec.kind == "map"
|
|
1427
|
+
and func_name in MAP_METHODS
|
|
1428
|
+
):
|
|
1429
|
+
m = self._collection_receiver_expr(namespace)
|
|
1430
|
+
arg_nodes = self._map_call_arg_nodes(
|
|
1431
|
+
func_name,
|
|
1432
|
+
node,
|
|
1433
|
+
functional=False,
|
|
1434
|
+
allow_keywords=False,
|
|
1435
|
+
)
|
|
1436
|
+
return self._map_param_method_expr(
|
|
1437
|
+
m, func_name, arg_nodes, namespace_spec
|
|
1438
|
+
)
|
|
504
1439
|
|
|
505
1440
|
# map.method(m, args...) — functional form
|
|
506
1441
|
if namespace == "map":
|
|
1442
|
+
if func_name == "new" or func_name in MAP_METHODS:
|
|
1443
|
+
# This dispatch point comes after lexical receiver routing, so
|
|
1444
|
+
# a parameter/local/global named ``map`` retains precedence
|
|
1445
|
+
# over the built-in namespace.
|
|
1446
|
+
self._map_call_arg_nodes(
|
|
1447
|
+
func_name,
|
|
1448
|
+
node,
|
|
1449
|
+
functional=True,
|
|
1450
|
+
allow_keywords=False,
|
|
1451
|
+
)
|
|
507
1452
|
if func_name == "new":
|
|
508
1453
|
spec = self._type_spec_from_expr(node) or TypeSpec.map(TypeSpec.primitive("string"), TypeSpec.primitive("float"))
|
|
509
|
-
return f"{self._type_spec_to_cpp(spec)}()"
|
|
1454
|
+
return f"{self._type_spec_to_cpp(spec)}::new_()"
|
|
510
1455
|
if func_name in MAP_METHODS and node.args:
|
|
511
1456
|
m = self._visit_expr(node.args[0])
|
|
512
|
-
rest = [self._visit_expr(a) for a in node.args[1:]]
|
|
513
1457
|
spec = self._type_spec_from_expr(node.args[0]) if node.args else None
|
|
514
|
-
return self.
|
|
1458
|
+
return self._map_param_method_expr(
|
|
1459
|
+
m, func_name, list(node.args[1:]), spec
|
|
1460
|
+
)
|
|
515
1461
|
return "0"
|
|
516
1462
|
|
|
517
|
-
if
|
|
518
|
-
|
|
519
|
-
|
|
1463
|
+
if (
|
|
1464
|
+
namespace_spec is not None
|
|
1465
|
+
and namespace_spec.kind == "matrix"
|
|
1466
|
+
and func_name in MATRIX_METHODS
|
|
1467
|
+
):
|
|
1468
|
+
arr = self._collection_receiver_expr(namespace)
|
|
1469
|
+
self._check_matrix_method_allowed(func_name, namespace_spec, node)
|
|
520
1470
|
param_names = MATRIX_METHOD_KWARGS.get(func_name)
|
|
521
1471
|
if param_names and node.kwargs:
|
|
522
1472
|
args = _merge_kwargs(node.args, node.kwargs, param_names, self._visit_expr)
|
|
@@ -533,10 +1483,15 @@ class CallVisitor:
|
|
|
533
1483
|
)
|
|
534
1484
|
|
|
535
1485
|
# Array method syntax: arr.push(val) where namespace is the array variable name
|
|
536
|
-
if
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1486
|
+
if (
|
|
1487
|
+
namespace_spec is not None
|
|
1488
|
+
and namespace_spec.kind == "array"
|
|
1489
|
+
and func_name in ARRAY_METHODS
|
|
1490
|
+
):
|
|
1491
|
+
arr = self._collection_receiver_expr(namespace)
|
|
1492
|
+
spec = namespace_spec
|
|
1493
|
+
arg_nodes = self._array_method_arg_nodes(func_name, node)
|
|
1494
|
+
args = self._array_method_args(func_name, arg_nodes, spec)
|
|
540
1495
|
return self._array_method_expr(arr, func_name, args, spec)
|
|
541
1496
|
|
|
542
1497
|
# Array operations — emit proper C++ vector operations
|
|
@@ -559,10 +1514,15 @@ class CallVisitor:
|
|
|
559
1514
|
elems = ", ".join(self._visit_expr(a) for a in node.args)
|
|
560
1515
|
return f"{self._type_spec_to_cpp(spec)}{{{elems}}}"
|
|
561
1516
|
# Method calls: array.method(arr, args...)
|
|
562
|
-
if func_name in ARRAY_METHODS and
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1517
|
+
if func_name in ARRAY_METHODS and (
|
|
1518
|
+
node.args or node.kwargs or func_name == "copy"
|
|
1519
|
+
):
|
|
1520
|
+
all_nodes = self._array_function_arg_nodes(func_name, node)
|
|
1521
|
+
if not all_nodes:
|
|
1522
|
+
return "0"
|
|
1523
|
+
arr = self._visit_expr(all_nodes[0])
|
|
1524
|
+
spec = self._type_spec_from_expr(all_nodes[0])
|
|
1525
|
+
rest = self._array_method_args(func_name, all_nodes[1:], spec)
|
|
566
1526
|
return self._array_method_expr(arr, func_name, rest, spec)
|
|
567
1527
|
return "0"
|
|
568
1528
|
|
|
@@ -1017,14 +1977,16 @@ class CallVisitor:
|
|
|
1017
1977
|
if not isinstance(node.args[0], _Ident):
|
|
1018
1978
|
self._codegen_error(node, f"matrix.{func_name} receiver must be a variable reference")
|
|
1019
1979
|
recv_name = node.args[0].name
|
|
1020
|
-
|
|
1980
|
+
recv_spec = self._collection_spec_for_name(recv_name)
|
|
1981
|
+
if recv_spec is None or recv_spec.kind != "matrix":
|
|
1021
1982
|
self._codegen_error(node, f"matrix.{func_name}: receiver '{recv_name}' is not a known matrix variable")
|
|
1022
|
-
self._check_matrix_method_allowed(func_name,
|
|
1983
|
+
self._check_matrix_method_allowed(func_name, recv_spec, node)
|
|
1023
1984
|
if func_name == "sort":
|
|
1024
1985
|
if isinstance(node.args[0], _Ident):
|
|
1025
1986
|
recv_name = node.args[0].name
|
|
1026
|
-
|
|
1027
|
-
|
|
1987
|
+
recv_spec = self._collection_spec_for_name(recv_name)
|
|
1988
|
+
if recv_spec is not None and recv_spec.kind == "matrix":
|
|
1989
|
+
self._check_matrix_method_allowed(func_name, recv_spec, node)
|
|
1028
1990
|
obj = self._visit_expr(node.args[0])
|
|
1029
1991
|
param_names = MATRIX_METHOD_KWARGS.get(func_name)
|
|
1030
1992
|
if param_names:
|
|
@@ -1071,21 +2033,21 @@ class CallVisitor:
|
|
|
1071
2033
|
if namespace in self._udt_defs and func_name == "new":
|
|
1072
2034
|
fields = self._udt_defs[namespace]
|
|
1073
2035
|
field_names = [f.name for f in fields]
|
|
1074
|
-
|
|
2036
|
+
init_nodes = {}
|
|
1075
2037
|
for i, a in enumerate(node.args):
|
|
1076
2038
|
if i < len(field_names):
|
|
1077
|
-
|
|
2039
|
+
init_nodes[field_names[i]] = a
|
|
1078
2040
|
for k, v in node.kwargs.items():
|
|
1079
|
-
|
|
2041
|
+
init_nodes[k] = v
|
|
1080
2042
|
field_inits = []
|
|
1081
2043
|
field_specs = self._udt_field_type_specs.get(namespace, {})
|
|
1082
2044
|
for f in fields:
|
|
1083
|
-
|
|
1084
|
-
if f.name in
|
|
1085
|
-
|
|
2045
|
+
value_node = None
|
|
2046
|
+
if f.name in init_nodes:
|
|
2047
|
+
value_node = init_nodes[f.name]
|
|
1086
2048
|
elif f.default:
|
|
1087
|
-
|
|
1088
|
-
if
|
|
2049
|
+
value_node = f.default
|
|
2050
|
+
if value_node is not None:
|
|
1089
2051
|
# Fix narrowing: brace-init (``T{.field = v}``) disallows
|
|
1090
2052
|
# narrowing. Pine ``int`` UDT fields are emitted as
|
|
1091
2053
|
# ``int64_t`` (see base.py) but are initialised from
|
|
@@ -1094,24 +2056,40 @@ class CallVisitor:
|
|
|
1094
2056
|
f_cpp_type = self._type_spec_to_cpp(field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name))
|
|
1095
2057
|
if f_cpp_type == "int":
|
|
1096
2058
|
f_cpp_type = "int64_t"
|
|
2059
|
+
val = self._visit_rhs_value(
|
|
2060
|
+
value_node,
|
|
2061
|
+
target_cpp_type=(
|
|
2062
|
+
f_cpp_type
|
|
2063
|
+
if (self._is_nullable_collection_cpp_type(f_cpp_type)
|
|
2064
|
+
or f_cpp_type in self._udt_defs
|
|
2065
|
+
or f_cpp_type in DRAWING_TYPE_TO_CPP.values())
|
|
2066
|
+
else None
|
|
2067
|
+
),
|
|
2068
|
+
)
|
|
1097
2069
|
if f_cpp_type == "int64_t":
|
|
1098
2070
|
if "na<double>" in val:
|
|
1099
2071
|
val = val.replace("na<double>()", "na<int64_t>()")
|
|
1100
2072
|
else:
|
|
1101
2073
|
val = f"(int64_t)({val})"
|
|
2074
|
+
elif (self._is_nullable_collection_cpp_type(f_cpp_type)
|
|
2075
|
+
and val == "na<double>()"):
|
|
2076
|
+
val = f"{f_cpp_type}{{}}"
|
|
1102
2077
|
field_inits.append(f".{f.name} = {val}")
|
|
1103
|
-
|
|
1104
|
-
# last declared field, so this designator stays in declaration order).
|
|
1105
|
-
# A bare default-constructed UDT keeps ``__pf_na = true`` (na); only a
|
|
1106
|
-
# real ``.new(...)`` flips it false so ``na(obj)`` reports correctly.
|
|
1107
|
-
field_inits.append(".__pf_na = false")
|
|
1108
|
-
return f"{namespace}{{{', '.join(field_inits)}}}"
|
|
2078
|
+
return self._emit_udt_new_expr(namespace, field_inits)
|
|
1109
2079
|
|
|
1110
2080
|
# UDT copy: TypeName.copy(obj)
|
|
1111
2081
|
if namespace in self._udt_defs and func_name == "copy":
|
|
1112
|
-
if node.args:
|
|
1113
|
-
|
|
1114
|
-
|
|
2082
|
+
if len(node.args) != 1 or node.kwargs:
|
|
2083
|
+
self._codegen_error(
|
|
2084
|
+
node,
|
|
2085
|
+
f"{namespace}.copy(...) expects exactly one object argument",
|
|
2086
|
+
hint="Prefer the canonical object.copy() form.",
|
|
2087
|
+
)
|
|
2088
|
+
self._reject_unsupported_udt_copy(namespace, node)
|
|
2089
|
+
return self._emit_udt_copy_expr(
|
|
2090
|
+
namespace,
|
|
2091
|
+
self._visit_expr(node.args[0]),
|
|
2092
|
+
)
|
|
1115
2093
|
|
|
1116
2094
|
# Safety net before the generic emitter. Every builtin namespace and
|
|
1117
2095
|
# bare builtin that codegen knows how to emit has been dispatched (and
|
|
@@ -1162,33 +2140,96 @@ class CallVisitor:
|
|
|
1162
2140
|
def _visit_arg_for_series(arg_node, arg_idx):
|
|
1163
2141
|
"""Visit a function argument, returning Series ref for series params."""
|
|
1164
2142
|
if arg_idx in _func_series_param_indices:
|
|
1165
|
-
|
|
2143
|
+
expected_cpp_type = self._series_param_element_cpp_type(
|
|
2144
|
+
fi_lookup,
|
|
2145
|
+
arg_idx,
|
|
2146
|
+
self._callable_target_callsite_idx(fi_lookup, node),
|
|
2147
|
+
)
|
|
2148
|
+
chart_execution_context = (
|
|
2149
|
+
id(node) not in self._requested_context_inline_node_ids
|
|
2150
|
+
and (
|
|
2151
|
+
getattr(self, "_active_func_name", None),
|
|
2152
|
+
self._current_instance_name,
|
|
2153
|
+
) not in self._requested_context_only_inline_contexts
|
|
2154
|
+
)
|
|
2155
|
+
if not chart_execution_context and isinstance(arg_node, Identifier):
|
|
2156
|
+
# request.security/request.security_lower_tf own a separate
|
|
2157
|
+
# evaluator clock and inline their UDF expressions in
|
|
2158
|
+
# security.py. Preserve the pre-factor direct-Series
|
|
2159
|
+
# lowering in the otherwise-emitted helper bodies.
|
|
1166
2160
|
aname = arg_node.name
|
|
1167
|
-
|
|
1168
|
-
|
|
2161
|
+
if (
|
|
2162
|
+
(aname in BAR_FIELDS or aname in BAR_SERIES_PUSH)
|
|
2163
|
+
and expected_cpp_type == "double"
|
|
2164
|
+
):
|
|
1169
2165
|
return f"_s_{aname}"
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
2166
|
+
safe = self._safe_name(aname)
|
|
2167
|
+
is_current_series_param = (
|
|
2168
|
+
aname in self._current_func_series_params
|
|
2169
|
+
)
|
|
2170
|
+
if (
|
|
2171
|
+
is_current_series_param
|
|
2172
|
+
and self._current_func_series_param_types.get(aname)
|
|
2173
|
+
== expected_cpp_type
|
|
2174
|
+
):
|
|
2175
|
+
return safe
|
|
2176
|
+
if self._active_var_remap and safe in self._active_var_remap:
|
|
2177
|
+
safe = self._active_var_remap[safe]
|
|
2178
|
+
if (
|
|
2179
|
+
not is_current_series_param
|
|
2180
|
+
and self._binding_is_series(aname, safe)
|
|
2181
|
+
and self._series_type_for(aname)
|
|
2182
|
+
== expected_cpp_type
|
|
2183
|
+
):
|
|
1175
2184
|
return safe
|
|
1176
2185
|
expr_cpp = self._visit_expr(arg_node)
|
|
1177
|
-
cpp_t =
|
|
1178
|
-
|
|
1179
|
-
cpp_t
|
|
2186
|
+
cpp_t = expected_cpp_type
|
|
2187
|
+
bridge_cpp = self._series_bridge_value_expr(
|
|
2188
|
+
expr_cpp, cpp_t
|
|
2189
|
+
)
|
|
1180
2190
|
member = self._inline_history_member(
|
|
1181
|
-
|
|
2191
|
+
(
|
|
2192
|
+
"udf_series_arg"
|
|
2193
|
+
if chart_execution_context
|
|
2194
|
+
else "series_arg"
|
|
2195
|
+
),
|
|
2196
|
+
node,
|
|
2197
|
+
arg_idx=arg_idx,
|
|
1182
2198
|
)
|
|
2199
|
+
if not chart_execution_context:
|
|
2200
|
+
return (
|
|
2201
|
+
f"([&]() -> const Series<{cpp_t}>& {{ "
|
|
2202
|
+
f"{cpp_t} _sv = {bridge_cpp}; "
|
|
2203
|
+
f"if (history_advances_new_bar()) {member}.push(_sv); "
|
|
2204
|
+
f"else {member}.update(_sv); "
|
|
2205
|
+
f"return {member}; }}())"
|
|
2206
|
+
)
|
|
2207
|
+
# A chart-executed plain UDF's history-reading parameter
|
|
2208
|
+
# belongs to this lexical call site, not to the caller Series
|
|
2209
|
+
# object. The on_bar preamble has already advanced its
|
|
2210
|
+
# synthetic buffer by one chart slot, so execution replaces
|
|
2211
|
+
# that slot rather than pushing a second one.
|
|
1183
2212
|
return (
|
|
1184
2213
|
f"([&]() -> const Series<{cpp_t}>& {{ "
|
|
1185
|
-
f"{cpp_t} _sv =
|
|
1186
|
-
f"
|
|
1187
|
-
f"else {member}.update(_sv); "
|
|
2214
|
+
f"{cpp_t} _sv = {bridge_cpp}; "
|
|
2215
|
+
f"{member}.update(_sv); "
|
|
1188
2216
|
f"return {member}; }}())"
|
|
1189
2217
|
)
|
|
2218
|
+
# A concrete nullable collection specialization learned through an
|
|
2219
|
+
# untyped wrapper chain also target-types its call arguments.
|
|
2220
|
+
if fi_lookup is not None:
|
|
2221
|
+
param_specs = getattr(fi_lookup, "param_type_specs", []) or []
|
|
2222
|
+
if arg_idx < len(param_specs):
|
|
2223
|
+
param_spec = param_specs[arg_idx]
|
|
2224
|
+
if (param_spec is not None
|
|
2225
|
+
and param_spec.kind in {"map", "matrix"}):
|
|
2226
|
+
return self._visit_rhs_value(
|
|
2227
|
+
arg_node,
|
|
2228
|
+
target_cpp_type=self._type_spec_to_cpp(param_spec),
|
|
2229
|
+
)
|
|
1190
2230
|
return self._visit_expr(arg_node)
|
|
1191
2231
|
|
|
2232
|
+
ordered_arg_nodes: list = []
|
|
1192
2233
|
if node.kwargs:
|
|
1193
2234
|
# Try to resolve kwargs using FuncInfo params for user-defined functions
|
|
1194
2235
|
fi = self._func_info_map.get(func_name)
|
|
@@ -1196,16 +2237,28 @@ class CallVisitor:
|
|
|
1196
2237
|
param_names = list(fi.node.params) # params is list[str]
|
|
1197
2238
|
# Merge kwargs then visit with series awareness
|
|
1198
2239
|
merged = _merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
|
|
2240
|
+
ordered_arg_nodes = list(merged)
|
|
1199
2241
|
all_args = [_visit_arg_for_series(a, i) for i, a in enumerate(merged)]
|
|
1200
2242
|
elif sigs.is_intrinsic_function(namespace, func_name):
|
|
1201
2243
|
# Known intrinsic — use signature registry for kwargs resolution
|
|
1202
2244
|
param_names = sigs.get_param_names(namespace, func_name)
|
|
1203
|
-
|
|
2245
|
+
merged = _merge_kwargs(
|
|
2246
|
+
node.args, node.kwargs, param_names, lambda a: a
|
|
2247
|
+
)
|
|
2248
|
+
ordered_arg_nodes = list(merged)
|
|
2249
|
+
all_args = [self._visit_expr(a) for a in merged]
|
|
1204
2250
|
else:
|
|
1205
2251
|
# Unknown function: positional args + kwargs values as fallback
|
|
1206
|
-
|
|
1207
|
-
all_args
|
|
2252
|
+
ordered_arg_nodes = [*node.args, *node.kwargs.values()]
|
|
2253
|
+
all_args = [
|
|
2254
|
+
_visit_arg_for_series(a, i)
|
|
2255
|
+
for i, a in enumerate(node.args)
|
|
2256
|
+
]
|
|
2257
|
+
all_args.extend(
|
|
2258
|
+
self._visit_expr(v) for v in node.kwargs.values()
|
|
2259
|
+
)
|
|
1208
2260
|
else:
|
|
2261
|
+
ordered_arg_nodes = list(node.args)
|
|
1209
2262
|
all_args = [_visit_arg_for_series(a, i) for i, a in enumerate(node.args)]
|
|
1210
2263
|
# Drawing-style/visual CONSTANT passed positionally into a user function's
|
|
1211
2264
|
# ``string`` parameter: ``label.style_*`` / ``size.*`` / other
|
|
@@ -1224,6 +2277,7 @@ class CallVisitor:
|
|
|
1224
2277
|
if fi and fi.node and fi.name == "isInSession" and len(fi.node.params) >= 2 and len(all_args) == 1:
|
|
1225
2278
|
# Mirror Pine default `timeframe.period` instead of hard-coding 15m.
|
|
1226
2279
|
all_args.append("script_tf_")
|
|
2280
|
+
ordered_arg_nodes.append(None)
|
|
1227
2281
|
prefix = f"{namespace}::" if namespace else ""
|
|
1228
2282
|
# Use safe name for user-defined functions to avoid member name collision
|
|
1229
2283
|
emit_name = self._func_safe_name(func_name) if func_name in self._func_names else func_name
|
|
@@ -1253,7 +2307,15 @@ class CallVisitor:
|
|
|
1253
2307
|
# Inside a per-call-site variant: propagate call-site index to
|
|
1254
2308
|
# sub-functions that also have variants (for state isolation)
|
|
1255
2309
|
emit_name = f"{self._func_safe_name(func_name)}_cs{self._active_call_site_idx}"
|
|
1256
|
-
|
|
2310
|
+
call_head = f"{prefix}{emit_name}"
|
|
2311
|
+
if namespace is None and func_name in self._func_names:
|
|
2312
|
+
return self._ordered_user_call_expr(
|
|
2313
|
+
call_head,
|
|
2314
|
+
ordered_arg_nodes,
|
|
2315
|
+
all_args,
|
|
2316
|
+
source_order_nodes=[*node.args, *node.kwargs.values()],
|
|
2317
|
+
)
|
|
2318
|
+
return f"{call_head}({', '.join(all_args)})"
|
|
1257
2319
|
|
|
1258
2320
|
def _coerce_drawing_style_string_args(self, func_name, arg_nodes, all_args) -> None:
|
|
1259
2321
|
"""In-place coerce positional args bound to a ``std::string`` user-function
|
|
@@ -1314,6 +2376,24 @@ class CallVisitor:
|
|
|
1314
2376
|
x = self._visit_expr(node.args[0])
|
|
1315
2377
|
return f"(is_na({x}) ? {member} : ({member} = {x}))"
|
|
1316
2378
|
|
|
2379
|
+
@staticmethod
|
|
2380
|
+
def _strategy_close_callsite_token(node: FuncCall) -> str:
|
|
2381
|
+
"""Return the stable runtime token for an authored close statement.
|
|
2382
|
+
|
|
2383
|
+
A generated strategy is compiled from one Pine source unit, so the
|
|
2384
|
+
parser's 1-based line and column identify the syntactic callsite. The
|
|
2385
|
+
same inner UDF/loop node retains that location across every emitted or
|
|
2386
|
+
runtime execution. Zero is reserved for source-less synthetic calls
|
|
2387
|
+
so existing compatibility behavior remains available.
|
|
2388
|
+
"""
|
|
2389
|
+
loc = node.loc
|
|
2390
|
+
if loc is None or loc.line <= 0 or loc.col <= 0:
|
|
2391
|
+
return "0ULL"
|
|
2392
|
+
token = ((int(loc.line) & 0xFFFFFFFF) << 32) | (
|
|
2393
|
+
int(loc.col) & 0xFFFFFFFF
|
|
2394
|
+
)
|
|
2395
|
+
return f"{token}ULL"
|
|
2396
|
+
|
|
1317
2397
|
def _visit_strategy_call(self, func_name: str, node: FuncCall) -> str:
|
|
1318
2398
|
if func_name in ("convert_to_account", "convert_to_symbol"):
|
|
1319
2399
|
p = self._resolve_func_args(node, f"strategy.{func_name}")
|
|
@@ -1359,10 +2439,17 @@ class CallVisitor:
|
|
|
1359
2439
|
qty = self._visit_expr(p.get("qty")) if p.get("qty") is not None else "na<double>()"
|
|
1360
2440
|
qty_pct = self._visit_expr(p.get("qty_percent")) if p.get("qty_percent") is not None else "na<double>()"
|
|
1361
2441
|
immediately = self._visit_expr(p.get("immediately")) if p.get("immediately") is not None else "false"
|
|
1362
|
-
|
|
2442
|
+
callsite = self._strategy_close_callsite_token(node)
|
|
2443
|
+
return f"strategy_close({close_id}, {comment}, {qty}, {qty_pct}, {immediately}, {callsite})"
|
|
1363
2444
|
|
|
1364
2445
|
if func_name == "close_all":
|
|
1365
|
-
|
|
2446
|
+
p = self._resolve_func_args(node, "strategy.close_all")
|
|
2447
|
+
comment = self._visit_expr(p.get("comment")) if p.get("comment") is not None else '""'
|
|
2448
|
+
immediately = self._visit_expr(p.get("immediately")) if p.get("immediately") is not None else "false"
|
|
2449
|
+
# The engine's ID-less strategy_close path closes the complete
|
|
2450
|
+
# position. Reuse it so close_all preserves the Pine order comment
|
|
2451
|
+
# and same-tick fill flag instead of silently discarding both.
|
|
2452
|
+
return f'strategy_close("", {comment}, na<double>(), na<double>(), {immediately})'
|
|
1366
2453
|
|
|
1367
2454
|
if func_name == "exit":
|
|
1368
2455
|
p = self._resolve_func_args(node, "strategy.exit")
|