@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
|
@@ -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)
|