@pineforge/codegen-pyodide 0.8.0 → 0.9.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.
@@ -37,6 +37,20 @@ class TACallSite:
37
37
  returns_tuple: bool # e.g., MACD, supertrend
38
38
  node: Any = None # the FuncCall AST node
39
39
  is_static: bool = False # true if global scope & arguments are recursively static
40
+ # Name of the user function that OWNS this site, or None for a top-level
41
+ # site. For an ORIGINAL site this is the function whose body textually
42
+ # contains the ``ta.*`` call. For a CLONE minted in
43
+ # ``_handle_user_func_call`` this is the CALLEE (the function being
44
+ # called, NOT the caller whose body visit triggered the clone) -- the
45
+ # clone belongs to the callee's per-call-site namespace.
46
+ #
47
+ # The codegen's dead-code pass keys off this rather than off
48
+ # ``func_ta_ranges`` slices, because a function's slice can include
49
+ # clones of ANOTHER (live) function's sites (minted while visiting a
50
+ # caller's body). Marking such a borrowed clone dead would leave the
51
+ # owning callee's emitted clone body referencing undeclared members
52
+ # (regression: quantbyboji-nq-hma-midday ``_ta_change_*_cs1``).
53
+ owner_func: str | None = None
40
54
 
41
55
 
42
56
  @dataclass
@@ -65,6 +79,21 @@ class FuncInfo:
65
79
  # ``Sample s = build_sample(...)`` then ``s.score()`` dispatches
66
80
  # correctly. Probe: data/validation/udt-method-probe-20-udt-return-from-func.
67
81
  udt_return_type: str | None = None
82
+ # Parallel to ``node.params``; each entry is a ``TypeSpec`` (or ``None``)
83
+ # carrying UDT / drawing-handle / precise-scalar typing that the coarse
84
+ # ``param_types`` (PineType) cannot represent. Populated from the
85
+ # function's declared parameter type hints (authoritative) and, for
86
+ # untyped params, from the call-site argument type. The codegen prefers
87
+ # this over ``param_types`` when emitting each parameter's C++ type so a
88
+ # ``pivot hi`` parameter emits as ``pivot hi`` (not ``double hi``) and an
89
+ # untyped ``s`` used as a string emits as ``std::string s``.
90
+ param_type_specs: list = field(default_factory=list)
91
+ # ``TypeSpec`` of the function's return value when it is a collection the
92
+ # coarse ``return_type`` (PineType) cannot represent — today this covers
93
+ # array-returning functions (``buildPDLevels() => array.from(...)`` ->
94
+ # ``std::vector<double>``). UDT / drawing-handle returns use
95
+ # ``udt_return_type``; tuple returns use ``returns_tuple``.
96
+ return_type_spec: Any = None
68
97
 
69
98
 
70
99
  @dataclass
@@ -72,6 +101,13 @@ class FixnanCallSite:
72
101
  """Per-call-site state for ``fixnan(...)`` (one previous-value member each)."""
73
102
  member_name: str # e.g., "_prev_fixnan_1"
74
103
  pine_type: Any # PineType
104
+ node: Any = None # the FuncCall AST node (for variant-aware lookup)
105
+ # Name of the user function that OWNS this site, or None for a top-level
106
+ # site. Mirrors ``TACallSite.owner_func``: the codegen's dead-code pass
107
+ # and per-variant clone logic key off this so a fixnan site minted inside
108
+ # a dead caller's body but cloned for a live callee survives, and each
109
+ # emitted function variant references its OWN fixnan member.
110
+ owner_func: str | None = None
75
111
 
76
112
 
77
113
  @dataclass
@@ -104,9 +140,33 @@ class SecurityCallInfo:
104
140
  gaps: Any = None
105
141
  lookahead: Any = None
106
142
  ta_range: Any = None
143
+ # request.security(ticker.heikinashi(syminfo.tickerid), ...) — the chart's own
144
+ # symbol read as Heikin-Ashi candles. The engine applies the HA OHLC transform
145
+ # to each (aggregated) bar before evaluating the security expression; the
146
+ # expression's close/open/high/low therefore see HA values. Same-symbol only.
147
+ heikinashi: bool = False
107
148
  depends_on_mutable_globals: bool = False
108
149
  mutable_globals: tuple[str, ...] = ()
109
150
  is_lower_tf_array: bool = False
