pineforge-codegen 0.7.3__py3-none-any.whl → 0.7.5__py3-none-any.whl

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.

Potentially problematic release.


This version of pineforge-codegen might be problematic. Click here for more details.

@@ -51,3 +51,42 @@ def transpile(pine_source: str, *, check_support: bool = True, filename: str = "
51
51
  # ``if (trace_enabled_) { trace(...); ... }`` block.
52
52
  ctx.pf_trace_pragmas = pragmas
53
53
  return CodeGen(ctx).generate()
54
+
55
+
56
+ def transpile_full(pine_source: str, *, check_support: bool = True,
57
+ filename: str = "<input>") -> dict:
58
+ """Transpile like :func:`transpile`, plus the host-UI input manifest.
59
+
60
+ Runs ONE pipeline pass (Lexer -> Parser -> support check -> Analyzer ->
61
+ CodeGen.generate) and returns the generated C++ alongside the data the
62
+ cloud Studio needs to auto-build a backtest "override params" form:
63
+
64
+ - ``cpp``: the generated C++ source (identical to :func:`transpile`).
65
+ - ``inputs``: a list of ``InputDef`` dicts (one per top-level
66
+ ``var = input.*(...)`` declaration). Each has ``title`` / ``type`` /
67
+ ``default`` and optionally ``min`` / ``max`` / ``step`` / ``options``
68
+ (omitted when the corresponding signature argument is absent or
69
+ references a non-const value). See
70
+ :meth:`CodeGen.extract_input_manifest`.
71
+ - ``strategyParams``: the literal ``strategy(...)`` kwargs the analyzer
72
+ surfaced (e.g. ``initial_capital``, ``pyramiding``).
73
+
74
+ Args mirror :func:`transpile`.
75
+
76
+ Returns:
77
+ ``{"cpp": str, "inputs": list[dict], "strategyParams": dict}``.
78
+ """
79
+ pragmas = extract_pf_trace_pragmas(pine_source)
80
+ tokens = Lexer(pine_source, filename=filename).tokenize()
81
+ ast = Parser(tokens, source=pine_source, filename=filename).parse()
82
+ if check_support:
83
+ check_support_or_raise(ast, filename=filename)
84
+ ctx = Analyzer(ast, filename=filename).analyze()
85
+ ctx.pf_trace_pragmas = pragmas
86
+ gen = CodeGen(ctx)
87
+ cpp = gen.generate()
88
+ return {
89
+ "cpp": cpp,
90
+ "inputs": gen.extract_input_manifest(),
91
+ "strategyParams": dict(ctx.strategy_params),
92
+ }
@@ -611,17 +611,27 @@ class TopLevelEmitter:
611
611
  lines.append(" static_cast<MagnifierDistribution>(magnifier_dist));")
612
612
  else:
613
613
  # The magnifier-aware run() overload handles ratio=1 (no
614
- # aggregation) and empty itf/stf (auto-detect) on its own, so
615
- # we only need to fall back to the simple ``run(bars, n)``
616
- # path when the caller explicitly opted out of magnifier AND
617
- # there is no timeframe aggregation in play. Previously the
618
- # short-circuit ``itf.empty() || stf.empty() || itf == stf``
619
- # silently dropped the bar_magnifier flag whenever the host
620
- # passed empty TFs (the validator's default) turning every
621
- # magnifier-opt-in run into a non-magnifier run and producing
622
- # 0.21% exit-price drift on the magnifier-distribution probes.
614
+ # aggregation), empty itf (auto-detect from bar timestamps),
615
+ # and empty stf (default-to-input) entirely on its own, so we
616
+ # route through it whenever ANY TF/magnifier knob is set. We
617
+ # only fall back to the simple ``run(bars, n)`` path when the
618
+ # caller passed NO magnifier AND NO timeframe at all.
619
+ #
620
+ # The previous guard ``(!itf.empty() && !stf.empty() && itf != stf)``
621
+ # required BOTH timeframes to be present before aggregating.
622
+ # The cloud caller passes ``input_tf=""`` (auto-detect) with a
623
+ # concrete ``script_tf="240"``; that made the guard false and
624
+ # the chosen ``script_tf`` was silently ignored — the strategy
625
+ # ran on raw 1m bars with no aggregation. It also dropped the
626
+ # bar_magnifier flag whenever the host passed empty TFs,
627
+ # producing 0.21% exit-price drift on the magnifier probes.
628
+ #
629
+ # Over-approximating to ``!itf.empty() || !stf.empty()`` is
630
+ # always correct: the TF-aware overload is a no-op when the
631
+ # ratio resolves to 1, so the only thing lost in that case is
632
+ # the precalc optimization — never correctness.
623
633
  lines.append(" bool needs_full_run = (bar_magnifier != 0)")
624
- lines.append(" || (!itf.empty() && !stf.empty() && itf != stf);")
634
+ lines.append(" || !itf.empty() || !stf.empty();")
625
635
  lines.append(" if (!needs_full_run) {")
626
636
  lines.append(" strat->run(bars, n);")
627
637
  lines.append(" } else {")
@@ -865,7 +875,7 @@ class TopLevelEmitter:
865
875
  lines.append(" bool bar_magnifier = false,")
866
876
  lines.append(" int magnifier_samples = 4,")
867
877
  lines.append(" MagnifierDistribution magnifier_dist = MagnifierDistribution::ENDPOINTS) {")
868
- lines.append(" bool needs_dynamic = bar_magnifier || (!input_tf.empty() && !script_tf.empty() && input_tf != script_tf);")
878
+ lines.append(" bool needs_dynamic = bar_magnifier || !input_tf.empty() || !script_tf.empty();")
869
879
  lines.append(" if (needs_dynamic) {")
870
880
  lines.append(" _use_precalc = false;")
871
881
  lines.append(" } else {")
@@ -15,13 +15,32 @@ Mixin contract — host class must provide:
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
- from ..ast_nodes import FuncCall, Identifier, MemberAccess, StringLiteral
18
+ from ..ast_nodes import (
19
+ BoolLiteral,
20
+ FuncCall,
21
+ Identifier,
22
+ MemberAccess,
23
+ NumberLiteral,
24
+ StringLiteral,
25
+ VarDecl,
26
+ )
19
27
  from .. import signatures as sigs
20
28
 
21
29
 
22
30
  class InputHelper:
23
31
  """``input.*`` call analysis helpers — defaults, titles, getter dispatch, enum guard."""
24
32
 
33
+ # Maps the input function short-name -> the form-facing type tag emitted in
34
+ # the input manifest (consumed by the host UI's override form). ``price``
35
+ # is a float slider, ``time`` an int timestamp; everything string-like
36
+ # collapses to "string".
37
+ _FORM_TYPE = {
38
+ "int": "int", "float": "float", "bool": "bool", "string": "string",
39
+ "source": "source", "enum": "enum", "price": "float", "time": "int",
40
+ "color": "string", "timeframe": "string", "session": "string",
41
+ "symbol": "string", "text_area": "string",
42
+ }
43
+
25
44
  def _is_input_call(self, node: FuncCall) -> bool:
26
45
  """True if ``node`` is an ``input(...)`` or ``input.<type>(...)`` call."""
27
46
  func_name, namespace = self._resolve_callee(node.callee)
@@ -187,3 +206,125 @@ class InputHelper:
187
206
  f"{ename}.{dv.member} is not a member of enum {ename} "
188
207
  "(internal: Analyzer should reject this first)"
189
208
  )
