@pineforge/codegen-pyodide 0.10.1 → 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/codegen/base.py +141 -1
- package/pineforge_codegen/codegen/emit_top.py +15 -0
- package/pineforge_codegen/codegen/security.py +160 -0
- package/pineforge_codegen/codegen/tables.py +130 -29
- package/pineforge_codegen/codegen/types.py +6 -1
- package/pineforge_codegen/codegen/visit_call.py +33 -15
- package/pineforge_codegen/support_checker.py +68 -4
- package/pineforge_codegen-0.10.2.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/pineforge_codegen-0.10.1.tar.gz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pineforge/codegen-pyodide",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Gate-validated Pyodide payload for the PineScript v6 -> C++ transpiler: archive (run in Pyodide), unpacked source, introspected tables, and release metadata.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -514,6 +514,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
514
514
|
# the declaration-derived checkpoint inventory.
|
|
515
515
|
self._inline_history_members: list[dict] = []
|
|
516
516
|
self._inline_history_member_by_key: dict[tuple, str] = {}
|
|
517
|
+
# request.security/request.security_lower_tf inline their Pine helper
|
|
518
|
+
# expressions on the requested clock instead of calling the ordinary
|
|
519
|
+
# emitted UDF methods. The chart-clocked plain-UDF history factor must
|
|
520
|
+
# therefore exclude those source nodes and any emitted helper context
|
|
521
|
+
# reached only through them.
|
|
522
|
+
self._requested_context_inline_node_ids: set[int] = set()
|
|
523
|
+
self._requested_context_only_inline_contexts: set[
|
|
524
|
+
tuple[str, str | None]
|
|
525
|
+
] = set()
|
|
517
526
|
# Unique lambda-local names used when an array lowering references its
|
|
518
527
|
# receiver more than once. The binding keeps temporary-producing or
|
|
519
528
|
# side-effectful receivers single-evaluation (see TypeInferer).
|
|
@@ -3173,7 +3182,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3173
3182
|
"""
|
|
3174
3183
|
self._inline_history_members = []
|
|
3175
3184
|
self._inline_history_member_by_key = {}
|
|
3176
|
-
counters = {
|
|
3185
|
+
counters = {
|
|
3186
|
+
"hist_call": 0,
|
|
3187
|
+
"series_arg": 0,
|
|
3188
|
+
"udf_series_arg": 0,
|
|
3189
|
+
}
|
|
3177
3190
|
|
|
3178
3191
|
def walk_nodes(value):
|
|
3179
3192
|
"""Yield AST nodes in stable field order, including tuple elements.
|
|
@@ -3345,6 +3358,113 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3345
3358
|
lexical.update(self._func_collection_types.get(owner, {}))
|
|
3346
3359
|
return lexical
|
|
3347
3360
|
|
|
3361
|
+
def plain_udf_info(call: FuncCall):
|
|
3362
|
+
if not isinstance(call.callee, Identifier):
|
|
3363
|
+
return None
|
|
3364
|
+
candidate = self._func_info_map.get(call.callee.name)
|
|
3365
|
+
if (
|
|
3366
|
+
candidate is None
|
|
3367
|
+
or candidate.node is None
|
|
3368
|
+
or getattr(candidate, "is_udt_method", False)
|
|
3369
|
+
):
|
|
3370
|
+
return None
|
|
3371
|
+
return candidate
|
|
3372
|
+
|
|
3373
|
+
# Requested-context UDF expressions are lowered independently by
|
|
3374
|
+
# security.py. Classify their otherwise-emitted helper variants so
|
|
3375
|
+
# this chart-clocked factor neither allocates unused buffers for them
|
|
3376
|
+
# nor changes the legacy bodies that remain in generated C++.
|
|
3377
|
+
requested_node_ids: set[int] = set()
|
|
3378
|
+
for security_call in self._security_calls:
|
|
3379
|
+
expression = security_call.get("expr_node")
|
|
3380
|
+
if expression is None:
|
|
3381
|
+
continue
|
|
3382
|
+
requested_node_ids.update(
|
|
3383
|
+
id(child) for child in walk_nodes(expression)
|
|
3384
|
+
)
|
|
3385
|
+
self._requested_context_inline_node_ids = requested_node_ids
|
|
3386
|
+
|
|
3387
|
+
def emitted_context_for_call(
|
|
3388
|
+
fi,
|
|
3389
|
+
call: FuncCall,
|
|
3390
|
+
owner: str | None,
|
|
3391
|
+
owner_context: str | None,
|
|
3392
|
+
) -> str | None:
|
|
3393
|
+
dispatch = self._instance_dispatch.get(
|
|
3394
|
+
(owner_context, id(call))
|
|
3395
|
+
)
|
|
3396
|
+
if dispatch is not None:
|
|
3397
|
+
return dispatch
|
|
3398
|
+
target_cs = target_cs_for_context(
|
|
3399
|
+
fi, call, owner, owner_context
|
|
3400
|
+
)
|
|
3401
|
+
if target_cs is not None:
|
|
3402
|
+
return f"{self._func_cpp_base_name(fi.name)}_cs{target_cs}"
|
|
3403
|
+
return None
|
|
3404
|
+
|
|
3405
|
+
requested_roots: set[tuple[str, str | None]] = set()
|
|
3406
|
+
chart_roots: set[tuple[str, str | None]] = set()
|
|
3407
|
+
context_edges: dict[
|
|
3408
|
+
tuple[str, str | None], set[tuple[str, str | None]]
|
|
3409
|
+
] = {}
|
|
3410
|
+
for call in (
|
|
3411
|
+
candidate
|
|
3412
|
+
for candidate in walk_nodes(self.ctx.ast)
|
|
3413
|
+
if isinstance(candidate, FuncCall)
|
|
3414
|
+
):
|
|
3415
|
+
fi = plain_udf_info(call)
|
|
3416
|
+
if fi is None:
|
|
3417
|
+
continue
|
|
3418
|
+
owner = owner_by_node.get(id(call))
|
|
3419
|
+
if id(call) in requested_node_ids:
|
|
3420
|
+
owner_contexts = (
|
|
3421
|
+
[None]
|
|
3422
|
+
if owner is None
|
|
3423
|
+
else self._inline_history_contexts_for_owner(owner)
|
|
3424
|
+
)
|
|
3425
|
+
for owner_context in owner_contexts:
|
|
3426
|
+
requested_roots.add((
|
|
3427
|
+
fi.name,
|
|
3428
|
+
emitted_context_for_call(
|
|
3429
|
+
fi, call, owner, owner_context
|
|
3430
|
+
),
|
|
3431
|
+
))
|
|
3432
|
+
continue
|
|
3433
|
+
if owner is None:
|
|
3434
|
+
target = (
|
|
3435
|
+
fi.name,
|
|
3436
|
+
emitted_context_for_call(fi, call, None, None),
|
|
3437
|
+
)
|
|
3438
|
+
chart_roots.add(target)
|
|
3439
|
+
continue
|
|
3440
|
+
for owner_context in self._inline_history_contexts_for_owner(owner):
|
|
3441
|
+
source = (owner, owner_context)
|
|
3442
|
+
target = (
|
|
3443
|
+
fi.name,
|
|
3444
|
+
emitted_context_for_call(
|
|
3445
|
+
fi, call, owner, owner_context
|
|
3446
|
+
),
|
|
3447
|
+
)
|
|
3448
|
+
context_edges.setdefault(source, set()).add(target)
|
|
3449
|
+
|
|
3450
|
+
def reachable(
|
|
3451
|
+
roots: set[tuple[str, str | None]],
|
|
3452
|
+
) -> set[tuple[str, str | None]]:
|
|
3453
|
+
found = set(roots)
|
|
3454
|
+
pending = list(roots)
|
|
3455
|
+
while pending:
|
|
3456
|
+
source = pending.pop()
|
|
3457
|
+
for target in context_edges.get(source, ()):
|
|
3458
|
+
if target in found:
|
|
3459
|
+
continue
|
|
3460
|
+
found.add(target)
|
|
3461
|
+
pending.append(target)
|
|
3462
|
+
return found
|
|
3463
|
+
|
|
3464
|
+
self._requested_context_only_inline_contexts = (
|
|
3465
|
+
reachable(requested_roots) - reachable(chart_roots)
|
|
3466
|
+
)
|
|
3467
|
+
|
|
3348
3468
|
for node in walk_nodes(self.ctx.ast):
|
|
3349
3469
|
owner = owner_by_node.get(id(node))
|
|
3350
3470
|
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
@@ -3403,6 +3523,26 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3403
3523
|
expected_cpp_type = self._series_param_element_cpp_type(
|
|
3404
3524
|
fi, idx, target_cs
|
|
3405
3525
|
)
|
|
3526
|
+
# Pine gives each plain-UDF written call its own
|
|
3527
|
+
# chart-aligned parameter history. Even an exact Series
|
|
3528
|
+
# actual therefore needs a call-site-owned buffer: skipped
|
|
3529
|
+
# evaluations hold the last value in that bar's slot
|
|
3530
|
+
# instead of exposing the caller's independently changing
|
|
3531
|
+
# history. Typed methods retain their established direct
|
|
3532
|
+
# binding/bridge behavior below.
|
|
3533
|
+
chart_execution_context = (
|
|
3534
|
+
id(node) not in requested_node_ids
|
|
3535
|
+
and (owner, context)
|
|
3536
|
+
not in self._requested_context_only_inline_contexts
|
|
3537
|
+
)
|
|
3538
|
+
if not method_call and chart_execution_context:
|
|
3539
|
+
register_one(
|
|
3540
|
+
"udf_series_arg",
|
|
3541
|
+
(id(node), idx),
|
|
3542
|
+
expected_cpp_type,
|
|
3543
|
+
context,
|
|
3544
|
+
)
|
|
3545
|
+
continue
|
|
3406
3546
|
needs_bridge = True
|
|
3407
3547
|
if isinstance(arg, Identifier):
|
|
3408
3548
|
if (
|
|
@@ -949,6 +949,21 @@ class TopLevelEmitter:
|
|
|
949
949
|
f"{info['member_name']}.clear();"
|
|
950
950
|
)
|
|
951
951
|
|
|
952
|
+
# A history-reading plain-UDF parameter advances on the chart clock,
|
|
953
|
+
# even when lazy control flow skips its written call on this bar. Seed
|
|
954
|
+
# the new slot with the prior current value (``na`` before first reach);
|
|
955
|
+
# an executed call later in the bar updates this same slot with its
|
|
956
|
+
# scalar actual. Typed-method ``series_arg`` bridges intentionally keep
|
|
957
|
+
# their existing execution-clock behavior.
|
|
958
|
+
for info in self._inline_history_members:
|
|
959
|
+
if info["kind"] != "udf_series_arg":
|
|
960
|
+
continue
|
|
961
|
+
member = info["member_name"]
|
|
962
|
+
lines.append(
|
|
963
|
+
f" if (history_advances_new_bar()) "
|
|
964
|
+
f"{member}.push({member}.current());"
|
|
965
|
+
)
|
|
966
|
+
|
|
952
967
|
# a. Push bar field series (with bar magnifier support)
|
|
953
968
|
for field_name in sorted(self.ctx.series_bar_fields):
|
|
954
969
|
push_expr = BAR_SERIES_PUSH.get(field_name, f"current_bar_.{field_name}")
|
|
@@ -2844,6 +2844,156 @@ class SecurityEmitter:
|
|
|
2844
2844
|
)
|
|
2845
2845
|
return out
|
|
2846
2846
|
|
|
2847
|
+
def _security_lazy_ta_keys(
|
|
2848
|
+
self,
|
|
2849
|
+
sec_id: int,
|
|
2850
|
+
expr_node,
|
|
2851
|
+
info: dict,
|
|
2852
|
+
) -> set[tuple[int, tuple]]:
|
|
2853
|
+
"""``(ta_index, binding_signature)`` pairs Pine reaches only lazily.
|
|
2854
|
+
|
|
2855
|
+
Pine evaluates ``and``/``or`` with left-to-right short-circuit and a
|
|
2856
|
+
ternary evaluates its condition plus exactly one branch. A ``ta.*``
|
|
2857
|
+
call sitting in a skipped operand is **not evaluated on that bar**, so
|
|
2858
|
+
its series state does not advance. The evaluator used to hoist every
|
|
2859
|
+
collected site into an unconditional ``auto _secval_N = ...compute(...)``
|
|
2860
|
+
prologue, which advanced every site on every HTF bar and desynchronised
|
|
2861
|
+
the series against TradingView.
|
|
2862
|
+
|
|
2863
|
+
A site returned here is dropped from that prologue. ``_build_security_expr``
|
|
2864
|
+
then falls through to its inline
|
|
2865
|
+
``(security_series_slot_is_new(N) ? m.compute(a) : m.recompute(a))``
|
|
2866
|
+
form, emitted in expression position — C++'s own ``&&`` / ``||`` /
|
|
2867
|
+
``?:`` short-circuit then advances the state exactly when Pine would,
|
|
2868
|
+
and the relational lowering's ``_pna_l`` / ``_pna_r`` temporaries keep
|
|
2869
|
+
the evaluation order left-to-right.
|
|
2870
|
+
|
|
2871
|
+
Deliberately conservative — anything not provably single-reach and
|
|
2872
|
+
conditional keeps the existing eager hoist, so scripts without
|
|
2873
|
+
conditional security TA regenerate byte-identically:
|
|
2874
|
+
|
|
2875
|
+
- A site reached more than once must stay hoisted: sharing one
|
|
2876
|
+
``_secval_*`` is what keeps it to a single advance per bar, whereas
|
|
2877
|
+
two inline copies would advance it twice (or zero times).
|
|
2878
|
+
- Laziness does **not** propagate through a global binding. In the
|
|
2879
|
+
requested context that global is its own unconditional top-level
|
|
2880
|
+
statement, so it evaluates on every HTF bar wherever it is read
|
|
2881
|
+
(this is what keeps chart-side ``t0``-shaped code exact).
|
|
2882
|
+
- Sites with a history offset (``ta.ema(close, 55)[1]``) keep the
|
|
2883
|
+
committed ``_secval_*`` their per-bar Series push reads.
|
|
2884
|
+
- Securities that rebind mutable globals, and multi-statement helper
|
|
2885
|
+
calls, bail entirely: both lower through statement emitters that
|
|
2886
|
+
consume ``ta_results`` outside this expression.
|
|
2887
|
+
"""
|
|
2888
|
+
if info.get("mutable_globals"):
|
|
2889
|
+
return set()
|
|
2890
|
+
|
|
2891
|
+
occurrences: dict[tuple[int, tuple], list[bool]] = {}
|
|
2892
|
+
unsafe = False
|
|
2893
|
+
|
|
2894
|
+
def walk(node, lazy: bool, binding_stack, resolving: set[str]) -> None:
|
|
2895
|
+
nonlocal unsafe
|
|
2896
|
+
if node is None or unsafe:
|
|
2897
|
+
return
|
|
2898
|
+
|
|
2899
|
+
if isinstance(node, Identifier):
|
|
2900
|
+
binding = None
|
|
2901
|
+
if not self._security_identifier_is_global_binding(node):
|
|
2902
|
+
binding = self._security_lookup_helper_binding_context(
|
|
2903
|
+
node.name, binding_stack
|
|
2904
|
+
)
|
|
2905
|
+
if binding is not None:
|
|
2906
|
+
bound, bound_stack = binding
|
|
2907
|
+
if isinstance(bound, str):
|
|
2908
|
+
return
|
|
2909
|
+
bind_key = f"bind:{id(bound)}"
|
|
2910
|
+
if bind_key in resolving:
|
|
2911
|
+
return
|
|
2912
|
+
resolving.add(bind_key)
|
|
2913
|
+
walk(bound, lazy, bound_stack, resolving)
|
|
2914
|
+
resolving.discard(bind_key)
|
|
2915
|
+
return
|
|
2916
|
+
|
|
2917
|
+
if self._global_mutable_infos.get(node.name) is not None:
|
|
2918
|
+
unsafe = True
|
|
2919
|
+
return
|
|
2920
|
+
|
|
2921
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
2922
|
+
if (
|
|
2923
|
+
self._security_identifier_is_global_binding(node)
|
|
2924
|
+
and node.name in global_expr_map
|
|
2925
|
+
and node.name not in resolving
|
|
2926
|
+
):
|
|
2927
|
+
resolving.add(node.name)
|
|
2928
|
+
walk(global_expr_map[node.name], False, (), resolving)
|
|
2929
|
+
resolving.remove(node.name)
|
|
2930
|
+
return
|
|
2931
|
+
|
|
2932
|
+
if isinstance(node, FuncCall) and isinstance(node.callee, Identifier):
|
|
2933
|
+
func_name = node.callee.name
|
|
2934
|
+
if func_name in self._func_names:
|
|
2935
|
+
call_key = f"func:{func_name}"
|
|
2936
|
+
if call_key in resolving:
|
|
2937
|
+
return
|
|
2938
|
+
resolving.add(call_key)
|
|
2939
|
+
plan = self._security_helper_call_plan(node, binding_stack)
|
|
2940
|
+
if plan["mode"] == "expr":
|
|
2941
|
+
walk(plan["expr"], lazy, plan["binding_stack"], resolving)
|
|
2942
|
+
else:
|
|
2943
|
+
unsafe = True
|
|
2944
|
+
resolving.discard(call_key)
|
|
2945
|
+
return
|
|
2946
|
+
|
|
2947
|
+
if isinstance(node, BinOp) and node.op in ("and", "or"):
|
|
2948
|
+
walk(node.left, lazy, binding_stack, resolving)
|
|
2949
|
+
walk(node.right, True, binding_stack, resolving)
|
|
2950
|
+
return
|
|
2951
|
+
|
|
2952
|
+
if isinstance(node, Ternary):
|
|
2953
|
+
walk(node.condition, lazy, binding_stack, resolving)
|
|
2954
|
+
walk(node.true_val, True, binding_stack, resolving)
|
|
2955
|
+
walk(node.false_val, True, binding_stack, resolving)
|
|
2956
|
+
return
|
|
2957
|
+
|
|
2958
|
+
site = self._get_ta_site(node)
|
|
2959
|
+
if site is not None:
|
|
2960
|
+
idx = self._ta_index_by_site_id.get(id(site))
|
|
2961
|
+
if idx is not None:
|
|
2962
|
+
key = (idx, self._security_binding_stack_signature(binding_stack))
|
|
2963
|
+
occurrences.setdefault(key, []).append(lazy)
|
|
2964
|
+
|
|
2965
|
+
for child in vars(node).values():
|
|
2966
|
+
walk_value(child, lazy, binding_stack, resolving)
|
|
2967
|
+
|
|
2968
|
+
def walk_value(value, lazy: bool, binding_stack, resolving: set[str]) -> None:
|
|
2969
|
+
if value is None or unsafe:
|
|
2970
|
+
return
|
|
2971
|
+
if hasattr(value, "__dict__"):
|
|
2972
|
+
walk(value, lazy, binding_stack, resolving)
|
|
2973
|
+
return
|
|
2974
|
+
if isinstance(value, (list, tuple)):
|
|
2975
|
+
for item in value:
|
|
2976
|
+
walk_value(item, lazy, binding_stack, resolving)
|
|
2977
|
+
return
|
|
2978
|
+
if isinstance(value, dict):
|
|
2979
|
+
for item in value.values():
|
|
2980
|
+
walk_value(item, lazy, binding_stack, resolving)
|
|
2981
|
+
|
|
2982
|
+
walk(expr_node, False, (), set())
|
|
2983
|
+
if unsafe:
|
|
2984
|
+
return set()
|
|
2985
|
+
|
|
2986
|
+
hist_indices = set(self._security_ta_hist_idx_by_sec.get(sec_id, ()))
|
|
2987
|
+
inline_helper_ta_indices = set(info.get("inline_helper_ta_indices", []))
|
|
2988
|
+
return {
|
|
2989
|
+
key
|
|
2990
|
+
for key, reaches in occurrences.items()
|
|
2991
|
+
if key[0] not in hist_indices
|
|
2992
|
+
and key[0] not in inline_helper_ta_indices
|
|
2993
|
+
and len(reaches) == 1
|
|
2994
|
+
and reaches[0]
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2847
2997
|
def _emit_security_ohlc_hist_pushes(self, sec_id: int, lines: list[str]) -> None:
|
|
2848
2998
|
"""Emit the OHLC history-offset Series pushes for ``sec_id``, gated on
|
|
2849
2999
|
``is_complete``.
|
|
@@ -2909,6 +3059,7 @@ class SecurityEmitter:
|
|
|
2909
3059
|
ta_indices = info.get("ta_indices") or []
|
|
2910
3060
|
security_mutable_names = set(info.get("mutable_globals", []))
|
|
2911
3061
|
inline_helper_ta_indices = set(info.get("inline_helper_ta_indices", []))
|
|
3062
|
+
lazy_ta_keys = self._security_lazy_ta_keys(sec_id, expr_node, info)
|
|
2912
3063
|
|
|
2913
3064
|
lines.append(f" void _eval_security_{sec_id}(const Bar& bar, bool is_complete) {{")
|
|
2914
3065
|
|
|
@@ -2949,6 +3100,15 @@ class SecurityEmitter:
|
|
|
2949
3100
|
site = self.ctx.ta_call_sites[idx]
|
|
2950
3101
|
variants = (info.get("ta_variants") or {}).get(idx, [])
|
|
2951
3102
|
for variant in variants:
|
|
3103
|
+
if (idx, variant["signature"]) in lazy_ta_keys:
|
|
3104
|
+
# Pine only reaches this site through a
|
|
3105
|
+
# short-circuited operand or an untaken ternary
|
|
3106
|
+
# branch. Leave it out of the eager prologue:
|
|
3107
|
+
# _build_security_expr emits its
|
|
3108
|
+
# compute()/recompute() inline in expression
|
|
3109
|
+
# position, where C++'s &&/||/?: short-circuit
|
|
3110
|
+
# advances the series exactly on the bars Pine does.
|
|
3111
|
+
continue
|
|
2952
3112
|
helper_binding_stack = variant.get("binding_stack", ())
|
|
2953
3113
|
compute_args = self._security_ta_compute_args_for_site(
|
|
2954
3114
|
sec_id,
|
|
@@ -459,45 +459,143 @@ ORDER_DIRECTION_MAP = {
|
|
|
459
459
|
# ---------------------------------------------------------------------------
|
|
460
460
|
|
|
461
461
|
|
|
462
|
-
def _checked_array_index_prelude(
|
|
462
|
+
def _checked_array_index_prelude(
|
|
463
|
+
*,
|
|
464
|
+
normalize_negative: bool = True,
|
|
465
|
+
allow_size: bool = False,
|
|
466
|
+
name: str = "index",
|
|
467
|
+
) -> str:
|
|
463
468
|
"""C++ body shared by checked single-index operations.
|
|
464
469
|
|
|
465
|
-
Pine v6 explicitly gives end-relative negative indices to
|
|
466
|
-
``
|
|
467
|
-
``percentrank``
|
|
468
|
-
|
|
469
|
-
|
|
470
|
+
Pine v6 explicitly gives end-relative negative indices to ``array.get``,
|
|
471
|
+
``array.set``, ``array.insert``, and ``array.remove``. Other indexed
|
|
472
|
+
functions such as ``percentrank`` and the ``fill``/``slice`` range
|
|
473
|
+
endpoints still need the same NA/non-finite/range checks, but must reject
|
|
474
|
+
negative indices instead of normalizing them (``normalize_negative``).
|
|
475
|
+
|
|
476
|
+
``allow_size`` widens the upper bound from ``index < size`` to
|
|
477
|
+
``index <= size``. An *element access* is invalid at ``size``, but an
|
|
478
|
+
*insertion point* is not: ``array.insert(id, size, v)`` appends, and the
|
|
479
|
+
half-open ``[index_from, index_to)`` ranges of ``array.fill`` /
|
|
480
|
+
``array.slice`` legally end at ``size``.
|
|
481
|
+
|
|
482
|
+
``name`` renames every emitted local so two endpoints can be checked in
|
|
483
|
+
one C++ scope without colliding. ``name="index"`` reproduces the original
|
|
484
|
+
identifiers byte-for-byte.
|
|
470
485
|
"""
|
|
486
|
+
raw_value = f"__pf_raw_{name}_value"
|
|
487
|
+
raw_type = f"__pf_raw_{name}_type"
|
|
488
|
+
raw_text = f"__pf_raw_{name}_text"
|
|
489
|
+
raw_wide = f"__pf_raw_{name}_wide"
|
|
490
|
+
raw = f"__pf_raw_{name}"
|
|
491
|
+
checked = f"__pf_array_{name}"
|
|
492
|
+
size = "__pf_array_size" if name == "index" else f"__pf_array_size_{name}"
|
|
471
493
|
checked_index = (
|
|
472
|
-
"
|
|
473
|
-
if normalize_negative
|
|
474
|
-
else "__pf_raw_index"
|
|
494
|
+
f"{raw}<0?{raw}+{size}:{raw}" if normalize_negative else raw
|
|
475
495
|
)
|
|
496
|
+
upper = ">" if allow_size else ">="
|
|
476
497
|
return (
|
|
477
|
-
"using
|
|
478
|
-
"if constexpr(!std::is_same_v<
|
|
479
|
-
"if(is_na(
|
|
498
|
+
f"using {raw_type}=std::decay_t<decltype({raw_value})>; "
|
|
499
|
+
f"if constexpr(!std::is_same_v<{raw_type},bool>) {{ "
|
|
500
|
+
f"if(is_na({raw_value})) "
|
|
480
501
|
"pine_runtime_error(std::string(\"Index na is out of bounds. Array size is \")+"
|
|
481
502
|
"std::to_string((int64_t)__pf_array.size())); } "
|
|
482
|
-
"if constexpr(std::is_floating_point_v<
|
|
483
|
-
"if(!std::isfinite(
|
|
484
|
-
"std::string
|
|
485
|
-
"pine_runtime_error(std::string(\"Index \")+
|
|
503
|
+
f"if constexpr(std::is_floating_point_v<{raw_type}>) {{ "
|
|
504
|
+
f"if(!std::isfinite({raw_value})) {{ "
|
|
505
|
+
f"std::string {raw_text}={raw_value}>0?\"inf\":\"-inf\"; "
|
|
506
|
+
f"pine_runtime_error(std::string(\"Index \")+{raw_text}+"
|
|
486
507
|
"\" is out of bounds. Array size is \"+"
|
|
487
508
|
"std::to_string((int64_t)__pf_array.size())); } "
|
|
488
|
-
"long double
|
|
489
|
-
"if(
|
|
490
|
-
"
|
|
509
|
+
f"long double {raw_wide}=(long double){raw_value}; "
|
|
510
|
+
f"if({raw_wide}<(long double)std::numeric_limits<int64_t>::min()||"
|
|
511
|
+
f"{raw_wide}>(long double)std::numeric_limits<int64_t>::max()) "
|
|
491
512
|
"pine_runtime_error(std::string(\"Index \")+"
|
|
492
|
-
"std::to_string((double)
|
|
513
|
+
f"std::to_string((double){raw_value})+"
|
|
493
514
|
"\" is out of bounds. Array size is \"+"
|
|
494
515
|
"std::to_string((int64_t)__pf_array.size())); } "
|
|
495
|
-
"int64_t
|
|
496
|
-
"int64_t
|
|
497
|
-
f"int64_t
|
|
498
|
-
"if(
|
|
499
|
-
"pine_runtime_error(std::string(\"Index \")+std::to_string(
|
|
500
|
-
"\" is out of bounds. Array size is \"+std::to_string(
|
|
516
|
+
f"int64_t {raw}=(int64_t){raw_value}; "
|
|
517
|
+
f"int64_t {size}=(int64_t)__pf_array.size(); "
|
|
518
|
+
f"int64_t {checked}={checked_index}; "
|
|
519
|
+
f"if({checked}<0||{checked}{upper}{size}) "
|
|
520
|
+
f"pine_runtime_error(std::string(\"Index \")+std::to_string({raw})+"
|
|
521
|
+
f"\" is out of bounds. Array size is \"+std::to_string({size})); "
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _checked_array_range_prelude() -> str:
|
|
526
|
+
"""Validate the half-open ``[index_from, index_to)`` range of fill/slice.
|
|
527
|
+
|
|
528
|
+
Both endpoints are checked with ``allow_size`` (``index_to`` is exclusive,
|
|
529
|
+
so ``size`` is a legal endpoint) and without negative normalization
|
|
530
|
+
(neither function appears in the Pine v6 reference's negative-indexing
|
|
531
|
+
set). An inverted range is rejected rather than silently treated as empty:
|
|
532
|
+
``begin()+from > begin()+to`` is undefined behaviour in the STL forms this
|
|
533
|
+
replaces, and no evidence pins TradingView's behaviour there.
|
|
534
|
+
"""
|
|
535
|
+
return (
|
|
536
|
+
_checked_array_index_prelude(
|
|
537
|
+
normalize_negative=False, allow_size=True, name="index_from"
|
|
538
|
+
)
|
|
539
|
+
+ _checked_array_index_prelude(
|
|
540
|
+
normalize_negative=False, allow_size=True, name="index_to"
|
|
541
|
+
)
|
|
542
|
+
+ "if(__pf_array_index_from>__pf_array_index_to) "
|
|
543
|
+
"pine_runtime_error(std::string(\"Index range \")+"
|
|
544
|
+
"std::to_string(__pf_array_index_from)+\"..\"+"
|
|
545
|
+
"std::to_string(__pf_array_index_to)+"
|
|
546
|
+
"\" is invalid. Array size is \"+"
|
|
547
|
+
"std::to_string(__pf_array_size_index_from)); "
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _checked_array_insert(a: str, args: list[str]) -> str:
|
|
552
|
+
"""``array.insert(id, index, value)`` — index is an INSERTION point.
|
|
553
|
+
|
|
554
|
+
``index == size`` appends, so the bound is ``index <= size``; a negative
|
|
555
|
+
index is end-relative, exactly as for ``get``/``set``/``remove``.
|
|
556
|
+
"""
|
|
557
|
+
check = _checked_array_index_prelude(allow_size=True)
|
|
558
|
+
return (
|
|
559
|
+
"[&](auto&& __pf_array){ "
|
|
560
|
+
"return [&](auto&& __pf_raw_index_value){ "
|
|
561
|
+
"return [&](auto&& __pf_array_value){ "
|
|
562
|
+
f"{check}"
|
|
563
|
+
"__pf_array.insert(__pf_array.begin()+(size_t)__pf_array_index, "
|
|
564
|
+
"__pf_array_value); "
|
|
565
|
+
f"}}(({args[1]})); }}(({args[0]})); }}(({a}))"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _checked_array_fill_range(a: str, args: list[str]) -> str:
|
|
570
|
+
"""``array.fill(id, value, index_from, index_to)`` — bounded range fill."""
|
|
571
|
+
check = _checked_array_range_prelude()
|
|
572
|
+
return (
|
|
573
|
+
"[&](auto&& __pf_array){ "
|
|
574
|
+
"return [&](auto&& __pf_array_value){ "
|
|
575
|
+
"return [&](auto&& __pf_raw_index_from_value){ "
|
|
576
|
+
"return [&](auto&& __pf_raw_index_to_value){ "
|
|
577
|
+
f"{check}"
|
|
578
|
+
"std::fill(__pf_array.begin()+(size_t)__pf_array_index_from, "
|
|
579
|
+
"__pf_array.begin()+(size_t)__pf_array_index_to, __pf_array_value); "
|
|
580
|
+
f"}}(({args[2]})); }}(({args[1]})); }}(({args[0]})); }}(({a}))"
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def checked_array_slice(a: str, args: list[str], *, result_type: str) -> str:
|
|
585
|
+
"""``array.slice(id, index_from, index_to)`` — bounded range extraction.
|
|
586
|
+
|
|
587
|
+
``result_type`` stays caller-supplied so the typed method lane keeps
|
|
588
|
+
emitting the receiver's own element type; only the bounds checks are new.
|
|
589
|
+
"""
|
|
590
|
+
check = _checked_array_range_prelude()
|
|
591
|
+
return (
|
|
592
|
+
"[&](auto&& __pf_array){ "
|
|
593
|
+
"return [&](auto&& __pf_raw_index_from_value){ "
|
|
594
|
+
"return [&](auto&& __pf_raw_index_to_value){ "
|
|
595
|
+
f"{check}"
|
|
596
|
+
f"return {result_type}(__pf_array.begin()+(size_t)__pf_array_index_from, "
|
|
597
|
+
"__pf_array.begin()+(size_t)__pf_array_index_to); "
|
|
598
|
+
f"}}(({args[1]})); }}(({args[0]})); }}(({a}))"
|
|
501
599
|
)
|
|
502
600
|
|
|
503
601
|
|
|
@@ -600,7 +698,7 @@ ARRAY_METHODS = {
|
|
|
600
698
|
"set": _checked_array_set,
|
|
601
699
|
"push": lambda a, args: f"{a}.push_back({args[0]})",
|
|
602
700
|
"unshift": lambda a, args: f"{a}.insert({a}.begin(), {args[0]})",
|
|
603
|
-
"insert":
|
|
701
|
+
"insert": _checked_array_insert,
|
|
604
702
|
"pop": lambda a, args: _checked_array_end_remove(a, "pop"),
|
|
605
703
|
"shift": lambda a, args: _checked_array_end_remove(a, "shift"),
|
|
606
704
|
"remove": _checked_array_remove,
|
|
@@ -609,7 +707,7 @@ ARRAY_METHODS = {
|
|
|
609
707
|
"size": lambda a, args: f"(double){a}.size()",
|
|
610
708
|
"clear": lambda a, args: f"{a}.clear()",
|
|
611
709
|
"fill": lambda a, args: f"std::fill({a}.begin(), {a}.end(), {args[0]})" if len(args) == 1
|
|
612
|
-
else
|
|
710
|
+
else _checked_array_fill_range(a, args),
|
|
613
711
|
"includes": lambda a, args: f"(std::find({a}.begin(), {a}.end(), {args[0]}) != {a}.end())",
|
|
614
712
|
"indexof": lambda a, args: f"[&](){{ auto it=std::find({a}.begin(),{a}.end(),{args[0]}); return it!={a}.end()?(double)(it-{a}.begin()):-1.0; }}()",
|
|
615
713
|
"lastindexof": lambda a, args: f"[&](){{ for(int i=(int){a}.size()-1;i>=0;i--)if({a}[i]=={args[0]})return(double)i; return -1.0; }}()",
|
|
@@ -619,7 +717,7 @@ ARRAY_METHODS = {
|
|
|
619
717
|
),
|
|
620
718
|
"reverse": lambda a, args: f"std::reverse({a}.begin(),{a}.end())",
|
|
621
719
|
"copy": lambda a, args: f"std::vector<double>({a})",
|
|
622
|
-
"slice": lambda a, args:
|
|
720
|
+
"slice": lambda a, args: checked_array_slice(a, args, result_type="std::vector<double>"),
|
|
623
721
|
"concat": lambda a, args: f"{a}.insert({a}.end(),{args[0]}.begin(),{args[0]}.end())",
|
|
624
722
|
"sum": lambda a, args: f"({a}.empty()?na<double>():std::accumulate({a}.begin(),{a}.end(),0.0))",
|
|
625
723
|
"avg": lambda a, args: f"({a}.empty()?na<double>():std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size())",
|
|
@@ -667,6 +765,9 @@ CHECKED_ARRAY_METHOD_KWARGS: dict[str, list[str]] = {
|
|
|
667
765
|
"set": ["index", "value"],
|
|
668
766
|
"remove": ["index"],
|
|
669
767
|
"percentrank": ["index"],
|
|
768
|
+
"insert": ["index", "value"],
|
|
769
|
+
"fill": ["value", "index_from", "index_to"],
|
|
770
|
+
"slice": ["index_from", "index_to"],
|
|
670
771
|
"first": [],
|
|
671
772
|
"last": [],
|
|
672
773
|
"pop": [],
|
|
@@ -50,6 +50,7 @@ from .tables import (
|
|
|
50
50
|
MATRIX_RETURNING_METHODS,
|
|
51
51
|
PINE_TYPE_TO_CPP,
|
|
52
52
|
TA_RETURNS_BOOL,
|
|
53
|
+
checked_array_slice,
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
# Collection (array / map / matrix) methods that MUTATE the receiver in place.
|
|
@@ -960,7 +961,11 @@ class TypeInferer:
|
|
|
960
961
|
if method == "copy":
|
|
961
962
|
lower_receiver = lambda recv: f"{arr_cpp_type}({recv})"
|
|
962
963
|
elif method == "slice":
|
|
963
|
-
|
|
964
|
+
# Bounds-checked in the shared helper; the element type stays
|
|
965
|
+
# caller-supplied so the typed lane keeps its own vector type.
|
|
966
|
+
lower_receiver = lambda recv: checked_array_slice(
|
|
967
|
+
recv, args, result_type=arr_cpp_type
|
|
968
|
+
)
|
|
964
969
|
elif method == "join" and elem_cpp == "std::string":
|
|
965
970
|
sep = args[0] if args else 'std::string(",")'
|
|
966
971
|
lower_receiver = lambda recv: f"[&](){{ std::string r; for(size_t i=0;i<{recv}.size();i++){{ if(i>0)r+={sep}; r+={recv}[i]; }} return r; }}()"
|
|
@@ -2145,25 +2145,25 @@ class CallVisitor:
|
|
|
2145
2145
|
arg_idx,
|
|
2146
2146
|
self._callable_target_callsite_idx(fi_lookup, node),
|
|
2147
2147
|
)
|
|
2148
|
-
|
|
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.
|
|
2149
2160
|
aname = arg_node.name
|
|
2150
|
-
# Bar field: pass _s_close instead of current_bar_.close
|
|
2151
2161
|
if (
|
|
2152
2162
|
(aname in BAR_FIELDS or aname in BAR_SERIES_PUSH)
|
|
2153
2163
|
and expected_cpp_type == "double"
|
|
2154
2164
|
):
|
|
2155
2165
|
return f"_s_{aname}"
|
|
2156
|
-
# Exact Series binding: pass the Series object directly.
|
|
2157
|
-
# A raw name can also denote a scalar sibling/callable
|
|
2158
|
-
# shadow, in which case lexical state must override the
|
|
2159
|
-
# legacy ``ctx.series_vars`` union and fall through to the
|
|
2160
|
-
# synthetic history bridge below.
|
|
2161
2166
|
safe = self._safe_name(aname)
|
|
2162
|
-
# Function parameters are lexical C++ arguments in every
|
|
2163
|
-
# emitted variant. The legacy function-series clone table
|
|
2164
|
-
# can contain the same raw name, but applying it here makes
|
|
2165
|
-
# cs1+ ignore the actual parameter and read an unrelated
|
|
2166
|
-
# class member instead.
|
|
2167
2167
|
is_current_series_param = (
|
|
2168
2168
|
aname in self._current_func_series_params
|
|
2169
2169
|
)
|
|
@@ -2188,13 +2188,31 @@ class CallVisitor:
|
|
|
2188
2188
|
expr_cpp, cpp_t
|
|
2189
2189
|
)
|
|
2190
2190
|
member = self._inline_history_member(
|
|
2191
|
-
|
|
2191
|
+
(
|
|
2192
|
+
"udf_series_arg"
|
|
2193
|
+
if chart_execution_context
|
|
2194
|
+
else "series_arg"
|
|
2195
|
+
),
|
|
2196
|
+
node,
|
|
2197
|
+
arg_idx=arg_idx,
|
|
2192
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.
|
|
2193
2212
|
return (
|
|
2194
2213
|
f"([&]() -> const Series<{cpp_t}>& {{ "
|
|
2195
2214
|
f"{cpp_t} _sv = {bridge_cpp}; "
|
|
2196
|
-
f"
|
|
2197
|
-
f"else {member}.update(_sv); "
|
|
2215
|
+
f"{member}.update(_sv); "
|
|
2198
2216
|
f"return {member}; }}())"
|
|
2199
2217
|
)
|
|
2200
2218
|
# A concrete nullable collection specialization learned through an
|
|
@@ -516,11 +516,20 @@ class SupportChecker:
|
|
|
516
516
|
# then ``request.security(haTicker, ...)``, or ``reqSym = cond ? other :
|
|
517
517
|
# syminfo.tickerid``. First binding wins (closest to declaration).
|
|
518
518
|
self._scalar_defs: dict[str, ASTNode] = {}
|
|
519
|
+
# EVERY reassignment value bound to a scalar name (``name := <expr>``
|
|
520
|
+
# and the compound forms). A declaration alone does not pin the value a
|
|
521
|
+
# ``request.security`` symbol argument actually carries: the engine is
|
|
522
|
+
# registered with no symbol at all, so an accepted-but-divergent rebind
|
|
523
|
+
# silently backtests the CHART feed. Collected in a whole-AST pre-pass
|
|
524
|
+
# (below) as well as during the visit, so a rebind that lexically
|
|
525
|
+
# FOLLOWS the request.security call cannot slip past.
|
|
526
|
+
self._scalar_rebinds: dict[str, list[ASTNode]] = {}
|
|
519
527
|
|
|
520
528
|
# -- Public API --
|
|
521
529
|
|
|
522
530
|
def check(self) -> list[Diagnostic]:
|
|
523
531
|
self._collect_user_definitions(self._ast)
|
|
532
|
+
self._collect_scalar_rebinds(self._ast)
|
|
524
533
|
for stmt in self._ast.body:
|
|
525
534
|
self._visit(stmt)
|
|
526
535
|
return self._diagnostics
|
|
@@ -533,6 +542,42 @@ class SupportChecker:
|
|
|
533
542
|
|
|
534
543
|
# -- Setup --
|
|
535
544
|
|
|
545
|
+
def _record_scalar_rebind(self, node: Assignment) -> None:
|
|
546
|
+
"""Remember ``name := <expr>`` (and the compound forms) by name.
|
|
547
|
+
|
|
548
|
+
A compound op (``+=``, ``*=``, …) records its right-hand operand, which
|
|
549
|
+
is never a current-symbol expression — so a string-built symbol is
|
|
550
|
+
rejected by the same rule that rejects a plain divergent ``:=``.
|
|
551
|
+
"""
|
|
552
|
+
target = node.target
|
|
553
|
+
if not isinstance(target, Identifier) or node.value is None:
|
|
554
|
+
return
|
|
555
|
+
recorded = self._scalar_rebinds.setdefault(target.name, [])
|
|
556
|
+
if not any(value is node.value for value in recorded):
|
|
557
|
+
recorded.append(node.value)
|
|
558
|
+
|
|
559
|
+
def _collect_scalar_rebinds(self, node: ASTNode) -> None:
|
|
560
|
+
"""Pre-pass: collect every scalar rebind anywhere in the AST.
|
|
561
|
+
|
|
562
|
+
The visit walks top-level statements in source order, so a rebind that
|
|
563
|
+
FOLLOWS a ``request.security`` call would otherwise never be seen by
|
|
564
|
+
that call's symbol check. Collecting up front is strictly fail-closed:
|
|
565
|
+
it can only add rejections, never remove one.
|
|
566
|
+
"""
|
|
567
|
+
if isinstance(node, Assignment):
|
|
568
|
+
self._record_scalar_rebind(node)
|
|
569
|
+
for value in vars(node).values():
|
|
570
|
+
if isinstance(value, ASTNode):
|
|
571
|
+
self._collect_scalar_rebinds(value)
|
|
572
|
+
elif isinstance(value, list):
|
|
573
|
+
for item in value:
|
|
574
|
+
if isinstance(item, ASTNode):
|
|
575
|
+
self._collect_scalar_rebinds(item)
|
|
576
|
+
elif isinstance(value, dict):
|
|
577
|
+
for item in value.values():
|
|
578
|
+
if isinstance(item, ASTNode):
|
|
579
|
+
self._collect_scalar_rebinds(item)
|
|
580
|
+
|
|
536
581
|
def _collect_user_definitions(self, ast: Program) -> None:
|
|
537
582
|
for stmt in ast.body:
|
|
538
583
|
if isinstance(stmt, TypeDecl):
|
|
@@ -1001,6 +1046,14 @@ class SupportChecker:
|
|
|
1001
1046
|
)
|
|
1002
1047
|
self._visit_children(node)
|
|
1003
1048
|
|
|
1049
|
+
def _visit_Assignment(self, node: Assignment) -> None:
|
|
1050
|
+
# ``_scalar_defs`` records only the DECLARATION, so a later ``:=``
|
|
1051
|
+
# rebind used to be invisible to the request.security symbol check.
|
|
1052
|
+
# (``check`` also pre-collects these; recording here keeps the visit
|
|
1053
|
+
# self-consistent for any caller that drives visitors directly.)
|
|
1054
|
+
self._record_scalar_rebind(node)
|
|
1055
|
+
self._visit_children(node)
|
|
1056
|
+
|
|
1004
1057
|
def _visit_TupleAssign(self, node: TupleAssign) -> None:
|
|
1005
1058
|
drawing_mask = self._tuple_assign_drawing_mask(node.value)
|
|
1006
1059
|
if drawing_mask:
|
|
@@ -1723,10 +1776,21 @@ class SupportChecker:
|
|
|
1723
1776
|
or self._is_current_symbol_expr(node.false_val, _seen))
|
|
1724
1777
|
# Def-use: resolve a bare identifier through its declaration value so an
|
|
1725
1778
|
# aliased symbol is accepted (``haTicker = ticker.heikinashi(...)`` then
|
|
1726
|
-
# ``request.security(haTicker, ...)``).
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1779
|
+
# ``request.security(haTicker, ...)``). Every ``:=`` rebind of the name
|
|
1780
|
+
# must resolve to the chart symbol too: the declaration alone does not
|
|
1781
|
+
# pin the value the call actually receives. Name-cycle-guarded.
|
|
1782
|
+
if isinstance(node, Identifier) and node.name not in _seen:
|
|
1783
|
+
definition = self._scalar_defs.get(node.name)
|
|
1784
|
+
rebinds = self._scalar_rebinds.get(node.name)
|
|
1785
|
+
if definition is not None or rebinds:
|
|
1786
|
+
_seen.add(node.name)
|
|
1787
|
+
if rebinds and not all(
|
|
1788
|
+
self._is_current_symbol_expr(value, _seen) for value in rebinds
|
|
1789
|
+
):
|
|
1790
|
+
return False
|
|
1791
|
+
if definition is None:
|
|
1792
|
+
return False
|
|
1793
|
+
return self._is_current_symbol_expr(definition, _seen)
|
|
1730
1794
|
return False
|
|
1731
1795
|
|
|
1732
1796
|
# -- Pine timeframe-literal validation --
|
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codegen": "0.10.
|
|
2
|
+
"codegen": "0.10.2",
|
|
3
3
|
"pyodide": "314.0.0",
|
|
4
4
|
"python": "3.14.0",
|
|
5
5
|
"emscripten": "emscripten_5_0_3",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "97ebb8d0b14c32d3c88b9521c8f13150ee9cb9e3859227af970ec4dcbfa6bf71"
|
|
7
7
|
}
|
package/tables.json
CHANGED
|
Binary file
|