151
+ # Name of the user function whose body contains this call ("" at global
152
+ # scope). A ``request.security(sym, tf, ...)`` whose ``tf`` is that
153
+ # function's parameter cannot be resolved at class scope (the security
154
+ # evaluator is a class method, not the function body) — the codegen resolves
155
+ # such a param-tf from the function's call sites instead.
156
+ containing_func: str = ""
157
+ # Set when this SecurityCallInfo is one of N per-call-site CLONES of a
158
+ # single source request.security(...) whose ``timeframe`` is a UDF
159
+ # parameter called from >= 2 sites with >= 2 distinct literal timeframes
160
+ # (e.g. ``scoreFromRange(tf) => request.security(sym, tf, ...)`` called as
161
+ # ``scoreFromRange("15")``, ``scoreFromRange("240")``, ...). Each clone's
162
+ # ``timeframe`` is rewritten to a literal StringLiteral for its call site
163
+ # and gets its own unique ``sec_id``; ``callsite_idx`` is the SAME index
164
+ # ``func_call_cs_map`` assigns that call site for the existing per-
165
+ # call-site UDF-body-cloning mechanism, so the codegen can pick the right
166
+ # clone's ``sec_id`` while emitting that call site's specialized function
167
+ # body (``self._active_call_site_idx``). None for an ordinary
168
+ # (non-cloned) security call.
169
+ callsite_idx: int | None = None
110
170
 
111
171
 
112
172
  @dataclass
@@ -125,6 +185,14 @@ class AnalyzerContext:
125
185
  var_members: list = field(default_factory=list) # [(name, PineType, init_expr_str)]
126
186
  func_infos: list = field(default_factory=list)
127
187
  fixnan_sites: list = field(default_factory=list)
188
+ # Per-function fixnan site ownership (func_name -> list of indices into
189
+ # ``fixnan_sites``). Used by the codegen to clone fixnan members per
190
+ # call-site variant and to skip fixnan state owned by dead functions.
191
+ func_fixnan_indices: dict = field(default_factory=dict)
192
+ # (func_name, cs_idx) -> {orig_member_name: cloned_member_name}. Like
193
+ # ``func_cs_ta_clone_names`` but for fixnan: populated only when the
194
+ # default ``{base}_cs{cs_idx}`` clone name collides.
195
+ func_cs_fixnan_clone_names: dict = field(default_factory=dict)
128
196
  strategy_params: dict = field(default_factory=dict)
129
197
  diagnostics: list = field(default_factory=list) # warnings
130
198
  filename: str = "<stdin>"
@@ -135,6 +203,21 @@ class AnalyzerContext:
135
203
  func_ta_ranges: dict = field(default_factory=dict) # func_name -> (start_idx, end_idx)
136
204
  func_call_cs_map: dict = field(default_factory=dict) # call_node_id -> (func_name, call_site_index)
137
205
  func_call_site_counts: dict = field(default_factory=dict) # func_name -> int
206
+ # Functions that need per-call-site body cloning PURELY because they
207
+ # contain a request.security(...) whose timeframe is a UDF parameter
208
+ # called with >= 2 distinct literal timeframes (the security-tf-
209
+ # monomorphization case), even though they have no TA call sites or
210
+ # series/var members of their own (the usual trigger for cloning). The
211
+ # codegen's function-emission gate ORs this in alongside has_ta/
212
+ # has_series so self._active_call_site_idx is actually set while it
213
+ # emits each call site's body — required for the per-clone
214
+ # SecurityCallInfo.callsite_idx disambiguation in visit_call.py to work.
215
+ func_security_clone_only: set = field(default_factory=set)
216
+ # (func_name, cs_idx) -> {orig_member_name: cloned_member_name}. Populated by
217
+ # the analyzer ONLY for clones whose default ``{base}_cs{cs_idx}`` name would
218
+ # collide with a clone minted through another enclosing function; lets codegen
219
+ # use the disambiguated name instead of re-deriving a colliding one.
220
+ func_cs_ta_clone_names: dict = field(default_factory=dict)
138
221
  # UDT / enum definitions:
139
222
  udt_defs: dict = field(default_factory=dict) # type_name -> {field_name: PineType}
140
223
  enum_defs: dict = field(default_factory=dict) # enum_name -> [member names]
@@ -145,12 +228,20 @@ class AnalyzerContext:
145
228
  # Per-function var_members + series_vars (used when emitting per-function call-site variants):