209
+
210
+ # ------------------------------------------------------------------
211
+ # Input manifest extraction (host UI override-form source of truth)
212
+ # ------------------------------------------------------------------
213
+
214
+ def _literal_or_none(self, node):
215
+ """Return a JSON scalar for a *const* literal AST node, else None.
216
+
217
+ ``None`` signals non-const (an identifier, computed expression, …) so
218
+ callers can omit a bound/option that references a runtime value.
219
+ Enum member refs (``Dir.Up``) collapse to the ``"Dir.Up"`` string tag.
220
+ """
221
+ if isinstance(node, StringLiteral):
222
+ return node.value
223
+ # BoolLiteral must be checked before NumberLiteral: a Pine ``true`` is a
224
+ # BoolLiteral (not a NumberLiteral), but guarding the order keeps intent
225
+ # explicit and future-proof against bool/int node overlap.
226
+ if isinstance(node, BoolLiteral):
227
+ return node.value
228
+ if isinstance(node, NumberLiteral):
229
+ return node.value
230
+ # enum member ref like ``Dir.Up`` -> "Dir.Up" (string tag)
231
+ if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
232
+ return f"{node.object.name}.{node.member}"
233
+ return None
234
+
235
+ def _merged_args(self, node: FuncCall, func_name, namespace):
236
+ """Merge positional args + kwargs into signature-positional order.
237
+
238
+ Returns ``(param_names | None, merged_list)``. Mirrors the merge logic
239
+ in :meth:`_get_input_title` / :meth:`_get_input_default` so manifest
240
+ extraction reads bounds/options off the same positions codegen does.
241
+ """
242
+ if namespace == "input" and func_name in sigs.INPUT_FUNCTIONS:
243
+ names = sigs.get_param_names("input", func_name)
244
+ elif func_name == "input" and namespace is None:
245
+ names = sigs.get_param_names(None, "input")
246
+ else:
247
+ names = None
248
+ merged = list(node.args)
249
+ if names:
250
+ for i, pname in enumerate(names):
251
+ if pname in node.kwargs:
252
+ while len(merged) <= i:
253
+ merged.append(None)
254
+ if merged[i] is None:
255
+ merged[i] = node.kwargs[pname]
256
+ return names, merged
257
+
258
+ def extract_input_manifest(self) -> list[dict]:
259
+ """Walk top-level ``var = input.*(...)`` decls into an InputDef list.
260
+
261
+ Each entry: ``{title, type, default[, min, max, step, options]}``. The
262
+ optional keys are emitted only when the corresponding signature
263
+ argument is a const literal; a bound/option referencing a non-literal
264
+ is omitted (never crashes). One pass over ``self.ctx.ast.body``.
265
+ """
266
+ out: list[dict] = []
267
+ for stmt in self.ctx.ast.body:
268
+ if not (
269
+ isinstance(stmt, VarDecl)
270
+ and isinstance(stmt.value, FuncCall)
271
+ and self._is_input_call(stmt.value)
272
+ ):
273
+ continue
274
+ node = stmt.value
275
+ func_name, namespace = self._resolve_callee(node.callee)
276
+ names, merged = self._merged_args(node, func_name, namespace)
277
+ title = self._get_input_title(node, var_name=stmt.name)
278
+ default_node = self._get_input_default(node)
279
+ default_val = (
280
+ self._literal_or_none(default_node)
281
+ if default_node is not None
282
+ else None
283
+ )
284
+ if namespace == "input":
285
+ form_type = self._FORM_TYPE.get(func_name, "string")
286
+ else:
287
+ # Plain ``input(...)``: Pine types the result by its defval.
288
+ # The codegen already emits the matching scalar getter, so the
289
+ # manifest must mirror that — infer from the resolved default's
290
+ # Python type. ``bool`` MUST be tested before ``int`` because
291
+ # ``isinstance(True, int)`` is True. A None/non-literal default
292
+ # falls back to "string".
293
+ if isinstance(default_val, bool):
294
+ form_type = "bool"
295
+ elif isinstance(default_val, int):
296
+ form_type = "int"
297
+ elif isinstance(default_val, float):
298
+ form_type = "float"
299
+ elif isinstance(default_val, str):
300
+ form_type = "string"
301
+ else:
302
+ form_type = "string"
303
+ entry: dict = {
304
+ "title": title,
305
+ "type": form_type,
306
+ "default": default_val,
307
+ }
308
+ # Pull min/max/step/options by signature param name; emit only
309
+ # const literals so the override form never references a runtime
310
+ # value it can't reproduce.
311
+ if names:
312
+ idx = {n: i for i, n in enumerate(names)}
313
+ for key, pname in (("min", "minval"), ("max", "maxval"), ("step", "step")):
314
+ i = idx.get(pname)
315
+ if i is not None and i < len(merged) and merged[i] is not None:
316
+ v = self._literal_or_none(merged[i])
317
+ # bool is an int subclass — exclude it from numeric bounds
318
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
319
+ entry[key] = v
320
+ oi = idx.get("options")
321
+ if oi is not None and oi < len(merged) and merged[oi] is not None:
322
+ opts_node = merged[oi]
323
+ elems = getattr(opts_node, "elements", None)
324
+ if elems is not None:
325
+ vals = [self._literal_or_none(e) for e in elems]
326
+ # any non-const element -> omit the whole options list
327
+ if vals and all(isinstance(v, str) for v in vals):
328
+ entry["options"] = vals
329
+ out.append(entry)
330
+ return out
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pineforge-codegen
3
- Version: 0.7.3
3
+ Version: 0.7.5
4
4
  Summary: PineScript v6 to C++ transpiler that targets the pineforge-engine runtime.
