@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.
- package/LICENSE +19 -3
- package/README.md +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +6 -5
- package/pineforge_codegen/analyzer/base.py +3 -3
- package/pineforge_codegen/analyzer/types.py +7 -3
- package/pineforge_codegen/codegen/base.py +36 -17
- package/pineforge_codegen/codegen/capabilities.py +143 -10
- package/pineforge_codegen/codegen/checked_settings.py +161 -4
- package/pineforge_codegen/codegen/collection_history.py +163 -12
- package/pineforge_codegen/codegen/emit_top.py +29 -13
- package/pineforge_codegen/codegen/helpers.py +25 -1
- package/pineforge_codegen/codegen/host_members.py +1 -0
- package/pineforge_codegen/codegen/input.py +23 -8
- package/pineforge_codegen/codegen/order_shapes.py +356 -0
- package/pineforge_codegen/codegen/run_stops.py +52 -0
- package/pineforge_codegen/codegen/security.py +66 -10
- package/pineforge_codegen/codegen/tables.py +144 -28
- package/pineforge_codegen/codegen/tv_number_format.py +9 -2
- package/pineforge_codegen/codegen/types.py +85 -4
- package/pineforge_codegen/codegen/visit_call.py +235 -55
- package/pineforge_codegen/codegen/visit_expr.py +40 -8
- package/pineforge_codegen/codegen/visit_stmt.py +5 -4
- package/pineforge_codegen/collection_history.py +122 -20
- package/pineforge_codegen/diagnostics_catalog.json +1 -0
- package/pineforge_codegen/external_requests.py +30 -9
- package/pineforge_codegen/matrix_overloads.py +7 -0
- package/pineforge_codegen/parser.py +7 -2
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +101 -10
- package/pineforge_codegen-1.4.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -3
- 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,
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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})"
|