@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
@@ -11,11 +11,14 @@ Mixin contract — host class must provide:
11
11
  - ``self._resolve_callee`` (``NamingHelper``).
12
12
  - ``self._visit_expr`` (visitor mixin, currently on ``base.py``;
13
13
  used to render the title-arg fallback expression).
14
+ - ``self._cpp_string_escape`` (``NamingHelper``).
14
15
  """
15
16
 
16
17
  from __future__ import annotations
17
18
 
18
19
  from ..ast_nodes import (
20
+ ASTNode,
21
+ BinOp,
19
22
  BoolLiteral,
20
23
  FuncCall,
21
24
  Identifier,
@@ -25,6 +28,7 @@ from ..ast_nodes import (
25
28
  VarDecl,
26
29
  )
27
30
  from .. import signatures as sigs
31
+ from ..pine_spelling import expr_start, global_input_calls, input_binding_names
28
32
 
29
33
 
30
34
  class InputHelper:
@@ -89,14 +93,23 @@ class InputHelper:
89
93
  return node.kwargs["defval"]
90
94
  return None
91
95
 
92
- def _get_input_title(self, node: FuncCall, var_name: str | None = None) -> str:
93
- """Pull the title string from an ``input(...)`` call.
94
-
95
- Reads positional arg #2 or the ``title=`` kwarg via the
96
- signatures registry. When the title isn't a ``StringLiteral``
97
- (e.g. computed at runtime), the rendered C++ expression is
98
- returned. Falls back to ``var_name`` when no title is provided
99
- so generated UI labels still have a stable identity."""
96
+ def _input_default_value(self, node: FuncCall) -> tuple[bool, object]:
97
+ """``(True, value)`` when the defval is the constant a ``v = input...``
98
+ binding records as ``v``'s known value -- a number, bool or string
99
+ literal, or a declared enum member (its index) -- else ``(False, None)``.
100
+ Only such an input is a stable scalar the TA reset can re-read."""
101
+ default = self._get_input_default(node)
102
+ if isinstance(default, (NumberLiteral, BoolLiteral, StringLiteral)):
103
+ return True, default.value
104
+ if isinstance(default, MemberAccess) and isinstance(default.object, Identifier):
105
+ members = self._enum_defs.get(default.object.name)
106
+ if members is not None and default.member in members:
107
+ return True, members.index(default.member)
108
+ return False, None
109
+
110
+ def _input_title_node(self, node: FuncCall):
111
+ """The title argument of an ``input(...)`` call -- positional arg #2 or
112
+ the ``title=`` kwarg, via the signatures registry -- or None."""
100
113
  func_name, namespace = self._resolve_callee(node.callee)
101
114
  param_names = None
102
115
  if namespace == "input" and func_name in sigs.INPUT_FUNCTIONS:
@@ -112,12 +125,127 @@ class InputHelper:
112
125
  if i >= len(merged) or merged[i] is None:
113
126
  merged[i] = node.kwargs[pname]
114
127
  if len(merged) > 1 and merged[1] is not None:
115
- title_node = merged[1]
116
- if isinstance(title_node, StringLiteral):
117
- return title_node.value
118
- return self._visit_expr(title_node)
128
+ return merged[1]
129
+ return None
130
+
131
+ def _input_binding_names(self) -> dict[int, str]:
132
+ """``id(call) -> name`` for the global-scope input calls a declaration
133
+ names (``pine_spelling.input_binding_names``)."""
134
+ names = getattr(self, "_input_binding_names_cache", None)
135
+ if names is None:
136
+ names = input_binding_names(self.ctx.ast.body)
137
+ self._input_binding_names_cache = names
138
+ return names
139
+
140
+ def _get_input_title(self, node: FuncCall, var_name: str | None = None) -> str:
141
+ """The key an ``input(...)`` call is read and listed by: its title's
142
+ string value, else the name of the declaration binding it, else
143
+ ``var_name``, else "".
144
+
145
+ The binding name comes from the call node itself, so every getter of
146
+ one input -- the member, the TA reset, a request.security timeframe,
147
+ an alias's read (``b = a``) -- keys it like the manifest does, whatever
148
+ name the caller passes. A title that is not a compile-time string
149
+ constant is refused (``_input_title_value``)."""
150
+ title_node = self._input_title_node(node)
151
+ if title_node is not None:
152
+ title = self._input_title_value(title_node)
153
+ if title is None:
154
+ self._refuse_input_title(title_node)
155
+ return title
156
+ bound = self._input_binding_names().get(id(node))
157
+ if bound is not None:
158
+ return bound
119
159
  return var_name if var_name else ""
120
160
 
161
+ def _input_title_value(self, node, _seen: frozenset = frozenset()) -> str | None:
162
+ """A title expression's compile-time string value, or None. TradingView
163
+ takes ``title (const string)``: a string literal, a ``+`` of constant
164
+ strings, or a name bound once at global scope, never reassigned and not
165
+ ``var``, to one (``ctx.global_expr_map``)."""
166
+ if isinstance(node, StringLiteral):
167
+ return node.value
168
+ if isinstance(node, BinOp) and node.op == "+":
169
+ left = self._input_title_value(node.left, _seen)
170
+ right = self._input_title_value(node.right, _seen)
171
+ return None if left is None or right is None else left + right
172
+ if (isinstance(node, Identifier) and node.name not in _seen
173
+ and not self._known_var_is_lexically_shadowed(node.name)):
174
+ value = (getattr(self.ctx, "global_expr_map", None) or {}).get(node.name)
175
+ if value is not None:
176
+ return self._input_title_value(value, _seen | {node.name})
177
+ return None
178
+
179
+ def _refuse_input_title(self, title_node) -> None:
180
+ spelled = f"'{title_node.name}' " if isinstance(title_node, Identifier) else ""
181
+ self._codegen_error(
182
+ expr_start(title_node),
183
+ f"input title {spelled}is not a constant string: TradingView declares "
184
+ "title (const string), and PineForge keys an input override by its title.",
185
+ hint='Use a string literal, or a name bound once at global scope to one '
186
+ '(T = "Length").',
187
+ )
188
+
189
+ def _check_input_titles(self) -> None:
190
+ """Refuse the first input call anywhere in the script whose title is not
191
+ a compile-time string constant, before any getter keys it: its C++ text
192
+ used to become the key (``std::string("Len")``)."""
193
+ stack: list = [self.ctx.ast]
194
+ while stack:
195
+ node = stack.pop()
196
+ if isinstance(node, (list, tuple)):
197
+ stack.extend(reversed(node))
198
+ continue
199
+ if isinstance(node, dict):
200
+ stack.extend(reversed(list(node.values())))
201
+ continue
202
+ if not isinstance(node, ASTNode):
203
+ continue
204
+ if isinstance(node, FuncCall) and self._is_input_call(node):
205
+ title_node = self._input_title_node(node)
206
+ if title_node is not None and self._input_title_value(title_node) is None:
207
+ self._refuse_input_title(title_node)
208
+ stack.extend(reversed([v for k, v in vars(node).items()
209
+ if k not in ("loc", "annotations")]))
210
+
211
+ def _check_input_keys(self) -> None:
212
+ """Warn about inputs of the script that share an override key.
213
+
214
+ An override reaches every input keyed by its title, else by the name of
215
+ the declaration holding the call (``_get_input_title``, the manifest's
216
+ ``title``), so inputs that share one cannot be overridden apart: two
217
+ untitled calls in one declaration (``n = input.int(9) +
218
+ input.int(3)``), two outside any declaration (keyed ""), an untitled
219
+ call and another input titled with its name, or one title repeated
220
+ across input groups (TradingView tells them apart; PineForge's
221
+ override protocol does not). The script still transpiles unchanged --
222
+ at its defaults every input reads its own default -- and one warning
223
+ per shared key names every input that key reaches.
224
+ """
225
+ calls_by_key: dict[str, list[FuncCall]] = {}
226
+ for node, name in self._global_input_calls_with_names():
227
+ calls_by_key.setdefault(self._get_input_title(node, var_name=name), []).append(node)
228
+ for key, calls in calls_by_key.items():
229
+ if len(calls) < 2:
230
+ continue
231
+ spots = [f"{loc.line}:{loc.col}" for loc in (expr_start(c).loc for c in calls)]
232
+ listed = f"{', '.join(spots[:-1])} and {spots[-1]}"
233
+ self._codegen_warning(
234
+ expr_start(calls[1]),
235
+ f"input key '{key}' is shared by the inputs at {listed}: PineForge "
236
+ "sets an input override by its title, else by the name of the "
237
+ "declaration holding it, so one override sets all of them.",
238
+ hint="Give each input its own title to override them apart.",
239
+ )
240
+
241
+ def _input_spelling_title(self, node: FuncCall) -> str | None:
242
+ """The key an untitled call's re-spelling must carry as ``title=``:
243
+ its binding name. A spelled call is re-parsed into a new node, which
244
+ no declaration names."""
245
+ if self._input_title_node(node) is not None:
246
+ return None
247
+ return self._input_binding_names().get(id(node))
248
+
121
249
  # Native price series accepted as an input.source defval. The analyzer
122
250
  # hard-rejects anything else (see support_checker), so a defval reaching
123
251
  # codegen is one of these or a user-series identifier.
@@ -202,16 +330,33 @@ class InputHelper:
202
330
  if self._is_source_input(node):
203
331
  default = self._get_input_default(node)
204
332
  base = self._source_defval_to_base_series(default)
205
- return f'get_input_source("{title}", {base})[0]'
333
+ return f'get_input_source({self._input_key_literal(title)}, {base})[0]'
206
334
  default = self._get_input_default(node)
207
335
  default_cpp = self._visit_expr(default) if default is not None else "0"
336
+ getter = self._input_getter_for_call(node, func_name, namespace)
337
+ default_cpp = self._coerce_string_input_default(getter, default_cpp)
338
+ return f'{getter}({self._input_key_literal(title)}, {default_cpp})'
339
+
340
+ _GETTER_CPP_TYPE = {
341
+ "get_input_bool": "bool", "get_input_int": "int",
342
+ "get_input_string": "std::string", "get_input_double": "double",
343
+ }
344
+
345
+ def _input_getter_for_call(self, node: FuncCall, func_name: str | None,
346
+ namespace: str | None) -> str:
347
+ """The scalar getter an ``input`` call reads through.
348
+
349
+ The generic ``input(...)`` overload is typed by its defval in Pine
350
+ v6: ``input("x")`` is a string input, ``input(3)`` an int one and
351
+ ``input(true)`` a bool one (lab tv tape te_generic_input: its
352
+ ``str.tostring(n / 2)`` reads "1.5" and its string input spells the
353
+ order comments). The static getter table cannot see the default, so
354
+ the getter follows the default's literal type here. This matters for
355
+ TA lengths too: ``input(15)`` must route to ``get_input_int`` so the
356
+ RMA/EMA ctor receives an int."""
208
357
  getter = self._input_type_to_getter(func_name, namespace)
209
- # The generic ``input(...)`` overload is typed by its defval in Pine
210
- # v6 (an int default yields an int input). The static getter table
211
- # cannot see the default, so infer the getter from the default's
212
- # literal type here. This matters for TA lengths: ``input(15)`` must
213
- # route to ``get_input_int`` so the RMA/EMA ctor receives an int.
214
358
  if func_name == "input" and namespace is None:
359
+ default = self._get_input_default(node)
215
360
  if isinstance(default, BoolLiteral):
216
361
  getter = "get_input_bool"
217
362
  elif isinstance(default, NumberLiteral):
@@ -223,8 +368,22 @@ class InputHelper:
223
368
  getter = "get_input_double"
224
369
  elif isinstance(default, StringLiteral):
225
370
  getter = "get_input_string"
226
- default_cpp = self._coerce_string_input_default(getter, default_cpp)
227
- return f'{getter}("{title}", {default_cpp})'
371
+ return getter
372
+
373
+ def _generic_input_cpp_type(self, node: FuncCall) -> str:
374
+ """The C++ storage of a generic ``input(...)`` value: its getter's
375
+ type (``_input_getter_for_call``), a source input's double."""
376
+ if self._is_source_input(node):
377
+ return "double"
378
+ return self._GETTER_CPP_TYPE[self._input_getter_for_call(node, "input", None)]
379
+
380
+ def _input_key_literal(self, title: str) -> str:
381
+ """``title`` as the C++ string literal the input getters key it by.
382
+
383
+ A title is arbitrary Pine string text; pasted verbatim, a quote in it
384
+ ends the literal early and a backslash escapes the next character, so
385
+ the TU no longer compiles."""
386
+ return f'"{self._cpp_string_escape(title)}"'
228
387
 
