pineforge-codegen 0.7.3__py3-none-any.whl → 0.7.4__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.
- pineforge_codegen/__init__.py +39 -0
- pineforge_codegen/codegen/input.py +142 -1
- {pineforge_codegen-0.7.3.dist-info → pineforge_codegen-0.7.4.dist-info}/METADATA +1 -1
- {pineforge_codegen-0.7.3.dist-info → pineforge_codegen-0.7.4.dist-info}/RECORD +6 -6
- {pineforge_codegen-0.7.3.dist-info → pineforge_codegen-0.7.4.dist-info}/WHEEL +0 -0
- {pineforge_codegen-0.7.3.dist-info → pineforge_codegen-0.7.4.dist-info}/licenses/LICENSE +0 -0
pineforge_codegen/__init__.py
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
|
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
|
+
Version: 0.7.4
|
|
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=
|
|
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
|
|
@@ -21,7 +21,7 @@ pineforge_codegen/codegen/base.py,sha256=p9Tkh7VTLSTl34i-pyIYOm6PmmonAYOtRgfLKFX
|
|
|
21
21
|
pineforge_codegen/codegen/emit_top.py,sha256=htpiDaHI9q98ryd9eihKxGmVoZnbqz16vnw6Ax8L028,46076
|
|
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=
|
|
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.
|
|
33
|
-
pineforge_codegen-0.7.
|
|
34
|
-
pineforge_codegen-0.7.
|
|
35
|
-
pineforge_codegen-0.7.
|
|
32
|
+
pineforge_codegen-0.7.4.dist-info/METADATA,sha256=F5YK4_kjNonb6ItChprZzg83f28xRs3fr-yyJYtKieg,17599
|
|
33
|
+
pineforge_codegen-0.7.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
34
|
+
pineforge_codegen-0.7.4.dist-info/licenses/LICENSE,sha256=Hf1kZ8OCaQ-nd2i92f2WEX1ZKCc6jqe-rtR4fVENQHY,7186
|
|
35
|
+
pineforge_codegen-0.7.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|