5
5
  Project-URL: Homepage, https://github.com/pineforge-4pass/pineforge-codegen-oss
6
6
  Project-URL: Issues, https://github.com/pineforge-4pass/pineforge-codegen-oss/issues
@@ -1,4 +1,4 @@
1
- pineforge_codegen/__init__.py,sha256=NyzzhFcKFvusgR_Q3kgAHRiP6o9xBYEFiU7KAHpwpcs,2482
1
+ pineforge_codegen/__init__.py,sha256=O8-sYnIHi1Kwo9h6khfDrKrGXZ85TQJlJfvkkgCo4QU,4158
2
2
  pineforge_codegen/ast_nodes.py,sha256=a5wW_wJ8QPy35guqJOVXU6D89kdTVrTNcd-lw0-dTaM,7680
3
3
  pineforge_codegen/errors.py,sha256=U6oUuK9GyEDBpSriXzhWyKMuTG-hyk4mND6gVjnxu6A,2931
4
4
  pineforge_codegen/lexer.py,sha256=afxd3MED6Utqi97HtkGbcoh1WMG0AsMs9-ZP3H9Pq_Q,18593
@@ -18,10 +18,10 @@ pineforge_codegen/analyzer/tables.py,sha256=KiI3gvYGWBw9PgX_PPXPS-fk8dfH54OjMCMG
18
18
  pineforge_codegen/analyzer/types.py,sha256=HAbEAnxkZxX7d5iFQA6KT0Ocs7kkUprX78z9DMcHVlc,12181
