@pineforge/codegen-pyodide 1.2.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 (34) hide show
  1. package/LICENSE +19 -3
  2. package/README.md +2 -1
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +6 -5
  5. package/pineforge_codegen/analyzer/base.py +3 -3
  6. package/pineforge_codegen/analyzer/types.py +7 -3
  7. package/pineforge_codegen/codegen/base.py +36 -17
  8. package/pineforge_codegen/codegen/capabilities.py +143 -10
  9. package/pineforge_codegen/codegen/checked_settings.py +161 -4
  10. package/pineforge_codegen/codegen/collection_history.py +163 -12
  11. package/pineforge_codegen/codegen/emit_top.py +29 -13
  12. package/pineforge_codegen/codegen/helpers.py +25 -1
  13. package/pineforge_codegen/codegen/host_members.py +1 -0
  14. package/pineforge_codegen/codegen/input.py +23 -8
  15. package/pineforge_codegen/codegen/order_shapes.py +356 -0
  16. package/pineforge_codegen/codegen/run_stops.py +52 -0
  17. package/pineforge_codegen/codegen/security.py +66 -10
  18. package/pineforge_codegen/codegen/tables.py +144 -28
  19. package/pineforge_codegen/codegen/tv_number_format.py +9 -2
  20. package/pineforge_codegen/codegen/types.py +85 -4
  21. package/pineforge_codegen/codegen/visit_call.py +235 -55
  22. package/pineforge_codegen/codegen/visit_expr.py +40 -8
  23. package/pineforge_codegen/codegen/visit_stmt.py +5 -4
  24. package/pineforge_codegen/collection_history.py +122 -20
  25. package/pineforge_codegen/diagnostics_catalog.json +1 -0
  26. package/pineforge_codegen/external_requests.py +30 -9
  27. package/pineforge_codegen/matrix_overloads.py +7 -0
  28. package/pineforge_codegen/parser.py +7 -2
  29. package/pineforge_codegen/signatures.py +2 -2
  30. package/pineforge_codegen/support_checker.py +101 -10
  31. package/pineforge_codegen-1.4.0.tar.gz +0 -0
  32. package/release.json +2 -2
  33. package/tables.json +7 -3
  34. package/pineforge_codegen-1.2.0.tar.gz +0 -0
@@ -262,12 +262,18 @@ CPP_STANDARD_MACROS = frozenset("""
262
262
  PINEFORGE_HAS_NATIVE_LOWERING_V1 PINEFORGE_HAS_NATIVE_LIVE_V1
263
263
  PINEFORGE_HAS_AUX_SECURITY_FEED_V1 PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1
264
264
  PINEFORGE_HAS_EXPLICIT_PINE_CAP_V1
265
+ PINEFORGE_HAS_RUN_FAILURE_CODES_V1
266
+ _PF_NO_DATA_STOP _PF_OTHER_SYMBOL_STOP _PF_ARRAY_STOP _PF_COLLECTION_STOP
267
+ _PF_NA_STOP _PF_LIMIT_STOP _PF_UNSUPPORTED_STOP _PF_STRING_STOP
268
+ _PF_ENGINE_INVARIANT _PF_INVARIANT_AT _PF_SETTING_FAILURE
265
269
  PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1
266
270
  PINEFORGE_NO_STRATEGY_DECLS PF_PINE_TIME_HAS_SESSION_DAY
267
271
  PF_PINE_TIME_SESSION_DAY_ARGS PF_VWAP_HAS_SESSION_ANCHOR
268
272
  PF_VWAP_SESSION_ANCHOR_ARGS PF_ALMA_HAS_FLOOR
269
273
  PF_KC_HAS_USE_TRUE_RANGE PF_VWAP_HAS_ANCHOR_INPUT
270
274
  PF_PIVOT_LEVELS_HAS_ANCHOR
275
+ PF_ABI_VERSION PF_NATIVE_API_VERSION
276
+ PF_CAPABILITIES_API_VERSION PF_SETTINGS_API_VERSION
271
277
  """.split())
272
278
 
273
279
  # Identifiers used by the emitter as class/type names, namespaces, and
