@pineforge/codegen-pyodide 0.10.4 → 1.0.0

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 (50) 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 +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  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 +896 -156
  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 +946 -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 +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  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-1.0.0.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.tar.gz +0 -0
@@ -47,7 +47,7 @@ TA_CLASS_MAP = {
47
47
  "dmi": "ta::DMI",
48
48
  "sar": "ta::SAR",
49
49
  "bb": "ta::BB",
50
- "kc": "ta::KC",
50
+ "kc": "_PFKC",
51
51
  "wma": "ta::WMA",
52
52
  "hma": "ta::HMA",
53
53
  "stdev": "ta::StdDev",
@@ -72,7 +72,7 @@ TA_CLASS_MAP = {
72
72
  "highestbars": "ta::HighestBars",
73
73
  "lowestbars": "ta::LowestBars",
74
74
  # Batch 4 — remaining TA functions
75
- "alma": "ta::ALMA",
75
+ "alma": "_PFALMA",
76
76
  "swma": "ta::SWMA",
77
77
  "mfi": "ta::MFI",
78
78
  "cmo": "ta::CMO",
@@ -80,7 +80,7 @@ TA_CLASS_MAP = {
80
80
  "wpr": "ta::WPR",
81
81
  "cog": "ta::COG",
82
82
  "bbw": "ta::BBW",
83
- "kcw": "ta::KCW",
83
+ "kcw": "_PFKCW",
84
84
  "barssince": "ta::BarsSince",
85
85
  "valuewhen": "ta::ValueWhen",
86
86
  "correlation": "ta::Correlation",
@@ -90,6 +90,8 @@ TA_CLASS_MAP = {
90
90
  "vwap": "ta::VWAP",
91
91
  # 3-arg form: ta.vwap(source, anchor, stdev_mult) → tuple [vwap, upper, lower]
92
92
  "vwap_bands": "ta::VWAPBands",
93
+ "vwap_anchored": "_PFAnchoredVWAP",
94
+ "vwap_anchored_bands": "_PFAnchoredVWAPBands",
93
95
  "obv": "ta::OBV",
94
96
  "accdist": "ta::AccDist",
95
97
  "nvi": "ta::NVI",
@@ -126,10 +128,14 @@ TA_PERIOD_ARG = {
126
128
  # Task 5
127
129
  "mode": 1, "range": 1, "dev": 1,
128
130
  "rci": 1,
131
+ # ta::ValueWhen keeps max(1, max_occurrence + 1) values, its constructor's
132
+ # bound; compute() reads values_[occurrence]. See TA_COMPUTE_ARGS.
133
+ "valuewhen": 2,
129
134
  }
130
135
 
131
136
  # Functions that return tuples
132
- TA_TUPLE_RETURNS = {"macd", "supertrend", "dmi", "bb", "kc", "vwap_bands"}
137
+ TA_TUPLE_RETURNS = {"macd", "supertrend", "dmi", "bb", "kc", "vwap_bands",
138
+ "vwap_anchored_bands"}
133
139
  TA_TUPLE_ELEMENT_COUNTS = {
134
140
  "macd": 3,
135
141
  "supertrend": 2,
@@ -137,6 +143,7 @@ TA_TUPLE_ELEMENT_COUNTS = {
137
143
  "bb": 3,
138
144
  "kc": 3,
139
145
  "vwap_bands": 3,
146
+ "vwap_anchored_bands": 3,
140
147
  }
141
148
 
142
149
  # Functions with multiple constructor args
@@ -146,12 +153,12 @@ TA_MULTI_CTOR = {
146
153
  "supertrend": [0, 1], # factor, atr_period
147
154
  "dmi": [0, 1], # di_length, adx_smoothing
148
155
  "bb": [1, 2], # length, mult
149
- "kc": [1, 2], # length, mult
156
+ "kc": [1, 2, 3], # length, mult, useTrueRange
150
157
  "sar": [0, 1, 2], # start, increment, maximum
151
158
  "pivothigh": [0, 1], # left_bars, right_bars
152
159
  "pivotlow": [0, 1], # left_bars, right_bars
153
160
  # Batch 4
154
- "alma": [1, 2, 3], # length, offset, sigma
161
+ "alma": [1, 2, 3, 4], # length, offset, sigma, floor
155
162
  "mfi": [1], # length (src is compute arg, vol implicit)
156
163
  # Pine signature: ta.tsi(source, short_length, long_length) — positions
157
164
  # 1 and 2 carry the lengths that initialize the four nested EMAs;
@@ -162,7 +169,8 @@ TA_MULTI_CTOR = {
162
169
  "tsi": [1, 2], # short_length, long_length
163
170
  "wpr": [0], # length
164
171
  "bbw": [1, 2], # length, mult
165
- "kcw": [1, 2], # length, mult
172
+ "kcw": [1, 2, 3], # length, mult, useTrueRange
173
+ "vwap_anchored_bands": [2], # source, anchor, stdev_mult
166
174
  "tr": [0], # handle_na (compile-time bool)
167
175
  "stdev": [1, 2], # length, biased
168
176
  "variance": [1, 2], # length, biased
@@ -174,10 +182,43 @@ TA_MULTI_CTOR = {
174
182
  TA_COMPUTE_ARGS = {
175
183
  "stdev": [0],
176
184
  "variance": [0],
185
+ # ta::Change's constructor only bounds the kept history; compute() reads
186
+ # the lookback from its own ``length`` argument (default 1), so the length
187
+ # goes to both. Without it ``ta.change(src, 14)`` was a one-bar change.
188
+ "change": [0, 1],
189
+ # The mirror image: compute(condition, source, occurrence) always had the
190
+ # occurrence, but the history bound is the constructor's max_occurrence
191
+ # (default 1, two values kept), so every occurrence >= 2 read na.
192
+ "valuewhen": [0, 1, 2],
193
+ # ``ta.vwap(source, anchor)`` is split into a dedicated anchored site in
194
+ # call_handlers; this entry remains for the omitted-anchor scalar form.
195
+ "vwap": [0],
196
+ # ``ta.alma(series, length, offset, sigma, floor)``: ta::ALMA takes no
197
+ # floor (m = offset * (length - 1), unfloored), the value the support
198
+ # checker admits (false); ``floor`` used to reach ALMA::compute(src).
199
+ "alma": [0],
200
+ # ``ta.kc`` / ``ta.kcw(series, length, mult, useTrueRange)``: ta::KC
201
+ # always averages the true range, the value the support checker admits
202
+ # (true); ``useTrueRange`` used to reach a compute() overload that does
203
+ # not exist. high / low / close are appended implicitly.
204
+ "kc": [0],
205
+ "kcw": [0],
206
+ "vwap_anchored": [0, 1],
207
+ "vwap_anchored_bands": [0, 1],
208
+ }
209
+
210
+ # The one-argument forms ``ta.highest(length)`` / ``ta.lowest(length)`` /
211
+ # ``ta.highestbars(length)`` / ``ta.lowestbars(length)`` (positional or
212
+ # ``length=``) read this bar field as their source (TradingView: "One arg
213
+ # version: length is the number of bars back. Algorithm uses high as a source
214
+ # series.").
215
+ TA_LENGTH_ONLY_DEFAULT_SOURCE = {
216
+ "highest": "high", "lowest": "low",
217
+ "highestbars": "high", "lowestbars": "low",
177
218
  }
178
219
 
179
220
  # No-state functions (no constructor args, stateless or self-contained)
180
- TA_NO_CTOR = {"crossover", "crossunder", "cross", "cum", "swma", "barssince", "valuewhen",
221
+ TA_NO_CTOR = {"crossover", "crossunder", "cross", "cum", "swma", "barssince",
181
222
  "max", "min",
182
223
  "obv", "accdist", "nvi", "pvi", "pvt", "wad", "wvad", "iii", "vwap"}
183
224
 
@@ -40,10 +40,11 @@ from __future__ import annotations
40
40
  from typing import Any
41
41
 
42
42
  from ..ast_nodes import (
43
- ASTNode, BinOp, BoolLiteral, ExprStmt, FuncCall, Identifier, IfStmt,
43
+ ASTNode, BinOp, BoolLiteral, ColorLiteral, ExprStmt, FuncCall, Identifier, IfStmt,
44
44
  MemberAccess, NaLiteral, NumberLiteral, StringLiteral, Subscript, Ternary,
45
45
  SwitchStmt, TupleLiteral, UnaryOp,
46
46
  )
47
+ from ..errors import Phase
47
48
  from ..symbols import PineType, TypeSpec, method_receiver_type_name
48
49
 
49
50
  # Drawing-objects-as-data type names (spec §4.1). Defined locally — the
@@ -185,6 +186,10 @@ class TypeHelper:
185
186
  if left.name == "int" and right.name == "int":
186
187
  return TypeSpec.primitive("int")
187
188
  return None
189
+ if self._is_color_constructor(value):
190
+ # A packed-ARGB color, as codegen's ``_is_color_value`` reads it:
191
+ # the declaration and the constructor agree on ``int64_t``.
192
+ return TypeSpec.primitive("color")
188
193
  spec = self._type_spec_from_expr(value)
189
194
  if spec is not None:
190
195
  return spec
@@ -197,6 +202,26 @@ class TypeHelper:
197
202
  return self._pine_type_to_spec(sym.pine_type)
198
203
  return None
199
204
 
205
+ @classmethod
206
+ def _is_color_constructor(cls, value: ASTNode | None) -> bool:
207
+ """A color literal, ``color.<name>``, a ``color.new`` / ``rgb`` /
208
+ ``from_gradient`` call, or a conditional selecting one."""
209
+ if isinstance(value, ColorLiteral):
210
+ return True
211
+ if isinstance(value, MemberAccess):
212
+ return (isinstance(value.object, Identifier)
213
+ and value.object.name == "color")
214
+ if isinstance(value, FuncCall):
215
+ callee = value.callee
216
+ return (isinstance(callee, MemberAccess)
217
+ and isinstance(callee.object, Identifier)
218
+ and callee.object.name == "color"
219
+ and callee.member in ("new", "rgb", "from_gradient"))
220
+ if isinstance(value, Ternary):
221
+ return (cls._is_color_constructor(value.true_val)
222
+ or cls._is_color_constructor(value.false_val))
223
+ return False
224
+
200
225
  @staticmethod
201
226
  def _selection_terminal_expr(
202
227
  body: list[ASTNode] | None,
@@ -241,6 +266,13 @@ class TypeHelper:
241
266
  def _type_spec_from_expr(self, value: ASTNode | None) -> TypeSpec | None:
242
267
  if value is None:
243
268
  return None
269
+ budget = getattr(self, "_budget", None)
270
+ if budget is not None:
271
+ # A method chain re-infers each receiver more than once, so this
272
+ # recursion can outgrow the visitor checkpoints.
273
+ self._budget_visit_count += 1
274
+ if self._budget_visit_count % 128 == 0:
275
+ budget.check(value.loc, Phase.ANALYZER)
244
276
  if isinstance(value, NumberLiteral):
245
277
  return TypeSpec.primitive(
246
278
  "float" if isinstance(value.value, float) else "int"
@@ -26,6 +26,28 @@ class ASTNode:
26
26
  annotations: dict | None = field(default=None, compare=False)
27
27
 
28
28
 
29
+ class ArgOrder:
30
+ """The written order of a call's positional and keyword arguments.
31
+
32
+ It holds the same nodes as the call's ``args`` and ``kwargs``. It is not a
33
+ list, tuple or dict and has no ``__dict__``, so the generic AST walkers,
34
+ which descend into those, do not visit every argument a second time: the
35
+ second visit doubled the work at each nested call, and ``f(f(f(...)))``
36
+ cost 2**depth.
37
+ """
38
+
39
+ __slots__ = ("nodes",)
40
+
41
+ def __init__(self, nodes) -> None:
42
+ self.nodes = tuple(nodes)
43
+
44
+ def __iter__(self):
45
+ return iter(self.nodes)
46
+
47
+ def __len__(self) -> int:
48
+ return len(self.nodes)
49
+
50
+
29
51
  # ---------------------------------------------------------------------------
30
52
  # Top-level / structural nodes
31
53
  # ---------------------------------------------------------------------------
@@ -44,8 +66,17 @@ class StrategyDecl(ASTNode):
44
66
 
45
67
  @dataclass
46
68
  class ImportStmt(ASTNode):
47
- """import <path> — parsed for error reporting."""
69
+ """``import <user>/<name>/<version> [as <alias>]``.
70
+
71
+ ``path`` is ``user/name/version``; ``user``, ``name``, ``version`` and
72
+ ``alias`` stay None when the line does not have that form, and ``path``
73
+ is then the line's tokens as written.
74
+ """
48
75
  path: str = ""
76
+ user: str | None = None
77
+ name: str | None = None
78
+ version: int | None = None
79
+ alias: str | None = None
49
80
 
50
81
 
51
82
  # ---------------------------------------------------------------------------
@@ -0,0 +1,185 @@
1
+ """Declarations in the script's top-level blocks that cannot share a member.
2
+
3
+ The codegen holds every declaration of the script's top level -- a direct one
4
+ and one inside a top-level ``if`` / loop / ``switch`` block -- in one class
5
+ member per name. Pine scopes a block's declaration to its block, so two blocks
6
+ may declare one name with types one member cannot hold: a counter ``k = 0`` in
7
+ one block and ``k = array.get(names, i)`` (a string) in another, a script of
8
+ the fruit-fly probes' shape, did not compile. The codegen names each
9
+ declaration whose type cannot share the member of the name
10
+ (``CodeGen.block_locals_needing_names``, by ``decl_key``); ``transpile()``
11
+ gives each of them, and every read of it in its block, a name of its own
12
+ (``k__pfblk1``) and runs again (``pineforge_codegen._generate``). Scripts
13
+ whose declarations share their members keep their C++ byte for byte.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from .ast_nodes import (
19
+ ASTNode, ArgOrder, Assignment, ExprStmt, ForInStmt, ForStmt, FuncCall,
20
+ Identifier, IfStmt, Program, SwitchStmt, TupleAssign, VarDecl, WhileStmt,
21
+ )
22
+
23
+ _BLOCKS = (IfStmt, ForStmt, ForInStmt, WhileStmt, SwitchStmt)
24
+
25
+
26
+ def decl_key(node: VarDecl) -> tuple | None:
27
+ """A declaration's identity across the passes of one transpile: where
28
+ it is written."""
29
+ loc = node.loc
30
+ return None if loc is None else (loc.file, loc.line, loc.col)
31
+
32
+
33
+ def block_declarations(body: list):
34
+ """(declaration, in a block) for every declaration the codegen holds in a
35
+ class member of the script's top level, in source order: the direct ones
36
+ and those in its statement blocks (``if`` / loop / ``switch``, nested
37
+ too), as ``CodeGen`` hoists them. An ``if`` or ``switch`` value's block
38
+ keeps its declarations to itself."""
39
+ for stmt in body:
40
+ yield from _declarations(stmt, False)
41
+
42
+
43
+ def _declarations(node, nested: bool):
44
+ if isinstance(node, VarDecl):
45
+ yield node, nested
46
+ elif isinstance(node, IfStmt):
47
+ for stmt in (*node.body, *node.else_body):
48
+ yield from _declarations(stmt, True)
49
+ elif isinstance(node, (ForStmt, ForInStmt, WhileStmt)):
50
+ for stmt in node.body:
51
+ yield from _declarations(stmt, True)
52
+ elif isinstance(node, SwitchStmt):
53
+ for _value, body in node.cases:
54
+ for stmt in body:
55
+ yield from _declarations(stmt, True)
56
+ for stmt in node.default_body:
57
+ yield from _declarations(stmt, True)
58
+
59
+
60
+ def rename_block_locals(program: Program, keys: frozenset) -> None:
61
+ """Give each top-level block declaration ``keys`` names a name of its own,
62
+ and every read of it in its block (a nested block's declaration of the
63
+ same name, or a loop variable of it, shadows it there)."""
64
+ if not keys:
65
+ return
66
+ renamer = _Renamer(program, keys)
67
+ renamer.block(program.body, {})
68
+
69
+
70
+ class _Renamer:
71
+ def __init__(self, program: Program, keys: frozenset) -> None:
72
+ self._keys = keys
73
+ self._taken = _names(program)
74
+ self._next = 0
75
+
76
+ def _alloc(self, name: str) -> str:
77
+ while True:
78
+ self._next += 1
79
+ new = f"{name}__pfblk{self._next}"
80
+ if new not in self._taken:
81
+ self._taken.add(new)
82
+ return new
83
+
84
+ def block(self, stmts: list, visible: dict) -> None:
85
+ names = dict(visible)
86
+ for stmt in stmts:
87
+ self.stmt(stmt, names)
88
+
89
+ def stmt(self, stmt, names: dict) -> None:
90
+ if isinstance(stmt, VarDecl):
91
+ self.expr(stmt.value, names)
92
+ if decl_key(stmt) in self._keys:
93
+ new = self._alloc(stmt.name)
94
+ names[stmt.name] = new
95
+ stmt.name = new
96
+ else:
97
+ names.pop(stmt.name, None)
98
+ elif isinstance(stmt, TupleAssign):
99
+ self.expr(stmt.value, names)
100
+ for bound in stmt.names:
101
+ names.pop(bound, None)
102
+ elif isinstance(stmt, Assignment):
103
+ self.expr(stmt.target, names)
104
+ self.expr(stmt.value, names)
105
+ elif isinstance(stmt, ExprStmt):
106
+ self.expr(stmt.expr, names)
107
+ elif isinstance(stmt, _BLOCKS):
108
+ self.block_node(stmt, names)
109
+
110
+ def block_node(self, node, names: dict) -> None:
111
+ if isinstance(node, IfStmt):
112
+ self.expr(node.condition, names)
113
+ self.block(node.body, names)
114
+ self.block(node.else_body, names)
115
+ elif isinstance(node, ForStmt):
116
+ for part in (node.start, node.end, node.step):
117
+ self.expr(part, names)
118
+ inner = dict(names)
119
+ inner.pop(node.var, None)
120
+ self.block(node.body, inner)
121
+ elif isinstance(node, ForInStmt):
122
+ self.expr(node.iterable, names)
123
+ inner = dict(names)
124
+ for bound in [node.var, *(node.vars or ())]:
125
+ inner.pop(bound, None)
126
+ self.block(node.body, inner)
127
+ elif isinstance(node, WhileStmt):
128
+ self.expr(node.condition, names)
129
+ self.block(node.body, names)
130
+ elif isinstance(node, SwitchStmt):
131
+ self.expr(node.expr, names)
132
+ for value, body in node.cases:
133
+ self.expr(value, names)
134
+ self.block(body, names)
135
+ self.block(node.default_body, names)
136
+
137
+ def expr(self, node, names: dict) -> None:
138
+ if not names or node is None:
139
+ return
140
+ if isinstance(node, list):
141
+ for item in node:
142
+ self.expr(item, names)
143
+ return
144
+ if not isinstance(node, ASTNode):
145
+ return
146
+ if isinstance(node, Identifier):
147
+ node.name = names.get(node.name, node.name)
148
+ return
149
+ if isinstance(node, _BLOCKS):
150
+ self.block_node(node, names)
151
+ return
152
+ for key, value in vars(node).items():
153
+ if key in ("loc", "annotations"):
154
+ continue
155
+ if key == "callee" and isinstance(node, FuncCall) and isinstance(value, Identifier):
156
+ continue # a function's name, not a variable's
157
+ if isinstance(value, dict):
158
+ self.expr(list(value.values()), names)
159
+ elif isinstance(value, (ASTNode, list)):
160
+ self.expr(value, names)
161
+
162
+
163
+ def _names(root) -> set[str]:
164
+ """Every identifier and declared name ``root`` spells."""
165
+ found: set[str] = set()
166
+ stack = [root]
167
+ while stack:
168
+ node = stack.pop()
169
+ if isinstance(node, (list, tuple)):
170
+ stack.extend(node)
171
+ elif isinstance(node, dict):
172
+ stack.extend(node.values())
173
+ elif isinstance(node, ArgOrder):
174
+ continue
175
+ elif isinstance(node, ASTNode):
176
+ for key, value in vars(node).items():
177
+ if key in ("loc", "annotations"):
178
+ continue
179
+ if isinstance(value, str) and key in ("name", "var"):
180
+ found.add(value)
181
+ elif key in ("params", "names", "vars") and isinstance(value, list):
182
+ found.update(v for v in value if isinstance(v, str))
183
+ else:
184
+ stack.append(value)
185
+ return found
@@ -0,0 +1,42 @@
1
+ """Bind the keyword arguments of ``nz`` and ``fixnan`` to their positions.
2
+
3
+ TradingView names the parameters ``nz(source, replacement)`` and
4
+ ``fixnan(source)``, and evaluates a call's arguments in parameter order
5
+ whatever order they are written in (lab tv probes pf-oi-kw-names and
6
+ pf-oi-kw-order: ``nz(replacement = r(), source = s())`` runs ``s`` first).
7
+ The analyzer and the codegen read these calls by position, so a keyword
8
+ form reached them as a call without its source (``nz(source = x)`` and
9
+ ``fixnan(source = x)`` raised IndexError) or with its replacement dropped
10
+ (``nz(x, replacement = y)`` read 0). The support checker refuses a call
11
+ whose arguments bind to none of TradingView's signatures; this pass then
12
+ rewrites every call that binds with its arguments in parameter order.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from .ast_nodes import FuncCall, Identifier, Program
18
+ from .limits import iter_ast_nodes
19
+ from . import signatures as sigs
20
+
21
+ POSITIONAL_BUILTINS = ("nz", "fixnan")
22
+
23
+
24
+ def bind_builtin_keywords(program: Program) -> Program:
25
+ for node, _depth in iter_ast_nodes(program):
26
+ if (not isinstance(node, FuncCall) or not node.kwargs
27
+ or not isinstance(node.callee, Identifier)
28
+ or node.callee.name not in POSITIONAL_BUILTINS):
29
+ continue
30
+ names = sigs.BUILTIN_FUNCTIONS[node.callee.name].param_names
31
+ if not set(node.kwargs) <= set(names[len(node.args):]):
32
+ continue # unbound: the support checker refused it
33
+ bound = list(node.args)
34
+ for name in names[len(node.args):]:
35
+ if name not in node.kwargs:
36
+ break
37
+ bound.append(node.kwargs[name])
38
+ if len(bound) != len(node.args) + len(node.kwargs):
39
+ continue # a hole before a later keyword: left as written
40
+ node.args = bound
41
+ node.kwargs = {}
42
+ return program