@pineforge/codegen-pyodide 0.10.3 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +767 -77
  7. package/pineforge_codegen/analyzer/call_handlers.py +268 -42
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +907 -159
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +961 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1198 -101
  25. package/pineforge_codegen/codegen/tables.py +205 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +2055 -62
  28. package/pineforge_codegen/codegen/visit_call.py +929 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +765 -63
  30. package/pineforge_codegen/codegen/visit_stmt.py +610 -52
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen/symbols.py +4 -2
  47. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  48. package/release.json +2 -2
  49. package/tables.json +23 -21
  50. package/transpile.worker.mjs +24 -16
  51. package/pineforge_codegen-0.10.3.tar.gz +0 -0
@@ -0,0 +1,683 @@
1
+ """Pine v5 rules inside the bodies of an inlined v5 library.
2
+
3
+ A library keeps its own ``//@version``: TradingView compiles it on its own,
4
+ and a v6 script can import a v5 one (``jdehorty/MLExtensions/2`` and
5
+ ``jdehorty/KernelFunctions/2`` are v5). Where v5 and v6 read the same code
6
+ differently, a v5 library body must be lowered with v5's rules.
7
+ ``V5_RULES`` enumerates every difference TradingView's migration guide to v6
8
+ lists ("Here are the changes that affect v5 scripts") and what PineForge does
9
+ with it inside a v5 library body: lower it with v5's rule here or in the
10
+ codegen (``annotations["pine_version"] == 5`` on the inlined function or
11
+ method), refuse the construct by the library's name, or nothing because the
12
+ difference cannot reach a library body's lowering.
13
+
14
+ Each implemented rule is pinned by TradingView's tape of a synthetic v5
15
+ script (``tests/fixtures/xsym_lib_tv``), replayed through a synthetic v5
16
+ library holding the same code (``tests/test_e2e_library_v5.py``).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+
23
+ from .ast_nodes import (
24
+ ArgOrder, ASTNode, Assignment, BinOp, BoolLiteral, ColorLiteral, ExprStmt,
25
+ ForInStmt, ForStmt, FuncCall, FuncDef, Identifier, IfStmt, MemberAccess,
26
+ MethodDef, NaLiteral, NumberLiteral, StringLiteral, Subscript, SwitchStmt,
27
+ Ternary, TupleAssign, TupleLiteral, TypeDecl, UnaryOp, VarDecl, WhileStmt,
28
+ )
29
+ from .errors import CompileError, Diagnostic, Level, Phase, SourceLocation
30
+ from .library_modules import LibraryModule
31
+ from . import signatures as sigs
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class V5Rule:
36
+ change: str # the migration guide's wording
37
+ disposition: str # "implemented" | "refused" | "not applicable"
38
+ how: str
39
+
40
+
41
+ # The migration guide's list, in its order (pine-script-docs
42
+ # migration-guides/to-pine-version-6, read 2026-09-28).
43
+ V5_RULES: dict[str, V5Rule] = {
44
+ "implicit-bool-cast": V5Rule(
45
+ "Values of the int and float types are no longer implicitly cast to bool.",
46
+ "implemented",
47
+ "the codegen's truthiness helper reads na and 0 as false in every "
48
+ "bool context, which is v5's implicit cast"),
49
+ "bool-na": V5Rule(
50
+ "Boolean values can no longer be na, and the na(), nz(), and fixnan() "
51
+ "functions no longer accept bool arguments.",
52
+ "refused",
53
+ "a v5 bool na (a comparison with na, an na literal, a bool's history "
54
+ "before the first bar, an if without else) reads as false wherever v5 "
55
+ "casts it (conditions, not, and, or, ?:) and across the library "
56
+ "boundary (TradingView's v6 caller reads it false); na(), nz(), "
57
+ "fixnan() and str.tostring/str.format of a bool, and == / != with a "
58
+ "bool operand, observe the third state and are refused"),
59
+ "lazy-and-or": V5Rule(
60
+ "The and and or operators now evaluate conditions lazily.",
61
+ "implemented",
62
+ "the codegen evaluates both operands of and/or, left then right, "
63
+ "every time the expression is evaluated; ?: stays lazy (v5 too)"),
64
+ "dynamic-requests": V5Rule(
65
+ "All request.*() functions can now execute dynamically.",
66
+ "refused",
67
+ "a request.*() call in a v5 library body"),
68
+ "const-int-division": V5Rule(
69
+ "Division of two const int values can now return a fractional value.",
70
+ "implemented",
71
+ "a division of two const ints is truncated toward zero, int(a / b); "
72
+ "a const is a literal, a const declaration or a variable never "
73
+ "reassigned with a const value, arithmetic, ?:, int(), math.abs/"
74
+ "max/min/round/floor/ceil and str.length of consts; a typed "
75
+ "parameter, a reassigned variable, an input, nz() or any series is "
76
+ "not, and a division whose constness depends on an untyped "
77
+ "parameter (v5 types it per call) is refused"),
78
+ "when-parameter": V5Rule(
79
+ "The when parameter is removed from all applicable strategy.*() functions.",
80
+ "not applicable", "a library cannot call strategy.*()"),
81
+ "default-margin": V5Rule(
82
+ "The default long and short margin percentage for strategies is now 100.",
83
+ "not applicable", "a strategy() declaration, never a library's"),
84
+ "excess-orders": V5Rule(
85
+ "Strategies now trim the oldest orders in their results instead of "
86
+ "raising an error when they exceed the 9000 trade limit.",
87
+ "not applicable", "the importing strategy's order book"),
88
+ "exit-parameter-pairs": V5Rule(
89
+ "The strategy.exit() command no longer ignores relative parameters "
90
+ "... when the call also includes arguments for the related absolute "
91
+ "parameters.",
92
+ "not applicable", "a library cannot call strategy.exit()"),
93
+ "literal-and-field-history": V5Rule(
94
+ "The history-referencing operator [] can no longer reference the "
95
+ "history of literal values or fields of user-defined types directly.",
96
+ "refused",
97
+ "[] on a literal, a built-in constant or a user-defined type's field "
98
+ "in a v5 library body"),
99
+ "repeated-parameters": V5Rule(
100
+ "Function calls can no longer include more than one argument for the "
101
+ "same parameter.",
102
+ "refused",
103
+ "the parser refuses a keyword argument given twice (v5 kept the "
104
+ "first), located in the library"),
105
+ "series-offset": V5Rule(
106
+ "The offset parameter of plot() and other functions no longer accepts "
107
+ "series values.",
108
+ "not applicable", "plot() and its kin run at the global scope; a "
109
+ "library's global code is never inlined"),
110
+ "unique-type-na": V5Rule(
111
+ "na values are no longer allowed in place of built-in constants of "
112
+ "unique types.",
113
+ "not applicable", "those parameters style plots and drawings, which "
114
+ "the codegen drops"),
115
+ "timeframe-period-multiplier": V5Rule(
116
+ 'The value of timeframe.period now always includes a multiplier '
117
+ '(e.g., "1D" instead of "D").',
118
+ "implemented",
119
+ 'timeframe.period reads "D", "W" and "M" for a 1D, 1W and 1M chart'),
120
+ "negative-array-index": V5Rule(
121
+ "Some array.*() functions now accept negative index arguments.",
122
+ "implemented",
123
+ "array get/set/insert/remove stop the run on a negative index "
124
+ "(TradingView's v5 runtime error), where v6 counts from the end"),
125
+ "mutable-const": V5Rule(
126
+ "Some mutable variables are no longer erroneously marked as const.",
127
+ "refused",
128
+ "a reassigned variable passed where a ta.*() or math.sum() length is "
129
+ "(v5 runs it at its first value); a reassigned variable is not const "
130
+ "in a division (TradingView's tape: fractional)"),
131
+ "transp-parameter": V5Rule(
132
+ "The transp parameter is removed from all applicable functions.",
133
+ "not applicable", "plot-style functions, global scope only"),
134
+ "default-colors": V5Rule(
135
+ "Some default colors and color constants have updated values.",
136
+ "implemented",
137
+ "color.red, color.teal and color.yellow keep their v5 values "
138
+ "(#FF5252, #00897B, #FFEB3B); label.new's default text color is a "
139
+ "drawing style the codegen drops"),
140
+ "dynamic-for-boundary": V5Rule(
141
+ "The for loop statement now evaluates its end boundary dynamically "
142
+ "before every iteration.",
143
+ "implemented",
144
+ "the codegen evaluates a for loop's `to` once, before the first "
145
+ "iteration"),
146
+ }
147
+
148
+ _V5_COLORS = {"red": "#FF5252", "teal": "#00897B", "yellow": "#FFEB3B"}
149
+
150
+ # Built-in namespaces whose members are constants: ``[]`` on one is history
151
+ # of a constant (refused under v5).
152
+ _CONSTANT_NAMESPACES = frozenset({
153
+ "color", "shape", "location", "size", "position", "text", "xloc", "yloc",
154
+ "extend", "plot", "hline", "display", "format", "order", "currency",
155
+ "dayofweek", "barmerge", "font", "scale", "alert", "adjustment",
156
+ "backadjustment", "settlement_as_close", "label", "line", "strategy",
157
+ })
158
+ _NAMESPACES = _CONSTANT_NAMESPACES | frozenset({
159
+ "ta", "math", "str", "array", "map", "matrix", "input", "request",
160
+ "syminfo", "timeframe", "barstate", "session", "chart", "table",
161
+ "linefill", "polyline", "log", "runtime", "ticker", "dividends",
162
+ "earnings", "splits", "footprint", "volume_row", "box",
163
+ })
164
+
165
+ # What the codegen lowers by v5's rules only inside a v5 function or method
166
+ # (``CodeGen._pine_v5_body``): a top-level declaration or a type's field
167
+ # default, emitted with the script's code, refuses it.
168
+ _BODY_ONLY_OPS = frozenset({"and", "or", "==", "!="})
169
+ _BODY_ONLY_CALLS = frozenset({"na", "nz", "fixnan", "string"})
170
+ _BODY_ONLY_MEMBER_CALLS = frozenset({
171
+ ("str", "tostring"), ("str", "format"),
172
+ ("array", "get"), ("array", "set"), ("array", "insert"), ("array", "remove"),
173
+ })
174
+ _BODY_ONLY_METHODS = frozenset({"get", "set", "insert", "remove"})
175
+
176
+ _INT_BUILTIN_VARS = frozenset({
177
+ "bar_index", "last_bar_index", "time", "time_close", "timenow",
178
+ "time_tradingday", "year", "month", "weekofyear", "dayofmonth",
179
+ "dayofweek", "hour", "minute", "second",
180
+ })
181
+
182
+ # Qualifier lattice of the v5 const-int analysis.
183
+ C, N, U = "const", "nonconst", "unknown"
184
+
185
+
186
+ def _combine(*qs: str) -> str:
187
+ if N in qs:
188
+ return N
189
+ if U in qs:
190
+ return U
191
+ return C
192
+
193
+
194
+ def _kind_of_hint(hint) -> str:
195
+ if not isinstance(hint, str):
196
+ return "?"
197
+ return {"int": "int", "float": "float", "bool": "bool",
198
+ "string": "string"}.get(hint, "?")
199
+
200
+
201
+ class _Refusal(Exception):
202
+ def __init__(self, node: ASTNode | None, message: str) -> None:
203
+ super().__init__(message)
204
+ self.node = node
205
+
206
+
207
+ class _Callable:
208
+ """What the const-int analysis knows of one inlined body."""
209
+
210
+ def __init__(self, node, module: "_V5Module") -> None:
211
+ self.module = module
212
+ hints = list((node.annotations or {}).get("param_type_hints") or ())
213
+ self.params = {p: (hints[i] if i < len(hints) else None)
214
+ for i, p in enumerate(node.params)}
215
+ self.decls: dict[str, list[VarDecl]] = {}
216
+ self.bound: set[str] = set() # tuple, loop and other bindings
217
+ self.reassigned: set[str] = set()
218
+ for stmt in _statements(node.body):
219
+ if isinstance(stmt, VarDecl):
220
+ self.decls.setdefault(stmt.name, []).append(stmt)
221
+ elif isinstance(stmt, TupleAssign):
222
+ self.bound.update(n for n in stmt.names if n != "_")
223
+ elif isinstance(stmt, Assignment) and isinstance(stmt.target, Identifier):
224
+ self.reassigned.add(stmt.target.name)
225
+ elif isinstance(stmt, ForStmt) and stmt.var:
226
+ self.bound.add(stmt.var)
227
+ elif isinstance(stmt, ForInStmt):
228
+ self.bound.update(v for v in [stmt.var, *(stmt.vars or [])] if v)
229
+
230
+
231
+ class _V5Module:
232
+ def __init__(self, lib: LibraryModule, definitions: list, names: dict[str, str]) -> None:
233
+ self.lib = lib
234
+ self.definitions = definitions
235
+ self.globals: dict[str, VarDecl] = {}
236
+ self.reassigned: set[str] = set()
237
+ for stmt in definitions:
238
+ if isinstance(stmt, VarDecl):
239
+ self.globals[stmt.name] = stmt
240
+ for stmt in lib.program.body:
241
+ if isinstance(stmt, Assignment) and isinstance(stmt.target, Identifier):
242
+ original = stmt.target.name
243
+ self.reassigned.add(names.get(original, original))
244
+
245
+
246
+ def _statements(body):
247
+ """Every statement of a body, nested blocks included."""
248
+ stack = list(reversed(body or []))
249
+ while stack:
250
+ stmt = stack.pop()
251
+ yield stmt
252
+ blocks = []
253
+ if isinstance(stmt, IfStmt):
254
+ blocks = [stmt.body, stmt.else_body]
255
+ elif isinstance(stmt, (ForStmt, ForInStmt, WhileStmt)):
256
+ blocks = [stmt.body]
257
+ elif isinstance(stmt, SwitchStmt):
258
+ blocks = [b for _v, b in stmt.cases] + [stmt.default_body]
259
+ for value in (getattr(stmt, "value", None), getattr(stmt, "expr", None)):
260
+ if isinstance(value, (IfStmt, SwitchStmt, ForStmt, WhileStmt)):
261
+ stack.append(value)
262
+ for block in reversed(blocks):
263
+ stack.extend(reversed(block or []))
264
+
265
+
266
+ def _walk(expr):
267
+ """``expr`` and every AST node below it."""
268
+ stack = [expr]
269
+ while stack:
270
+ node = stack.pop()
271
+ if not isinstance(node, ASTNode):
272
+ continue
273
+ yield node
274
+ for key, value in vars(node).items():
275
+ if key in ("loc", "annotations"):
276
+ continue
277
+ if isinstance(value, ASTNode):
278
+ stack.append(value)
279
+ elif isinstance(value, list):
280
+ stack.extend(v for v in value if isinstance(v, ASTNode))
281
+ elif isinstance(value, dict):
282
+ stack.extend(v for v in value.values() if isinstance(v, ASTNode))
283
+
284
+
285
+ class _Lowering:
286
+ """The v5 lowering of one inlined definition."""
287
+
288
+ def __init__(self, module: _V5Module, node) -> None:
289
+ self.module = module
290
+ self.node = node
291
+ self.fn = _Callable(node, module) if isinstance(node, (FuncDef, MethodDef)) else None
292
+
293
+ # -- the const-int analysis ------------------------------------------
294
+
295
+ def kind(self, expr) -> tuple[str, str]:
296
+ """(kind, qualifier) of ``expr``: kind int/float/bool/string/na/?,
297
+ qualifier const/nonconst/unknown under v5's rules (TradingView's tape
298
+ of xc_v5_lib, ``tests/fixtures/xsym_lib_tv``)."""
299
+ if isinstance(expr, NumberLiteral):
300
+ return ("int" if isinstance(expr.value, int) else "float"), C
301
+ if isinstance(expr, NaLiteral):
302
+ return "na", C
303
+ if isinstance(expr, BoolLiteral):
304
+ return "bool", C
305
+ if isinstance(expr, StringLiteral):
306
+ return "string", C
307
+ if isinstance(expr, ColorLiteral):
308
+ return "color", C
309
+ if isinstance(expr, Identifier):
310
+ return self._identifier(expr.name)
311
+ if isinstance(expr, UnaryOp):
312
+ k, q = self.kind(expr.operand)
313
+ return ("bool", q) if expr.op == "not" else (k, q)
314
+ if isinstance(expr, BinOp):
315
+ if expr.op in ("and", "or", "==", "!=", "<", ">", "<=", ">="):
316
+ return "bool", _combine(self.kind(expr.left)[1], self.kind(expr.right)[1])
317
+ (kl, ql), (kr, qr) = self.kind(expr.left), self.kind(expr.right)
318
+ q = _combine(ql, qr)
319
+ if expr.op == "/":
320
+ if kl == kr == "int":
321
+ return ("int", C) if q == C else ("float", q)
322
+ return ("float" if "float" in (kl, kr) else "?"), q
323
+ if kl == kr and kl in ("int", "float", "string"):
324
+ return kl, q
325
+ if {kl, kr} <= {"int", "float"}:
326
+ return "float", q
327
+ return "?", q
328
+ if isinstance(expr, Ternary):
329
+ (_kc, qc), (ka, qa), (kb, qb) = (self.kind(expr.condition),
330
+ self.kind(expr.true_val),
331
+ self.kind(expr.false_val))
332
+ q = _combine(qc, qa, qb)
333
+ if ka == kb:
334
+ return ka, q
335
+ if {ka, kb} <= {"int", "float"}:
336
+ return "float", q
337
+ return "?", q
338
+ if isinstance(expr, FuncCall):
339
+ return self._call_kind(expr)
340
+ if isinstance(expr, Subscript):
341
+ return self.kind(expr.object)[0], N
342
+ if isinstance(expr, MemberAccess):
343
+ if isinstance(expr.object, Identifier) and expr.object.name == "color":
344
+ return "color", C
345
+ return "?", N
346
+ return "?", U
347
+
348
+ def _identifier(self, name: str) -> tuple[str, str]:
349
+ fn = self.fn
350
+ if fn is not None and name in fn.params:
351
+ hint = fn.params[name]
352
+ # A typed parameter is series (or simple) whatever the argument
353
+ # (TradingView: xc_v5_lib's typedDiv(5, 5, 5) divides its int,
354
+ # simple int and series int parameters by 2 as 2.5); an untyped
355
+ # one takes its call's qualifier (a probe outside this
356
+ # repository: 2 for the argument 5, 0.5 for bar_index on bar 1).
357
+ return (_kind_of_hint(hint), N) if hint else ("?", U)
358
+ if fn is not None and (name in fn.decls or name in fn.bound):
359
+ if name in fn.bound or name in fn.reassigned:
360
+ return self._decl_kind(fn.decls.get(name)), N
361
+ if len(fn.decls[name]) != 1:
362
+ # Declared in sibling blocks: each is its own variable.
363
+ return self._decl_kind(fn.decls[name]), U
364
+ decl = fn.decls[name][0]
365
+ k, q = self.kind(decl.value)
366
+ return (_kind_of_hint(decl.type_hint) if decl.type_hint else k), q
367
+ glob = self.module.globals.get(name)
368
+ if glob is not None:
369
+ if name in self.module.reassigned:
370
+ return self._decl_kind([glob]), N
371
+ k, q = self.kind(glob.value)
372
+ return (_kind_of_hint(glob.type_hint) if glob.type_hint else k), q
373
+ if name in _INT_BUILTIN_VARS:
374
+ return "int", N
375
+ if name == "na":
376
+ return "na", C
377
+ return "?", N
378
+
379
+ def _decl_kind(self, decls) -> str:
380
+ for decl in decls or ():
381
+ if decl.type_hint:
382
+ return _kind_of_hint(decl.type_hint)
383
+ return "?"
384
+
385
+ def _call_kind(self, call: FuncCall) -> tuple[str, str]:
386
+ callee = call.callee
387
+ args = list(call.args) + list(call.kwargs.values())
388
+ kinds = [self.kind(a) for a in args]
389
+ q = _combine(*(k[1] for k in kinds)) if kinds else C
390
+ if isinstance(callee, Identifier):
391
+ name = callee.name
392
+ if name == "int":
393
+ return "int", q
394
+ if name == "float":
395
+ return "float", q
396
+ if name == "bool":
397
+ return "bool", q
398
+ if name == "string":
399
+ return "string", q
400
+ if name == "nz":
401
+ # TradingView: nz(7) / 2 divides as 3.5.
402
+ return (kinds[0][0] if kinds else "?"), N
403
+ if name == "na":
404
+ return "bool", q
405
+ # A user function's result: series unless every argument is a
406
+ # const, where v5's per-call typing is unknown.
407
+ return "?", (U if q == C else N)
408
+ if isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier):
409
+ ns, member = callee.object.name, callee.member
410
+ if ns == "math":
411
+ if member in ("abs",) and kinds:
412
+ return kinds[0][0], q
413
+ if member in ("max", "min"):
414
+ kk = {k for k, _ in kinds}
415
+ return ("int" if kk == {"int"} else "float"), q
416
+ if member in ("round", "floor", "ceil") and len(args) == 1:
417
+ return "int", q
418
+ return "float", q
419
+ if ns == "str" and member == "length":
420
+ return "int", q
421
+ if ns in _NAMESPACES:
422
+ return "?", N
423
+ return "?", (U if q == C else N)
424
+
425
+ # -- rewriting --------------------------------------------------------
426
+
427
+ def lower(self) -> None:
428
+ node = self.node
429
+ if isinstance(node, (FuncDef, MethodDef)):
430
+ self._block(node.body)
431
+ notes = node.annotations or {}
432
+ node.annotations = dict(notes, param_defaults=[
433
+ None if d is None else self.expr(d)
434
+ for d in notes.get("param_defaults") or ()])
435
+ elif isinstance(node, (VarDecl, TupleAssign)):
436
+ node.value = self.expr(node.value)
437
+ self._check_outside_body(node.value, "a top-level declaration")
438
+ elif isinstance(node, TypeDecl):
439
+ for fld in node.fields:
440
+ if fld.default is not None:
441
+ fld.default = self.expr(fld.default)
442
+ self._check_outside_body(fld.default, "a field's default")
443
+
444
+ def _check_outside_body(self, expr, where: str) -> None:
445
+ """Refuse in ``expr``, emitted outside the library's functions, what
446
+ the codegen lowers by v5's rules only inside them."""
447
+ for node in _walk(expr):
448
+ what = None
449
+ if isinstance(node, BinOp) and node.op in _BODY_ONLY_OPS:
450
+ what = f"'{node.op}'"
451
+ elif isinstance(node, FuncCall):
452
+ callee = node.callee
453
+ if isinstance(callee, Identifier) and callee.name in _BODY_ONLY_CALLS:
454
+ what = f"{callee.name}()"
455
+ elif isinstance(callee, MemberAccess):
456
+ ns = callee.object.name if isinstance(callee.object, Identifier) else None
457
+ if (ns, callee.member) in _BODY_ONLY_MEMBER_CALLS:
458
+ what = f"{ns}.{callee.member}()"
459
+ elif ns not in _NAMESPACES and callee.member in _BODY_ONLY_METHODS:
460
+ what = f"the method {callee.member}()"
461
+ if what is not None:
462
+ raise _Refusal(node, (
463
+ f"{what} in {where}: v5 reads it differently from v6, and "
464
+ "PineForge applies v5's rule only inside the library's "
465
+ "functions and methods"))
466
+
467
+ def _block(self, stmts) -> None:
468
+ for i, stmt in enumerate(stmts or []):
469
+ stmts[i] = self.stmt(stmt)
470
+
471
+ def stmt(self, stmt):
472
+ if isinstance(stmt, VarDecl):
473
+ stmt.value = self.expr(stmt.value)
474
+ elif isinstance(stmt, TupleAssign):
475
+ stmt.value = self.expr(stmt.value)
476
+ elif isinstance(stmt, Assignment):
477
+ stmt.target = self.expr(stmt.target)
478
+ stmt.value = self.expr(stmt.value)
479
+ elif isinstance(stmt, ExprStmt):
480
+ stmt.expr = self.expr(stmt.expr)
481
+ elif isinstance(stmt, (IfStmt, ForStmt, ForInStmt, WhileStmt, SwitchStmt)):
482
+ self._block_node(stmt)
483
+ return stmt
484
+
485
+ def _block_node(self, node):
486
+ if isinstance(node, IfStmt):
487
+ node.condition = self.expr(node.condition)
488
+ self._block(node.body)
489
+ self._block(node.else_body)
490
+ elif isinstance(node, ForStmt):
491
+ node.start, node.end = self.expr(node.start), self.expr(node.end)
492
+ if node.step is not None:
493
+ node.step = self.expr(node.step)
494
+ self._block(node.body)
495
+ elif isinstance(node, ForInStmt):
496
+ node.iterable = self.expr(node.iterable)
497
+ self._block(node.body)
498
+ elif isinstance(node, WhileStmt):
499
+ node.condition = self.expr(node.condition)
500
+ self._block(node.body)
501
+ elif isinstance(node, SwitchStmt):
502
+ if node.expr is not None:
503
+ node.expr = self.expr(node.expr)
504
+ node.cases = [(None if v is None else self.expr(v), b) for v, b in node.cases]
505
+ for _v, body in node.cases:
506
+ self._block(body)
507
+ self._block(node.default_body)
508
+ return node
509
+
510
+ def expr(self, node):
511
+ if node is None or not isinstance(node, ASTNode):
512
+ return node
513
+ if isinstance(node, (IfStmt, ForStmt, ForInStmt, WhileStmt, SwitchStmt)):
514
+ return self._block_node(node)
515
+ if isinstance(node, MemberAccess):
516
+ if isinstance(node.object, Identifier):
517
+ ns, member = node.object.name, node.member
518
+ if ns == "color" and member in _V5_COLORS:
519
+ return self._at(ColorLiteral(value=_V5_COLORS[member]), node)
520
+ if ns == "timeframe" and member == "period":
521
+ return self._v5_timeframe_period(node)
522
+ node.object = self.expr(node.object)
523
+ return node
524
+ if isinstance(node, Subscript):
525
+ self._check_history(node)
526
+ node.object = self.expr(node.object)
527
+ node.index = self.expr(node.index)
528
+ return node
529
+ if isinstance(node, FuncCall):
530
+ self._check_call(node)
531
+ node.callee = self.expr(node.callee) if not isinstance(node.callee, Identifier) else node.callee
532
+ replaced = {}
533
+ args = []
534
+ for a in node.args:
535
+ new = self.expr(a)
536
+ if new is not a:
537
+ replaced[id(a)] = new
538
+ args.append(new)
539
+ kwargs = {}
540
+ for k, a in node.kwargs.items():
541
+ new = self.expr(a)
542
+ if new is not a:
543
+ replaced[id(a)] = new
544
+ kwargs[k] = new
545
+ node.args, node.kwargs = args, kwargs
546
+ if replaced:
547
+ notes = node.annotations or {}
548
+ order = notes.get("call_arg_order")
549
+ if isinstance(order, ArgOrder):
550
+ node.annotations = dict(notes, call_arg_order=ArgOrder(
551
+ replaced.get(id(n), n) for n in order))
552
+ return node
553
+ if isinstance(node, BinOp):
554
+ node.left = self.expr(node.left)
555
+ node.right = self.expr(node.right)
556
+ if node.op == "/":
557
+ return self._division(node)
558
+ return node
559
+ if isinstance(node, UnaryOp):
560
+ node.operand = self.expr(node.operand)
561
+ return node
562
+ if isinstance(node, Ternary):
563
+ node.condition = self.expr(node.condition)
564
+ node.true_val = self.expr(node.true_val)
565
+ node.false_val = self.expr(node.false_val)
566
+ return node
567
+ if isinstance(node, TupleLiteral):
568
+ node.elements = [self.expr(e) for e in node.elements]
569
+ return node
570
+ return node
571
+
572
+ @staticmethod
573
+ def _at(new: ASTNode, like: ASTNode) -> ASTNode:
574
+ new.loc = like.loc
575
+ return new
576
+
577
+ def _division(self, node: BinOp):
578
+ (kl, ql), (kr, qr) = self.kind(node.left), self.kind(node.right)
579
+ if "float" in (kl, kr) or "na" in (kl, kr) or N in (ql, qr):
580
+ return node
581
+ if kl == kr == "int" and ql == qr == C:
582
+ # v5 divides two const ints as integers, truncating toward zero
583
+ # (TradingView: -7 / 2 = -3, 7 / -2 = -3); int() of v6's
584
+ # fractional quotient is that.
585
+ callee = self._at(Identifier(name="int"), node)
586
+ call = self._at(FuncCall(callee=callee, args=[node], kwargs={}), node)
587
+ return call
588
+ raise _Refusal(node, (
589
+ "v5 divides two const ints as integers, and whether both operands "
590
+ "of this division are const depends on its call (an untyped "
591
+ "parameter takes its argument's qualifier in v5); PineForge does "
592
+ "not specialize a v5 function per call"))
593
+
594
+ def _v5_timeframe_period(self, node: MemberAccess):
595
+ """``timeframe.period`` as v5 spells it: no multiplier of 1 for a
596
+ day, week or month (TradingView: "D" on a 1D chart)."""
597
+ def period():
598
+ return self._at(MemberAccess(
599
+ object=self._at(Identifier(name="timeframe"), node), member="period"), node)
600
+
601
+ result = period()
602
+ for v6, v5 in (("1M", "M"), ("1W", "W"), ("1D", "D")):
603
+ cond = self._at(BinOp(left=period(), op="==",
604
+ right=self._at(StringLiteral(value=v6), node)), node)
605
+ result = self._at(Ternary(condition=cond,
606
+ true_val=self._at(StringLiteral(value=v5), node),
607
+ false_val=result), node)
608
+ return result
609
+
610
+ def _check_history(self, node: Subscript) -> None:
611
+ obj = node.object
612
+ if isinstance(obj, (NumberLiteral, StringLiteral, BoolLiteral, ColorLiteral, NaLiteral)):
613
+ raise _Refusal(node, "[] on a literal reads its history, which v5 "
614
+ "allows and PineForge does not implement")
615
+ if isinstance(obj, MemberAccess) and isinstance(obj.object, Identifier):
616
+ if obj.object.name in _CONSTANT_NAMESPACES:
617
+ raise _Refusal(node, "[] on a built-in constant reads its history, "
618
+ "which v5 allows and PineForge does not implement")
619
+ if obj.object.name not in _NAMESPACES:
620
+ raise _Refusal(node, "[] on a user-defined type's field reads its "
621
+ "history directly, which v5 allows and "
622
+ "PineForge does not implement")
623
+ elif isinstance(obj, MemberAccess):
624
+ raise _Refusal(node, "[] on a user-defined type's field reads its "
625
+ "history directly, which v5 allows and PineForge "
626
+ "does not implement")
627
+
628
+ def _check_call(self, call: FuncCall) -> None:
629
+ callee = call.callee
630
+ if not (isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier)):
631
+ return
632
+ ns, member = callee.object.name, callee.member
633
+ if ns == "request":
634
+ raise _Refusal(call, f"request.{member}() in a v5 library runs under "
635
+ "v5's non-dynamic request rules, which PineForge "
636
+ "does not implement")
637
+ length_params: set[int | str] = set()
638
+ if ns == "ta" and member in sigs.TA_FUNCTIONS:
639
+ for sig in sigs.TA_FUNCTIONS[member].signatures:
640
+ for i, p in enumerate(sig.params):
641
+ if p.pine_type == sigs.I:
642
+ length_params.update((i, p.name))
643
+ elif ns == "math" and member == "sum":
644
+ length_params.update((1, "length"))
645
+ if not length_params:
646
+ return
647
+ bound = [(i, a) for i, a in enumerate(call.args)] + list(call.kwargs.items())
648
+ for key, arg in bound:
649
+ if key in length_params and self._reads_reassigned(arg):
650
+ raise _Refusal(arg, (
651
+ f"a reassigned variable passed as {ns}.{member}()'s length: "
652
+ "v5 marks it const and runs the call at its first value, "
653
+ "which PineForge does not implement"))
654
+
655
+ def _reads_reassigned(self, expr) -> bool:
656
+ fn = self.fn
657
+ return any(
658
+ isinstance(node, Identifier)
659
+ and ((fn is not None and node.name in fn.reassigned)
660
+ or node.name in self.module.reassigned)
661
+ for node in _walk(expr))
662
+
663
+
664
+ def lower_v5_modules(modules: list[tuple[LibraryModule, list, dict[str, str]]],
665
+ filename: str) -> None:
666
+ """Lower the reachable definitions of every v5 module in ``modules``
667
+ (library, its inlined definitions in source order, its top-level renames)
668
+ with v5's rules, refusing by the library's name what PineForge does not
669
+ implement."""
670
+ for lib, definitions, names in modules:
671
+ if lib.pine_version != 5:
672
+ continue
673
+ module = _V5Module(lib, definitions, names)
674
+ for node in definitions:
675
+ try:
676
+ _Lowering(module, node).lower()
677
+ except _Refusal as exc:
678
+ loc = getattr(exc.node, "loc", None) or SourceLocation(
679
+ file=lib.path, line=1, col=1, end_col=1)
680
+ raise CompileError([Diagnostic(
681
+ level=Level.ERROR, phase=Phase.PARSER, location=loc,
682
+ message=f"library '{lib.path}' is //@version=5: {exc}",
683
+ )]) from None