@pineforge/codegen-pyodide 1.3.0 → 1.4.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 (30) hide show
  1. package/package.json +1 -1
  2. package/pineforge_codegen/__init__.py +6 -5
  3. package/pineforge_codegen/analyzer/base.py +3 -3
  4. package/pineforge_codegen/analyzer/types.py +7 -3
  5. package/pineforge_codegen/codegen/base.py +26 -16
  6. package/pineforge_codegen/codegen/checked_settings.py +113 -2
  7. package/pineforge_codegen/codegen/collection_history.py +163 -12
  8. package/pineforge_codegen/codegen/emit_top.py +24 -8
  9. package/pineforge_codegen/codegen/helpers.py +19 -1
  10. package/pineforge_codegen/codegen/host_members.py +1 -0
  11. package/pineforge_codegen/codegen/input.py +23 -8
  12. package/pineforge_codegen/codegen/run_stops.py +52 -0
  13. package/pineforge_codegen/codegen/security.py +12 -5
  14. package/pineforge_codegen/codegen/tables.py +132 -28
  15. package/pineforge_codegen/codegen/tv_number_format.py +9 -2
  16. package/pineforge_codegen/codegen/types.py +85 -4
  17. package/pineforge_codegen/codegen/visit_call.py +174 -55
  18. package/pineforge_codegen/codegen/visit_expr.py +32 -6
  19. package/pineforge_codegen/codegen/visit_stmt.py +3 -0
  20. package/pineforge_codegen/collection_history.py +122 -20
  21. package/pineforge_codegen/diagnostics_catalog.json +1 -0
  22. package/pineforge_codegen/external_requests.py +30 -9
  23. package/pineforge_codegen/matrix_overloads.py +7 -0
  24. package/pineforge_codegen/parser.py +7 -2
  25. package/pineforge_codegen/signatures.py +2 -2
  26. package/pineforge_codegen/support_checker.py +82 -0
  27. package/pineforge_codegen-1.4.0.tar.gz +0 -0
  28. package/release.json +2 -2
  29. package/tables.json +7 -3
  30. package/pineforge_codegen-1.3.0.tar.gz +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pineforge/codegen-pyodide",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Gate-validated Pyodide payload for the PineScript v6 -> C++ transpiler: archive (run in Pyodide), unpacked source, introspected tables, and release metadata.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -164,11 +164,12 @@ def transpile_full(pine_source: str, *, check_support: bool = True,
164
164
  - ``cpp``: the generated C++ source (identical to :func:`transpile`).