146
229
  func_var_members: dict = field(default_factory=dict)
147
230
  func_series_vars: dict = field(default_factory=dict)
231
+ # Per-function array-return TypeSpec (see FuncInfo.return_type_spec).
232
+ func_return_type_specs: dict = field(default_factory=dict)
148
233
  # var_name -> UDT type name for variables instantiated via TypeName.new(...)
149
234
  udt_var_types: dict[str, str] = field(default_factory=dict)
150
235
  # var_name -> structured collection/UDT type metadata
151
236
  collection_types: dict[str, TypeSpec] = field(default_factory=dict)
152
237
  # UDT name -> field_name -> structured type metadata
153
238
  udt_field_type_specs: dict[str, dict[str, TypeSpec]] = field(default_factory=dict)
239
+ # id(block_node) -> {raw_var_name: scope_unique_member_name} for block-scoped
240
+ # ``var``/``varip`` declarations whose raw name collides with a same-named
241
+ # block var in a sibling scope. Codegen activates the rename via
242
+ # ``_active_var_remap`` while emitting that block's statements so reads/writes
243
+ # of the var resolve to the disambiguated member.
244
+ block_var_renames: dict[int, dict[str, str]] = field(default_factory=dict)
154
245
  # ``// @pf-trace name=expr`` pragmas in source order. Populated by
155
246
  # :func:`pineforge_codegen.pragmas.extract_pf_trace_pragmas` from
156
247
  # the original source text and attached after :class:`Analyzer`
@@ -89,8 +89,22 @@ class DiagnosticsHelper:
89
89
  loc,
90
90
  )
91
91
 
92
+ # Non-atomic node kinds whose serialized infix form must be parenthesized
93
+ # when used as an operand, so the string re-parses to the SAME tree. A TA
94
+ # ctor-arg string produced here is later re-parsed and lowered by the
95
+ # codegen (``_runtime_ctor_arg_for_reset``); a flattened ``(a - b) / c``
96
+ # would otherwise silently reassociate under C++ precedence.
97
+ _NONATOMIC_EXPR_NODES = (BinOp, UnaryOp, Ternary)
98
+
99
+ def _operand_to_str(self, node: ASTNode) -> str:
100
+ s = self._expr_to_str(node)
101
+ if isinstance(node, self._NONATOMIC_EXPR_NODES):
102
+ return f"({s})"
103
+ return s
104
+
92
105
  def _expr_to_str(self, node: ASTNode) -> str:
93
- """Convert an expression node to a rough string representation."""
106
+ """Convert an expression node to a string that re-parses to the same
107
+ tree (grouping preserved for non-atomic operands)."""
94
108
  if isinstance(node, NumberLiteral):
95
109
  return str(node.value)
96
110
  if isinstance(node, StringLiteral):
@@ -104,9 +118,9 @@ class DiagnosticsHelper:
104
118
  if isinstance(node, MemberAccess):
105
119
  return f"{self._expr_to_str(node.object)}.{node.member}"
106
120
  if isinstance(node, BinOp):
107
- return f"{self._expr_to_str(node.left)} {node.op} {self._expr_to_str(node.right)}"
121
+ return f"{self._operand_to_str(node.left)} {node.op} {self._operand_to_str(node.right)}"
108
122
  if isinstance(node, UnaryOp):
109
- return f"{node.op}{self._expr_to_str(node.operand)}"
123
+ return f"{node.op}{self._operand_to_str(node.operand)}"
110
124
  if isinstance(node, FuncCall):
111
125
  args = ", ".join(self._expr_to_str(a) for a in node.args)
112
126
  callee_str = self._expr_to_str(node.callee)
@@ -114,5 +128,5 @@ class DiagnosticsHelper:
114
128
  if isinstance(node, Subscript):
115
129
  return f"{self._expr_to_str(node.object)}[{self._expr_to_str(node.index)}]"
116
130
  if isinstance(node, Ternary):
117
- return f"{self._expr_to_str(node.condition)} ? {self._expr_to_str(node.true_val)} : {self._expr_to_str(node.false_val)}"
131
+ return f"{self._operand_to_str(node.condition)} ? {self._operand_to_str(node.true_val)} : {self._operand_to_str(node.false_val)}"
118
132
  return "<?>"
