@pineforge/codegen-pyodide 1.1.0 → 1.3.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.
@@ -32,6 +32,29 @@ class Diagnostic:
32
32
  message: str
33
33
  hint: str | None = None
34
34
 
35
+ # The stable code (``PF-E1203`` / ``PF-W0412``) and named arguments of the
36
+ # catalog template the English ``message`` and ``hint`` render from
37
+ # (``diagnostic_codes``, ``diagnostics_catalog.json``). Both are read off
38
+ # the text when first asked for, after the transpile: the text and the
39
+ # C++ never depend on them.
40
+ @property
41
+ def code(self) -> str:
42
+ return self._coded()[0]
43
+
44
+ @property
45
+ def args(self) -> dict:
46
+ return self._coded()[1]
47
+
48
+ def _coded(self) -> tuple[str, dict]:
49
+ severity = getattr(self.level, "value", self.level)
50
+ key = (severity, self.message, self.hint)
51
+ cached = self.__dict__.get("_pf_coded")
52
+ if cached is None or cached[0] != key:
53
+ from .diagnostic_codes import classify
54
+ cached = (key, classify(severity, self.message, self.hint))
55
+ self.__dict__["_pf_coded"] = cached
56
+ return cached[1]
57
+
35
58
 
36
59
  class CompileError(Exception):
37
60
  def __init__(self, diagnostics: list[Diagnostic]):
@@ -85,6 +85,19 @@ _RECORDED_PARAMS = {
85
85
  # RequestRef}``: the run stops there only when no data is installed for the
86
86
  # request that carries the same ref (``REQUEST_REF_ANNOTATION``).
87
87
  UNPINNED_ANNOTATION = "pf_request_unpinned"
88
+ CAPABILITY_UNPINNED_ANNOTATION = "pf_capability_unpinned_requests"
89
+
90
+
91
+ def _record_unpinned_capability(program, request):
92
+ notes = program.annotations = dict(program.annotations or {})
93
+ sites = notes.setdefault(CAPABILITY_UNPINNED_ANNOTATION, [])
94
+ sites.append({
95
+ "function": f"request.{request.callee.member}",
96
+ "symbol_node": request.args[0] if request.args else request.kwargs.get("symbol"),
97
+ "tf_node": request.args[1] if len(request.args) > 1 else request.kwargs.get("timeframe"),
98
+ "lookahead_node": request.kwargs.get("lookahead"),
99
+ "gaps_node": request.kwargs.get("gaps"),
100
+ })
88
101
  REQUEST_REF_ANNOTATION = "pf_request_ref"
89
102
  # On a request whose symbol can select the chart's or another symbol's: it
90
103
  # read the chart before it read a feed, and keeps that lowering where no feed
@@ -785,10 +798,12 @@ def lower_no_data_requests(program: Program) -> Program:
785
798
  backed.append(node)
786
799
  continue
787
800
  if lowering == ABSENT_LOWERING:
801
+ _record_unpinned_capability(program, node)
788
802
  node.annotations = {**node.annotations, UNPINNED_ANNOTATION: unpinned_message(node)}
789
803
  continue
790
804
  swaps[id(node)] = _na_of(node, funcs)
791
805
  if lowering == "unpinned":
806
+ _record_unpinned_capability(program, node)
792
807
  unpinned.append(node)
793
808
  if unpinned or backed:
794
809
  index = ScriptIndex(program)
@@ -853,6 +868,7 @@ def unpin_requests(program: Program, reasons: dict[int, str]) -> None:
853
868
  "results."))
854
869
  continue
855
870
  lowered = _na_of(request, funcs)
871
+ _record_unpinned_capability(program, request)
856
872
  marker = (request.annotations or {}).get(UNPINNED_ANNOTATION)
857
873
  if isinstance(marker, dict):