165
165
  - ``inputs``: a list of ``InputDef`` dicts (one per global-scope
166
166
  ``input(...)`` / ``input.*(...)`` call, inline calls included, in
167
- source order). Each has ``title`` / ``type`` /
168
- ``default`` and optionally ``min`` / ``max`` / ``step`` / ``options``
169
- (omitted when the corresponding signature argument is absent or
170
- references a non-const value); an ``input.symbol`` entry also has
171
- ``kind: "symbol"``. See :meth:`CodeGen.extract_input_manifest`.
167
+ source order). Each has ``title`` / ``type`` / ``default`` /
168
+ ``supported`` (the checked-settings receipt's flag) and optionally
169
+ ``min`` / ``max`` / ``step`` / ``options`` (omitted when the
170
+ corresponding signature argument is absent or is not a literal the
171
+ receipt holds); an ``input.symbol`` entry also has ``kind: "symbol"``.
172
+ See :meth:`CodeGen.extract_input_manifest`.
172
173
  - ``strategyParams``: the literal ``strategy(...)`` kwargs the analyzer
173
174
  surfaced (e.g. ``initial_capital``, ``pyramiding``).
174
175
  - ``diagnostics``: the warnings (:class:`~pineforge_codegen.errors.Diagnostic`,
@@ -1758,7 +1758,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
1758
1758
  for collection_owner, collection_names in persistent_collections.items():
1759
1759
  if series_owner == collection_owner:
1760
1760
  continue
1761
- for exact_name in exact_series_names & collection_names:
1761
+ for exact_name in sorted(exact_series_names & collection_names):
1762
1762
  raw_name = next(
1763
1763
  (
1764
1764
  name for name in names
@@ -4999,7 +4999,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
4999
4999
  # ``exit_scope()`` can bind a same-named top-level collection
5000
5000
  # and falsely turn a scalar UDF return into an array.
5001
5001
  ret_spec = terminal_direct_return_spec
5002
- if ret_spec is not None and ret_spec.kind == "array":
5002
+ if ret_spec is not None and ret_spec.kind in ("array", "matrix"):
5003
5003
  self._func_return_type_specs[node.name] = ret_spec
5004
5004
  if (terminal_direct_return_spec is not None
5005
5005
  and terminal_direct_return_spec.kind == "map"):
@@ -5150,7 +5150,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
5150
5150
  ret_type = self._statement_value_type(node.body[-1], ret_type)
5151
5151
  if terminal_ret_expr is not None:
5152
5152
  terminal_spec = self._type_spec_from_expr(terminal_ret_expr)
5153
- if terminal_spec is not None and terminal_spec.kind == "map":
5153
+ if terminal_spec is not None and terminal_spec.kind in ("map", "matrix"):
5154
5154
  return_type_spec = terminal_spec
5155
5155
  if terminal_spec is not None and terminal_spec.kind == "udt":
5156
5156
  # Methods may return ``self`` or another UDT-typed
@@ -45,6 +45,7 @@ from ..ast_nodes import (
45
45
  SwitchStmt, TupleLiteral, UnaryOp,
46
46
  )
47
47
  from ..errors import Phase
48
+ from ..matrix_overloads import matrix_sum_has_rhs
48
49
  from ..symbols import PineType, TypeSpec, method_receiver_type_name
49
50
 
50
51
  # Drawing-objects-as-data type names (spec §4.1). Defined locally — the
@@ -537,8 +538,10 @@ class TypeHelper:
537
538
  else:
538
539
  elem = TypeSpec.primitive("float")
539
540
  return TypeSpec.matrix(elem)
540
- if ns == "matrix" and func in _MATRIX_RETURNING_METHODS:
541
- receiver = value.args[0] if value.args else value.kwargs.get("id")
541
+ if ns == "matrix" and (func in _MATRIX_RETURNING_METHODS or (
542
+ func == "sum" and matrix_sum_has_rhs(value, namespace=True))):
543
+ receiver = (value.args[0] if value.args else
544
+ value.kwargs.get("id1", value.kwargs.get("id")))
542
545
  receiver_spec = self._type_spec_from_expr(receiver)
543
546
  if receiver_spec is not None and receiver_spec.kind == "matrix":
544
547
  return receiver_spec
@@ -607,7 +610,8 @@ class TypeHelper:
607
610
  if func == "size":
608
611
  return TypeSpec.primitive("int")
609
612
  if recv_spec is not None and recv_spec.kind == "matrix":
610
- if func in _MATRIX_RETURNING_METHODS:
613
+ if func in _MATRIX_RETURNING_METHODS or (
614
+ func == "sum" and matrix_sum_has_rhs(value)):
611
615
  return recv_spec
612
616
  if func in ("row", "col"):
613
617
  return TypeSpec.array(recv_spec.element)
@@ -31,6 +31,7 @@ from ..symbols import PineType, TypeSpec, method_receiver_type_name
31
31
  from .. import signatures as sigs
32
32
  from ..errors import CompileError, Diagnostic, Level, Phase, SourceLocation
33
33
  from ..limits import TimeBudget, iter_ast_nodes
34
+ from ..matrix_overloads import matrix_sum_has_rhs
34
35
  from ..session_reads import emitted_session_reads
35
36
  from ..block_locals import block_declarations, decl_key
36
37
  from ..pine_spelling import (
@@ -1082,7 +1083,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1082
1083
  # (index, signature) of each TA variant computed inline, and
1083
1084
  # (index, _SECURITY_THROUGH_GLOBAL) of a global's site a
1084
1085
  # multi-statement helper reads, which the prologue computes.
1085
- "inline_helper_ta_indices": sorted(inline_helper_ta_indices, key=repr),
1086
+ "inline_helper_ta_indices": inline_helper_ta_indices,
1086
1087
  "depends_on_mutable_globals": item.get("depends_on_mutable_globals", False),
1087
1088
  "mutable_globals": list(item.get("mutable_globals", [])),
1088
1089
  "is_lower_tf_array": bool(item.get("is_lower_tf_array", False)),
@@ -2156,6 +2157,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2156
2157
  # to the analyzer's default ``double`` and the emitted C++
2157
2158
  # fails to compile (``double = PineMatrix``).
2158
2159
  or fn in MATRIX_RETURNING_METHODS
2160
+ or (fn == "sum" and matrix_sum_has_rhs(expr, namespace=True))
2159
2161
  ):
2160
2162
  if fn == "new":
2161
2163
  targs = self._template_args_from_call(expr) if hasattr(expr, "annotations") else []
@@ -2213,7 +2215,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2213
2215
  outer_callee = expr.callee
2214
2216
  if (
2215
2217
  isinstance(outer_callee, MemberAccess)
2216
- and outer_callee.member in MATRIX_RETURNING_METHODS
2218
+ and (outer_callee.member in MATRIX_RETURNING_METHODS or (
2219
+ outer_callee.member == "sum" and matrix_sum_has_rhs(expr)))
2217
2220
  ):
2218
2221
  recv_name2 = self._extract_receiver_name(expr)
2219
2222
  if recv_name2 is not None and recv_name2 in self._matrix_specs:
@@ -4553,7 +4556,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4553
4556
  "",
4554
4557
  " Snapshot snapshot() {",
4555
4558
  " if (_pf_generation_ == std::numeric_limits<uint64_t>::max()) {",
4556
- ' throw std::overflow_error("UDT checkpoint generation exhausted");',
4559
+ ' _PF_ENGINE_INVARIANT("UDT checkpoint generation exhausted", std::overflow_error);',
4557
4560
  " }",
4558
4561
  " ++_pf_generation_;",
4559
4562
  " _pf_undo_.clear();",
@@ -4565,13 +4568,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4565
4568
  " bool empty() const { return _pf_undo_.empty(); }",
4566
4569
  " void record(uint64_t generation, std::function<void()> undo) {",
4567
4570
  " if (!_pf_active_ || generation != _pf_generation_) {",
4568
- ' throw std::runtime_error("invalid UDT undo generation");',
4571
+ ' _PF_ENGINE_INVARIANT("invalid UDT undo generation", std::runtime_error);',
4569
4572
  " }",
4570
4573
  " _pf_undo_.push_back(std::move(undo));",
4571
4574
  " }",
4572
4575
  " void restore(const Snapshot& snapshot) {",
4573
4576
  " if (!_pf_active_ || snapshot.generation != _pf_generation_) {",
4574
- ' throw std::runtime_error("invalid UDT coordinator checkpoint token");',
4577
+ ' _PF_ENGINE_INVARIANT("invalid UDT coordinator checkpoint token", std::runtime_error);',
4575
4578
  " }",
4576
4579
  " for (auto entry = _pf_undo_.rbegin();",
4577
4580
  " entry != _pf_undo_.rend(); ++entry) (*entry)();",
@@ -4595,13 +4598,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4595
4598
  "",
4596
4599
  " void capture(std::size_t index) {",
4597
4600
  " if (!_pf_checkpoint_active_) return;",
4598
- " auto& slot = _pf_records_.at(index);",
4601
+ " auto& slot = _PF_INVARIANT_AT(_pf_records_, index);",
4599
4602
  " if (slot.logged_generation",
4600
4603
  " == _pf_checkpoint_generation_) return;",
4601
4604
  " auto snapshot = _PFRecordTraits::take(slot.value);",
4602
4605
  " _pf_coordinator_->record(_pf_checkpoint_generation_,",
4603
4606
  " [this, index, snapshot = std::move(snapshot)]() mutable {",
4604
- " auto& restore_slot = _pf_records_.at(index);",
4607
+ " auto& restore_slot = _PF_INVARIANT_AT(_pf_records_, index);",
4605
4608
  " _PFRecordTraits::restore(restore_slot.value, snapshot);",
4606
4609
  " restore_slot.logged_generation = 0;",
4607
4610
  " });",
@@ -4617,7 +4620,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4617
4620
  f" {self._udt_undo_coordinator_cpp_name}* coordinator)",
4618
4621
  " : _pf_coordinator_(coordinator) {",
4619
4622
  " if (!_pf_coordinator_)",
4620
- ' throw std::invalid_argument("UDT arena requires undo coordinator");',
4623
+ ' _PF_ENGINE_INVARIANT("UDT arena requires undo coordinator", std::invalid_argument);',
4621
4624
  " }",
4622
4625
  f" {self._udt_arena_template_cpp_name}(",
4623
4626
  f" const {self._udt_arena_template_cpp_name}&) = delete;",
@@ -4639,7 +4642,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4639
4642
  " _PFHandle create(_PFRecord value) {",
4640
4643
  " if (_pf_records_.size() > static_cast<std::size_t>(",
4641
4644
  " std::numeric_limits<int32_t>::max())) {",
4642
- ' throw std::length_error("UDT object-ID capacity exceeded");',
4645
+ ' _PF_LIMIT_STOP("udt_objects", 2147483647LL, "UDT object-ID capacity exceeded");',
4643
4646
  " }",
4644
4647
  " const auto id = static_cast<int32_t>(_pf_records_.size());",
4645
4648
  " _pf_records_.push_back(_PFSlot{std::move(value), 0});",
@@ -4652,7 +4655,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4652
4655
  " _PFRecord& get(_PFHandle value) {",
4653
4656
  " if (value.__pf_id < 0",
4654
4657
  " || static_cast<std::size_t>(value.__pf_id) >= _pf_records_.size()) {",
4655
- ' throw std::runtime_error("UDT access on na or invalid object ID");',
4658
+ ' _PF_NA_STOP("udt_object", "UDT access on na or invalid object ID");',
4656
4659
  " }",
4657
4660
  " const auto index = static_cast<std::size_t>(value.__pf_id);",
4658
4661
  " capture(index);",
@@ -4661,7 +4664,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4661
4664
  " const _PFRecord& get(_PFHandle value) const {",
4662
4665
  " if (value.__pf_id < 0",
4663
4666
  " || static_cast<std::size_t>(value.__pf_id) >= _pf_records_.size()) {",
4664
- ' throw std::runtime_error("UDT access on na or invalid object ID");',
4667
+ ' _PF_NA_STOP("udt_object", "UDT access on na or invalid object ID");',
4665
4668
  " }",
4666
4669
  " return _pf_records_[static_cast<std::size_t>(value.__pf_id)].value;",
4667
4670
  " }",
@@ -4671,14 +4674,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4671
4674
  " std::size_t size() const { return _pf_records_.size(); }",
4672
4675
  " _PFRecord& record_at(std::size_t index) {",
4673
4676
  " capture(index);",
4674
- " return _pf_records_.at(index).value;",
4677
+ " return _PF_INVARIANT_AT(_pf_records_, index).value;",
4675
4678
  " }",
4676
4679
  " const _PFRecord& record_at(std::size_t index) const {",
4677
- " return _pf_records_.at(index).value;",
4680
+ " return _PF_INVARIANT_AT(_pf_records_, index).value;",
4678
4681
  " }",
4679
4682
  " Snapshot snapshot() {",
4680
4683
  " if (!_pf_coordinator_->active()) {",
4681
- ' throw std::runtime_error("UDT coordinator checkpoint is not active");',
4684
+ ' _PF_ENGINE_INVARIANT("UDT coordinator checkpoint is not active", std::runtime_error);',
4682
4685
  " }",
4683
4686
  " _pf_checkpoint_generation_ = _pf_coordinator_->generation();",
4684
4687
  " _pf_checkpoint_size_ = _pf_records_.size();",
@@ -4693,7 +4696,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4693
4696
  " || snapshot.size != _pf_checkpoint_size_",
4694
4697
  " || _pf_records_.size() < snapshot.size",
4695
4698
  " || !_pf_coordinator_->empty()) {",
4696
- ' throw std::runtime_error("invalid UDT checkpoint token");',
4699
+ ' _PF_ENGINE_INVARIANT("invalid UDT checkpoint token", std::runtime_error);',
4697
4700
  " }",
4698
4701
  " _pf_records_.resize(snapshot.size);",
4699
4702
  " }",
@@ -4984,6 +4987,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4984
4987
  if name in self._global_collection_types
4985
4988
  else self._callable_var_collection_spec(name)
4986
4989
  )
4990
+ history_value_cpp = self._array_history_value_cpp_type(name)
4991
+ if history_value_cpp is not None:
4992
+ lines.append(f" {history_value_cpp} {safe};")
4993
+ continue
4987
4994
  if callable_collection_spec is not None:
4988
4995
  lines.append(
4989
4996
  f" {self._type_spec_to_cpp(callable_collection_spec)} {safe};"
@@ -5182,7 +5189,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5182
5189
  seen_global.add(name)
5183
5190
  safe = self._safe_name(name)
5184
5191
 
5185
- if name in self._matrix_specs:
5192
+ history_value_cpp = self._array_history_value_cpp_type(name)
5193
+ if history_value_cpp is not None:
5194
+ lines.append(f" {history_value_cpp} {safe};")
5195
+ elif name in self._matrix_specs:
5186
5196
  lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[name])} {safe};")
5187
5197
  elif name in self._array_vars:
5188
5198
  lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(name))} {safe};")
