@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.
Files changed (31) hide show
  1. package/glue.py +2 -1
  2. package/package.json +1 -1
  3. package/pineforge_codegen/__init__.py +24 -9
  4. package/pineforge_codegen/analyzer/base.py +190 -1
  5. package/pineforge_codegen/analyzer/call_handlers.py +2 -0
  6. package/pineforge_codegen/analyzer/contracts.py +6 -0
  7. package/pineforge_codegen/analyzer/types.py +16 -5
  8. package/pineforge_codegen/ast_nodes.py +7 -0
  9. package/pineforge_codegen/codegen/base.py +136 -14
  10. package/pineforge_codegen/codegen/checked_settings.py +272 -0
  11. package/pineforge_codegen/codegen/collection_history.py +200 -0
  12. package/pineforge_codegen/codegen/emit_top.py +76 -14
  13. package/pineforge_codegen/codegen/helpers.py +12 -2
  14. package/pineforge_codegen/codegen/host_members.py +8 -0
  15. package/pineforge_codegen/codegen/input.py +36 -3
  16. package/pineforge_codegen/codegen/security.py +56 -1
  17. package/pineforge_codegen/codegen/ta.py +65 -1
  18. package/pineforge_codegen/codegen/types.py +136 -8
  19. package/pineforge_codegen/codegen/visit_call.py +92 -3
  20. package/pineforge_codegen/codegen/visit_expr.py +202 -3
  21. package/pineforge_codegen/codegen/visit_stmt.py +19 -4
  22. package/pineforge_codegen/collection_history.py +1595 -0
  23. package/pineforge_codegen/external_requests.py +3 -0
  24. package/pineforge_codegen/parser.py +19 -4
  25. package/pineforge_codegen/request_discovery.py +506 -0
  26. package/pineforge_codegen/support_checker.py +37 -0
  27. package/pineforge_codegen-1.1.0.tar.gz +0 -0
  28. package/release.json +2 -2
  29. package/tables.json +1 -1
  30. package/transpile.worker.mjs +2 -1
  31. 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
- lines.append(f" Series<{cpp_type}> {cloned_safe}{series_suffix};")
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
- if orig_safe in [self._safe_name(n) for n in self.ctx.series_vars]:
1991
- cpp_type = self._series_type_for(orig_safe)
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),), self._infer_type(node.object), owner
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),), self._infer_type(node.object), owner
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
- lines.append(
4851
- f" {self._type_spec_to_cpp(callable_udt_spec)} {safe};"
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
- lines.append(f" {self._safe_name(udt_type)} {safe};")
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
+ ])