19
19
  pineforge_codegen/codegen/__init__.py,sha256=jvwwf93pqyHnFSvGPcD8Mhr_kAk0Uc4W6IqhKu8UIlI,2232
20
20
  pineforge_codegen/codegen/base.py,sha256=p9Tkh7VTLSTl34i-pyIYOm6PmmonAYOtRgfLKFXHnQo,74019
21
- pineforge_codegen/codegen/emit_top.py,sha256=htpiDaHI9q98ryd9eihKxGmVoZnbqz16vnw6Ax8L028,46076
21
+ pineforge_codegen/codegen/emit_top.py,sha256=dyuLuPh8SoZcxiC3wwa7wVikPMOBY0bFDCq8L2VvoNI,46638
22
22
  pineforge_codegen/codegen/helpers.py,sha256=TIsTUjrri4DobEJ9Cr-rl9s6LILWuKWolROUu1-f4mw,7313
23
23
  pineforge_codegen/codegen/helpers_syminfo.py,sha256=GFrx9i2HXSCmGIpIds0cFrtJ6IQiznfXxH8aI8h3DQ4,4878
24
- pineforge_codegen/codegen/input.py,sha256=uiaOLWnAmu4wdXD0qPFqH5HUJsZDGntulwVzWyNFXt0,8996
24
+ pineforge_codegen/codegen/input.py,sha256=z-qkYIUrmlU5EW1siUiXdbs7r6eY6-FfLXuKB5F-E1g,15587
25
25
  pineforge_codegen/codegen/security.py,sha256=dsmWLtH3GMRmHeRCpMUk2WDn_8WCDeU76L-tOFGdzcE,67565
26
26
  pineforge_codegen/codegen/ta.py,sha256=ticEy6RLk8B2Qos9-eXUpirpZd07Pt0N9UyYNrke_mY,12983
27
27
  pineforge_codegen/codegen/tables.py,sha256=p3BD7ln8EhmtayVBCufOKOSCmgDB10ut42E18_yI7vw,36289
@@ -29,7 +29,7 @@ pineforge_codegen/codegen/types.py,sha256=kt2tXtFhFmm4cWBuXnvPHfop-AWIB7drUiKNaD
29
29
  pineforge_codegen/codegen/visit_call.py,sha256=V21US4tHVYg5pHcD5y8Jc9yxghiBcw907TqUtNlE91Y,74801
30
30
  pineforge_codegen/codegen/visit_expr.py,sha256=kXrwIRouG4uo-e0lze23Kat3p5LFSfldgML088s4G_U,36762
31
31
  pineforge_codegen/codegen/visit_stmt.py,sha256=Vgtx_j_xYjH-qntnUlZiyYXIX9oioijN8vmBLrneYcA,38125
32
- pineforge_codegen-0.7.3.dist-info/METADATA,sha256=mwGPahKN3GtA9pg008jy9zahj9i-AsMop651FJl-FPg,17599
33
- pineforge_codegen-0.7.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
34
- pineforge_codegen-0.7.3.dist-info/licenses/LICENSE,sha256=Hf1kZ8OCaQ-nd2i92f2WEX1ZKCc6jqe-rtR4fVENQHY,7186
35
- pineforge_codegen-0.7.3.dist-info/RECORD,,
32
+ pineforge_codegen-0.7.5.dist-info/METADATA,sha256=_I6Xwm4bYMuH8WNQUk60as8KufIyRK4Ok8F_3ZurcuE,17599
33
+ pineforge_codegen-0.7.5.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
34
+ pineforge_codegen-0.7.5.dist-info/licenses/LICENSE,sha256=Hf1kZ8OCaQ-nd2i92f2WEX1ZKCc6jqe-rtR4fVENQHY,7186
35
+ pineforge_codegen-0.7.5.dist-info/RECORD,,