@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.
@@ -0,0 +1,798 @@
1
+ """Bounded lowering for finite-choice ``ta.highest``/``ta.lowest`` lengths.
2
+
3
+ Pine permits a ``series int`` length for its rolling extrema. PineForge's
4
+ runtime extrema objects intentionally keep one fixed constructor length, so a
5
+ series-selected length cannot be represented by mutating or rebuilding one
6
+ object: after a shorter window has discarded history, a later longer window
7
+ cannot recover it.
8
+
9
+ The small, exact subset handled here needs no runtime ABI change. When a
10
+ top-level extrema declaration chooses between two bar-invariant lengths, emit
11
+ one fixed extrema history for each choice, advance both on every bar, and
12
+ select the value for the current condition. For example::
13
+
14
+ n = regime ? fastInput : slowInput
15
+ lo = ta.lowest(low, n)
16
+
17
+ is lowered in the AST to the semantic equivalent::
18
+
19
+ pf_ta_choice_1_selected = n
20
+ pf_ta_choice_1_true = ta.lowest(low, fastInput)
21
+ pf_ta_choice_1_false = ta.lowest(low, slowInput)
22
+ lo = pf_ta_choice_1_selected == fastInput ?
23
+ pf_ta_choice_1_true : pf_ta_choice_1_false
24
+
25
+ The transform is deliberately narrow:
26
+
27
+ * only direct top-level variable declarations;
28
+ * only ``ta.highest`` and ``ta.lowest`` positional call forms;
29
+ * exactly one ternary (inline or through a top-level length alias), with a
30
+ direct comparison operator as its condition;
31
+ * each branch must be a positive integer literal or a direct immutable
32
+ ``input.int`` alias whose declared domain is provably positive; and
33
+ * the source must be a plain identifier (the one-argument default-source form
34
+ is also accepted).
35
+
36
+ The authored length is snapshotted exactly once at the original call site.
37
+ Aliases and every transitive selector/arm alias must be ordinary immutable
38
+ declarations: ``var``/``varip`` dependencies and any name reassigned anywhere
39
+ in the program stay unsupported. Direct inline ``input.int`` arms are also
40
+ left unsupported so lowering cannot duplicate their declarations.
41
+
42
+ Everything else retains the existing loud unsupported-length diagnostic. In
43
+ particular, this is not a claim of arbitrary series-length support for EMA,
44
+ SMA, pivots, UDF-local sites, request.security evaluators, or unbounded
45
+ run-time lengths.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import copy
51
+ from dataclasses import replace
52
+
53
+ from .ast_nodes import (
54
+ Assignment,
55
+ BinOp,
56
+ FuncCall,
57
+ FuncDef,
58
+ Identifier,
59
+ MemberAccess,
60
+ MethodDef,
61
+ NumberLiteral,
62
+ Program,
63
+ Subscript,
64
+ Ternary,
65
+ TupleAssign,
66
+ TupleLiteral,
67
+ UnaryOp,
68
+ VarDecl,
69
+ )
70
+
71
+
72
+ _EXTREMA_NAMES = frozenset({"highest", "lowest"})
73
+ _COMPARISON_OPS = frozenset({"==", "!=", "<", "<=", ">", ">="})
74
+ _ARITHMETIC_OPS = frozenset({"+", "-", "*", "/", "%"})
75
+ _BAR_IDENTIFIERS = frozenset({
76
+ "open", "high", "low", "close", "volume",
77
+ "hl2", "hlc3", "ohlc4", "hlcc4",
78
+ "time", "time_close", "bar_index", "last_bar_index", "timenow",
79
+ })
80
+
81
+
82
+ def _call_name(node: FuncCall) -> tuple[str | None, str | None]:
83
+ callee = node.callee
84
+ if not isinstance(callee, MemberAccess):
85
+ return None, None
86
+ if not isinstance(callee.object, Identifier):
87
+ return None, None
88
+ return callee.object.name, callee.member
89
+
90
+
91
+ def _is_input_int_call(node) -> bool:
92
+ return (
93
+ isinstance(node, FuncCall)
94
+ and _call_name(node) == ("input", "int")
95
+ )
96
+
97
+
98
+ def _is_positive_int_literal(node) -> bool:
99
+ return (
100
+ isinstance(node, NumberLiteral)
101
+ and isinstance(node.value, int)
102
+ and not isinstance(node.value, bool)
103
+ and node.value > 0
104
+ )
105
+
106
+
107
+ def _is_positive_bounded_input_int(node) -> bool:
108
+ """Whether an input owns a provably positive run-time domain."""
109
+
110
+ if not _is_input_int_call(node) or not node.args:
111
+ return False
112
+ if not _is_positive_int_literal(node.args[0]):
113
+ return False
114
+
115
+ minval = node.kwargs.get("minval")
116
+ if _is_positive_int_literal(minval):
117
+ return True
118
+
119
+ options = node.kwargs.get("options")
120
+ return (
121
+ isinstance(options, TupleLiteral)
122
+ and bool(options.elements)
123
+ and all(_is_positive_int_literal(item) for item in options.elements)
124
+ )
125
+
126
+
127
+ def _is_fixed_int_length(
128
+ node,
129
+ definitions: dict[str, tuple[int, VarDecl]],
130
+ *,
131
+ before_index: int,
132
+ ambiguous_names: frozenset[str] = frozenset(),
133
+ ) -> bool:
134
+ """Whether ``node`` is one bar-invariant integer length choice.
135
+
136
+ The only admitted leaves are a positive literal or one direct, unique,
137
+ top-level ``input.int`` alias whose entire selectable domain is positive.
138
+ This deliberately excludes alias chains and unconstrained inputs.
139
+ """
140
+
141
+ if _is_positive_int_literal(node):
142
+ return True
143
+ # A direct inline input call would be copied into both fixed histories.
144
+ if _is_input_int_call(node):
145
+ return False
146
+ if (
147
+ not isinstance(node, Identifier)
148
+ or node.name in ambiguous_names
149
+ ):
150
+ return False
151
+ found = definitions.get(node.name)
152
+ if found is None:
153
+ return False
154
+ def_index, declaration = found
155
+ if def_index >= before_index or declaration.value is None:
156
+ return False
157
+ if declaration.type_hint not in (None, "int"):
158
+ return False
159
+ return _is_positive_bounded_input_int(declaration.value)
160
+
161
+
162
+ def _is_numeric_expr(
163
+ node,
164
+ definitions: dict[str, tuple[int, VarDecl]],
165
+ *,
166
+ before_index: int,
167
+ assigned_names: set[str],
168
+ ambiguous_names: frozenset[str],
169
+ authored_names: frozenset[str],
170
+ seen: frozenset[str] = frozenset(),
171
+ ) -> bool:
172
+ """Prove the small numeric expression subset used by Wellman's regime."""
173
+
174
+ if isinstance(node, NumberLiteral):
175
+ return not isinstance(node.value, bool)
176
+ if isinstance(node, Identifier):
177
+ if node.name in _BAR_IDENTIFIERS:
178
+ return node.name not in authored_names
179
+ if node.name in seen or node.name in ambiguous_names:
180
+ return False
181
+ found = definitions.get(node.name)
182
+ if found is None:
183
+ return False
184
+ def_index, declaration = found
185
+ if (
186
+ def_index >= before_index
187
+ or declaration.value is None
188
+ or declaration.is_var
189
+ or declaration.is_varip
190
+ or declaration.name in assigned_names
191
+ # Wellman's selector aliases are float-valued. An explicit int
192
+ # alias can mask a float-to-int annotation error, so integer length
193
+ # inputs are admitted only by the separate fixed-length proof.
194
+ or declaration.type_hint not in (None, "float")
195
+ ):
196
+ return False
197
+ return _is_numeric_expr(
198
+ declaration.value,
199
+ definitions,
200
+ before_index=def_index,
201
+ assigned_names=assigned_names,
202
+ ambiguous_names=ambiguous_names,
203
+ authored_names=authored_names,
204
+ seen=seen | {node.name},
205
+ )
206
+ if isinstance(node, UnaryOp):
207
+ return node.op in {"+", "-"} and _is_numeric_expr(
208
+ node.operand,
209
+ definitions,
210
+ before_index=before_index,
211
+ assigned_names=assigned_names,
212
+ ambiguous_names=ambiguous_names,
213
+ authored_names=authored_names,
214
+ seen=seen,
215
+ )
216
+ if isinstance(node, BinOp):
217
+ return node.op in _ARITHMETIC_OPS and all(
218
+ _is_numeric_expr(
219
+ child,
220
+ definitions,
221
+ before_index=before_index,
222
+ assigned_names=assigned_names,
223
+ ambiguous_names=ambiguous_names,
224
+ authored_names=authored_names,
225
+ seen=seen,
226
+ )
227
+ for child in (node.left, node.right)
228
+ )
229
+ if isinstance(node, Ternary):
230
+ return _is_numeric_comparison(
231
+ node.condition,
232
+ definitions,
233
+ before_index=before_index,
234
+ assigned_names=assigned_names,
235
+ ambiguous_names=ambiguous_names,
236
+ authored_names=authored_names,
237
+ seen=seen,
238
+ ) and all(
239
+ _is_numeric_expr(
240
+ child,
241
+ definitions,
242
+ before_index=before_index,
243
+ assigned_names=assigned_names,
244
+ ambiguous_names=ambiguous_names,
245
+ authored_names=authored_names,
246
+ seen=seen,
247
+ )
248
+ for child in (node.true_val, node.false_val)
249
+ )
250
+ if isinstance(node, FuncCall):
251
+ namespace, name = _call_name(node)
252
+ if namespace == "input" and name in {"int", "float"}:
253
+ return True
254
+ # Keep the TA proof intentionally evidence-bound to Wellman's ATRs.
255
+ if not (
256
+ namespace == "ta"
257
+ and name == "atr"
258
+ and not node.kwargs
259
+ and len(node.args) == 1
260
+ ):
261
+ return False
262
+ length = node.args[0]
263
+ if not _is_fixed_int_length(
264
+ length,
265
+ definitions,
266
+ before_index=before_index,
267
+ ambiguous_names=ambiguous_names,
268
+ ):
269
+ return False
270
+ if isinstance(length, Identifier):
271
+ _def_index, declaration = definitions[length.name]
272
+ if (
273
+ declaration.is_var
274
+ or declaration.is_varip
275
+ or length.name in assigned_names
276
+ ):
277
+ return False
278
+ return True
279
+ return False
280
+
281
+
282
+ def _is_numeric_comparison(
283
+ node,
284
+ definitions: dict[str, tuple[int, VarDecl]],
285
+ *,
286
+ before_index: int,
287
+ assigned_names: set[str],
288
+ ambiguous_names: frozenset[str],
289
+ authored_names: frozenset[str],
290
+ seen: frozenset[str] = frozenset(),
291
+ ) -> bool:
292
+ """Whether ``node`` is a comparator over two proven numeric operands."""
293
+
294
+ return (
295
+ isinstance(node, BinOp)
296
+ and node.op in _COMPARISON_OPS
297
+ and _is_numeric_expr(
298
+ node.left,
299
+ definitions,
300
+ before_index=before_index,
301
+ assigned_names=assigned_names,
302
+ ambiguous_names=ambiguous_names,
303
+ authored_names=authored_names,
304
+ seen=seen,
305
+ )
306
+ and _is_numeric_expr(
307
+ node.right,
308
+ definitions,
309
+ before_index=before_index,
310
+ assigned_names=assigned_names,
311
+ ambiguous_names=ambiguous_names,
312
+ authored_names=authored_names,
313
+ seen=seen,
314
+ )
315
+ )
316
+
317
+
318
+ def _resolve_choice(
319
+ node,
320
+ definitions: dict[str, tuple[int, VarDecl]],
321
+ *,
322
+ before_index: int,
323
+ seen: frozenset[str] = frozenset(),
324
+ ambiguous_names: frozenset[str] = frozenset(),
325
+ aliases: tuple[VarDecl, ...] = (),
326
+ ) -> tuple[Ternary, int, tuple[VarDecl, ...]] | None:
327
+ """Resolve one inline or top-level-aliased ternary length expression."""
328
+
329
+ if isinstance(node, Ternary):
330
+ return node, before_index, aliases
331
+ if (
332
+ not isinstance(node, Identifier)
333
+ or node.name in seen
334
+ or node.name in ambiguous_names
335
+ ):
336
+ return None
337
+ found = definitions.get(node.name)
338
+ if found is None:
339
+ return None
340
+ def_index, declaration = found
341
+ if def_index >= before_index or declaration.value is None:
342
+ return None
343
+ return _resolve_choice(
344
+ declaration.value,
345
+ definitions,
346
+ before_index=def_index,
347
+ seen=seen | {node.name},
348
+ ambiguous_names=ambiguous_names,
349
+ aliases=aliases + (declaration,),
350
+ )
351
+
352
+
353
+ def _depends_on_series(
354
+ node,
355
+ definitions: dict[str, tuple[int, VarDecl]],
356
+ *,
357
+ before_index: int,
358
+ seen: frozenset[str] = frozenset(),
359
+ ambiguous_names: frozenset[str] = frozenset(),
360
+ ) -> bool:
361
+ """Conservatively identify a condition that is definitely bar-varying.
362
+
363
+ A stable input/timeframe selector was already supported by the ordinary
364
+ one-object runtime-reset route. Expanding it would be semantically valid
365
+ but would churn generated output and state for no benefit, so this pass is
366
+ enabled only when a series dependency is visible through simple top-level
367
+ aliases.
368
+ """
369
+
370
+ if isinstance(node, Identifier):
371
+ if node.name in _BAR_IDENTIFIERS:
372
+ return True
373
+ if node.name in seen or node.name in ambiguous_names:
374
+ return False
375
+ found = definitions.get(node.name)
376
+ if found is None:
377
+ return False
378
+ def_index, declaration = found
379
+ if def_index >= before_index or declaration.value is None:
380
+ return False
381
+ return _depends_on_series(
382
+ declaration.value,
383
+ definitions,
384
+ before_index=def_index,
385
+ seen=seen | {node.name},
386
+ ambiguous_names=ambiguous_names,
387
+ )
388
+ if isinstance(node, Subscript):
389
+ return True
390
+ if isinstance(node, (BinOp,)):
391
+ return _depends_on_series(
392
+ node.left, definitions, before_index=before_index, seen=seen,
393
+ ambiguous_names=ambiguous_names,
394
+ ) or _depends_on_series(
395
+ node.right, definitions, before_index=before_index, seen=seen,
396
+ ambiguous_names=ambiguous_names,
397
+ )
398
+ if isinstance(node, UnaryOp):
399
+ return _depends_on_series(
400
+ node.operand, definitions, before_index=before_index, seen=seen,
401
+ ambiguous_names=ambiguous_names,
402
+ )
403
+ if isinstance(node, Ternary):
404
+ return any(
405
+ _depends_on_series(
406
+ child, definitions, before_index=before_index, seen=seen,
407
+ ambiguous_names=ambiguous_names,
408
+ )
409
+ for child in (node.condition, node.true_val, node.false_val)
410
+ )
411
+ if isinstance(node, MemberAccess):
412
+ if isinstance(node.object, Identifier):
413
+ if node.object.name in {"barstate", "strategy"}:
414
+ return True
415
+ if node.object.name in {"input", "timeframe", "syminfo", "math"}:
416
+ return False
417
+ return _depends_on_series(
418
+ node.object, definitions, before_index=before_index, seen=seen,
419
+ ambiguous_names=ambiguous_names,
420
+ )
421
+ if isinstance(node, FuncCall):
422
+ namespace, _name = _call_name(node)
423
+ if namespace in {"ta", "strategy", "request"}:
424
+ return True
425
+ if namespace == "input":
426
+ return False
427
+ if namespace in {"math", "timeframe"}:
428
+ return any(
429
+ _depends_on_series(
430
+ arg, definitions, before_index=before_index, seen=seen,
431
+ ambiguous_names=ambiguous_names,
432
+ )
433
+ for arg in node.args
434
+ )
435
+ # Bare time/session functions and other calls are not admitted merely
436
+ # because their result might be dynamic. The bounded route requires a
437
+ # dependency it can prove, keeping unknown UDF semantics unchanged.
438
+ return False
439
+ return False
440
+
441
+
442
+ def _plain_extrema_call(node) -> tuple[str, object, int] | None:
443
+ """Return ``(name, source, length_index)`` for the bounded call form."""
444
+
445
+ if not isinstance(node, FuncCall) or node.kwargs:
446
+ return None
447
+ namespace, name = _call_name(node)
448
+ if namespace != "ta" or name not in _EXTREMA_NAMES:
449
+ return None
450
+ if len(node.args) == 1:
451
+ # ta.highest(length) / ta.lowest(length): the analyzer supplies the
452
+ # native high/low source after this transform.
453
+ return name, None, 0
454
+ if len(node.args) != 2 or not isinstance(node.args[0], Identifier):
455
+ return None
456
+ expected_source = "low" if name == "lowest" else "high"
457
+ if node.args[0].name != expected_source:
458
+ return None
459
+ return name, node.args[0], 1
460
+
461
+
462
+ def _all_bound_names(program: Program) -> set[str]:
463
+ """Collect authored binders so generated top-level names cannot collide."""
464
+
465
+ names: set[str] = set()
466
+
467
+ def walk(node) -> None:
468
+ if node is None:
469
+ return
470
+ if isinstance(node, VarDecl):
471
+ names.add(node.name)
472
+ if isinstance(node, (FuncDef, MethodDef)):
473
+ names.add(node.name)
474
+ params = getattr(node, "params", None)
475
+ if isinstance(params, list):
476
+ names.update(name for name in params if isinstance(name, str))
477
+ var = getattr(node, "var", None)
478
+ if isinstance(var, str):
479
+ names.add(var)
480
+ vars_ = getattr(node, "vars", None)
481
+ if isinstance(vars_, list):
482
+ names.update(name for name in vars_ if isinstance(name, str))
483
+ tuple_names = getattr(node, "names", None)
484
+ if isinstance(tuple_names, list):
485
+ names.update(name for name in tuple_names if isinstance(name, str))
486
+ if not hasattr(node, "__dict__"):
487
+ return
488
+ for value in vars(node).values():
489
+ if isinstance(value, list):
490
+ for child in value:
491
+ if isinstance(child, tuple):
492
+ for item in child:
493
+ walk(item)
494
+ else:
495
+ walk(child)
496
+ elif isinstance(value, dict):
497
+ for child in value.values():
498
+ walk(child)
499
+ else:
500
+ walk(value)
501
+
502
+ walk(program)
503
+ return names
504
+
505
+
506
+ def _assigned_names(program: Program) -> set[str]:
507
+ """Return top-level-scope names explicitly written anywhere in a bar.
508
+
509
+ Assignments nested in control flow still mutate the surrounding Pine
510
+ variable. Callable bodies are included conservatively as well: even an
511
+ authored-invalid attempt to mutate a global dependency must not become
512
+ executable merely because this lowering ran first.
513
+ """
514
+
515
+ names: set[str] = set()
516
+
517
+ def walk(node) -> None:
518
+ if node is None:
519
+ return
520
+ if isinstance(node, Assignment) and isinstance(node.target, Identifier):
521
+ names.add(node.target.name)
522
+ if isinstance(node, TupleAssign):
523
+ names.update(node.names)
524
+ if not hasattr(node, "__dict__"):
525
+ return
526
+ for value in vars(node).values():
527
+ if isinstance(value, list):
528
+ for child in value:
529
+ if isinstance(child, tuple):
530
+ for item in child:
531
+ walk(item)
532
+ else:
533
+ walk(child)
534
+ elif isinstance(value, dict):
535
+ for child in value.values():
536
+ walk(child)
537
+ else:
538
+ walk(value)
539
+
540
+ for statement in program.body:
541
+ walk(statement)
542
+ return names
543
+
544
+
545
+ def _dependency_declarations(
546
+ node,
547
+ definitions: dict[str, tuple[int, VarDecl]],
548
+ *,
549
+ before_index: int,
550
+ ambiguous_names: frozenset[str] = frozenset(),
551
+ authored_names: frozenset[str] = frozenset(),
552
+ ) -> tuple[dict[str, VarDecl], bool]:
553
+ """Resolve all visible top-level aliases transitively used by ``node``."""
554
+
555
+ dependencies: dict[str, VarDecl] = {}
556
+ valid = True
557
+
558
+ def walk(current, limit: int) -> None:
559
+ nonlocal valid
560
+ if current is None:
561
+ return
562
+ if isinstance(current, Identifier):
563
+ if current.name in ambiguous_names:
564
+ valid = False
565
+ return
566
+ found = definitions.get(current.name)
567
+ if found is None:
568
+ if current.name in authored_names:
569
+ # A tuple/loop/callable/other authored binder is not a
570
+ # visible unique top-level VarDecl in this bounded model.
571
+ # It must not be mistaken for an external Pine builtin.
572
+ valid = False
573
+ return
574
+ def_index, declaration = found
575
+ if def_index >= limit:
576
+ # A known global exists, but not yet at this authored lexical
577
+ # point. Treat it as an invalid forward reference rather than
578
+ # silently compiling against a first-bar default.
579
+ valid = False
580
+ return
581
+ if current.name in dependencies:
582
+ return
583
+ dependencies[current.name] = declaration
584
+ walk(declaration.value, def_index)
585
+ return
586
+ if isinstance(current, (FuncDef, MethodDef)):
587
+ return
588
+ if not hasattr(current, "__dict__"):
589
+ return
590
+ for value in vars(current).values():
591
+ if isinstance(value, list):
592
+ for child in value:
593
+ if isinstance(child, tuple):
594
+ for item in child:
595
+ walk(item, limit)
596
+ else:
597
+ walk(child, limit)
598
+ elif isinstance(value, dict):
599
+ for child in value.values():
600
+ walk(child, limit)
601
+ else:
602
+ walk(value, limit)
603
+
604
+ walk(node, before_index)
605
+ return dependencies, valid
606
+
607
+
608
+ def expand_finite_choice_extrema_lengths(program: Program) -> Program:
609
+ """Return ``program`` with the bounded finite-choice extrema lowering.
610
+
611
+ The parser's original tree is left untouched. This matters because the
612
+ support checker runs on the authored program and because callers may reuse
613
+ a parsed AST in tests or diagnostics.
614
+ """
615
+
616
+ if not isinstance(program, Program):
617
+ return program
618
+
619
+ definition_occurrences: dict[str, list[tuple[int, VarDecl]]] = {}
620
+ for index, statement in enumerate(program.body):
621
+ if isinstance(statement, VarDecl):
622
+ definition_occurrences.setdefault(statement.name, []).append(
623
+ (index, statement)
624
+ )
625
+ definitions = {
626
+ name: occurrences[-1]
627
+ for name, occurrences in definition_occurrences.items()
628
+ }
629
+ ambiguous_names = frozenset(
630
+ name
631
+ for name, occurrences in definition_occurrences.items()
632
+ if len(occurrences) != 1
633
+ )
634
+ top_level_tuple_names = frozenset(
635
+ name
636
+ for statement in program.body
637
+ if isinstance(statement, TupleAssign)
638
+ for name in statement.names
639
+ )
640
+ used_names = _all_bound_names(program)
641
+ authored_names = frozenset(used_names)
642
+ if authored_names.intersection({"ta", "input"} | _BAR_IDENTIFIERS):
643
+ # Namespace or bar-series spellings shadowed by any authored binder
644
+ # cannot be proven to denote Pine's native values in this pre-analysis
645
+ # pass. Fail closed for the whole bounded transform.
646
+ return program
647
+ assigned_names = _assigned_names(program)
648
+ counter = 0
649
+
650
+ def fresh_choice_names() -> tuple[str, str, str]:
651
+ nonlocal counter
652
+ while True:
653
+ counter += 1
654
+ candidates = tuple(
655
+ f"pf_ta_length_choice_{counter}_{suffix}"
656
+ for suffix in ("selected", "true", "false")
657
+ )
658
+ if not any(candidate in used_names for candidate in candidates):
659
+ used_names.update(candidates)
660
+ return candidates
661
+
662
+ body: list = []
663
+ for index, statement in enumerate(program.body):
664
+ if not isinstance(statement, VarDecl):
665
+ body.append(statement)
666
+ continue
667
+ if (
668
+ statement.name in ambiguous_names
669
+ or statement.name in top_level_tuple_names
670
+ or statement.is_var
671
+ or statement.is_varip
672
+ or statement.type_hint not in (None, "float")
673
+ ):
674
+ # Rolling extrema return float; lowering must not erase an authored
675
+ # duplicate/persistent result binder or incompatible result
676
+ # annotation via the synthetic per-bar selection.
677
+ body.append(statement)
678
+ continue
679
+
680
+ matched = _plain_extrema_call(statement.value)
681
+ if matched is None:
682
+ body.append(statement)
683
+ continue
684
+ _name, _source, length_index = matched
685
+ if _source is not None and _source.name in authored_names:
686
+ # The bounded two-argument route is only for Pine's native low or
687
+ # high series, never any authored binder shadowing that spelling.
688
+ body.append(statement)
689
+ continue
690
+ call = statement.value
691
+ length_node = call.args[length_index]
692
+ resolved_choice = _resolve_choice(
693
+ length_node,
694
+ definitions,
695
+ before_index=index,
696
+ ambiguous_names=ambiguous_names,
697
+ )
698
+ if resolved_choice is None:
699
+ body.append(statement)
700
+ continue
701
+ choice, choice_index, choice_aliases = resolved_choice
702
+ if any(
703
+ declaration.type_hint not in (None, "int")
704
+ for declaration in choice_aliases
705
+ ):
706
+ body.append(statement)
707
+ continue
708
+ if not _is_numeric_comparison(
709
+ choice.condition,
710
+ definitions,
711
+ before_index=choice_index,
712
+ assigned_names=assigned_names,
713
+ ambiguous_names=ambiguous_names,
714
+ authored_names=authored_names,
715
+ ):
716
+ body.append(statement)
717
+ continue
718
+ if not _depends_on_series(
719
+ choice.condition,
720
+ definitions,
721
+ before_index=choice_index,
722
+ ambiguous_names=ambiguous_names,
723
+ ):
724
+ body.append(statement)
725
+ continue
726
+ if not _is_fixed_int_length(
727
+ choice.true_val,
728
+ definitions,
729
+ before_index=choice_index,
730
+ ambiguous_names=ambiguous_names,
731
+ ) or not _is_fixed_int_length(
732
+ choice.false_val,
733
+ definitions,
734
+ before_index=choice_index,
735
+ ambiguous_names=ambiguous_names,
736
+ ):
737
+ body.append(statement)
738
+ continue
739
+
740
+ dependencies, dependencies_valid = _dependency_declarations(
741
+ length_node,
742
+ definitions,
743
+ before_index=index,
744
+ ambiguous_names=ambiguous_names,
745
+ authored_names=authored_names,
746
+ )
747
+ if not dependencies_valid or any(
748
+ declaration.is_var
749
+ or declaration.is_varip
750
+ or name in assigned_names
751
+ for name, declaration in dependencies.items()
752
+ ):
753
+ body.append(statement)
754
+ continue
755
+
756
+ selected_name, true_name, false_name = fresh_choice_names()
757
+
758
+ true_args = copy.deepcopy(call.args)
759
+ false_args = copy.deepcopy(call.args)
760
+ true_args[length_index] = copy.deepcopy(choice.true_val)
761
+ false_args[length_index] = copy.deepcopy(choice.false_val)
762
+ true_call = replace(call, args=true_args, kwargs={})
763
+ false_call = replace(call, args=false_args, kwargs={})
764
+ true_decl = VarDecl(
765
+ name=true_name,
766
+ value=true_call,
767
+ loc=call.loc,
768
+ )
769
+ false_decl = VarDecl(
770
+ name=false_name,
771
+ value=false_call,
772
+ loc=call.loc,
773
+ )
774
+ selected_length_decl = VarDecl(
775
+ name=selected_name,
776
+ value=copy.deepcopy(length_node),
777
+ loc=call.loc,
778
+ )
779
+ selected = Ternary(
780
+ condition=BinOp(
781
+ left=Identifier(name=selected_name, loc=call.loc),
782
+ op="==",
783
+ right=copy.deepcopy(choice.true_val),
784
+ loc=call.loc,
785
+ ),
786
+ true_val=Identifier(name=true_name, loc=call.loc),
787
+ false_val=Identifier(name=false_name, loc=call.loc),
788
+ loc=call.loc,
789
+ )
790
+
791
+ body.extend((
792
+ selected_length_decl,
793
+ true_decl,
794
+ false_decl,
795
+ replace(statement, value=selected),
796
+ ))
797
+
798
+ return replace(program, body=body)