@@ -7,8 +7,12 @@ from ..errors import CompileError
7
7
  from .tables import ALERT_FREQ_VALUES, NAME_ECHO_STRING_MEMBERS, ORDER_DIRECTION_MAP
8
8
 
9
9
 
10
- _QUOTED_STRING = r'"(?:[^"\\]|\\.)*"'
10
+ _QUOTED_STRING = r'"(?:[^"\\\x00\r\n]|\\(?:["\\nrt]|000))*"'
11
11
  _STRING_LITERAL = re.compile(rf'(?:std::string\({_QUOTED_STRING}\)|{_QUOTED_STRING})')
12
+ _STRING_ESCAPE = re.compile(r'\\(?:["\\nrt]|000)')
13
+ _STRING_ESCAPE_VALUES = {
14
+ r'\"': '"', r'\\': '\\', r'\n': '\n', r'\r': '\r', r'\t': '\t', r'\000': '\0',
15
+ }
12
16
 
13
17
 
14
18
  def _string_setting_arg(expr, lowered: str) -> str | None:
@@ -45,22 +49,97 @@ def _visit_setting_arg(emitter, expr) -> str:
45
49
  return "0"
46
50
 
47
51
 
52
+ def _string_setting_value(lowered: str) -> str:
53
+ """Decode the emitter's literals as native std::string(const char*) values.
54
+
55
+ Only the serializer's escape grammar is accepted; Unicode stays literal.
56
+ The receipt's string constructors stop at the first NUL, including one
57
+ followed by digits, while an escaped backslash remains ordinary data.
58
+ """
59
+ if not _STRING_LITERAL.fullmatch(lowered):
60
+ raise ValueError("setting value is not an emitted C++ string literal")
61
+ literal = lowered[len("std::string("):-1] if lowered.startswith("std::string(") else lowered
62
+ decoded = _STRING_ESCAPE.sub(lambda match: _STRING_ESCAPE_VALUES[match.group()], literal[1:-1])
63
+ return decoded.partition("\0")[0]
64
+
65
+
66
+ _NUMERIC_LITERAL = re.compile(r"[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?(?:LL|ULL|L|UL|[fF])?")
67
+ _NUMERIC_SUFFIX = re.compile(r"(?:LL|ULL|L|UL|[fF])$")
68
+
69
+
70
+ def _numeric_setting_value(lowered: str) -> int | float | bool | None:
71
+ """The number or bool a lowered C++ setting expression is, or None.
72
+
73
+ Only a plain literal, signed or in parentheses, is read: what the codegen
74
+ emits for a literal or for a named constant it inlines. Anything the C++
75
+ computes at run time (an arithmetic expression, a call, a series) stays
76
+ with the receipt, which evaluates it.
77
+ """
78
+ text = lowered.strip()
79
+ while text.startswith("(") and text.endswith(")"):
80
+ text = text[1:-1].strip()
81
+ if text in ("true", "false"):
82
+ return text == "true"
83
+ if not _NUMERIC_LITERAL.fullmatch(text):
84
+ return None
85
+ text = _NUMERIC_SUFFIX.sub("", text)
86
+ return float(text) if re.search(r"[.eE]", text) else int(text)
87
+
88
+
89
+ def _is_number(value) -> bool:
90
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
91
+
92
+
48
93
  def emit_settings_members(emitter, lines: list[str], constructor: list[str]) -> None:
49
94
  lines.extend([
50
95
  " bool _pf_setting_failed_ = false;",
51
96
  " std::string _pf_setting_failure_;",
97
+ "#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
98
+ " ::pineforge::RunFailureInfo _pf_setting_failure_info_{::pineforge::RunFailureCode::none, {}};",
99
+ "#ifdef PF_SETTINGS_API_VERSION",
100
+ " using _pf_setting_error_base = ::pineforge::checked_settings::LatchedSettingsFailure;",
101
+ "#else",
102
+ " using _pf_setting_error_base = std::runtime_error;",
103
+ "#endif",
104
+ " struct _pf_latched_setting_error : _pf_setting_error_base, ::pineforge::RunFailureInfo {",
105
+ " _pf_latched_setting_error(const std::string& text, const ::pineforge::RunFailureInfo& info)",
106
+ " : _pf_setting_error_base(text), ::pineforge::RunFailureInfo(info) {}",
107
+ " };",
108
+ "#endif",
52
109
  " void _pf_record_failure(const char* entrypoint, const char* message) noexcept {",
110
+ "#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
111
+ " try { ::pineforge::note_run_failure_unknown(*this, entrypoint); } catch (...) {}",
112
+ "#else",
53
113
  " try { last_error_ = entrypoint; last_error_ += \": \"; last_error_ += message; } catch (...) {}",
114
+ "#endif",
115
+ " }",
116
+ " void _pf_record_failure(const char* entrypoint, const std::exception& error) noexcept {",
117
+ "#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
118
+ " try { ::pineforge::note_run_failure(*this, entrypoint, error); } catch (...) {}",
119
+ "#else",
120
+ " _pf_record_failure(entrypoint, error.what());",
121
+ "#endif",
54
122
  " }",
123
+ "#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
124
+ " template <typename FailureInfo>",
125
+ " void _pf_record_setting_failure(const char* entrypoint, const char* message, FailureInfo failure_info) noexcept {",
126
+ "#else",
55
127
  " void _pf_record_setting_failure(const char* entrypoint, const char* message) noexcept {",
128
+ "#endif",
56
129
  " if (!_pf_setting_failed_) {",
57
130
  " _pf_setting_failed_ = true;",
58
131
  ' try { _pf_setting_failure_ = entrypoint; _pf_setting_failure_ += ": "; _pf_setting_failure_ += message; } catch (...) {}',
132
+ "#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
133
+ " try { _pf_setting_failure_info_ = failure_info(); }",
134
+ " catch (...) { _pf_setting_failure_info_ = ::pineforge::RunFailureInfo(::pineforge::RunFailureCode::out_of_memory, {}); }",
135
+ "#endif",
59
136
  " }",
60
137
  " _pf_refuse_failed_setting(nullptr);",
61
138
  " }",
62
139
  " void _pf_require_settings_ok() const {",
63
- "#ifdef PF_SETTINGS_API_VERSION",
140
+ "#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
141
+ ' if (_pf_setting_failed_) throw _pf_latched_setting_error(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_, _pf_setting_failure_info_);',
142
+ "#elif defined(PF_SETTINGS_API_VERSION)",
64
143
  ' if (_pf_setting_failed_) throw ::pineforge::checked_settings::LatchedSettingsFailure(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_);',
65
144
  "#else",
66
145
  ' if (_pf_setting_failed_) throw std::runtime_error(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_);',
@@ -68,12 +147,17 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
68
147
  " }",
69
148
  " bool _pf_refuse_failed_setting(ReportC* out) noexcept {",
70
149
  " if (!_pf_setting_failed_) return false;",
150
+ "#ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1",
151
+ ' try { ::pineforge::note_run_failure(*this, _pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_, _pf_setting_failure_info_.run_failure()); } catch (...) {}',
152
+ "#else",
71
153
  ' try { last_error_ = _pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_; } catch (...) {}',
154
+ "#endif",
72
155
  " if (out) *out = ReportC{};",
73
156
  " return true;",
74
157
  " }",
75
158
  ])
