arxo-codegen 0.4.0__py3-none-any.whl

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 (152) hide show
  1. arxo_codegen/__init__.py +5 -0
  2. arxo_codegen/__main__.py +7 -0
  3. arxo_codegen/active-outcomes.json +6837 -0
  4. arxo_codegen/check.py +158 -0
  5. arxo_codegen/check_active_canaries.py +262 -0
  6. arxo_codegen/check_api.py +76 -0
  7. arxo_codegen/check_conformance.py +414 -0
  8. arxo_codegen/check_matrix.py +131 -0
  9. arxo_codegen/check_packages.py +281 -0
  10. arxo_codegen/check_semantic_probes.py +293 -0
  11. arxo_codegen/check_xlsx.py +320 -0
  12. arxo_codegen/consumer_runner.py +316 -0
  13. arxo_codegen/core_support.py +203 -0
  14. arxo_codegen/coverage.py +341 -0
  15. arxo_codegen/deliver.py +180 -0
  16. arxo_codegen/examples/xlsx/pricing.lawir.json +409 -0
  17. arxo_codegen/examples/xlsx/pricing.xlsx.json +52 -0
  18. arxo_codegen/excel_plan.py +602 -0
  19. arxo_codegen/harness/verify/ci/gates/backends/check_backends.py +1248 -0
  20. arxo_codegen/harness/verify/ci/gates/backends/l1_fixtures.py +463 -0
  21. arxo_codegen/inert-nodes.json +62 -0
  22. arxo_codegen/inert.py +91 -0
  23. arxo_codegen/inputs.py +59 -0
  24. arxo_codegen/package.py +565 -0
  25. arxo_codegen/plan.py +4053 -0
  26. arxo_codegen/recertify_registry.py +100 -0
  27. arxo_codegen/runtime-cert.json +5259 -0
  28. arxo_codegen/runtime_cert.py +233 -0
  29. arxo_codegen/survey.py +272 -0
  30. arxo_codegen/target_runner.py +101 -0
  31. arxo_codegen/targets/bsl/cert.bsl +113 -0
  32. arxo_codegen/targets/bsl/emit.py +356 -0
  33. arxo_codegen/targets/bsl/json.bsl +243 -0
  34. arxo_codegen/targets/bsl/runner.bsl +210 -0
  35. arxo_codegen/targets/bsl/runtime.bsl +858 -0
  36. arxo_codegen/targets/cs/Cert.cs +90 -0
  37. arxo_codegen/targets/cs/Runner.cs +154 -0
  38. arxo_codegen/targets/cs/Runtime.cs +243 -0
  39. arxo_codegen/targets/cs/emit.py +223 -0
  40. arxo_codegen/targets/dart/cert.dart +99 -0
  41. arxo_codegen/targets/dart/emit.py +206 -0
  42. arxo_codegen/targets/dart/json.dart +166 -0
  43. arxo_codegen/targets/dart/runner.dart +154 -0
  44. arxo_codegen/targets/dart/runtime.dart +293 -0
  45. arxo_codegen/targets/erl/cert.erl +64 -0
  46. arxo_codegen/targets/erl/emit.py +216 -0
  47. arxo_codegen/targets/erl/rt.erl +238 -0
  48. arxo_codegen/targets/erl/runner.erl +130 -0
  49. arxo_codegen/targets/ex/cert.exs +62 -0
  50. arxo_codegen/targets/ex/emit.py +204 -0
  51. arxo_codegen/targets/ex/runner.exs +124 -0
  52. arxo_codegen/targets/ex/runtime.ex +227 -0
  53. arxo_codegen/targets/go/apiBase.go +483 -0
  54. arxo_codegen/targets/go/apicheck.go +97 -0
  55. arxo_codegen/targets/go/cert.go +218 -0
  56. arxo_codegen/targets/go/emit.py +1806 -0
  57. arxo_codegen/targets/go/evaluate.go +1826 -0
  58. arxo_codegen/targets/go/runner.go +38 -0
  59. arxo_codegen/targets/go/runtime.go +2612 -0
  60. arxo_codegen/targets/java/Cert.java +91 -0
  61. arxo_codegen/targets/java/Json.java +148 -0
  62. arxo_codegen/targets/java/Runner.java +151 -0
  63. arxo_codegen/targets/java/Runtime.java +243 -0
  64. arxo_codegen/targets/java/emit.py +198 -0
  65. arxo_codegen/targets/kt/Cert.kt +67 -0
  66. arxo_codegen/targets/kt/Json.kt +103 -0
  67. arxo_codegen/targets/kt/Runner.kt +126 -0
  68. arxo_codegen/targets/kt/Runtime.kt +196 -0
  69. arxo_codegen/targets/kt/emit.py +195 -0
  70. arxo_codegen/targets/lua/cert.lua +82 -0
  71. arxo_codegen/targets/lua/emit.py +250 -0
  72. arxo_codegen/targets/lua/json.lua +133 -0
  73. arxo_codegen/targets/lua/runner.lua +167 -0
  74. arxo_codegen/targets/lua/runtime.lua +541 -0
  75. arxo_codegen/targets/php/cert.php +84 -0
  76. arxo_codegen/targets/php/emit.py +229 -0
  77. arxo_codegen/targets/php/json.php +144 -0
  78. arxo_codegen/targets/php/runner.php +189 -0
  79. arxo_codegen/targets/php/runtime.php +660 -0
  80. arxo_codegen/targets/py/api_base.py +334 -0
  81. arxo_codegen/targets/py/apicheck.py +51 -0
  82. arxo_codegen/targets/py/cert.py +140 -0
  83. arxo_codegen/targets/py/emit.py +1140 -0
  84. arxo_codegen/targets/py/evaluate.py +980 -0
  85. arxo_codegen/targets/py/runner.py +38 -0
  86. arxo_codegen/targets/py/runtime.py +1634 -0
  87. arxo_codegen/targets/rb/cert.rb +73 -0
  88. arxo_codegen/targets/rb/emit.py +203 -0
  89. arxo_codegen/targets/rb/runner.rb +130 -0
  90. arxo_codegen/targets/rb/runtime.rb +255 -0
  91. arxo_codegen/targets/rs/Cargo.lock +142 -0
  92. arxo_codegen/targets/rs/Cargo.toml +16 -0
  93. arxo_codegen/targets/rs/cert.rs +106 -0
  94. arxo_codegen/targets/rs/emit.py +233 -0
  95. arxo_codegen/targets/rs/prefetch.rs +1 -0
  96. arxo_codegen/targets/rs/runner.rs +181 -0
  97. arxo_codegen/targets/rs/runtime.rs +448 -0
  98. arxo_codegen/targets/scala/Cert.scala +77 -0
  99. arxo_codegen/targets/scala/Json.scala +140 -0
  100. arxo_codegen/targets/scala/Runner.scala +121 -0
  101. arxo_codegen/targets/scala/Runtime.scala +269 -0
  102. arxo_codegen/targets/scala/emit.py +193 -0
  103. arxo_codegen/targets/swift/cert.swift +105 -0
  104. arxo_codegen/targets/swift/emit.py +231 -0
  105. arxo_codegen/targets/swift/json.swift +208 -0
  106. arxo_codegen/targets/swift/runner.swift +133 -0
  107. arxo_codegen/targets/swift/runtime.swift +528 -0
  108. arxo_codegen/targets/ts/apiBase.ts +250 -0
  109. arxo_codegen/targets/ts/apicheck.ts +37 -0
  110. arxo_codegen/targets/ts/cert.ts +96 -0
  111. arxo_codegen/targets/ts/emit.py +1214 -0
  112. arxo_codegen/targets/ts/evaluate.ts +894 -0
  113. arxo_codegen/targets/ts/package-lock.json +45 -0
  114. arxo_codegen/targets/ts/package.json +9 -0
  115. arxo_codegen/targets/ts/runner.ts +31 -0
  116. arxo_codegen/targets/ts/runtime.ts +1369 -0
  117. arxo_codegen/targets/xlsx/README.ru.md +176 -0
  118. arxo_codegen/targets/xlsx/emit.py +198 -0
  119. arxo_codegen/temporal.py +59 -0
  120. arxo_codegen/test_alias_types.py +171 -0
  121. arxo_codegen/test_api_contract.py +89 -0
  122. arxo_codegen/test_calendar_policy.py +48 -0
  123. arxo_codegen/test_check.py +49 -0
  124. arxo_codegen/test_check_active_canaries.py +96 -0
  125. arxo_codegen/test_check_conformance.py +132 -0
  126. arxo_codegen/test_check_semantic_probes.py +85 -0
  127. arxo_codegen/test_check_xlsx.py +52 -0
  128. arxo_codegen/test_consumer_runner.py +175 -0
  129. arxo_codegen/test_core_support.py +80 -0
  130. arxo_codegen/test_e0212_decision_time.py +63 -0
  131. arxo_codegen/test_head_aggregate.py +71 -0
  132. arxo_codegen/test_inert.py +143 -0
  133. arxo_codegen/test_instant_types.py +113 -0
  134. arxo_codegen/test_key_conflict_scope.py +92 -0
  135. arxo_codegen/test_key_conflict_targets.py +17 -0
  136. arxo_codegen/test_package_layout.py +163 -0
  137. arxo_codegen/test_plan_language_version.py +44 -0
  138. arxo_codegen/test_positions_refusal.py +43 -0
  139. arxo_codegen/test_recertify_registry.py +77 -0
  140. arxo_codegen/test_ruby_target.py +41 -0
  141. arxo_codegen/test_runtime_cert.py +83 -0
  142. arxo_codegen/test_secondary_key_conflict_scope.py +17 -0
  143. arxo_codegen/test_stage_r.py +108 -0
  144. arxo_codegen/test_standalone_inputs.py +71 -0
  145. arxo_codegen/test_support.py +150 -0
  146. arxo_codegen/test_survey_inputs.py +115 -0
  147. arxo_codegen-0.4.0.dist-info/LICENSE +202 -0
  148. arxo_codegen-0.4.0.dist-info/METADATA +1673 -0
  149. arxo_codegen-0.4.0.dist-info/RECORD +152 -0
  150. arxo_codegen-0.4.0.dist-info/WHEEL +5 -0
  151. arxo_codegen-0.4.0.dist-info/entry_points.txt +13 -0
  152. arxo_codegen-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,660 @@