858
874
  lowered.annotations = {**(lowered.annotations or {}),
@@ -2843,8 +2843,9 @@ class SupportChecker:
2843
2843
  # Bare suffix shorthand — Pine treats "D"/"W"/"M"/"S" as 1<suffix>.
2844
2844
  # Accept the same shorthand here.
2845
2845
  return None
2846
- if not digits.isdigit():
2847
- # catches "1.5", "-15", "abc", etc.
2846
+ if not digits.isdecimal():
2847
+ # catches "1.5", "-15", "abc", and digits int() does not read
2848
+ # ("²"), etc.
2848
2849
  return f"non-integer timeframe magnitude '{digits}'"
2849
2850
  n = int(digits)
2850
2851
  if n <= 0:
@@ -2859,15 +2860,11 @@ class SupportChecker:
2859
2860
  return
2860
2861
  if not isinstance(tf_node, StringLiteral):
2861
2862
  return
2862
- err = self._validate_pine_tf_literal(tf_node.value)
2863
- if err is None:
2863
+ refusal = invalid_tf_literal(tf_node.value, fn_label)
2864
+ if refusal is None:
2864
2865
  return
2865
- self._err(
2866
- tf_node,
2867
- f"{fn_label}: invalid timeframe literal '{tf_node.value}'. "
2868
- "Expected Pine TF format like '1', '15', '1H', '1D', '15S'.",
2869
- hint=err,
2870
- )
2866
+ message, hint = refusal
2867
+ self._err(tf_node, message, hint=hint)
2871
2868
 
2872
2869
  def _check_request_security_lower_tf_symbol(self, node: FuncCall) -> None:
2873
2870
  """``request.security_lower_tf`` reads the chart's intrabars: the
@@ -2912,6 +2909,18 @@ class SupportChecker:
2912
2909
  self._check_tf_literal(tf_node, "request.security_lower_tf")
2913
2910
 
2914
2911
 
2912
+ def invalid_tf_literal(tf_str: str, fn_label: str) -> tuple[str, str] | None:
2913
+ """``(message, hint)`` refusing ``tf_str`` as the timeframe of
2914
+ ``fn_label`` (``request.security`` / ``request.security_lower_tf``), or
2915
+ None when it is a Pine timeframe. The codegen refuses a timeframe string
2916
+ it resolves from a constant or a helper parameter with the same text."""
2917
+ err = SupportChecker._validate_pine_tf_literal(tf_str)
2918
+ if err is None:
2919
+ return None
2920
+ return (f"{fn_label}: invalid timeframe literal '{tf_str}'. "
2921
+ "Expected Pine TF format like '1', '15', '1H', '1D', '15S'.", err)
2922
+
2923
+
2915
2924
  # ---------------------------------------------------------------------------
2916
2925
  # Convenience entry point
2917
2926
  # ---------------------------------------------------------------------------
Binary file
package/release.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "codegen": "1.1.0",
2
+ "codegen": "1.3.0",
3
3
  "pyodide": "314.0.0",
4
4
  "python": "3.14.0",
5
5
  "emscripten": "emscripten_5_0_3",
6
- "sha256": "37b1ca85a2006f6f07d52074a7180187536047bfbb857681978a372b1d7f9bef"
6
+ "sha256": "b058ef7c8ded42e7a80ccf551e62985e052d1e306aebf615ab9937544f060839"
7
7
  }
package/tables.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "CODEGEN_VERSION": "1.1.0",
2
+ "CODEGEN_VERSION": "1.3.0",
3
3
  "TA_CLASS_MAP_KEYS": [
4
4
  "sma",
5
5
  "ema",
@@ -32,6 +32,10 @@ def _diagnostic_entries(diagnostics) -> list:
32
32
  "col": loc.col if loc else 1,
33
33
  "message": message,
34
34
  "severity": getattr(d.level, "value", "error"),
35
+ # The stable code and named arguments of the catalog templates the
36
+ # message (and the hint after " — ") render from.
37
+ "code": d.code,
38
+ "args": d.args,
35
39
  }
36
40
  end_col = getattr(loc, "end_col", None) if loc else None
37
41
  if end_col is not None:
Binary file