@@ -110,7 +110,7 @@ TA_CLASS_MAP = {
110
110
  TA_PERIOD_ARG = {
111
111
  "sma": 1, "ema": 1, "rma": 1, "rsi": 1, "atr": 0,
112
112
  "highest": 1, "lowest": 1, "change": 1,
113
- "wma": 1, "hma": 1, "stdev": 1,
113
+ "wma": 1, "hma": 1,
114
114
  # Task 6
115
115
  "sum": 1,
116
116
  # Task 7 Batch 1
@@ -119,7 +119,7 @@ TA_PERIOD_ARG = {
119
119
  "mom": 1, "roc": 1, "rising": 1, "falling": 1, "cci": 1,
120
120
  # cum has no period arg — handled in TA_NO_CTOR
121
121
  # Task 7 Batch 3
122
- "variance": 1, "median": 1, "highestbars": 1, "lowestbars": 1,
122
+ "median": 1, "highestbars": 1, "lowestbars": 1,
123
123
  # Batch 4
124
124
  "cmo": 1, "cog": 1, "correlation": 2,
125
125
  "percentile_nearest_rank": 1, "percentile_linear_interpolation": 1,
@@ -130,6 +130,14 @@ TA_PERIOD_ARG = {
130
130
 
131
131
  # Functions that return tuples
132
132
  TA_TUPLE_RETURNS = {"macd", "supertrend", "dmi", "bb", "kc", "vwap_bands"}
133
+ TA_TUPLE_ELEMENT_COUNTS = {
134
+ "macd": 3,
135
+ "supertrend": 2,
136
+ "dmi": 3,
137
+ "bb": 3,
138
+ "kc": 3,
139
+ "vwap_bands": 3,
140
+ }
133
141
 
134
142
  # Functions with multiple constructor args
135
143
  TA_MULTI_CTOR = {
@@ -156,6 +164,16 @@ TA_MULTI_CTOR = {
156
164
  "bbw": [1, 2], # length, mult
157
165
  "kcw": [1, 2], # length, mult
158
166
  "tr": [0], # handle_na (compile-time bool)
167
+ "stdev": [1, 2], # length, biased
168
+ "variance": [1, 2], # length, biased
169
+ }
170
+
171
+ # Compute-arg indices: which positional args are forwarded to ``.compute()``.
172
+ # Entries here override the default analyzer behavior of forwarding every
173
+ # non-constructor argument.
174
+ TA_COMPUTE_ARGS = {
175
+ "stdev": [0],
176
+ "variance": [0],
159
177
  }
160
178
 
161
179
  # No-state functions (no constructor args, stateless or self-contained)
@@ -45,6 +45,12 @@ from ..ast_nodes import (
45
45
  )
46
46
  from ..symbols import PineType, TypeSpec
47
47
 
48
+ # Drawing-objects-as-data type names (spec §4.1). Defined locally — the
49
+ # analyzer must not import from ``codegen`` (codegen imports analyzer, so the
50
+ # reverse would be a cycle). Mirrors codegen.tables.DRAWING_TYPE_TO_CPP keys.
51
+ _DRAWING_TYPE_NAMES = frozenset({"line", "box", "label", "linefill", "chart.point"})
52
+ _DRAWING_NS = frozenset({"line", "box", "label", "linefill"})
53
+
48
54
 
49
55
  class TypeHelper:
50
56
  """Pine type-hint / expression inference.
@@ -100,8 +106,28 @@ class TypeHelper:
100
106
  return TypeSpec.map(key, val)
101
107
  if hint in self._udt_fields:
102
108
  return TypeSpec.udt(hint)
109
+ # Drawing-objects-as-data (P3): scalar ``line``/``box``/``label``/
110
+ # ``linefill``/``chart.point`` carry the handle identity via a udt
111
+ # TypeSpec. Without this the analyzer field-spec filter (base.py ~847)
112
+ # erases a scalar drawing field, collapsing it to double. Drawing names
113
+ # are NOT in _udt_fields.
114
+ if hint in _DRAWING_TYPE_NAMES:
115
+ return TypeSpec.udt(hint)
103
116
  return None
104
117
 
118
+ def _param_type_specs_from_def(self, func_def) -> list:
119
+ """Per-parameter ``TypeSpec`` (or ``None``) from a function's DECLARED
120
+ parameter type hints — the authoritative source for typed params
121
+ (``pivot hi``, ``string tf``, ``line[] arr``). Untyped params are
122
+ ``None`` here so regular-function call-site inference can fill them.
123
+ """
124
+ hints = (getattr(func_def, "annotations", None) or {}).get("param_type_hints", [])
125
+ specs: list = []
126
+ for i in range(len(func_def.params)):
127
+ hint = hints[i] if i < len(hints) else None
128
+ specs.append(self._type_spec_from_hint(hint) if hint else None)
129
+ return specs
130
+
105
131
  def _template_args_from_call(self, node: FuncCall) -> list[str]:
106
132
  callee = node.callee
107
133
  ann = getattr(callee, "annotations", None) or {}
@@ -116,6 +142,17 @@ class TypeHelper:
116
142
  func = cal.member if isinstance(cal, MemberAccess) else None
117
143
  ns = cal.object.name if isinstance(cal, MemberAccess) and isinstance(cal.object, Identifier) else None
118
144
  targs = self._template_args_from_call(value)
145
+ # Drawing-objects-as-data return typing: *.new / *.copy -> handle of
146
+ # the self-type; linefill.get_line* -> line; chart.point.* -> point.
147
+ if ns in _DRAWING_NS:
148
+ if func in ("new", "copy"):
149
+ return TypeSpec.udt(ns)
150
+ if ns == "linefill" and func in ("get_line1", "get_line2"):
151
+ return TypeSpec.udt("line")
152
+ if (isinstance(cal, MemberAccess) and isinstance(cal.object, MemberAccess)
153
+ and isinstance(cal.object.object, Identifier)
154
+ and cal.object.object.name == "chart" and cal.object.member == "point"):
155
+ return TypeSpec.udt("chart.point")
119
156
  if ns == "array" and func in ("new", "new_float", "new_int", "new_bool", "new_string", "from"):
120
157
  if func == "new_float":
121
158
  return TypeSpec.array(TypeSpec.primitive("float"))
@@ -194,10 +231,26 @@ class TypeHelper:
194
231
  return recv_spec.element
195
232
  if func == "eigenvalues":
196
233
  return TypeSpec.array(TypeSpec.primitive("float"))
234
+ # Drawing method-form: a.copy() -> same handle; lf.get_line*() -> line.
235
+ if (recv_spec is not None and recv_spec.kind == "udt"
236
+ and recv_spec.name in _DRAWING_TYPE_NAMES):
237
+ if func == "copy":
238
+ return recv_spec
239
+ if recv_spec.name == "linefill" and func in ("get_line1", "get_line2"):
240
+ return TypeSpec.udt("line")
197
241
  if isinstance(value, Identifier):
198
242
  sym = self._symbols.resolve(value.name)
199
243
  if sym is not None and sym.type_spec is not None:
200
244
  return sym.type_spec
245
+ if isinstance(value, FuncCall):
246
+ # User-function return spec (e.g. an array-returning
247
+ # ``buildPDLevels() => array.from(...)``), so a caller's
248
+ # ``allLevels = buildPDLevels()`` infers an array TypeSpec.
249
+ cal = value.callee
250
+ fname = cal.member if isinstance(cal, MemberAccess) else (
251
+ cal.name if isinstance(cal, Identifier) else None)
252
+ if fname and fname in getattr(self, "_func_return_type_specs", {}):
253
+ return self._func_return_type_specs[fname]
201
254
  if isinstance(value, MemberAccess):
202
255
  owner = self._type_spec_from_expr(value.object)
203
256
  if owner is not None and owner.kind == "udt" and owner.name:
@@ -41,6 +41,8 @@ from .tables import (
41
41
  SYMINFO_MEMBER_MAP,
42
42
  COLOR_CONST_MAP,
43
43
  ARRAY_METHODS,
44
+ ARRAY_DRAWING_NEW_CTORS,
45
+ ARRAY_NEW_CTORS,
44
46
  MAP_METHODS,
45
47
  MATRIX_METHODS,
46
48
  MATRIX_METHOD_KWARGS,
@@ -69,6 +71,8 @@ __all__ = [
69
71
  "SYMINFO_MEMBER_MAP",
70
72
  "COLOR_CONST_MAP",
71
73
  "ARRAY_METHODS",
74
+ "ARRAY_DRAWING_NEW_CTORS",
75
+ "ARRAY_NEW_CTORS",
72
76
  "MAP_METHODS",
73
77
  "MATRIX_METHODS",
74
78
  "MATRIX_METHOD_KWARGS",