1
+ <?php
2
+ // Рантайм L0 для PHP 8. Тот же контракт, что у runtime.rb: точная десятичная
3
+ // арифметика §58, деньги с валютой, названный режим округления, календарный
4
+ // день, член перечисления. Целых произвольной длины в ядре PHP нет (bcmath и
5
+ // gmp — расширения, которых в сборке может не быть) — свои: модуль по
6
+ // основанию 10^7 и знак.
7
+ //
8
+ // Ловушка PHP: `==` сравнивает строки, похожие на числа, как числа ("1.0" ==
9
+ // "1", "1e3" == "1000"), поэтому текст сравнивается только `===`, а порядок
10
+ // дня — `strcmp`.
11
+ declare(strict_types=1);
12
+
13
+ /** Названный отказ рантайма: то, что срез не считает, а не сбой. Раннер
14
+ * печатает его как JSON с кодом выхода 3; любое другое исключение — крах. */
15
+ final class Refused extends Exception {}
16
+
17
+ final class BigI
18
+ {
19
+ public const BASE = 10000000;
20
+
21
+ /** @param int[] $m младшая конечность первой; ноль — пустой массив */
22
+ public function __construct(public readonly bool $neg, public readonly array $m) {}
23
+
24
+ public static function of(bool $neg, array $m): BigI
25
+ {
26
+ while ($m && end($m) === 0) {
27
+ array_pop($m);
28
+ }
29
+ return new BigI($m ? $neg : false, $m);
30
+ }
31
+
32
+ public static function int(int $n): BigI
33
+ {
34
+ $neg = $n < 0;
35
+ $n = abs($n);
36
+ $m = [];
37
+ while ($n > 0) {
38
+ $m[] = $n % self::BASE;
39
+ $n = intdiv($n, self::BASE);
40
+ }
41
+ return self::of($neg, $m);
42
+ }
43
+
44
+ public static function parse(string $s): BigI
45
+ {
46
+ $neg = str_starts_with($s, '-');
47
+ if ($neg) {
48
+ $s = substr($s, 1);
49
+ }
50
+ if (!preg_match('/^\d+$/', $s)) {
51
+ throw new RuntimeException("целое: не десятичная запись: $s");
52
+ }
53
+ $m = [];
54
+ for ($end = strlen($s); $end > 0; $end -= 7) {
55
+ $start = max(0, $end - 7);
56
+ $m[] = (int) substr($s, $start, $end - $start);
57
+ }
58
+ return self::of($neg, $m);
59
+ }
60
+
61
+ public function isZero(): bool { return !$this->m; }
62
+ public function abs(): BigI { return new BigI(false, $this->m); }
63
+ public function negate(): BigI { return self::of(!$this->neg, $this->m); }
64
+
65
+ public function str(): string
66
+ {
67
+ if (!$this->m) {
68
+ return '0';
69
+ }
70
+ $n = count($this->m);
71
+ $s = ($this->neg ? '-' : '') . $this->m[$n - 1];
72
+ for ($i = $n - 2; $i >= 0; $i--) {
73
+ $s .= str_pad((string) $this->m[$i], 7, '0', STR_PAD_LEFT);
74
+ }
75
+ return $s;
76
+ }
77
+
78
+ /** Малое целое: вызывающий обязан сперва проверить диапазон. */
79
+ public function small(): int
80
+ {
81
+ $n = 0;
82
+ for ($i = count($this->m) - 1; $i >= 0; $i--) {
83
+ $n = $n * self::BASE + $this->m[$i];
84
+ }
85
+ return $this->neg ? -$n : $n;
86
+ }
87
+
88
+ public static function magCmp(array $a, array $b): int
89
+ {
90
+ if (count($a) !== count($b)) {
91
+ return count($a) < count($b) ? -1 : 1;
92
+ }
93
+ for ($i = count($a) - 1; $i >= 0; $i--) {
94
+ if ($a[$i] !== $b[$i]) {
95
+ return $a[$i] < $b[$i] ? -1 : 1;
96
+ }
97
+ }
98
+ return 0;
99
+ }
100
+
101
+ private static function magAdd(array $a, array $b): array
102
+ {
103
+ $r = [];
104
+ $carry = 0;
105
+ $n = max(count($a), count($b));
106
+ for ($i = 0; $i < $n; $i++) {
107
+ $t = ($a[$i] ?? 0) + ($b[$i] ?? 0) + $carry;
108
+ $r[] = $t % self::BASE;
109
+ $carry = intdiv($t, self::BASE);
110
+ }
111
+ if ($carry > 0) {
112
+ $r[] = $carry;
113
+ }
114
+ return $r;
115
+ }
116
+
117
+ /** |a| >= |b|. */
118
+ private static function magSub(array $a, array $b): array
119
+ {
120
+ $r = [];
121
+ $borrow = 0;
122
+ foreach ($a as $i => $limb) {
123
+ $t = $limb - ($b[$i] ?? 0) - $borrow;
124
+ $borrow = $t < 0 ? 1 : 0;
125
+ $r[] = $t < 0 ? $t + self::BASE : $t;
126
+ }
127
+ while ($r && end($r) === 0) {
128
+ array_pop($r);
129
+ }
130
+ return $r;
131
+ }
132
+
133
+ private static function magMul(array $a, array $b): array
134
+ {
135
+ if (!$a || !$b) {
136
+ return [];
137
+ }
138
+ $r = array_fill(0, count($a) + count($b) + 1, 0);
139
+ foreach ($a as $i => $ai) {
140
+ $carry = 0;
141
+ foreach ($b as $j => $bj) {
142
+ $t = $r[$i + $j] + $ai * $bj + $carry;
143
+ $r[$i + $j] = $t % self::BASE;
144
+ $carry = intdiv($t, self::BASE);
145
+ }
146
+ for ($k = $i + count($b); $carry > 0; $k++) {
147
+ $t = $r[$k] + $carry;
148
+ $r[$k] = $t % self::BASE;
149
+ $carry = intdiv($t, self::BASE);
150
+ }
151
+ }
152
+ while ($r && end($r) === 0) {
153
+ array_pop($r);
154
+ }
155
+ return $r;
156
+ }
157
+
158
+ private static function magMulSmall(array $a, int $s): array
159
+ {
160
+ $r = [];
161
+ $carry = 0;
162
+ foreach ($a as $limb) {
163
+ $t = $limb * $s + $carry;
164
+ $r[] = $t % self::BASE;
165
+ $carry = intdiv($t, self::BASE);
166
+ }
167
+ while ($carry > 0) {
168
+ $r[] = $carry % self::BASE;
169
+ $carry = intdiv($carry, self::BASE);
170
+ }
171
+ while ($r && end($r) === 0) {
172
+ array_pop($r);
173
+ }
174
+ return $r;
175
+ }
176
+
177
+ /** Деление модулей в столбик: цифра частного по основанию 10^7 подбирается
178
+ * двоичным поиском. Делитель ненулевой. */
179
+ private static function magDivMod(array $a, array $b): array
180
+ {
181
+ if (self::magCmp($a, $b) < 0) {
182
+ return [[], $a];
183
+ }
184
+ $q = array_fill(0, count($a), 0);
185
+ $rem = [];
186
+ for ($i = count($a) - 1; $i >= 0; $i--) {
187
+ array_unshift($rem, $a[$i]);
188
+ while ($rem && end($rem) === 0) {
189
+ array_pop($rem);
190
+ }
191
+ $lo = 0;
192
+ $hi = self::BASE - 1;
193
+ while ($lo < $hi) {
194
+ $mid = intdiv($lo + $hi + 1, 2);
195
+ if (self::magCmp(self::magMulSmall($b, $mid), $rem) <= 0) {
196
+ $lo = $mid;
197
+ } else {
198
+ $hi = $mid - 1;
199
+ }
200
+ }
201
+ if ($lo > 0) {
202
+ $rem = self::magSub($rem, self::magMulSmall($b, $lo));
203
+ }
204
+ $q[$i] = $lo;
205
+ }
206
+ return [$q, $rem];
207
+ }
208
+
209
+ public function add(BigI $y): BigI
210
+ {
211
+ if ($this->neg === $y->neg) {
212
+ return self::of($this->neg, self::magAdd($this->m, $y->m));
213
+ }
214
+ $c = self::magCmp($this->m, $y->m);
215
+ if ($c === 0) {
216
+ return self::int(0);
217
+ }
218
+ return $c > 0 ? self::of($this->neg, self::magSub($this->m, $y->m))
219
+ : self::of($y->neg, self::magSub($y->m, $this->m));
220
+ }
221
+
222
+ public function sub(BigI $y): BigI { return $this->add($y->negate()); }
223
+ public function mul(BigI $y): BigI { return self::of($this->neg !== $y->neg, self::magMul($this->m, $y->m)); }
224
+
225
+ public function cmp(BigI $y): int
226
+ {
227
+ if ($this->neg !== $y->neg) {
228
+ return $this->neg ? -1 : 1;
229
+ }
230
+ $c = self::magCmp($this->m, $y->m);
231
+ return $this->neg ? -$c : $c;
232
+ }
233
+
234
+ /** Частное и остаток с усечением к нулю: знак остатка — знак делимого. */
235
+ public function divMod(BigI $y): array
236
+ {
237
+ [$q, $r] = self::magDivMod($this->m, $y->m);
238
+ return [self::of($this->neg !== $y->neg, $q), self::of($this->neg, $r)];
239
+ }
240
+
241
+ public static function pow10(int $k): BigI
242
+ {
243
+ $m = [1];
244
+ for ($i = 0; $i < $k; $i++) {
245
+ $m = self::magMulSmall($m, 10);
246
+ }
247
+ return self::of(false, $m);
248
+ }
249
+
250
+ public static function gcd(BigI $x, BigI $y): BigI
251
+ {
252
+ $a = $x->abs();
253
+ $b = $y->abs();
254
+ while (!$b->isZero()) {
255
+ [, $r] = $a->divMod($b);
256
+ [$a, $b] = [$b, $r];
257
+ }
258
+ return $a;
259
+ }
260
+ }
261
+
262
+ final class Dec
263
+ {
264
+ public function __construct(public readonly BigI $u, public readonly int $s) {}
265
+ }
266
+
267
+ final class Money
268
+ {
269
+ public function __construct(public readonly BigI $u, public readonly int $s, public readonly string $cur) {}
270
+ }
271
+
272
+ /** Календарный день §2.15 в канонической записи ISO YYYY-MM-DD: порядок записи
273
+ * и есть порядок дня. Не string: у ключа атома и у сравнения свой вид. */
274
+ final class LDate
275
+ {
276
+ public function __construct(public readonly string $v) {}
277
+ }
278
+
279
+ /** Член перечисления §38 либо иная ссылка const_ref без объявления: значение —
280
+ * stable qualified identity, равенство по ней (§57), порядок не определён. */
281
+ final class Sym
282
+ {
283
+ public function __construct(public readonly string $id) {}
284
+ }
285
+
286
+ final class RT
287
+ {
288
+ private static string $legal = '';
289
+
290
+ public static function dec(string $lit): Dec
291
+ {
292
+ $parts = explode('.', $lit, 2);
293
+ $i = $parts[0];
294
+ $f = $parts[1] ?? '';
295
+ $neg = str_starts_with($i, '-');
296
+ $digits = ($neg ? substr($i, 1) : $i) . $f;
297
+ return new Dec(BigI::parse(($neg ? '-' : '') . $digits), strlen($f));
298
+ }
299
+
300
+ public static function money(string $lit, string $cur): Money
301
+ {
302
+ $d = self::dec($lit);
303
+ return new Money($d->u, $d->s, $cur);
304
+ }
305
+
306
+ public static function date(string $lit): LDate { return new LDate($lit); }
307
+ public static function sym(string $id): Sym { return new Sym($id); }
308
+
309
+ private static function isNum(mixed $v): bool { return $v instanceof Dec || $v instanceof Money; }
310
+
311
+ // §48/§58: сложение, вычитание, min/max требуют совпадения видов; у денег — и валюты.
312
+ private static function same(mixed $a, mixed $b, string $op): void
313
+ {
314
+ if (!(self::isNum($a) && self::isNum($b) && ($a instanceof Money) === ($b instanceof Money))) {
315
+ throw new Refused("§58: виды операндов не совпадают: $op");
316
+ }
317
+ if ($a instanceof Money && $a->cur !== $b->cur) {
318
+ throw new Refused("§58: разные валюты: $op");
319
+ }
320
+ }
321
+
322
+ private static function pair(Dec|Money $a, Dec|Money $b): array
323
+ {
324
+ $s = max($a->s, $b->s);
325
+ return [$a->u->mul(BigI::pow10($s - $a->s)), $b->u->mul(BigI::pow10($s - $b->s)), $s];
326
+ }
327
+
328
+ private static function make(BigI $u, int $s, Dec|Money $like): Dec|Money
329
+ {
330
+ return $like instanceof Money ? new Money($u, $s, $like->cur) : new Dec($u, $s);
331
+ }
332
+
333
+ // Целое значение: дробная часть нулевая (защита в глубину под статикой плана).
334
+ private static function integral(mixed $v): ?BigI
335
+ {
336
+ if (!($v instanceof Dec)) {
337
+ return null;
338
+ }
339
+ [$q, $r] = $v->u->divMod(BigI::pow10($v->s));
340
+ return $r->isZero() ? $q : null;
341
+ }
342
+
343
+ public static function add(mixed $a, mixed $b): Dec|Money
344
+ {
345
+ self::same($a, $b, 'add');
346
+ [$x, $y, $s] = self::pair($a, $b);
347
+ return self::make($x->add($y), $s, $a);
348
+ }
349
+
350
+ public static function sub(mixed $a, mixed $b): Dec|Money
351
+ {
352
+ self::same($a, $b, 'sub');
353
+ [$x, $y, $s] = self::pair($a, $b);
354
+ return self::make($x->sub($y), $s, $a);
355
+ }
356
+
357
+ // Правило скаляра §49: безразмерное масштабирует размерное; масштаб результата —
358
+ // сумма масштабов. Два размерных не перемножаются.
359
+ public static function mul(mixed $a, mixed $b): Dec|Money
360
+ {
361
+ if (!(self::isNum($a) && self::isNum($b))) {
362
+ throw new Refused('§58: виды операндов не совпадают: mul');
363
+ }
364
+ if ($a instanceof Money && $b instanceof Money) {
365
+ throw new Refused('§58: произведение двух размерных величин не определено');
366
+ }
367
+ return self::make($a->u->mul($b->u), $a->s + $b->s, $a instanceof Money ? $a : $b);
368
+ }
369
+
370
+ // Точное десятичное частное num/den при 10-гладком знаменателе (§18), иначе null.
371
+ private static function exact(BigI $num, BigI $den): ?array
372
+ {
373
+ $g = BigI::gcd($num, $den);
374
+ if ($g->isZero()) {
375
+ $g = BigI::int(1);
376
+ }
377
+ [$num] = $num->divMod($g);
378
+ [$den] = $den->divMod($g);
379
+ if ($den->neg) {
380
+ $num = $num->negate();
381
+ $den = $den->negate();
382
+ }
383
+ $twos = $fives = 0;
384
+ for (;;) {
385
+ [$q, $r] = $den->divMod(BigI::int(2));
386
+ if (!$r->isZero()) {
387
+ break;
388
+ }
389
+ $den = $q;
390
+ $twos++;
391
+ }
392
+ for (;;) {
393
+ [$q, $r] = $den->divMod(BigI::int(5));
394
+ if (!$r->isZero()) {
395
+ break;
396
+ }
397
+ $den = $q;
398
+ $fives++;
399
+ }
400
+ if ($den->cmp(BigI::int(1)) !== 0) {
401
+ return null;
402
+ }
403
+ $k = max($twos, $fives);
404
+ $f = BigI::int(1);
405
+ for ($i = $twos; $i < $k; $i++) {
406
+ $f = $f->mul(BigI::int(2));
407
+ }
408
+ for ($i = $fives; $i < $k; $i++) {
409
+ $f = $f->mul(BigI::int(5));
410
+ }
411
+ return [$num->mul($f), $k];
412
+ }
413
+
414
+ // Таблица делимых §58 в срезе: Decimal/Decimal, Money/Decimal, Money/Money одной
415
+ // валюты. Integer/Integer даёт Rational — вне среза, отвергается планом статически.
416
+ public static function div(mixed $a, mixed $b): Dec|Money
417
+ {
418
+ if (!(self::isNum($a) && self::isNum($b))) {
419
+ throw new Refused('§58: деление вне таблицы делимых');
420
+ }
421
+ if ($b->u->isZero()) {
422
+ throw new Refused('деление на ноль — evaluation error (§58)');
423
+ }
424
+ $like = null;
425
+ if ($a instanceof Money && $b instanceof Money) {
426
+ if ($a->cur !== $b->cur) {
427
+ throw new Refused('CURRENCY_MISMATCH: §58 делит Money на Money только при тождественной валюте');
428
+ }
429
+ } elseif ($a instanceof Money) {
430
+ $like = $a;
431
+ } elseif ($b instanceof Money) {
432
+ throw new Refused('§58: деление вне таблицы делимых');
433
+ }
434
+ $q = self::exact($a->u->mul(BigI::pow10($b->s)), $b->u->mul(BigI::pow10($a->s)));
435
+ if ($q === null) {
436
+ if ($like !== null) {
437
+ throw new Refused('INEXACT_DIVISION: округление только явной политикой (§50/§18)');
438
+ }
439
+ throw new Refused('§58: частное не 10-гладкое — Rational вне среза');
440
+ }
441
+ return $like === null ? new Dec($q[0], $q[1]) : new Money($q[0], $q[1], $like->cur);
442
+ }
443
+
444
+ // Евклидов остаток §58: 0 <= r < |b| при любых знаках операндов.
445
+ public static function mod(mixed $a, mixed $b): Dec
446
+ {
447
+ $x = self::integral($a);
448
+ $y = self::integral($b);
449
+ if ($x === null || $y === null) {
450
+ throw new Refused('§58: mod определён только для Integer');
451
+ }
452
+ if ($y->isZero()) {
453
+ throw new Refused('остаток от деления на ноль — evaluation error (§58)');
454
+ }
455
+ [, $r] = $x->divMod($y->abs());
456
+ if ($r->neg) {
457
+ $r = $r->add($y->abs());
458
+ }
459
+ return new Dec($r, 0);
460
+ }
461
+
462
+ public static function min(mixed $a, mixed $b): Dec|Money
463
+ {
464
+ self::same($a, $b, 'min');
465
+ [$x, $y] = self::pair($a, $b);
466
+ return $x->cmp($y) <= 0 ? $a : $b;
467
+ }
468
+
469
+ public static function max(mixed $a, mixed $b): Dec|Money
470
+ {
471
+ self::same($a, $b, 'max');
472
+ [$x, $y] = self::pair($a, $b);
473
+ return $x->cmp($y) >= 0 ? $a : $b;
474
+ }
475
+
476
+ public static function abs(mixed $v): Dec|Money
477
+ {
478
+ if (!self::isNum($v)) {
479
+ throw new Refused('§58: abs — не числовой вид');
480
+ }
481
+ return self::make($v->u->abs(), $v->s, $v);
482
+ }
483
+
484
+ // §58 (E-0075): только Integer pow Integer; порядок проверок — знак, предел,
485
+ // вычисление, диапазон — часть контракта. Умножение останавливается, как
486
+ // только модуль превысил 2^63: при |основание| >= 1 он дальше не убывает, а
487
+ // при нулевом основании не растёт, — исход тот же, что у полного вычисления.
488
+ public static function pow(mixed $base, mixed $exp): Dec
489
+ {
490
+ $b = self::integral($base);
491
+ $e = self::integral($exp);
492
+ if ($b === null || $e === null) {
493
+ throw new Refused('§58: pow определён только для Integer');
494
+ }
495
+ if ($e->neg) {
496
+ throw new Refused('§58: pow — показатель отрицателен');
497
+ }
498
+ if ($e->cmp(BigI::int(4096)) > 0) {
499
+ throw new Refused('RESOURCE_LIMIT: pow — показатель превышает 4096');
500
+ }
501
+ $min = BigI::parse('-9223372036854775808');
502
+ $max = BigI::parse('9223372036854775807');
503
+ $r = BigI::int(1);
504
+ for ($i = $e->small(); $i > 0; $i--) {
505
+ $r = $r->mul($b);
506
+ if (BigI::magCmp($r->m, $min->m) > 0) {
507
+ break;
508
+ }
509
+ }
510
+ if ($r->cmp($min) < 0 || $r->cmp($max) > 0) {
511
+ throw new Refused('RESOURCE_LIMIT: pow — результат вне 64-битного диапазона');
512
+ }
513
+ return new Dec($r, 0);
514
+ }
515
+
516
+ public static function round(mixed $m, mixed $digits, string $mode): Dec|Money
517
+ {
518
+ if ($mode !== 'HALF_UP') {
519
+ throw new Refused("§50: режим округления не реализован: $mode");
520
+ }
521
+ if (!self::isNum($m)) {
522
+ throw new Refused('§50: round — не числовой вид');
523
+ }
524
+ $n = is_int($digits) ? $digits : $digits->u->divMod(BigI::pow10($digits->s))[0]->small();
525
+ if ($m->s <= $n) {
526
+ return self::make($m->u->mul(BigI::pow10($n - $m->s)), $n, $m);
527
+ }
528
+ $drop = BigI::pow10($m->s - $n);
529
+ [$q, $r] = $m->u->abs()->divMod($drop);
530
+ $up = $r->add($r)->cmp($drop) >= 0 ? $q->add(BigI::int(1)) : $q; // HALF_UP — от нуля
531
+ return self::make($m->u->neg ? $up->negate() : $up, $n, $m);
532
+ }
533
+
534
+ private static function order(string $op, int $c): bool
535
+ {
536
+ return match ($op) {
537
+ 'lt' => $c < 0,
538
+ 'le' => $c <= 0,
539
+ 'gt' => $c > 0,
540
+ 'ge' => $c >= 0,
541
+ 'eq' => $c === 0,
542
+ 'ne' => $c !== 0,
543
+ default => throw new Refused("сравнение вне среза рантайма: $op"),
544
+ };
545
+ }
546
+
547
+ public static function cmp(mixed $a, string $op, mixed $b): bool
548
+ {
549
+ if ($a instanceof Sym || $b instanceof Sym) {
550
+ $same = $a instanceof Sym && $b instanceof Sym && $a->id === $b->id;
551
+ if ($op === 'eq') {
552
+ return $same;
553
+ }
554
+ if ($op === 'ne') {
555
+ return !$same;
556
+ }
557
+ throw new Refused('§57: вид enum не упорядочен');
558
+ }
559
+ if ($a instanceof LDate || $b instanceof LDate) {
560
+ if ($a instanceof LDate && $b instanceof LDate) {
561
+ return self::order($op, strcmp($a->v, $b->v));
562
+ }
563
+ if ($op === 'eq') {
564
+ return false;
565
+ }
566
+ if ($op === 'ne') {
567
+ return true;
568
+ }
569
+ throw new Refused('§57: Date сравнивается только с Date');
570
+ }
571
+ if (is_string($a) || is_string($b)) {
572
+ if ($op === 'eq') {
573
+ return $a === $b;
574
+ }
575
+ if ($op === 'ne') {
576
+ return $a !== $b;
577
+ }
578
+ throw new Refused("порядок на Text вне среза рантайма: $op");
579
+ }
580
+ if (($a instanceof Money) !== ($b instanceof Money)) {
581
+ throw new Refused('§49: сравнение денег с безразмерным');
582
+ }
583
+ if ($a instanceof Money && $a->cur !== $b->cur) {
584
+ throw new Refused('§49: разные валюты');
585
+ }
586
+ [$x, $y] = self::pair($a, $b);
587
+ return self::order($op, $x->cmp($y));
588
+ }
589
+
590
+ public static function eq(mixed $a, mixed $b): bool
591
+ {
592
+ if ($a instanceof Sym || $b instanceof Sym) {
593
+ return $a instanceof Sym && $b instanceof Sym && $a->id === $b->id;
594
+ }
595
+ if ($a instanceof LDate || $b instanceof LDate) {
596
+ return $a instanceof LDate && $b instanceof LDate && $a->v === $b->v;
597
+ }
598
+ if (is_string($a) || is_string($b)) {
599
+ return $a === $b;
600
+ }
601
+ if (($a instanceof Money) !== ($b instanceof Money)) {
602
+ return false;
603
+ }
604
+ if ($a instanceof Money && $a->cur !== $b->cur) {
605
+ return false;
606
+ }
607
+ [$x, $y] = self::pair($a, $b);
608
+ return $x->cmp($y) === 0;
609
+ }
610
+
611
+ private static function norm(Dec|Money $d): string
612
+ {
613
+ $u = $d->u;
614
+ $s = $d->s;
615
+ while ($s > 0) {
616
+ [$q, $r] = $u->divMod(BigI::int(10));
617
+ if (!$r->isZero()) {
618
+ break;
619
+ }
620
+ $u = $q;
621
+ $s--;
622
+ }
623
+ return $u->str() . 'e-' . $s;
624
+ }
625
+
626
+ // Разделитель и экран внутри текста экранируются: иначе ["a|e:b","c"] и
627
+ // ["a","b|e:c"] склеивались в один ключ, и второй факт терялся.
628
+ private static function esc(string $text): string
629
+ {
630
+ return str_replace(['\\', '|'], ['\\\\', '\\|'], $text);
631
+ }
632
+
633
+ public static function key(array $row): string
634
+ {
635
+ $parts = [];
636
+ foreach ($row as $v) {
637
+ $parts[] = match (true) {
638
+ is_string($v) => 'e:' . self::esc($v),
639
+ $v instanceof LDate => 't:' . $v->v,
640
+ $v instanceof Sym => 'c:' . self::esc($v->id),
641
+ $v instanceof Money => 'm:' . $v->cur . ':' . self::norm($v),
642
+ default => 'd:' . self::norm($v),
643
+ };
644
+ }
645
+ return implode('|', $parts);
646
+ }
647
+
648
+ public static function setLegalTime(string $t): void
649
+ {
650
+ self::$legal = $t;
651
+ }
652
+
653
+ public static function legalTime(): string
654
+ {
655
+ if (self::$legal === '') {
656
+ throw new Refused('legalTime не подан делом');
657
+ }
658
+ return self::$legal;
659
+ }
660
+ }