76
159
  inputs = []
160
+ emitter._input_settings_metadata = {}
77
161
  for node, binding in emitter._global_input_calls_with_names():
78
162
  func_name, namespace = emitter._resolve_callee(node.callee)
79
163
  name = emitter._get_input_title(node, var_name=binding)
@@ -90,6 +174,7 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
90
174
  }[getter]
91
175
  kind = emitter._FORM_TYPE.get(func_name, value_type) if namespace == "input" else value_type
92
176
  options = []
177
+ numeric_options = []
93
178
  option_values = []
94
179
  supported = "true"
95
180
  default_serialized = None
@@ -146,6 +231,7 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
146
231
  options.append(option_cpp)
147
232
  else:
148
233
  options.append(f'::pineforge::checked_settings::number({option_cpp})')
234
+ numeric_options.append(option_cpp)
149
235
  if getter == "get_input_string":
150
236
  if arguments.get("options") is not None and declared is None:
151
237
  supported = "false"
@@ -165,6 +251,31 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
165
251
  f'{64 if getter == "get_input_int64" else 32}, {supported}, '
166
252
  f'{{{", ".join(option_values)}}}, "{kind}"}}')
167
253
  inputs.append((metadata, effective))
254
+ manifest = {}
255
+ if getter == "get_input_string":
256
+ manifest["default"] = _string_setting_value(default_serialized)
257
+ if arguments.get("options") is not None or supported == "false":
258
+ manifest["options"] = [_string_setting_value(option) for option in options]
259
+ elif value_type == "source":
260
+ manifest["default"] = source
261
+ manifest["options"] = [_string_setting_value(option) for option in options]
262
+ elif value_type == "enum":
263
+ manifest["options"] = [_string_setting_value(option) for option in options]
264
+ elif namespace == "input" and kind in ("int", "float", "bool"):
265
+ # A typed numeric or bool input publishes the numbers the receipt
266
+ # holds, where its C++ is a plain literal.
267
+ default_value = _numeric_setting_value(default_cpp) if default is not None else None
268
+ if default_value is not None and isinstance(default_value, bool) == (kind == "bool"):
269
+ manifest["default"] = default_value
270
+ for field, lowered in zip(("min", "max", "step"), constraints):
271
+ bound = _numeric_setting_value(lowered)
272
+ if _is_number(bound):
273
+ manifest[field] = bound
274
+ values = [_numeric_setting_value(option) for option in numeric_options]
275
+ if values and all(_is_number(value) for value in values):
276
+ manifest["options"] = values
277
+ manifest["supported"] = supported == "true"
278
+ emitter._input_settings_metadata[id(node)] = manifest
168
279
 
169
280
  overrides = [
170
281
  ("initial_capital", "float", "initial_capital", "initial_capital", "0.0"),