229
388
  def _coerce_string_input_default(self, getter: str, default_cpp: str) -> str:
230
389
  """String getters take a ``std::string`` default. An unresolved default
@@ -310,75 +469,83 @@ class InputHelper:
310
469
  return names, merged
311
470
 
312
471
  def extract_input_manifest(self) -> list[dict]:
313
- """Walk top-level ``var = input.*(...)`` decls into an InputDef list.
472
+ """Walk the script's global-scope ``input.*()`` calls into an InputDef
473
+ list, one entry per call site in source order: a top-level
474
+ ``var = input.*(...)`` declaration and an inline call inside an
475
+ expression (``ta.ema(close, input.int(9, "Fast"))``) alike.
314
476
 
315
477
  Each entry: ``{title, type, default[, min, max, step, options]}``. The
316
478
  optional keys are emitted only when the corresponding signature
317
479
  argument is a const literal; a bound/option referencing a non-literal
318
- is omitted (never crashes). One pass over ``self.ctx.ast.body``.
480
+ is omitted (never crashes). ``title`` is the key the emitted C++ reads
481
+ the input by: the title argument, else the name of the declaration
482
+ holding the call (``pine_spelling.input_binding_names``), else "".
319
483
  """
320
- out: list[dict] = []
484
+ return [self._input_manifest_entry(node, name)
485
+ for node, name in self._global_input_calls_with_names()]
486
+
487
+ def _global_input_calls_with_names(self) -> list[tuple[FuncCall, str | None]]:
488
+ """Every global-scope input call in source order, with the name of the
489
+ declaration holding it (None outside a declaration)."""
490
+ out = []
321
491
  for stmt in self.ctx.ast.body:
