@pineforge/codegen-pyodide 1.0.0 → 1.1.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.
- package/glue.py +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +24 -9
- package/pineforge_codegen/analyzer/base.py +190 -1
- package/pineforge_codegen/analyzer/call_handlers.py +2 -0
- package/pineforge_codegen/analyzer/contracts.py +6 -0
- package/pineforge_codegen/analyzer/types.py +16 -5
- package/pineforge_codegen/ast_nodes.py +7 -0
- package/pineforge_codegen/codegen/base.py +136 -14
- package/pineforge_codegen/codegen/checked_settings.py +272 -0
- package/pineforge_codegen/codegen/collection_history.py +200 -0
- package/pineforge_codegen/codegen/emit_top.py +76 -14
- package/pineforge_codegen/codegen/helpers.py +12 -2
- package/pineforge_codegen/codegen/host_members.py +8 -0
- package/pineforge_codegen/codegen/input.py +36 -3
- package/pineforge_codegen/codegen/security.py +56 -1
- package/pineforge_codegen/codegen/ta.py +65 -1
- package/pineforge_codegen/codegen/types.py +136 -8
- package/pineforge_codegen/codegen/visit_call.py +92 -3
- package/pineforge_codegen/codegen/visit_expr.py +202 -3
- package/pineforge_codegen/codegen/visit_stmt.py +19 -4
- package/pineforge_codegen/collection_history.py +1595 -0
- package/pineforge_codegen/external_requests.py +3 -0
- package/pineforge_codegen/parser.py +19 -4
- package/pineforge_codegen/request_discovery.py +506 -0
- package/pineforge_codegen/support_checker.py +37 -0
- package/pineforge_codegen-1.1.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +2 -1
- package/pineforge_codegen-1.0.0.tar.gz +0 -0
|
@@ -145,6 +145,7 @@ TA_TUPLE_RESULT_TYPES = {
|
|
|
145
145
|
|
|
146
146
|
# CPP_RESERVED + the NamingHelper mixin are pulled in from helpers.py so the
|
|
147
147
|
# small naming/walk utilities can be shared with future visitor mixins.
|
|
148
|
+
from .collection_history import CollectionHistoryEmitter
|
|
148
149
|
from .helpers import (
|
|
149
150
|
CPP_RESERVED, INLINE_HISTORY_KINDS, SESSION_FLAG_MEMBERS, NamingHelper,
|
|
150
151
|
cpp_code_only, na_preserving_int_cast, pine_truth_cast,
|
|
@@ -194,7 +195,7 @@ from .drawing import DrawingVisitor
|
|
|
194
195
|
# CodeGen class
|
|
195
196
|
# ---------------------------------------------------------------------------
|
|
196
197
|
|
|
197
|
-
class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, DrawingVisitor, NamingHelper):
|
|
198
|
+
class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEmitter, TaSiteHelper, TypeInferer, InputHelper, DrawingVisitor, CollectionHistoryEmitter, NamingHelper):
|
|
198
199
|
"""Generate C++ from an AnalyzerContext (visitor pattern).
|
|
199
200
|
|
|
200
201
|
Mixin chain (Python MRO is left-to-right; method names are
|
|
@@ -1945,7 +1946,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1945
1946
|
)
|
|
1946
1947
|
elif (orig_safe in self._series_var_member_names
|
|
1947
1948
|
or vname in self.ctx.series_vars):
|
|
1948
|
-
|
|
1949
|
+
# A history-read var object keeps its references, as
|
|
1950
|
+
# its base member does.
|
|
1951
|
+
udt_t = udt_spec.name if udt_spec is not None else member_udt_type
|
|
1952
|
+
element = (self._safe_name(udt_t) if udt_t in self._udt_defs
|
|
1953
|
+
else cpp_type)
|
|
1954
|
+
lines.append(f" Series<{element}> {cloned_safe}{series_suffix};")
|
|
1949
1955
|
elif collection_spec is not None:
|
|
1950
1956
|
lines.append(
|
|
1951
1957
|
f" {self._type_spec_to_cpp(collection_spec)} {cloned_safe};"
|
|
@@ -1987,8 +1993,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1987
1993
|
lines.append(f" {cpp_type} {cloned_safe};")
|
|
1988
1994
|
return
|
|
1989
1995
|
# Non-var series var
|
|
1990
|
-
|
|
1991
|
-
|
|
1996
|
+
raw_names = [n for n in self.ctx.series_vars if self._safe_name(n) == orig_safe]
|
|
1997
|
+
if raw_names:
|
|
1998
|
+
# A history-read object or drawing (a receiver ``this`` spelled
|
|
1999
|
+
# ``pf_safe_this``) holds handles, as its base member does.
|
|
2000
|
+
handles = {self._series_handle_cpp_type(n) for n in raw_names}
|
|
2001
|
+
cpp_type = (handles.pop() if len(handles) == 1 and None not in handles
|
|
2002
|
+
else self._series_type_for(orig_safe))
|
|
1992
2003
|
lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
|
|
1993
2004
|
else:
|
|
1994
2005
|
lines.append(f" double {cloned_safe} = 0.0;")
|
|
@@ -2112,6 +2123,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2112
2123
|
declared_spec = self._type_spec_from_hint_name(stmt.type_hint)
|
|
2113
2124
|
elif isinstance(stmt.value, (Ternary, IfStmt, SwitchStmt)):
|
|
2114
2125
|
declared_spec = self._type_spec_from_expr(stmt.value)
|
|
2126
|
+
elif self._roots_at_collection_history(stmt.value):
|
|
2127
|
+
# A copy of an array's or a matrix's history, or a built-in's
|
|
2128
|
+
# result on one (``pm = m[1]``, ``c = (m[1]).copy()``).
|
|
2129
|
+
declared_spec = self._type_spec_from_expr(stmt.value)
|
|
2130
|
+
if declared_spec is not None and declared_spec.kind == "array":
|
|
2131
|
+
self._array_vars.add(stmt.name)
|
|
2132
|
+
self._collection_types.setdefault(stmt.name, declared_spec)
|
|
2133
|
+
continue
|
|
2115
2134
|
else:
|
|
2116
2135
|
continue
|
|
2117
2136
|
if declared_spec is None or declared_spec.kind != "matrix":
|
|
@@ -2225,10 +2244,17 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2225
2244
|
|
|
2226
2245
|
Walks chained ``FuncCall`` receivers (e.g. ``m.transpose().copy()``)
|
|
2227
2246
|
until it finds an ``Identifier`` so the source matrix's TypeSpec can
|
|
2228
|
-
be propagated through fluent call chains.
|
|
2247
|
+
be propagated through fluent call chains. A history read (``m[1]``)
|
|
2248
|
+
is the variable's: its copy has the variable's type.
|
|
2229
2249
|
"""
|
|
2230
2250
|
if not isinstance(call_node, FuncCall):
|
|
2231
2251
|
return None
|
|
2252
|
+
|
|
2253
|
+
def unwrap(node):
|
|
2254
|
+
if isinstance(node, Subscript) and isinstance(node.object, Identifier):
|
|
2255
|
+
return node.object
|
|
2256
|
+
return node
|
|
2257
|
+
|
|
2232
2258
|
callee = call_node.callee
|
|
2233
2259
|
# Method form: m.method(...) — possibly chained: m.foo().bar()
|
|
2234
2260
|
if isinstance(callee, MemberAccess):
|
|
@@ -2240,12 +2266,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2240
2266
|
obj = inner_callee.object
|
|
2241
2267
|
else:
|
|
2242
2268
|
break
|
|
2269
|
+
obj = unwrap(obj)
|
|
2243
2270
|
if isinstance(obj, Identifier):
|
|
2244
2271
|
if obj.name != "matrix":
|
|
2245
2272
|
return obj.name
|
|
2246
2273
|
# matrix.method(m, ...) functional form
|
|
2247
2274
|
if call_node.args:
|
|
2248
|
-
first = call_node.args[0]
|
|
2275
|
+
first = unwrap(call_node.args[0])
|
|
2249
2276
|
if isinstance(first, Identifier):
|
|
2250
2277
|
return first.name
|
|
2251
2278
|
return None
|
|
@@ -3623,13 +3650,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3623
3650
|
if len(states) > 1
|
|
3624
3651
|
}
|
|
3625
3652
|
|
|
3653
|
+
# A history parameter holding a user-defined object or a drawing keeps
|
|
3654
|
+
# the references its calls passed (``_series_param_element_cpp_type``).
|
|
3655
|
+
handle_cpp_types = {
|
|
3656
|
+
*DRAWING_TYPE_TO_CPP.values(),
|
|
3657
|
+
*(self._safe_name(name) for name in self._udt_defs),
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3626
3660
|
def register_one(
|
|
3627
3661
|
kind: str,
|
|
3628
3662
|
source_key: tuple,
|
|
3629
3663
|
cpp_type: str,
|
|
3630
3664
|
context: str | None,
|
|
3631
3665
|
) -> None:
|
|
3632
|
-
if cpp_type not in ("double", "int", "int64_t", "bool")
|
|
3666
|
+
if (cpp_type not in ("double", "int", "int64_t", "bool")
|
|
3667
|
+
and cpp_type not in handle_cpp_types):
|
|
3633
3668
|
cpp_type = "double"
|
|
3634
3669
|
key = (kind, *source_key, context)
|
|
3635
3670
|
if key in self._inline_history_member_by_key:
|
|
@@ -3721,6 +3756,27 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3721
3756
|
lexical.update(self._func_collection_types.get(owner, {}))
|
|
3722
3757
|
return lexical
|
|
3723
3758
|
|
|
3759
|
+
def owner_typed(owner: str | None, compute):
|
|
3760
|
+
"""``compute()`` with the owner's parameter types bound, as the
|
|
3761
|
+
emitter binds them: a history read of a typed parameter's field
|
|
3762
|
+
(``f(Outer p) => p.inner[1]``) types its Series by the field."""
|
|
3763
|
+
previous = self.__dict__.get("_current_func_param_specs")
|
|
3764
|
+
self._current_func_param_specs = owner_lexical_specs(owner)
|
|
3765
|
+
try:
|
|
3766
|
+
return compute()
|
|
3767
|
+
finally:
|
|
3768
|
+
if previous is None:
|
|
3769
|
+
self.__dict__.pop("_current_func_param_specs", None)
|
|
3770
|
+
else:
|
|
3771
|
+
self._current_func_param_specs = previous
|
|
3772
|
+
|
|
3773
|
+
def history_cpp_type(expr, owner: str | None) -> str:
|
|
3774
|
+
"""``_history_value_cpp_type`` of ``expr``, a reference's handle
|
|
3775
|
+
resolved with the owner's parameters bound; any other value keeps
|
|
3776
|
+
the inference every earlier build registered."""
|
|
3777
|
+
return (owner_typed(owner, lambda: self._reference_cpp_type(expr))
|
|
3778
|
+
or self._infer_type(expr))
|
|
3779
|
+
|
|
3724
3780
|
def plain_udf_info(call: FuncCall):
|
|
3725
3781
|
if not isinstance(call.callee, Identifier):
|
|
3726
3782
|
return None
|
|
@@ -3846,7 +3902,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3846
3902
|
owner = owner_by_node.get(id(node))
|
|
3847
3903
|
if isinstance(node, Subscript) and isinstance(node.object, FuncCall):
|
|
3848
3904
|
register(
|
|
3849
|
-
"hist_call", (id(node),),
|
|
3905
|
+
"hist_call", (id(node),), history_cpp_type(node.object, owner),
|
|
3906
|
+
owner,
|
|
3850
3907
|
)
|
|
3851
3908
|
elif (isinstance(node, Subscript)
|
|
3852
3909
|
and self._is_session_flag(node.object)):
|
|
@@ -3865,9 +3922,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3865
3922
|
self._session_call_flags.setdefault(owner, set()).add(flag)
|
|
3866
3923
|
register("session_call", (owner, flag), "bool", owner)
|
|
3867
3924
|
elif (isinstance(node, Subscript)
|
|
3868
|
-
and self._is_compound_history_object(node.object)
|
|
3925
|
+
and (self._is_compound_history_object(node.object)
|
|
3926
|
+
or (isinstance(node.object, (Ternary, MemberAccess))
|
|
3927
|
+
and owner_typed(owner, lambda: self._reference_cpp_type(
|
|
3928
|
+
node.object)) is not None))):
|
|
3869
3929
|
register(
|
|
3870
|
-
"hist_call", (id(node),),
|
|
3930
|
+
"hist_call", (id(node),), history_cpp_type(node.object, owner),
|
|
3931
|
+
owner,
|
|
3871
3932
|
)
|
|
3872
3933
|
|
|
3873
3934
|
if not isinstance(node, FuncCall):
|
|
@@ -4021,6 +4082,38 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4021
4082
|
("fn_global_hist", id(fi.node), name, self._current_instance_name)
|
|
4022
4083
|
)
|
|
4023
4084
|
|
|
4085
|
+
def _reference_cpp_type(self, expr) -> str | None:
|
|
4086
|
+
"""The handle type of an expression whose value is a user-defined
|
|
4087
|
+
object or a drawing reference (``Cell``, ``Box``), else None."""
|
|
4088
|
+
spec = self._type_spec_from_expr(expr)
|
|
4089
|
+
if (spec is None or spec.kind != "udt"
|
|
4090
|
+
or not (spec.name in DRAWING_TYPE_TO_CPP
|
|
4091
|
+
or spec.name in self._udt_defs)):
|
|
4092
|
+
return None
|
|
4093
|
+
return DRAWING_TYPE_TO_CPP.get(spec.name) or self._safe_name(spec.name)
|
|
4094
|
+
|
|
4095
|
+
def _registered_history_handle(self, node: Subscript) -> str | None:
|
|
4096
|
+
"""The handle type of the synthetic history Series the prepass
|
|
4097
|
+
registered for ``node`` (``_prepare_inline_history_members``), else
|
|
4098
|
+
None: emission reads the type the member was declared with."""
|
|
4099
|
+
member = self._inline_history_member_by_key.get(
|
|
4100
|
+
("hist_call", id(node), self._current_instance_name))
|
|
4101
|
+
if member is None:
|
|
4102
|
+
return None
|
|
4103
|
+
handles = {*DRAWING_TYPE_TO_CPP.values(),
|
|
4104
|
+
*(self._safe_name(name) for name in self._udt_defs)}
|
|
4105
|
+
for info in self._inline_history_members:
|
|
4106
|
+
if info["member_name"] == member:
|
|
4107
|
+
return info["cpp_type"] if info["cpp_type"] in handles else None
|
|
4108
|
+
return None
|
|
4109
|
+
|
|
4110
|
+
def _history_value_cpp_type(self, expr) -> str:
|
|
4111
|
+
"""The element type of the synthetic Series that keeps ``expr``'s
|
|
4112
|
+
history: a reference's handle (TradingView's history of such an
|
|
4113
|
+
expression is the reference it produced at its previous evaluation:
|
|
4114
|
+
fixtures/udt_history_tv udth_expr), else the scalar it infers."""
|
|
4115
|
+
return self._reference_cpp_type(expr) or self._infer_type(expr)
|
|
4116
|
+
|
|
4024
4117
|
def _is_compound_history_object(self, node) -> bool:
|
|
4025
4118
|
"""Whether ``node[k]`` is history on an operator expression or a
|
|
4026
4119
|
``session.*`` flag.
|
|
@@ -4038,6 +4131,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4038
4131
|
"""
|
|
4039
4132
|
if self._is_session_flag(node):
|
|
4040
4133
|
return True
|
|
4134
|
+
if (isinstance(node, (Ternary, MemberAccess))
|
|
4135
|
+
and self._reference_cpp_type(node) is not None):
|
|
4136
|
+
# ``(c ? a : b)[1]`` and an object-typed field's ``o.inner[1]``:
|
|
4137
|
+
# the reference the expression produced at its previous
|
|
4138
|
+
# evaluation (fixtures/udt_history_tv udth_expr).
|
|
4139
|
+
return True
|
|
4041
4140
|
return (isinstance(node, (BinOp, UnaryOp, Ternary))
|
|
4042
4141
|
and self._infer_type(node) in ("double", "int", "int64_t", "bool"))
|
|
4043
4142
|
|
|
@@ -4614,6 +4713,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4614
4713
|
# per-callsite instances are declared below inside GeneratedStrategy
|
|
4615
4714
|
# and therefore join the automatic COOF checkpoint inventory.
|
|
4616
4715
|
self._emit_lazy_source_clock_helper(lines)
|
|
4716
|
+
# The history of an array or a matrix variable: its support types
|
|
4717
|
+
# (pineforge_codegen/collection_history.py), emitted only for a script
|
|
4718
|
+
# that reads one.
|
|
4719
|
+
self._emit_collection_history_helper(lines)
|
|
4617
4720
|
# The chart-bar session type, inserted here once the class is lowered
|
|
4618
4721
|
# and known to call it (codegen/session_market.py).
|
|
4619
4722
|
_session_market_at = len(lines)
|
|
@@ -4847,9 +4950,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4847
4950
|
continue
|
|
4848
4951
|
callable_udt_spec = self._callable_var_udt_spec(name)
|
|
4849
4952
|
if callable_udt_spec is not None:
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4953
|
+
handle_cpp = self._type_spec_to_cpp(callable_udt_spec)
|
|
4954
|
+
if safe in self._series_var_member_names:
|
|
4955
|
+
# A history-read var object: its history holds the
|
|
4956
|
+
# references it held (``_series_handle_cpp_type``).
|
|
4957
|
+
lines.append(f" Series<{handle_cpp}> {safe}{_mbb};")
|
|
4958
|
+
else:
|
|
4959
|
+
lines.append(f" {handle_cpp} {safe};")
|
|
4853
4960
|
continue
|
|
4854
4961
|
# Detect array vars from init expression. Guard the substring
|
|
4855
4962
|
# heuristic against a UDT constructor that merely WRAPS array.new /
|
|
@@ -4941,7 +5048,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4941
5048
|
udt_type = udt_name
|
|
4942
5049
|
break
|
|
4943
5050
|
if udt_type:
|
|
4944
|
-
|
|
5051
|
+
if safe in self._series_var_member_names:
|
|
5052
|
+
# A history-read var object: its history holds the
|
|
5053
|
+
# references it held (``_series_handle_cpp_type``).
|
|
5054
|
+
lines.append(
|
|
5055
|
+
f" Series<{self._safe_name(udt_type)}> {safe}{_mbb};")
|
|
5056
|
+
else:
|
|
5057
|
+
lines.append(f" {self._safe_name(udt_type)} {safe};")
|
|
4945
5058
|
continue
|
|
4946
5059
|
cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
4947
5060
|
# Promote int->int64_t when init RHS is an int64-returning builtin
|
|
@@ -5150,6 +5263,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
5150
5263
|
lines.append(" DrawingArena<LabelRec> _pf_labels_{_PF_DRAWING_UNBOUNDED};")
|
|
5151
5264
|
lines.append(" DrawingArena<LinefillRec> _pf_linefills_{_PF_DRAWING_UNBOUNDED};")
|
|
5152
5265
|
|
|
5266
|
+
# 8e. The copies an array or a matrix variable's executions left
|
|
5267
|
+
# (pineforge_codegen/collection_history.py).
|
|
5268
|
+
for variable in self._collection_history_variables():
|
|
5269
|
+
lines.append(
|
|
5270
|
+
f" _PFCollectionHistory<{self._collection_history_cpp_type(variable)}> "
|
|
5271
|
+
f"{self._collection_history_member(variable)}"
|
|
5272
|
+
f"{self._collection_history_capacity(variable)};"
|
|
5273
|
+
)
|
|
5274
|
+
|
|
5153
5275
|
# 9. _var_initialized flag
|
|
5154
5276
|
if self.ctx.var_members:
|
|
5155
5277
|
lines.append(" bool _var_initialized = false;")
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Opt-in settings metadata, validation and exception-contained C exports."""
|
|
2
|
+
|
|
3
|
+
from ..ast_nodes import Identifier, MemberAccess
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def emit_settings_members(emitter, lines: list[str], constructor: list[str]) -> None:
|
|
7
|
+
lines.extend([
|
|
8
|
+
" bool _pf_setting_failed_ = false;",
|
|
9
|
+
" std::string _pf_setting_failure_;",
|
|
10
|
+
" void _pf_record_failure(const char* entrypoint, const char* message) noexcept {",
|
|
11
|
+
" try { last_error_ = entrypoint; last_error_ += \": \"; last_error_ += message; } catch (...) {}",
|
|
12
|
+
" }",
|
|
13
|
+
" void _pf_record_setting_failure(const char* entrypoint, const char* message) noexcept {",
|
|
14
|
+
" if (!_pf_setting_failed_) {",
|
|
15
|
+
" _pf_setting_failed_ = true;",
|
|
16
|
+
' try { _pf_setting_failure_ = entrypoint; _pf_setting_failure_ += ": "; _pf_setting_failure_ += message; } catch (...) {}',
|
|
17
|
+
" }",
|
|
18
|
+
" _pf_refuse_failed_setting(nullptr);",
|
|
19
|
+
" }",
|
|
20
|
+
" void _pf_require_settings_ok() const {",
|
|
21
|
+
"#ifdef PF_SETTINGS_API_VERSION",
|
|
22
|
+
' if (_pf_setting_failed_) throw ::pineforge::checked_settings::LatchedSettingsFailure(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_);',
|
|
23
|
+
"#else",
|
|
24
|
+
' if (_pf_setting_failed_) throw std::runtime_error(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_);',
|
|
25
|
+
"#endif",
|
|
26
|
+
" }",
|
|
27
|
+
" bool _pf_refuse_failed_setting(ReportC* out) noexcept {",
|
|
28
|
+
" if (!_pf_setting_failed_) return false;",
|
|
29
|
+
' try { last_error_ = _pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_; } catch (...) {}',
|
|
30
|
+
" if (out) *out = ReportC{};",
|
|
31
|
+
" return true;",
|
|
32
|
+
" }",
|
|
33
|
+
])
|
|
34
|
+
inputs = []
|
|
35
|
+
for node, binding in emitter._global_input_calls_with_names():
|
|
36
|
+
func_name, namespace = emitter._resolve_callee(node.callee)
|
|
37
|
+
name = emitter._get_input_title(node, var_name=binding)
|
|
38
|
+
key = emitter._input_key_literal(name)
|
|
39
|
+
default = emitter._get_input_default(node)
|
|
40
|
+
getter = emitter._input_getter_for_call(node, func_name, namespace)
|
|
41
|
+
default_cpp = emitter._visit_expr(default) if default is not None else "0"
|
|
42
|
+
default_cpp = emitter._coerce_string_input_default(getter, default_cpp)
|
|
43
|
+
value_type = {
|
|
44
|
+
"get_input_int": "int", "get_input_int64": "int",
|
|
45
|
+
"get_input_double": "float", "get_input_bool": "bool",
|
|
46
|
+
"get_input_string": "string",
|
|
47
|
+
}[getter]
|
|
48
|
+
kind = emitter._FORM_TYPE.get(func_name, value_type) if namespace == "input" else value_type
|
|
49
|
+
options = []
|
|
50
|
+
option_values = []
|
|
51
|
+
supported = "true"
|
|
52
|
+
default_serialized = None
|
|
53
|
+
names, merged = emitter._merged_args(node, func_name, namespace)
|
|
54
|
+
arguments = dict(zip(names or [], merged))
|
|
55
|
+
arguments.update(node.kwargs)
|
|
56
|
+
if emitter._is_source_input(node):
|
|
57
|
+
value_type = "source"
|
|
58
|
+
source = emitter._source_defval_to_base_series(default)[5:-1]
|
|
59
|
+
default_cpp = emitter._input_key_literal(source)
|
|
60
|
+
effective = f'(inputs_.count({key}) ? inputs_.at({key}) : {default_cpp})'
|
|
61
|
+
options = [emitter._input_key_literal(value)
|
|
62
|
+
for value in sorted(emitter._NATIVE_SOURCE_SERIES)]
|
|
63
|
+
elif namespace == "input" and func_name == "enum":
|
|
64
|
+
value_type = "enum"
|
|
65
|
+
declared = getattr(arguments.get("options"), "elements", None)
|
|
66
|
+
enum_members = ([default] if isinstance(default, MemberAccess) else []) + list(declared or [])
|
|
67
|
+
enum_names = {member.object.name for member in enum_members
|
|
68
|
+
if isinstance(member, MemberAccess) and isinstance(member.object, Identifier)}
|
|
69
|
+
if (len(enum_names) == 1 and enum_members
|
|
70
|
+
and all(isinstance(member, MemberAccess) and isinstance(member.object, Identifier)
|
|
71
|
+
for member in enum_members)):
|
|
72
|
+
enum_name = next(iter(enum_names))
|
|
73
|
+
members = emitter._enum_defs.get(enum_name, [])
|
|
74
|
+
selected = [member.member for member in declared] if declared is not None else members
|
|
75
|
+
options = [emitter._input_key_literal(f"{enum_name}.{member}")
|
|
76
|
+
for member in selected]
|
|
77
|
+
option_values = [emitter._input_key_literal(str(members.index(member)))
|
|
78
|
+
for member in selected if member in members]
|
|
79
|
+
supported = "true" if options and len(options) == len(option_values) else "false"
|
|
80
|
+
else:
|
|
81
|
+
supported = "false"
|
|
82
|
+
if not isinstance(default, MemberAccess):
|
|
83
|
+
supported = "false"
|
|
84
|
+
default_serialized = 'std::string("na")'
|
|
85
|
+
effective = f'(inputs_.count({key}) ? inputs_.at({key}) : std::string("na"))'
|
|
86
|
+
else:
|
|
87
|
+
effective = f'::pineforge::checked_settings::number({getter}({key}, {default_cpp}))'
|
|
88
|
+
else:
|
|
89
|
+
declared = getattr(arguments.get("options"), "elements", None)
|
|
90
|
+
if declared:
|
|
91
|
+
for option in declared:
|
|
92
|
+
option_cpp = emitter._visit_expr(option)
|
|
93
|
+
options.append(option_cpp if getter == "get_input_string" else
|
|
94
|
+
f'::pineforge::checked_settings::number({option_cpp})')
|
|
95
|
+
expression = f'{getter}({key}, {default_cpp})'
|
|
96
|
+
effective = (expression if getter == "get_input_string" else
|
|
97
|
+
f'::pineforge::checked_settings::number({expression})')
|
|
98
|
+
if default_serialized is None:
|
|
99
|
+
default_serialized = (default_cpp if value_type in ("string", "source") else
|
|
100
|
+
f'::pineforge::checked_settings::number({default_cpp})')
|
|
101
|
+
constraints = [emitter._visit_expr(arguments[name]) if arguments.get(name) is not None
|
|
102
|
+
else "std::numeric_limits<double>::quiet_NaN()"
|
|
103
|
+
for name in ("minval", "maxval", "step")]
|
|
104
|
+
metadata = (f'{{{key}, "{value_type}", {default_serialized}, '
|
|
105
|
+
f'{{{", ".join(options)}}}, {", ".join(constraints)}, '
|
|
106
|
+
f'{64 if getter == "get_input_int64" else 32}, {supported}, '
|
|
107
|
+
f'{{{", ".join(option_values)}}}, "{kind}"}}')
|
|
108
|
+
inputs.append((metadata, effective))
|
|
109
|
+
|
|
110
|
+
overrides = [
|
|
111
|
+
("initial_capital", "float", "initial_capital", "initial_capital", "0.0"),
|
|
112
|
+
("commission_value", "float", "commission_value", "commission_value", "0.0"),
|
|
113
|
+
("default_qty_value", "float", "default_qty_value", "default_qty_value", "0.0"),
|
|
114
|
+
("pyramiding", "int", "pyramiding", "pyramiding", "0.0"),
|
|
115
|
+
("slippage", "int", "slippage", "slippage", "0.0"),
|
|
116
|
+
("process_orders_on_close", "bool", "process_orders_on_close", "process_orders_on_close", "nan"),
|
|
117
|
+
("calc_on_order_fills", "bool", "calc_on_order_fills", "calc_on_order_fills", "nan"),
|
|
118
|
+
("close_entries_rule", "string", "close_entries_rule_any", "close_entries_rule", "nan"),
|
|
119
|
+
("default_qty_type", "string", "default_qty_type", "default_qty_type", "nan"),
|
|
120
|
+
("commission_type", "string", "commission_type", "commission_type", "nan"),
|
|
121
|
+
]
|
|
122
|
+
enum_options = {
|
|
123
|
+
"close_entries_rule": ["FIFO", "ANY"],
|
|
124
|
+
"default_qty_type": ["fixed", "percent_of_equity", "cash"],
|
|
125
|
+
"commission_type": ["percent", "cash_per_order", "cash_per_contract"],
|
|
126
|
+
}
|
|
127
|
+
lines.extend([
|
|
128
|
+
"#ifdef PF_SETTINGS_API_VERSION",
|
|
129
|
+
" static pineforge::source::PineStrategyConfig _pf_settings_declared_config() {",
|
|
130
|
+
" pineforge::source::PineStrategyConfig cfg{};",
|
|
131
|
+
])
|
|
132
|
+
lines.extend(statement for statement in constructor if statement.startswith(" cfg."))
|
|
133
|
+
lines.extend([
|
|
134
|
+
" return cfg;", " }",
|
|
135
|
+
" std::vector<::pineforge::checked_settings::Setting> _pf_settings_inputs() const {",
|
|
136
|
+
" return {",
|
|
137
|
+
])
|
|
138
|
+
lines.extend(f" {metadata}," for metadata, _effective in inputs)
|
|
139
|
+
lines.extend([
|
|
140
|
+
" };", " }",
|
|
141
|
+
" std::vector<::pineforge::checked_settings::Setting> _pf_settings_overrides() const {",
|
|
142
|
+
" const double _pf_nan = std::numeric_limits<double>::quiet_NaN();",
|
|
143
|
+
" const auto _pf_defaults = _pf_settings_declared_config();",
|
|
144
|
+
" return {",
|
|
145
|
+
])
|
|
146
|
+
override_effective = []
|
|
147
|
+
for name, value_type, config_field, override_field, minimum in overrides:
|
|
148
|
+
raw = f'config_.{config_field}'
|
|
149
|
+
declared_default = f'_pf_defaults.{config_field}'
|
|
150
|
+
overridden = f'override_.{override_field}'
|
|
151
|
+
selected = (f'(std::isnan({overridden}) ? {raw} : {overridden})'
|
|
152
|
+
if value_type == "float" else
|
|
153
|
+
f'({overridden} < 0 ? {raw} : {overridden})')
|
|
154
|
+
if name in enum_options:
|
|
155
|
+
options = enum_options[name]
|
|
156
|
+
default_value = f'_pf_{name}_word({declared_default})'
|
|
157
|
+
effective = f'_pf_{name}_word({selected})'
|
|
158
|
+
else:
|
|
159
|
+
options = []
|
|
160
|
+
default_value = f'::pineforge::checked_settings::number({declared_default})'
|
|
161
|
+
effective = f'::pineforge::checked_settings::number({selected})'
|
|
162
|
+
if value_type == "bool":
|
|
163
|
+
effective = f'::pineforge::checked_settings::number(static_cast<bool>({selected}))'
|
|
164
|
+
option_cpp = ", ".join(emitter._input_key_literal(option) for option in options)
|
|
165
|
+
floor = "_pf_nan" if minimum == "nan" else minimum
|
|
166
|
+
lines.append(f' {{"{name}", "{value_type}", {default_value}, '
|
|
167
|
+
f'{{{option_cpp}}}, {floor}}},')
|
|
168
|
+
override_effective.append(effective)
|
|
169
|
+
lines.extend([" };", " }"])
|
|
170
|
+
for name, options in enum_options.items():
|
|
171
|
+
lines.append(f' static std::string _pf_{name}_word(int _pf_value) {{')
|
|
172
|
+
for index, option in enumerate(options):
|
|
173
|
+
lines.append(f' if (_pf_value == {index}) return "{option}";')
|
|
174
|
+
lines.extend([' return "invalid";', ' }'])
|
|
175
|
+
lines.extend([
|
|
176
|
+
" void _pf_set_input_checked(const std::string& _pf_key, const std::string& _pf_value) {",
|
|
177
|
+
" if (_pf_refuse_failed_setting(nullptr)) throw ::pineforge::checked_settings::Error{PF_SETTINGS_RUN_FAILED, last_error_.c_str()};",
|
|
178
|
+
' ::pineforge::checked_settings::require(script_bars_processed() == 0 && stream_phase_ == StreamPhase::IDLE, "settings are frozen after execution begins", PF_SETTINGS_UNSUPPORTED);',
|
|
179
|
+
" const auto _pf_inputs = _pf_settings_inputs();",
|
|
180
|
+
" const ::pineforge::checked_settings::Setting* _pf_match = nullptr;",
|
|
181
|
+
" for (const auto& _pf_input : _pf_inputs) {",
|
|
182
|
+
" if (_pf_input.name != _pf_key) continue;",
|
|
183
|
+
' ::pineforge::checked_settings::require(_pf_match == nullptr, "ambiguous input key", PF_SETTINGS_UNSUPPORTED);',
|
|
184
|
+
" _pf_match = &_pf_input;",
|
|
185
|
+
" }",
|
|
186
|
+
' ::pineforge::checked_settings::require(_pf_match != nullptr, "unknown input key");',
|
|
187
|
+
" const auto _pf_canonical = ::pineforge::checked_settings::validate(*_pf_match, _pf_value);",
|
|
188
|
+
" set_input(_pf_key, _pf_canonical);",
|
|
189
|
+
' ::pineforge::checked_settings::require(inputs_.count(_pf_key) && inputs_.at(_pf_key) == _pf_canonical, "input was not installed", PF_SETTINGS_UNSUPPORTED);',
|
|
190
|
+
" }",
|
|
191
|
+
" void _pf_set_override_checked(const std::string& _pf_key, const std::string& _pf_value) {",
|
|
192
|
+
" if (_pf_refuse_failed_setting(nullptr)) throw ::pineforge::checked_settings::Error{PF_SETTINGS_RUN_FAILED, last_error_.c_str()};",
|
|
193
|
+
' ::pineforge::checked_settings::require(script_bars_processed() == 0 && stream_phase_ == StreamPhase::IDLE, "settings are frozen after execution begins", PF_SETTINGS_UNSUPPORTED);',
|
|
194
|
+
" for (const auto& _pf_override : _pf_settings_overrides()) {",
|
|
195
|
+
" if (_pf_override.name != _pf_key) continue;",
|
|
196
|
+
" auto _pf_alias = _pf_value;",
|
|
197
|
+
])
|
|
198
|
+
for name, options in enum_options.items():
|
|
199
|
+
lines.append(f' if (_pf_key == "{name}") {{')
|
|
200
|
+
for index, option in enumerate(options):
|
|
201
|
+
aliases = [str(index)]
|
|
202
|
+
if name == "close_entries_rule":
|
|
203
|
+
aliases.append(option.lower())
|
|
204
|
+
else:
|
|
205
|
+
prefix = "strategy.commission." if name == "commission_type" else "strategy."
|
|
206
|
+
aliases.append(prefix + option)
|
|
207
|
+
condition = " || ".join(f'_pf_value == "{alias}"' for alias in aliases)
|
|
208
|
+
lines.append(f' if ({condition}) _pf_alias = "{option}";')
|
|
209
|
+
lines.append(" }")
|
|
210
|
+
lines.extend([
|
|
211
|
+
" const auto _pf_canonical = ::pineforge::checked_settings::validate(_pf_override, _pf_alias);",
|
|
212
|
+
" set_strategy_override(_pf_key, _pf_canonical);",
|
|
213
|
+
" return;",
|
|
214
|
+
" }",
|
|
215
|
+
' throw ::pineforge::checked_settings::Error{PF_SETTINGS_INVALID_ARGUMENT, "unknown override key"};',
|
|
216
|
+
" }",
|
|
217
|
+
" std::string _pf_settings_receipt() const {",
|
|
218
|
+
' ::pineforge::checked_settings::require(!_pf_setting_failed_, _pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_.c_str(), PF_SETTINGS_RUN_FAILED);',
|
|
219
|
+
' std::string _pf_document = "{\\\"version\\\":1,\\\"inputs\\\":[";',
|
|
220
|
+
" const auto _pf_inputs = _pf_settings_inputs();",
|
|
221
|
+
])
|
|
222
|
+
for index, (_metadata, effective) in enumerate(inputs):
|
|
223
|
+
if index:
|
|
224
|
+
lines.append(" _pf_document += ',';")
|
|
225
|
+
lines.append(f" _pf_document += ::pineforge::checked_settings::describe(_pf_inputs[{index}], {effective});")
|
|
226
|
+
lines.extend([
|
|
227
|
+
' _pf_document += "],\\\"overrides\\\":[";',
|
|
228
|
+
" const auto _pf_overrides = _pf_settings_overrides();",
|
|
229
|
+
])
|
|
230
|
+
for index, effective in enumerate(override_effective):
|
|
231
|
+
if index:
|
|
232
|
+
lines.append(" _pf_document += ',';")
|
|
233
|
+
lines.append(f" _pf_document += ::pineforge::checked_settings::describe(_pf_overrides[{index}], {effective});")
|
|
234
|
+
lines.extend([' return _pf_document + "]}";', " }", "#endif"])
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def emit_settings_exports(lines: list[str]) -> None:
|
|
238
|
+
lines.extend([
|
|
239
|
+
"#ifdef PF_SETTINGS_API_VERSION",
|
|
240
|
+
" uint32_t strategy_settings_api_version(void) { return PF_SETTINGS_API_VERSION; }",
|
|
241
|
+
" int strategy_create_checked(const char* params_json, void** out, char* error, size_t error_capacity) {",
|
|
242
|
+
" if (out) *out = nullptr;",
|
|
243
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
244
|
+
' ::pineforge::checked_settings::require(out != nullptr, "strategy output pointer is null");',
|
|
245
|
+
' ::pineforge::checked_settings::require(!params_json || !*params_json, "params_json is reserved; use checked setters", PF_SETTINGS_UNSUPPORTED);',
|
|
246
|
+
" *out = new GeneratedStrategy();",
|
|
247
|
+
" });",
|
|
248
|
+
" }",
|
|
249
|
+
])
|
|
250
|
+
for kind in ("input", "override"):
|
|
251
|
+
lines.extend([
|
|
252
|
+
f" int strategy_set_{kind}_checked(void* s, const char* key, const char* value, char* error, size_t error_capacity) {{",
|
|
253
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
254
|
+
' ::pineforge::checked_settings::require(s && key && value, "null strategy, key or value");',
|
|
255
|
+
f" static_cast<GeneratedStrategy*>(s)->_pf_set_{kind}_checked(key, value);",
|
|
256
|
+
" });", " }",
|
|
257
|
+
])
|
|
258
|
+
lines.extend([
|
|
259
|
+
" int strategy_get_effective_settings(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
|
|
260
|
+
" if (required) *required = 0;",
|
|
261
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
262
|
+
' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
|
|
263
|
+
" ::pineforge::checked_settings::receipt(static_cast<GeneratedStrategy*>(s)->_pf_settings_receipt(), json, capacity, required);",
|
|
264
|
+
" });", " }",
|
|
265
|
+
" int run_backtest_full_checked(void* s, Bar* bars, int n, const char* input_tf, const char* script_tf, int bar_magnifier, int magnifier_samples, int magnifier_dist, ReportC* out, char* error, size_t error_capacity) {",
|
|
266
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
267
|
+
' ::pineforge::checked_settings::require(s && out && n >= 0 && (n == 0 || bars), "invalid batch arguments");',
|
|
268
|
+
" _pf_run_backtest_full_impl(s, bars, n, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist, out);",
|
|
269
|
+
" const auto& _pf_error = static_cast<GeneratedStrategy*>(s)->last_error();",
|
|
270
|
+
' ::pineforge::checked_settings::require(_pf_error.empty(), _pf_error.c_str(), PF_SETTINGS_RUN_FAILED);',
|
|
271
|
+
" });", " }", "#endif",
|
|
272
|
+
])
|