@@ -283,6 +289,9 @@ CPP_EMITTER_NAMES = frozenset("""
283
289
  commit_script_state set_strategy_override set_input
284
290
  set_magnifier_volume_weighted strategy_declares_bar_magnifier fill_report run
285
291
  strategy_capabilities_api_version strategy_capabilities_receipt
292
+ strategy_confirmed_bar_api_version strategy_confirmed_bar_receipt
293
+ strategy_order_shapes_api_version strategy_order_shapes_receipt
294
+ strategy_settings_api_version strategy_get_effective_settings
286
295
  precalculate
287
296
  strategy_entry strategy_close strategy_close_all strategy_exit
288
297
  strategy_exit_cancel_bracket strategy_cancel strategy_cancel_all strategy_order
@@ -291,6 +300,9 @@ CPP_EMITTER_NAMES = frozenset("""
291
300
  live_position_size pending_order_count market_admission_journal
292
301
  pine_time pine_time_close pine_time_tradingday pine_random
293
302
  pine_runtime_error pine_enum_str_at pine_session_ismarket
303
+ pine_no_data_stop pine_other_symbol_stop pine_array_stop pine_collection_stop
304
+ pine_na_stop pine_limit_stop pine_unsupported_stop pine_string_stop
305
+ pine_engine_invariant note_run_failure note_run_failure_unknown
294
306
  pine_session_ispostmarket pine_session_ispremarket _PFSessionMarket
295
307
  _pf_session_market_ script_tf_
296
308
  tf_change tf_is_daily tf_is_intraday tf_is_monthly tf_is_seconds
@@ -317,7 +329,8 @@ CPP_EMITTER_NAMES = frozenset("""
317
329
  get_input_int get_input_float get_input_bool get_input_string
318
330
  _pf_record_failure _pf_refuse_failed_setting _pf_settings_declared_config
319
331
  _pf_record_setting_failure _pf_require_settings_ok
320
- _pf_setting_failed_ _pf_setting_failure_
332
+ _pf_setting_failed_ _pf_setting_failure_ _pf_setting_failure_info_
333
+ _pf_setting_error_base _pf_latched_setting_error _pf_invariant_at
321
334
  _pf_settings_inputs _pf_settings_overrides _pf_set_input_checked
322
335
  _pf_set_override_checked _pf_settings_receipt
323
336
  _pf_close_entries_rule_word _pf_default_qty_type_word _pf_commission_type_word
@@ -369,12 +382,16 @@ CPP_TEMPORARY_NAMES = frozenset("""
369
382
  __pf_s __pf_r __pf_p __pf_i __pf_j __pf_t __pf_it __pf_m __pf_c __pf_n
370
383
  __pf_k __pf_b __pf_ma __pf_mb __pf_f __pf_d __pf_best __pf_bc __pf_rank
371
384
  __pf_idx __pf_occ
385
+ _pf_number_text
386
+ _pf_substring_text _pf_substring_begin _pf_substring_end _pf_substring_error
387
+ _pf_array_new_size _pf_array_new_value _pf_array_new_size_type _pf_array_new_error
372
388
  """.split())
373
389
  _CPP_TEMPORARY_PATTERN = re.compile(
374
390
  r"_v\d+|_secval_\d+(?:_v\d+)?|_tuple_result_\d+|_tuple_unused_\d+|__switch_val_\d+"
375
391
  r"|_for_(?:start|end|end_eval)_\d+|_pf_(?:str|array|round)_a\d+"
376
392
  r"|_pf_every_bar_ta_\d+|_pf_shared_\d+_\d+|__pf_array\w*|__pf_raw_\w+"
377
393
  r"|_pf_collection_hist_\w+"
394
+ r"|_pf_substring_arg\d+"
378
395
  )
379
396
 
380
397
 
@@ -432,8 +449,15 @@ class NamingHelper:
432
449
  .replace("\n", "\\n")
433
450
  .replace("\r", "\\r")
434
451
  .replace("\t", "\\t")
452
+ .replace("\0", "\\000")
435
453
  )
436
454
 
455
+ @staticmethod
456
+ def _cpp_comment_escape(s: str) -> str:
457
+ """Keep text (a script's string rendered into it) inside the C++
458
+ ``/* ... */`` comment it is written in."""
459
+ return s.replace("/*", "/ *").replace("*/", "* /")
460
+
437
461
  def _initialise_safe_names(self, ast) -> None:
438
462
  """Reserve all authored spellings before assigning escaped ones.
439
463
 
@@ -148,6 +148,7 @@ HOST_MEMBER_NAMES = frozenset({
148
148
  "set_pine_risk_max_intraday_filled_orders",
149
149
  "set_pine_risk_max_intraday_loss",
150
150
  "set_pine_risk_max_position_size",
151
+ "set_recorded_series",
151
152
  "set_strategy_override",
152
153
  "signed_position_size",
153
154
  "snapshot_script_state",
@@ -25,6 +25,7 @@ from ..ast_nodes import (
25
25
  MemberAccess,
26
26
  NumberLiteral,
27
27
  StringLiteral,
28
+ UnaryOp,
28
29
  VarDecl,
29
30
  )
30
31
  from .. import signatures as sigs
@@ -453,7 +454,7 @@ class InputHelper:
453
454
  # Input manifest extraction (host UI override-form source of truth)
454
455
  # ------------------------------------------------------------------
455
456
 
456
- def _literal_or_none(self, node):
457
+ def _literal_or_none(self, node, *, signed: bool = False):
457
458
  """Return a JSON scalar for a *const* literal AST node, else None.
458
459
 
459
460
  ``None`` signals non-const (an identifier, computed expression, …) so
@@ -469,6 +470,10 @@ class InputHelper:
469
470
  return node.value
470
471
  if isinstance(node, NumberLiteral):
471
472
  return node.value
473
+ if signed and isinstance(node, UnaryOp) and node.op in ("+", "-"):
474
+ value = self._literal_or_none(node.operand, signed=True)
475
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
476
+ return value if node.op == "+" else -value
472
477
  # enum member ref like ``Dir.Up`` -> "Dir.Up" (string tag)
473
478
  if isinstance(node, MemberAccess) and isinstance(node.object, Identifier):
474
479
  return f"{node.object.name}.{node.member}"
@@ -503,12 +508,17 @@ class InputHelper:
503
508
  ``var = input.*(...)`` declaration and an inline call inside an
504
509
  expression (``ta.ema(close, input.int(9, "Fast"))``) alike.
505
510
 
506
- Each entry: ``{title, type, default[, min, max, step, options]}``. The
507
- optional keys are emitted only when the corresponding signature
508
- argument is a const literal; a bound/option referencing a non-literal
509
- is omitted (never crashes). ``title`` is the key the emitted C++ reads
510
- the input by: the title argument, else the name of the declaration
511
- holding the call (``pine_spelling.input_binding_names``), else "".
511
+ Each entry: ``{title, type, default, supported[, min, max, step,
512
+ options]}``. ``supported``, a string input's default and options, a
513
+ source input's default and choices, an enum's choices and a typed
514
+ numeric input's literal default, bounds and options are the values
515
+ of the checked-settings receipt (the descriptor ``generate()``
516
+ builds with it). The optional keys are emitted only when the
517
+ corresponding signature argument is a const literal; a bound/option
518
+ referencing a non-literal is omitted (never crashes). ``title`` is
519
+ the key the emitted C++ reads the input by: the title argument, else
520
+ the name of the declaration holding the call
521
+ (``pine_spelling.input_binding_names``), else "".
512
522
  """
513
523
  return [self._input_manifest_entry(node, name)
514
524
  for node, name in self._global_input_calls_with_names()]
@@ -528,7 +538,7 @@ class InputHelper:
528
538
  title = self._get_input_title(node, var_name=var_name)
529
539
  default_node = self._get_input_default(node)
530
540
  default_val = (
531
- self._literal_or_none(default_node)
541
+ self._literal_or_none(default_node, signed=True)
532
542
  if default_node is not None
533
543
  else None
534
544
  )
@@ -581,4 +591,9 @@ class InputHelper:
581
591
  # any non-const element -> omit the whole options list
582
592
  if vals and all(isinstance(v, str) for v in vals):
583
593
  entry["options"] = vals
594
+ descriptors = getattr(self, "_input_settings_metadata", None)
595
+ if descriptors is None:
596
+ raise RuntimeError("the input manifest reads the settings descriptors "
597
+ "generate() builds: call generate() first")
598
+ entry.update(descriptors[id(node)])
584
599
  return entry
@@ -0,0 +1,356 @@
1
+ """Immutable facts about emitted order parameters and execution context."""
2
+
3
+ from collections import ChainMap, Counter
4
+ from dataclasses import dataclass
5
+ import json
6
+ import math
7
+ import re
8
+
9
+ from ..ast_nodes import (
10
+ BinOp, BoolLiteral, ForInStmt, ForStmt, FuncCall, FuncDef, Identifier, IfStmt,
11
+ MemberAccess, MethodDef, NumberLiteral, StringLiteral, SwitchStmt, TupleAssign,
12
+ UnaryOp, VarDecl, WhileStmt,
13
+ )
14
+ from ..limits import iter_ast_nodes, syntax_children
15
+ from .capabilities import READ_ONLY_STRATEGY_CALLS, _expression
16
+ from .host_members import HOST_MEMBER_NAMES
17
+
18
+
19
+ NUMERIC_PARAMETERS = frozenset((
20
+ "limit", "stop", "qty", "qty_percent", "trail_points", "trail_offset",
21
+ "trail_price", "profit_ticks", "loss_ticks",
22
+ ))
23
+ FINITE_BAR_VALUES = frozenset(("open", "high", "low", "close", "hl2", "hlc3", "ohlc4", "hlcc4"))
24
+ ENUM_VALUES = {
25
+ "strategy.oca.none": 0, "strategy.oca.cancel": 1, "strategy.oca.reduce": 2,
26
+ "strategy.fixed": 0, "strategy.percent_of_equity": 1, "strategy.cash": 2,
27
+ }
28
+ SETTING_DEFAULTS = {
29
+ "initial_capital": 10000.0, "default_qty_type": "fixed", "default_qty_value": 1.0,
30
+ "pyramiding": 1, "commission_type": "percent", "commission_value": 0.0,
31
+ "slippage": 0, "margin_long": 100.0, "margin_short": 100.0,
32
+ "close_entries_rule": "FIFO",
33
+ }
34
+ SETTING_ENUMS = {
35
+ "QtyType::FIXED": "fixed", "QtyType::PERCENT_OF_EQUITY": "percent_of_equity",
36
+ "QtyType::CASH": "cash", "CommissionType::PERCENT": "percent",
37
+ "CommissionType::CASH_PER_ORDER": "cash_per_order",
38
+ "CommissionType::CASH_PER_CONTRACT": "cash_per_contract",
39
+ }
40
+ CPP_TOKENS = re.compile(
41
+ r'R"(?P<delimiter>[^ ()\\\t\r\n]{0,16})\(.*?\)(?P=delimiter)"'
42
+ r'|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''
43
+ r'|//[^\n]*|/\*.*?(?:\*/|\Z)|[A-Za-z_][A-Za-z_0-9]*',
44
+ re.DOTALL,
45
+ )
46
+
47
+
48
+ def scan_host_reads(cpp: str) -> set[str]:
49
+ """Cross-check host tokens, excluding C++ strings and comments."""
50
+ return {match.group() for match in CPP_TOKENS.finditer(cpp)
51
+ if match.group() in HOST_MEMBER_NAMES}
52
+
53
+
54
+ class HostReadLines(list):
55
+ """Record scaffold references when each fragment enters the emission buffer."""
56
+
57
+ def __init__(self, reads: set[str]):
58
+ super().__init__()
59
+ self.reads = reads
60
+ self.in_comment = False
61
+
62
+ def _record(self, fragment: str) -> None:
63
+ if self.in_comment:
64
+ end = fragment.find("*/")
65
+ if end < 0:
66
+ return
67
+ fragment = fragment[end + 2:]
68
+ self.in_comment = False
69
+ for match in CPP_TOKENS.finditer(fragment):
70
+ token = match.group()
71
+ if token in HOST_MEMBER_NAMES:
72
+ self.reads.add(token)
73
+ if token.startswith("/*") and not token.endswith("*/"):
74
+ self.in_comment = True
75
+
76
+ def append(self, fragment: str) -> None:
77
+ self._record(fragment)
78
+ super().append(fragment)
79
+
80
+ def extend(self, fragments) -> None:
81
+ if fragments is self:
82
+ fragments = list(fragments)
83
+ for fragment in fragments:
84
+ self.append(fragment)
85
+
86
+ def _record_rewrite(self) -> None:
87
+ self.in_comment = False
88
+ for fragment in self:
89
+ self._record(fragment)
90
+
91
+ def insert(self, index: int, fragment: str) -> None:
92
+ super().insert(index, fragment)
93
+ self._record_rewrite()
94
+
95
+ def __setitem__(self, index, fragments) -> None:
96
+ super().__setitem__(index, fragments)
97
+ self._record_rewrite()
98
+
99
+ def __iadd__(self, fragments):
100
+ self.extend(fragments)
101
+ return self
102
+
103
+ def __imul__(self, count):
104
+ super().__imul__(count)
105
+ self._record_rewrite()
106
+ return self
107
+
108
+ def __delitem__(self, index) -> None:
109
+ super().__delitem__(index)
110
+ self._record_rewrite()
111
+
112
+ def clear(self) -> None:
113
+ super().clear()
114
+ self._record_rewrite()
115
+
116
+ def pop(self, index=-1):
117
+ fragment = super().pop(index)
118
+ self._record_rewrite()
119
+ return fragment
120
+
121
+ def remove(self, fragment) -> None:
122
+ super().remove(fragment)
123
+ self._record_rewrite()
124
+
125
+ def reverse(self) -> None:
126
+ super().reverse()
127
+ self._record_rewrite()
128
+
129
+ def sort(self, *, key=None, reverse=False) -> None:
130
+ try:
131
+ super().sort(key=key, reverse=reverse)
132
+ finally:
133
+ self._record_rewrite()
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class LoweredParameter:
138
+ text: str
139
+ node: object
140
+ default: str | None = None
141
+
142
+
143
+ @dataclass
144
+ class LoweredCall:
145
+ node: FuncCall
146
+ call: str
147
+ parameters: dict[str, LoweredParameter]
148
+ context: str
149
+ form: str | None = None
150
+
151
+
152
+ def record_order_call(emitter, node, call, parameters, form=None) -> None:
153
+ """Called only by a branch that lowers the corresponding order command."""
154
+ key = (id(node), getattr(emitter, "_current_instance_name", None))
155
+ emitter._order_shape_calls.setdefault(key, LoweredCall(
156
+ node, call, {name: LoweredParameter(*value) for name, value in parameters.items()},
157
+ "repeatable" if getattr(emitter, "_current_func_body", None) is not None else "straight", form,
158
+ ))
159
+
160
+
161
+ def _literal(node, bindings):
162
+ if isinstance(node, (NumberLiteral, StringLiteral, BoolLiteral)):
163
+ return node.value
164
+ if isinstance(node, Identifier):
165
+ return bindings.get(node.name)
166
+ if isinstance(node, UnaryOp) and node.op in ("+", "-"):
167
+ value = _literal(node.operand, bindings)
168
+ if type(value) in (int, float):
169
+ return value if node.op == "+" else -value
170
+ return None
171
+
172
+
173
+ def _finite(value) -> bool:
174
+ return type(value) in (int, float) and math.isfinite(value)
175
+
176
+
177
+ def _never_na(node, bindings) -> bool:
178
+ if _finite(_literal(node, bindings)):
179
+ return True
180
+ if isinstance(node, Identifier):
181
+ return node.name in FINITE_BAR_VALUES and node.name not in bindings
182
+ if isinstance(node, UnaryOp) and node.op in ("+", "-"):
183
+ return _never_na(node.operand, bindings)
184
+ if isinstance(node, BinOp):
185
+ if node.op in ("+", "-"):
186
+ return _never_na(node.left, bindings) and _never_na(node.right, bindings)
187
+ if node.op == "*":
188
+ return ((_finite(_literal(node.left, bindings)) and _never_na(node.right, bindings))
189
+ or (_finite(_literal(node.right, bindings)) and _never_na(node.left, bindings)))
190
+ return False
191
+
192
+
193
+ def parameter_class(name, parameter, bindings):
194
+ if parameter.text == parameter.default:
195
+ return "absent"
196
+ value = _literal(parameter.node, bindings)
197
+ if name in ("id", "from_entry"):
198
+ if parameter.node is None and parameter.text == '\"\"':
199
+ value = ""
200
+ if isinstance(value, str):
201
+ return ("global" if not value else "named") if name == "from_entry" else (
202
+ "literal" if value else "empty")
203
+ return "dynamic"
204
+ if name == "direction":
205
+ if parameter.text == "true":
206
+ return "long"
207
+ if parameter.text == "false":
208
+ return "short"
209
+ return "dynamic"
210
+ if name in NUMERIC_PARAMETERS:
211
+ if _finite(value):
212
+ return "literal"
213
+ return "never_na" if _never_na(parameter.node, bindings) else "maybe_na"
214
+ if name in ("comment", "oca_name"):
215
+ if value == "":
216
+ return "absent"
217
+ return "literal" if isinstance(value, str) else "dynamic"
218
+ if name in ("oca_type", "qty_type"):
219
+ if isinstance(parameter.node, MemberAccess):
220
+ value = ENUM_VALUES.get(_expression(parameter.node))
221
+ if _finite(value) and str(int(value)) == parameter.default and value == int(value):
222
+ return "absent"
223
+ return f"literal:{int(value)}" if _finite(value) and value == int(value) else "dynamic"
224
+ if name == "immediately":
225
+ return "literal:true" if value is True else "dynamic"
226
+ raise ValueError(f"unclassified lowered order parameter: {name}")
227
+
228
+
229
+ def _site_facts(ast, mutable_globals, recorded_nodes):
230
+ """Source ordering, lexical shadows, and repeatability for authored call sites."""
231
+ bindings = {}
232
+ for node in ast.body:
233
+ if isinstance(node, VarDecl):
234
+ value = _literal(node.value, bindings)
235
+ bindings[node.name] = (value if not node.is_var and not node.is_varip
236
+ and node.name not in mutable_globals else None)
237
+ facts = {}
238
+
239
+ def block(statements, repeatable, visible, names=()):
240
+ local = {name: None for name in names}
241
+ scope = ChainMap(local, visible)
242
+ for statement in statements:
243
+ walk(statement, repeatable, scope)
244
+ if isinstance(statement, VarDecl):
245
+ local[statement.name] = None
246
+ elif isinstance(statement, TupleAssign):
247
+ local.update({name: None for name in statement.names})
248
+
249
+ def walk(node, repeatable, visible):
250
+ if id(node) in recorded_nodes:
251
+ facts[id(node)] = (len(facts), "repeatable" if repeatable else "straight", dict(visible))
252
+ if isinstance(node, (FuncDef, MethodDef)):
253
+ block(node.body, True, visible,
254
+ (name if isinstance(name, str) else name[0] for name in node.params))
255
+ elif isinstance(node, (ForStmt, ForInStmt, WhileStmt)):
256
+ for field in ("start", "end", "step", "iterable", "condition"):
257
+ expression = getattr(node, field, None)
258
+ if expression is not None:
259
+ walk(expression, True, visible)
260
+ names = ([node.var] if node.var else node.vars or []) if isinstance(node, (ForStmt, ForInStmt)) else ()
261
+ block(node.body, True, visible, names)
262
+ elif isinstance(node, IfStmt):
263
+ walk(node.condition, repeatable, visible)
264
+ block(node.body, repeatable, visible)
265
+ block(node.else_body, repeatable, visible)
266
+ elif isinstance(node, SwitchStmt):
267
+ if node.expr is not None:
268
+ walk(node.expr, repeatable, visible)
269
+ for condition, statements in node.cases:
270
+ if condition is not None:
271
+ walk(condition, repeatable, visible)
272
+ block(statements, repeatable, visible)
273
+ block(node.default_body, repeatable, visible)
274
+ else:
275
+ for child in reversed(list(syntax_children(node))):
276
+ walk(child, repeatable, visible)
277
+
278
+ for statement in ast.body:
279
+ walk(statement, False, bindings)
280
+ return facts
281
+
282
+
283
+ def settings_echo(constructor):
284
+ settings = dict(SETTING_DEFAULTS)
285
+ pooc = False
286
+ for statement in constructor:
287
+ match = re.fullmatch(r" cfg\.(\w+) = (.*);", statement)
288
+ if match is None:
289
+ continue
290
+ name, text = match.groups()
291
+ if name == "process_orders_on_close":
292
+ pooc = text == "true"
293
+ elif name == "close_entries_rule_any":
294
+ settings["close_entries_rule"] = "ANY" if text == "true" else "FIFO"
295
+ elif name in settings:
296
+ if text.startswith("static_cast<int>("):
297
+ settings[name] = SETTING_ENUMS[text[len("static_cast<int>("):-1]]
298
+ else:
299
+ value = float(text)
300
+ settings[name] = (int(value) if name in ("pyramiding", "slippage") else value) if math.isfinite(value) else None
301
+ return settings, pooc
302
+
303
+
304
+ def order_shapes_document(emitter) -> str:
305
+ recorded_nodes = {id(record.node) for record in emitter._order_shape_calls.values()}
306
+ facts = _site_facts(emitter.ctx.ast, emitter.ctx.global_mutable_infos, recorded_nodes)
307
+ records = sorted(emitter._order_shape_calls.values(), key=lambda record: (
308
+ record.node.loc.line if record.node.loc else 0,
309
+ record.node.loc.col if record.node.loc else 0,
310
+ facts.get(id(record.node), (len(facts),))[0],
311
+ ))
312
+ calls = []
313
+ ids = []
314
+ for site, record in enumerate(records):
315
+ _order, context, bindings = facts.get(id(record.node), (site, record.context, {}))
316
+ descriptor = {"site": site, "call": record.call, "context": context}
317
+ if record.form is not None:
318
+ descriptor["form"] = record.form
319
+ descriptor.update({name: parameter_class(name, parameter, bindings)
320
+ for name, parameter in record.parameters.items()})
321
+ calls.append(descriptor)
322
+ ids.append({name: _literal(parameter.node, bindings) if parameter.node is not None else ""
323
+ for name, parameter in record.parameters.items() if name in ("id", "from_entry")})
324
+ entries = [(site, ids[site].get("id"), call.get("direction"))
325
+ for site, call in enumerate(calls) if call["call"] in ("entry", "order")
326
+ and call["id"] in ("literal", "empty")]
327
+ long_ids = {name for _site, name, direction in entries if direction == "long"}
328
+ short_ids = {name for _site, name, direction in entries if direction == "short"}
329
+ for site, call in enumerate(calls):
330
+ if call["call"] not in ("exit", "close", "cancel"):
331
+ continue
332
+ target = ids[site].get("from_entry" if call["call"] == "exit" else "id")
333
+ global_target = target == "" and call["call"] in ("exit", "close")
334
+ matching = [(entry_site, direction) for entry_site, name, direction in entries
335
+ if global_target or target is not None and target == name]
336
+ directions = {direction for _entry_site, direction in matching}
337
+ call["target"] = ("both" if "dynamic" in directions or directions >= {"long", "short"}
338
+ else "long" if "long" in directions else "short" if "short" in directions
339
+ else "dangling")
340
+ if call["call"] == "exit":
341
+ before = any(entry_site > site for entry_site, _direction in matching)
342
+ after = any(entry_site < site for entry_site, _direction in matching)
343
+ call["order"] = "mixed" if before and after or not matching else "before" if before else "after"
344
+ counts = Counter(name for _site, name, _direction in entries)
345
+ settings, pooc = settings_echo(emitter._order_shape_constructor)
346
+ unmodeled = {_expression(node.callee) for node, _depth in iter_ast_nodes(emitter.ctx.ast)
347
+ if isinstance(node, FuncCall) and (_expression(node.callee) or "").startswith("strategy.")
348
+ and _expression(node.callee) not in READ_ONLY_STRATEGY_CALLS
349
+ and id(node) not in recorded_nodes}
350
+ return json.dumps({
351
+ "version": 1, "process_orders_on_close": pooc, "calls": calls,
352
+ "entry_ids": {"long": len(long_ids), "short": len(short_ids),
353
+ "shared": len(long_ids & short_ids), "multi_site": sum(count > 1 for count in counts.values())},
354
+ "host_reads": sorted(emitter._order_shape_host_reads), "settings": settings,
355
+ "unmodeled": sorted(unmodeled),
356
+ }, sort_keys=True, separators=(",", ":"), allow_nan=False)
@@ -0,0 +1,52 @@
1
+ """Optional coded run stops, with the original throws on older engines."""
2
+
3
+ from .helpers import NamingHelper
4
+
5
+
6
+ RUN_STOP_SHIMS_CPP = r"""
7
+ #ifdef PINEFORGE_HAS_RUN_FAILURE_CODES_V1
8
+ #define _PF_NO_DATA_STOP(function, call, line, english) ::pineforge::pine_no_data_stop(function, call, line, english)
9
+ #define _PF_OTHER_SYMBOL_STOP(function, symbol, call, line, english) ::pineforge::pine_other_symbol_stop(function, symbol, call, line, english)
10
+ #define _PF_ARRAY_STOP(reason, method, english) ::pineforge::pine_array_stop(reason, method, std::string(english).c_str())
11
+ #define _PF_COLLECTION_STOP(reason, object, english) ::pineforge::pine_collection_stop(object, reason, english)
12
+ #define _PF_NA_STOP(object, english) ::pineforge::pine_na_stop(object, english)
13
+ #define _PF_LIMIT_STOP(limit, max, english) ::pineforge::pine_limit_stop(limit, max, english)
14
+ #define _PF_UNSUPPORTED_STOP(reason, line, english) ::pineforge::pine_unsupported_stop(reason, line, english)
15
+ #define _PF_STRING_STOP(reason, english) ::pineforge::pine_string_stop(reason, english)
16
+ #define _PF_ENGINE_INVARIANT(english, legacy_type) ::pineforge::pine_engine_invariant(english)
17
+ #define _PF_INVARIANT_AT(container, index) _pf_invariant_at(container, index)
18
+ #define _PF_SETTING_FAILURE(strategy, entrypoint, message, reason) (strategy)->_pf_record_setting_failure(entrypoint, message, [] { return reason ? ::pineforge::RunFailureInfo(::pineforge::RunFailureCode::setting_rejected, {{"entrypoint", entrypoint}, {"reason", reason}}) : ::pineforge::RunFailureInfo(::pineforge::RunFailureCode::setting_rejected, {{"entrypoint", entrypoint}}); })
19
+ template <typename Container, typename Index>
20
+ decltype(auto) _pf_invariant_at(Container& container, Index index) {
21
+ try { return container.at(index); }
22
+ catch (const std::out_of_range& error) { ::pineforge::pine_engine_invariant(error.what()); }
23
+ }
24
+ #else
25
+ #define _PF_NO_DATA_STOP(function, call, line, english) pine_runtime_error(std::string(english))
26
+ #define _PF_OTHER_SYMBOL_STOP(function, symbol, call, line, english) pine_runtime_error(std::string(english))
27
+ #define _PF_ARRAY_STOP(reason, method, english) pine_runtime_error(english)
28
+ #define _PF_COLLECTION_STOP(reason, object, english) pine_runtime_error(english)
29
+ #define _PF_NA_STOP(object, english) throw std::runtime_error(english)
30
+ #define _PF_LIMIT_STOP(limit, max, english) throw std::length_error(english)
31
+ #define _PF_UNSUPPORTED_STOP(reason, line, english) pine_runtime_error(std::string(english))
32
+ #define _PF_STRING_STOP(reason, english) pine_runtime_error(std::string(english))
33
+ #define _PF_ENGINE_INVARIANT(english, legacy_type) throw legacy_type(english)
34
+ #define _PF_INVARIANT_AT(container, index) (container).at(index)
35
+ #define _PF_SETTING_FAILURE(strategy, entrypoint, message, reason) (strategy)->_pf_record_setting_failure(entrypoint, message)
36
+ #endif
37
+ """
38
+
39
+
40
+ def request_stop(marker: dict) -> str:
41
+ """Metadata comes only from the original request's compile-time spelling."""
42
+ escape = NamingHelper._cpp_string_escape
43
+ function = f'"{escape(marker["function"])}"'
44
+ call = f'"{escape(marker["call"])}"'
45
+ english = f'"{escape(marker["message"])}"'
46
+ arguments = f'{function}, {call}, {marker["line"]}, {english}'
47
+ if marker["kind"] == "other_symbol":
48
+ literal = marker["symbol_literal"]
49
+ symbol = "nullptr" if literal is None else f'"{escape(literal)}"'
50
+ arguments = f'{function}, {symbol}, {call}, {marker["line"]}, {english}'
51
+ return f"_PF_OTHER_SYMBOL_STOP({arguments})"
52
+ return f"_PF_NO_DATA_STOP({arguments})"