322
- if not (
323
- isinstance(stmt, VarDecl)
324
- and isinstance(stmt.value, FuncCall)
325
- and self._is_input_call(stmt.value)
326
- ):
327
- continue
328
- node = stmt.value
329
- func_name, namespace = self._resolve_callee(node.callee)
330
- names, merged = self._merged_args(node, func_name, namespace)
331
- title = self._get_input_title(node, var_name=stmt.name)
332
- default_node = self._get_input_default(node)
333
- default_val = (
334
- self._literal_or_none(default_node)
335
- if default_node is not None
336
- else None
337
- )
338
- if namespace == "input":
339
- form_type = self._FORM_TYPE.get(func_name, "string")
340
- else:
341
- # Plain ``input(...)``: Pine types the result by its defval.
342
- # The codegen already emits the matching scalar getter, so the
343
- # manifest must mirror that — infer from the resolved default's
344
- # Python type. ``bool`` MUST be tested before ``int`` because
345
- # ``isinstance(True, int)`` is True. A None/non-literal default
346
- # falls back to "string".
347
- if isinstance(default_val, bool):
348
- form_type = "bool"
349
- elif isinstance(default_val, int):
350
- form_type = "int"
351
- elif isinstance(default_val, float):
352
- form_type = "float"
353
- elif isinstance(default_val, str):
354
- form_type = "string"
355
- else:
356
- form_type = "string"
357
- entry: dict = {
358
- "title": title,
359
- "type": form_type,
360
- "default": default_val,
361
- }
362
- # Pull min/max/step/options by signature param name; emit only
363
- # const literals so the override form never references a runtime
364
- # value it can't reproduce.
365
- if names:
366
- idx = {n: i for i, n in enumerate(names)}
367
- for key, pname in (("min", "minval"), ("max", "maxval"), ("step", "step")):
368
- i = idx.get(pname)
369
- if i is not None and i < len(merged) and merged[i] is not None:
370
- v = self._literal_or_none(merged[i])
371
- # bool is an int subclass — exclude it from numeric bounds
372
- if isinstance(v, (int, float)) and not isinstance(v, bool):
373
- entry[key] = v
374
- oi = idx.get("options")
375
- if oi is not None and oi < len(merged) and merged[oi] is not None:
376
- opts_node = merged[oi]
377
- elems = getattr(opts_node, "elements", None)
378
- if elems is not None:
379
- vals = [self._literal_or_none(e) for e in elems]
380
- # any non-const element -> omit the whole options list
381
- if vals and all(isinstance(v, str) for v in vals):
382
- entry["options"] = vals
383
- out.append(entry)
492
+ for node in global_input_calls(stmt):
493
+ out.append((node, stmt.name if isinstance(stmt, VarDecl) else None))
384
494
  return out
495
+
496
+ def _input_manifest_entry(self, node: FuncCall, var_name: str | None) -> dict:
497
+ func_name, namespace = self._resolve_callee(node.callee)
498
+ names, merged = self._merged_args(node, func_name, namespace)
499
+ title = self._get_input_title(node, var_name=var_name)
500
+ default_node = self._get_input_default(node)
501
+ default_val = (
502
+ self._literal_or_none(default_node)
503
+ if default_node is not None
504
+ else None
505
+ )
506
+ if namespace == "input":
507
+ form_type = self._FORM_TYPE.get(func_name, "string")
508
+ else:
509
+ # Plain ``input(...)``: Pine types the result by its defval.
510
+ # The codegen already emits the matching scalar getter, so the
511
+ # manifest must mirror that — infer from the resolved default's
512
+ # Python type. ``bool`` MUST be tested before ``int`` because
513
+ # ``isinstance(True, int)`` is True. A None/non-literal default
514
+ # falls back to "string".
515
+ if isinstance(default_val, bool):
516
+ form_type = "bool"
517
+ elif isinstance(default_val, int):
518
+ form_type = "int"
519
+ elif isinstance(default_val, float):
520
+ form_type = "float"
521
+ elif isinstance(default_val, str):
522
+ form_type = "string"
523
+ else:
524
+ form_type = "string"
525
+ entry: dict = {
526
+ "title": title,
527
+ "type": form_type,
528
+ "default": default_val,
529
+ }
530
+ # Pull min/max/step/options by signature param name; emit only
531
+ # const literals so the override form never references a runtime
532
+ # value it can't reproduce.
533
+ if names:
534
+ idx = {n: i for i, n in enumerate(names)}
535
+ for key, pname in (("min", "minval"), ("max", "maxval"), ("step", "step")):
536
+ i = idx.get(pname)
537
+ if i is not None and i < len(merged) and merged[i] is not None:
538
+ v = self._literal_or_none(merged[i])
539
+ # bool is an int subclass — exclude it from numeric bounds
540
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
541
+ entry[key] = v
542
+ oi = idx.get("options")
543
+ if oi is not None and oi < len(merged) and merged[oi] is not None:
544
+ opts_node = merged[oi]
545
+ elems = getattr(opts_node, "elements", None)
546
+ if elems is not None:
547
+ vals = [self._literal_or_none(e) for e in elems]
548
+ # any non-const element -> omit the whole options list
549
+ if vals and all(isinstance(v, str) for v in vals):
550
+ entry["options"] = vals
551
+ return entry